業(yè)務(wù)場(chǎng)景
簡(jiǎn)單分析一下短鏈接的業(yè)務(wù)場(chǎng)景挠唆。參照百度短鏈接http://dwz.cn/ 处窥。
- 根據(jù)長(zhǎng)鏈接生成一個(gè)短鏈接。
- 根據(jù)短鏈接解析出長(zhǎng)鏈接损搬。
簡(jiǎn)單實(shí)現(xiàn)
如何實(shí)現(xiàn)這個(gè)功能呢碧库?也許你會(huì)考慮實(shí)現(xiàn)一個(gè)算法,將長(zhǎng)鏈接轉(zhuǎn)成短鏈接巧勤,實(shí)現(xiàn)長(zhǎng)短的一一對(duì)應(yīng)嵌灰。然后再實(shí)現(xiàn)逆運(yùn)算,將短鏈接換算回長(zhǎng)鏈接颅悉。當(dāng)然這種算法是不可能存在的沽瞭。如果有那你就發(fā)現(xiàn)了世界上最牛的壓縮算法了。
其實(shí)短鏈接的實(shí)現(xiàn)并沒(méi)有一個(gè)固定的算法剩瓶,主要的原理就是把長(zhǎng)鏈接通過(guò)一定的規(guī)則得到一個(gè)短鏈接驹溃,然后把長(zhǎng)鏈接和短鏈接的關(guān)系記錄在數(shù)據(jù)庫(kù)中(你可以使用關(guān)系型數(shù)據(jù)庫(kù)或者非關(guān)系型數(shù)據(jù)庫(kù)NoSql)。當(dāng)用戶訪問(wèn)短鏈接時(shí)延曙,短鏈接服務(wù)根據(jù)短鏈接查找到對(duì)應(yīng)的長(zhǎng)鏈接豌鹤,然后進(jìn)行重定向。
那么我們通過(guò)什么規(guī)則來(lái)生成短鏈接呢枝缔?你可以通過(guò)發(fā)號(hào)策略布疙,給每一個(gè)過(guò)來(lái)的長(zhǎng)地址,發(fā)一個(gè)號(hào)即可愿卸,你可以用分布式key-value系統(tǒng)做發(fā)號(hào)器或者利用mysql的自增主鍵ID實(shí)現(xiàn)甚至你可以用NoSql實(shí)現(xiàn)灵临,這都可以。這里我使用的是mysql的自增主鍵ID實(shí)現(xiàn)的趴荸。
根據(jù)上邊的原理我們可以設(shè)計(jì)如下表結(jié)構(gòu):
CREATE TABLE IF NOT EXISTS `shortlink` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵ID',
`short_link` varchar(30) NOT NULL COMMENT '短鏈接內(nèi)容',
`long_link` varchar(255) NOT NULL COMMENT '長(zhǎng)鏈接內(nèi)容',
`long_link_sign` char(32) NOT NULL COMMENT '長(zhǎng)鏈接MD5加密后的字符串',
`visit_count` int(11) NOT NULL DEFAULT '0' COMMENT '訪問(wèn)次數(shù)',
`created_at` datetime NOT NULL COMMENT '創(chuàng)建時(shí)間',
`last_visit_at` datetime NOT NULL COMMENT '最后訪問(wèn)時(shí)間',
PRIMARY KEY (`id`),
UNIQUE KEY `long_link_sign` (`long_link_sign`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
生成短鏈接的邏輯就是:
- 判斷是否為合法的長(zhǎng)鏈接地址儒溉。
- 把長(zhǎng)鏈接做md5加密。
- 根據(jù)長(zhǎng)鏈接加密串查詢一下是否已經(jīng)生成過(guò)短鏈接发钝,如果有則直接返回短鏈接顿涣。
- 如果不存在則把長(zhǎng)鏈接、長(zhǎng)鏈接加密串插入數(shù)據(jù)庫(kù)并返回自增主鍵ID笼平。
- 把自增主鍵ID更新到short_link字段作為短鏈接的值园骆。
分庫(kù)分表實(shí)現(xiàn)
當(dāng)短鏈接特別多或者并發(fā)量高的時(shí)候單個(gè)表已經(jīng)不能承受我們的業(yè)務(wù)了。單個(gè)表或者單個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)存儲(chǔ)能力和并發(fā)能力都是有限的寓调。這個(gè)時(shí)候就我們要考慮分庫(kù)分表了锌唾。
短鏈接分庫(kù)分表需要考慮的幾個(gè)問(wèn)題。
- 怎么根據(jù)長(zhǎng)鏈接找到需要查詢和插入的數(shù)據(jù)庫(kù)和表?
- 怎么根據(jù)短鏈接找到需要查詢的數(shù)據(jù)庫(kù)和表晌涕?
問(wèn)題一我們可以根據(jù)長(zhǎng)鏈接生成的md5值通過(guò)某種算法算出所在的數(shù)據(jù)庫(kù)和數(shù)據(jù)表滋捶。
問(wèn)題二當(dāng)我們寫(xiě)入當(dāng)前長(zhǎng)鏈接對(duì)應(yīng)的短鏈接的時(shí)候可以把數(shù)據(jù)庫(kù)位和數(shù)據(jù)表位組合到生成的短鏈接中。下圖是短鏈接字符串的組合方式余黎。
根據(jù)上述方式重窟,假設(shè)我們用一位數(shù)據(jù)庫(kù)位和一位數(shù)據(jù)表位,而數(shù)據(jù)庫(kù)和數(shù)據(jù)表位由0123456789abcdefghijklmnopqrstuvwxyz字符表示那么一共可以組成36個(gè)數(shù)據(jù)庫(kù)惧财,每個(gè)數(shù)據(jù)庫(kù)中有36張表巡扇,一共可以組成1296張短鏈接表。假設(shè)每張表有1000萬(wàn)條數(shù)據(jù)垮衷,則可以支撐129億條短鏈接數(shù)據(jù)厅翔。當(dāng)然你也可以用其他組合支撐更大的數(shù)據(jù)量。
代碼實(shí)現(xiàn)大致如下:
<?php
/**
* Created by PhpStorm.
* User: duxiaokong
* Date: 2016/08/24
*/
use Cache;
use Redis;
class Shortlink
{
protected $table = 'shortlink';
/**
* 根據(jù)長(zhǎng)鏈接生成短鏈接
*
* @param string $long_link 長(zhǎng)鏈接
* @return bool|string
*/
public function createShortLink($long_link)
{
$long_link = trim($long_link);
//判斷長(zhǎng)鏈接是否為合法的url
$parts = parse_url($long_link);
if (!isset($parts['scheme']) || !isset($parts['host'])) {
return false;
}
//根據(jù)長(zhǎng)鏈接生成長(zhǎng)鏈接加密串
$private_key = 'duxiaokong';
$long_link_sign = md5($private_key . $long_link);
//根據(jù)長(zhǎng)鏈接加密串判斷此長(zhǎng)鏈接是否已經(jīng)存在短鏈接搀突。
$link_res = self::checkLongLinkSign($long_link_sign);
if (!empty($link_res)) {
return $link_res['short_link'];
}
//根據(jù)長(zhǎng)鏈接加密串獲取需要操作哪個(gè)數(shù)據(jù)庫(kù)和表
$table_info = self::routerLong($long_link_sign);
$data = [
'short_link' => '',
'long_link' => $long_link,
'long_link_sign' => $long_link_sign,
'created_at' => date('Y-m-d H:i:s'),
'last_visit_at' => date('Y-m-d H:i:s'),
'visit_count' => 0
];
//插入對(duì)應(yīng)數(shù)據(jù)庫(kù)$table_info['db_id'] 的對(duì)應(yīng)表$table_info['table_id']中并返回主鍵ID $id
if (!$id) {
return false;
}
//根據(jù)數(shù)據(jù) 庫(kù)位+表位+ID 生成短鏈接字符
$short_link = $this->makeShortUrl($table_info['db_id'], $table_info['table_id'], $id);
$data = ['short_link' => $short_link];
//把短鏈接更新到數(shù)據(jù)庫(kù)中
// update $table_info['table_name'] set short_link = '$short_link';
return $short_link;
}
/**
* 根據(jù)短鏈獲取長(zhǎng)鏈
*
* @param string $short_link 短鏈接
* @param bool $is_cache
* @return bool|mixed|static
*/
public function getLongUrl($short_link, $is_cache = true)
{
$short_link = trim($short_link);
if (empty($short_link)) {
return false;
}
//判斷短鏈接是否合法
if (!preg_match('/^[\w]{1,}$/', $short_link)) {
return false;
}
//根據(jù)短鏈接加密串獲取需要操作哪個(gè)數(shù)據(jù)庫(kù)和表
$table_info = self::routerShort($short_link);
$cache_k = $short_link;
//先從緩存中獲取數(shù)據(jù)刀闷,不存在查找對(duì)應(yīng)數(shù)據(jù)庫(kù)和表中的數(shù)據(jù)
if ($is_cache === true) {
$info = Cache::get($cache_k);
}
if (empty($info)) {
$db_id = $table_info['db_id'];
$table_name = $table_info['table_name'];
//$table_info = 從對(duì)應(yīng)數(shù)據(jù)庫(kù)查找數(shù)據(jù)
if (!empty($info)) {
Cache::put($cache_k, $info, $this->cache_time);
}
}
//增加此短鏈接的訪問(wèn)統(tǒng)計(jì)數(shù)
self::incCounter($info['id'], $table_info);
return $info;
}
/**
* 更新訪問(wèn)統(tǒng)計(jì)
*
* @param int $id
* @param int $visit_count
* @param string $visit_time
* @param int $db_id
* @param int $table_id
* @return bool
*/
public function updateCounter($id, $visit_count, $visit_time, $db_id, $table_id)
{
$db_id = trim($db_id);
$table_id = trim($table_id);
$id = trim($id);
$visit_count = trim($visit_count);
$visit_time = trim($visit_time);
if (!is_numeric($id) || !is_numeric($visit_count) || $id <= 0 || $visit_count <= 0) {
return false;
}
$table_name = $this->table . $table_id;
$data = ['last_visit_at' => $visit_time];
//更新到對(duì)應(yīng)數(shù)據(jù)庫(kù)中
}
/**
* 查詢長(zhǎng)鏈接是否存在
*
* @param string $long_link_sign
* @param bool $is_cache
* @return bool|mixed|static
*/
private function checkLongLinkSign($long_link_sign, $is_cache = true)
{
$long_link_sign = trim($long_link_sign);
if (empty($long_link_sign)) {
return false;
}
$cache_k = $long_link_sign;
if ($is_cache === true) {
//先存緩存中讀取數(shù)據(jù)
$info = Cache::get($cache_k);
}
if (empty($info)) {
//根據(jù)長(zhǎng)鏈接加密串獲取數(shù)據(jù)庫(kù)和表信息
$table_info = self::routerLong($long_link_sign);
$db_id = $table_info['db_id'];
$table_name = $table_info['table_name'];
//連接對(duì)應(yīng)的數(shù)據(jù)庫(kù)查詢此表中是否存在此條記錄
// $info = select * from $db.$table_name where long_link_sign = '$long_link_sign' limit 1;
if (!empty($info)) {
Cache::put($cache_k, $info, $this->cache_time);
}
}
return $info;
}
/**
* 訪問(wèn)量量自增1
*
* @param int $id
* @param array $table_info
* @return bool
*/
private function incCounter($id, $table_info)
{
// 不實(shí)時(shí)更新,先寫(xiě)入Redis或者memcache,然后每隔一段時(shí)間讀取Redis或者memcache中的數(shù)據(jù)更新到數(shù)據(jù)庫(kù)
$date = date('Ymd');
$now_time = date('Y-m-d H:i:s');
$val = $table_info['db_id'] . '|' . $table_info['table_id'] . '|' . $id;
try {
Redis::SADD('shortlink_list:' . $date, $val);
Redis::hIncrBy('shortlink_counter:' . $date, $val, 1);
Redis::hSet('shortlink_time:' . $date, $val, $now_time);
} catch (\Exception $e) {
return false;
}
return true;
}
/**
* 根據(jù)數(shù)據(jù)庫(kù)位+數(shù)據(jù)表位+ID 生成短鏈接字符串
*
* @param int $db_id 數(shù)據(jù)庫(kù)位
* @param int $table_id 數(shù)據(jù)表位
* @param int $id 自增ID
* @return string $code
*/
private function makeShortUrl($db_id, $table_id, $id)
{
$table_str = $db_id . $table_id;
//把主鍵ID轉(zhuǎn)換成字符串
$code_str = self::num_to_string($id);
return $table_str . $code_str;
}
/**
* 根據(jù)短鏈做表路由
*
* @param string $short_link 短鏈接
* @return array|bool
*/
private function routerShort($short_link)
{
$db_id = $short_link[0];
$table_id = $short_link[1];
//這里最好判斷一下$db_id和$table_id是否合法
$table_name = $this->table . $table_id;
return ['db_id' => $db_id, 'table_id' => $table_id, 'table_name' => $table_name];
}
/**
* 根據(jù)長(zhǎng)鏈接加密串做表路由
*
* @param string $long_link_sign
* @return array
*/
private function routerLong($long_link_sign)
{
$long_link_sign = strtolower($long_link_sign);
$db_id = substr($long_link_sign, 0, 1);
$table_id = substr($long_link_sign, 1, 1);
$table_name = $this->table . $table_id;
return ['db_id' => $db_id, 'table_id' => $table_id, 'table_name' => $table_name];
}
/**
* 十進(jìn)制數(shù)字轉(zhuǎn)字符串仰迁,默認(rèn)為64進(jìn)制字符串甸昏,其他進(jìn)制可使用base_convert函數(shù)
* @author dxk
* @date 2016-07-01
*
* @param $num
* @param string $pool
* @return bool|string
*/
private function num_to_string($num, $pool = '')
{
if (!is_numeric($num)) {
return false;
}
if (empty($pool)) {
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
$base = strlen($pool);
$code = "";
while ($num > $base - 1) {
$code = $pool[fmod($num, $base)] . $code;
$num = floor($num / $base);
}
$code = $pool[$num] . $code;
return $code;
}
/**
* 字符串轉(zhuǎn)十進(jìn)制數(shù)字,默認(rèn)為64進(jìn)制字符串徐许,其他進(jìn)制可使用base_convert函數(shù)
* @author dxk
* @date 2016-07-01
*
* @param $string
* @param string $pool
* @return bool|int
*/
private function string_to_num($string, $pool = '')
{
if (empty($pool)) {
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
$base = strlen($pool);
$num = 0;
while (strlen($string) > 0) {
$pos = strpos($pool, $string[0]);
if ($pos === false) {
return false;
}
$num += ($pos * pow($base, strlen($string) - 1));
$string = substr($string, 1);
}
return $num;
}
}
說(shuō)明:
- md5的值不是絕對(duì)不會(huì)重復(fù)的施蜜,雖然幾率很小。但是畢竟MD5值數(shù)量是有限的而字符串是無(wú)限的雌隅。如果你要求準(zhǔn)確性特別高這里的加密值你也可以用其他方式實(shí)現(xiàn)花墩。
- 這里你還可以有其他的實(shí)現(xiàn)方式。主要是解決如何根據(jù)長(zhǎng)鏈接查找數(shù)據(jù)庫(kù)和表澄步,然后根據(jù)短鏈接能找回對(duì)應(yīng)的數(shù)據(jù)庫(kù)和表。
- 除了上邊代碼中的緩存和泌,訪問(wèn)統(tǒng)計(jì)定時(shí)更新之外村缸,你也可以有其他方面的優(yōu)化。比如把一年內(nèi)都沒(méi)有訪問(wèn)過(guò)的數(shù)據(jù)移入歷史庫(kù)武氓。這樣的話如果用戶重新用一年前的長(zhǎng)鏈接生成短鏈接會(huì)重新生成一個(gè)短鏈(小部分?jǐn)?shù)據(jù)冗余)梯皿,如果用戶用之前的短鏈去獲取長(zhǎng)鏈則可以先查詢現(xiàn)用數(shù)據(jù)庫(kù)如果不存在再去查詢歷史數(shù)據(jù)庫(kù)。除此之外你還有很多可以去變通和優(yōu)化的地方县恕。
推薦資料:
- 短鏈接URL系統(tǒng)是怎么設(shè)計(jì)的东羹? http://www.codeceo.com/article/short-url-system-design.html