Mysql分库分表实践(短链接的实现)

业务场景

简单分析一下短链接的业务场景。参照百度短链接http://dwz.cn/

  • 根据长链接生成一个短链接。
  • 根据短链接解析出长链接。

简单实现

如何实现这个功能呢?也许你会考虑实现一个算法,将长链接转成短链接,实现长短的一一对应。然后再实现逆运算,将短链接换算回长链接。当然这种算法是不可能存在的。如果有那你就发现了世界上最牛的压缩算法了。

其实短链接的实现并没有一个固定的算法,主要的原理就是把长链接通过一定的规则得到一个短链接,然后把长链接和短链接的关系记录在数据库中(你可以使用关系型数据库或者非关系型数据库NoSql)。当用户访问短链接时,短链接服务根据短链接查找到对应的长链接,然后进行重定向。

那么我们通过什么规则来生成短链接呢?你可以通过发号策略,给每一个过来的长地址,发一个号即可,你可以用分布式key-value系统做发号器或者利用mysql的自增主键ID实现甚至你可以用NoSql实现,这都可以。这里我使用的是mysql的自增主键ID实现的。

根据上边的原理我们可以设计如下表结构:

CREATE TABLE IF NOT EXISTS `shortlink` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
  `short_link` varchar(30) NOT NULL COMMENT '短链接内容',
  `long_link` varchar(255) NOT NULL COMMENT '长链接内容',
  `long_link_sign` char(32) NOT NULL COMMENT '长链接MD5加密后的字符串',
  `visit_count` int(11) NOT NULL DEFAULT '0' COMMENT '访问次数',
  `created_at` datetime NOT NULL COMMENT '创建时间',
  `last_visit_at` datetime NOT NULL COMMENT '最后访问时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `long_link_sign` (`long_link_sign`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

生成短链接的逻辑就是:

  1. 判断是否为合法的长链接地址。
  2. 把长链接做md5加密。
  3. 根据长链接加密串查询一下是否已经生成过短链接,如果有则直接返回短链接。
  4. 如果不存在则把长链接、长链接加密串插入数据库并返回自增主键ID。
  5. 把自增主键ID更新到short_link字段作为短链接的值。

分库分表实现

当短链接特别多或者并发量高的时候单个表已经不能承受我们的业务了。单个表或者单个数据库的数据存储能力和并发能力都是有限的。这个时候就我们要考虑分库分表了。
短链接分库分表需要考虑的几个问题。

  1. 怎么根据长链接找到需要查询和插入的数据库和表?
  2. 怎么根据短链接找到需要查询的数据库和表?

问题一我们可以根据长链接生成的md5值通过某种算法算出所在的数据库和数据表。
问题二当我们写入当前长链接对应的短链接的时候可以把数据库位和数据表位组合到生成的短链接中。下图是短链接字符串的组合方式。

根据上述方式,假设我们用一位数据库位和一位数据表位,而数据库和数据表位由0123456789abcdefghijklmnopqrstuvwxyz字符表示那么一共可以组成36个数据库,每个数据库中有36张表,一共可以组成1296张短链接表。假设每张表有1000万条数据,则可以支撑129亿条短链接数据。当然你也可以用其他组合支撑更大的数据量。

代码实现大致如下:

<?php
/**
 * Created by PhpStorm.
 * User: duxiaokong
 * Date: 2016/08/24
 */
use Cache;
use Redis;

class Shortlink
{
    protected $table = 'shortlink';


