Mysql分庫(kù)分表實(shí)踐(短鏈接的實(shí)現(xiàn))

業(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;

生成短鏈接的邏輯就是:

  1. 判斷是否為合法的長(zhǎng)鏈接地址儒溉。
  2. 把長(zhǎng)鏈接做md5加密。
  3. 根據(jù)長(zhǎng)鏈接加密串查詢一下是否已經(jīng)生成過(guò)短鏈接发钝,如果有則直接返回短鏈接顿涣。
  4. 如果不存在則把長(zhǎng)鏈接、長(zhǎng)鏈接加密串插入數(shù)據(jù)庫(kù)并返回自增主鍵ID笼平。
  5. 把自增主鍵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)題。

  1. 怎么根據(jù)長(zhǎng)鏈接找到需要查詢和插入的數(shù)據(jù)庫(kù)和表?
  2. 怎么根據(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)化的地方县恕。

推薦資料:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市忠烛,隨后出現(xiàn)的幾起案子属提,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冤议,死亡現(xiàn)場(chǎng)離奇詭異斟薇,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)恕酸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門堪滨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人蕊温,你說(shuō)我怎么就攤上這事袱箱。” “怎么了义矛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵发笔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我症革,道長(zhǎng)筐咧,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任噪矛,我火速辦了婚禮量蕊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘艇挨。我一直安慰自己残炮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布缩滨。 她就那樣靜靜地躺著势就,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脉漏。 梳的紋絲不亂的頭發(fā)上苞冯,一...
    開(kāi)封第一講書(shū)人閱讀 49,837評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音侧巨,去河邊找鬼舅锄。 笑死,一個(gè)胖子當(dāng)著我的面吹牛司忱,可吹牛的內(nèi)容都是我干的皇忿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼坦仍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鳍烁!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起繁扎,我...
    開(kāi)封第一講書(shū)人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤幔荒,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體铺峭,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡墓怀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卫键。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片傀履。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖莉炉,靈堂內(nèi)的尸體忽然破棺而出钓账,到底是詐尸還是另有隱情,我是刑警寧澤絮宁,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布梆暮,位于F島的核電站,受9級(jí)特大地震影響绍昂,放射性物質(zhì)發(fā)生泄漏啦粹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一窘游、第九天 我趴在偏房一處隱蔽的房頂上張望唠椭。 院中可真熱鬧,春花似錦忍饰、人聲如沸贪嫂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)力崇。三九已至,卻和暖如春赢织,著一層夾襖步出監(jiān)牢的瞬間亮靴,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工于置, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留台猴,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓俱两,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親曹步。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宪彩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容