    /**
     * 根据长链接生成短链接
     *
     * @param string $long_link 长链接
     * @return bool|string
     */
    public function createShortLink($long_link)
    {
        $long_link = trim($long_link);
        //判断长链接是否为合法的url
        $parts = parse_url($long_link);
        if (!isset($parts['scheme']) || !isset($parts['host'])) {
            return false;
        }
        //根据长链接生成长链接加密串
        $private_key = 'duxiaokong';
        $long_link_sign = md5($private_key . $long_link);
        //根据长链接加密串判断此长链接是否已经存在短链接。
        $link_res = self::checkLongLinkSign($long_link_sign);
        if (!empty($link_res)) {
            return $link_res['short_link'];
        }
        //根据长链接加密串获取需要操作哪个数据库和表
        $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
        ];
        //插入对应数据库$table_info['db_id'] 的对应表$table_info['table_id']中并返回主键ID  $id
        if (!$id) {
            return false;
        }
        //根据数据 库位+表位+ID 生成短链接字符
        $short_link = $this->makeShortUrl($table_info['db_id'], $table_info['table_id'], $id);
        $data = ['short_link' => $short_link];
        //把短链接更新到数据库中
        // update $table_info['table_name'] set short_link = '$short_link';
        return $short_link;
    }

    /**
     * 根据短链获取长链
     *
     * @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;
        }
        //根据短链接加密串获取需要操作哪个数据库和表
        $table_info = self::routerShort($short_link);
        $cache_k = $short_link;
        //先从缓存中获取数据,不存在查找对应数据库和表中的数据
        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 = 从对应数据库查找数据
            if (!empty($info)) {
                Cache::put($cache_k, $info, $this->cache_time);
            }
        }
        //增加此短链接的访问统计数
        self::incCounter($info['id'], $table_info);
        return $info;
    }

    /**
     * 更新访问统计
     *
     * @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];
        //更新到对应数据库中
    }

    /**
     * 查询长链接是否存在
     *
     * @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) {
            //先存缓存中读取数据
            $info = Cache::get($cache_k);
        }
        if (empty($info)) {
            //根据长链接加密串获取数据库和表信息
            $table_info = self::routerLong($long_link_sign);
            $db_id = $table_info['db_id'];
            $table_name = $table_info['table_name'];
            //连接对应的数据库查询此表中是否存在此条记录
            // $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;
    }

    /**
     * 访问量量自增1
     *
     * @param int $id
     * @param array $table_info
     * @return bool
     */
    private function incCounter($id, $table_info)
    {
        // 不实时更新,先写入Redis或者memcache,然后每隔一段时间读取Redis或者memcache中的数据更新到数据库
        $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;
    }

    /**
     * 根据数据库位+数据表位+ID 生成短链接字符串
     *
     * @param int $db_id 数据库位
     * @param int $table_id 数据表位
     * @param int $id 自增ID
     * @return string $code
     */
    private function makeShortUrl($db_id, $table_id, $id)
    {
        $table_str = $db_id . $table_id;
        //把主键ID转换成字符串
        $code_str = self::num_to_string($id);
        return $table_str . $code_str;
    }

    /**
     * 根据短链做表路由
     *
     * @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];
    }

    /**
     * 根据长链接加密串做表路由
     *
     * @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];
    }

    /**
     * 十进制数字转字符串,默认为64进制字符串,其他进制可使用base_convert函数
     * @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;
    }


    /**
     * 字符串转十进制数字,默认为64进制字符串,其他进制可使用base_convert函数
     * @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;
    }
}

说明:

  • md5的值不是绝对不会重复的,虽然几率很小。但是毕竟MD5值数量是有限的而字符串是无限的。如果你要求准确性特别高这里的加密值你也可以用其他方式实现。
  • 这里你还可以有其他的实现方式。主要是解决如何根据长链接查找数据库和表,然后根据短链接能找回对应的数据库和表。
  • 除了上边代码中的缓存,访问统计定时更新之外,你也可以有其他方面的优化。比如把一年内都没有访问过的数据移入历史库。这样的话如果用户重新用一年前的长链接生成短链接会重新生成一个短链(小部分数据冗余),如果用户用之前的短链去获取长链则可以先查询现用数据库如果不存在再去查询历史数据库。除此之外你还有很多可以去变通和优化的地方。

推荐资料:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容