分库分表实战:可能是用户表最佳分库分表方案

在能满足业务场景的情况下,单表 > 分区 > 单库分表 > 分库分表,推荐优先级从左到右逐渐降低。

本篇文章主要讲用户表(或者类似这种业务属性的表)的分表方案,至于订单表,流水表等,本文的方案可能不是很合适,可以参考笔者另一篇文章《分库分表技术演进&最佳实践-修订篇》。

我们首先来看一下分表时主要需要做的事情:

  1. 选定分片键:既然是用户表那分片键非用户ID莫属;

  2. 修改代码:以sharding-jdbc这种client模式的中间件为例,主要是引入依赖,然后新增一些配置。业务代码并不怎么需要改动。

  3. 存量数据迁移;

  4. 业务发展超过容量评估后需要开发和运维介入扩容;

做过分库分表的都知道,第3步最麻烦,而且非常不好验证迁前后数据一致性(目前业界主流的迁移方案是存量数据迁移+利用binlog进行增量数据同步,待两边的数据持平后,将业务代码中的开关切到分表模式)。

第4步同样麻烦,业务增长完全超过当初分表设计的容量评估是很常见的事情,这也成为业务高速发展的一个隐患。而且互联网类型的业务都希望能做到7x24小时不停服务,这样就给扩容带来了更大的挑战。笔者看过比较好的方案就是58沈剑提出的成倍扩容方案。如下图所示,假设现在已经有2张表:tbuser1,tbuser2。且有两个库是主备关系,并且分表算法是hash(user_id)%2:

image.png

现在要扩容到4张表,做法是将两个库的主从关系切断。然后slave晋升为master,这样就有两个主库:master-1,master-2。新的分表算法是:

  • 库选择算法为:hash(userid)%4的结果为1或者2,就选master-1库,hash(userid)%4的结果为3或者0,就选master-2库;

  • 表的选择算法为:hash(userid)%2的结果为1则选tbuser1表,hash(userid)%2的结果为0则选tbuser2表。

如此以来,两个库中总计4张表,都冗余了1倍的数据:master-1中tbuser1冗余了3、7、11…,master-1中tbuser2冗余了4、8、12…,master-2中tbuser1冗余了1、5、9…,master-2中tbuser2冗余了2、6、10…。将这些冗余数据删掉后,库、表、数据示意图如下所示:

image.png

即使这样方案,还是避免不了分表时的存量数据迁移,以及分表后业务发展到一定时期后的繁琐扩容。那么有没有一种很好的方案,能够一劳永逸,分表时不需要存量数据迁移,用户量无论如何增长,扩容时都不需要迁移存量数据,只需要新增一个数据库示例,修改一下配置即可。软件开发行业,一个方案能撑过3~5年就是一个很优秀的方案,我们现在YY的是整个生命周期内都不用改动的完美的方案。没错,我们在寻找银弹

这个方案笔者在两个地方都接触到了:

  1. 某V厂面试时,部门老大提出的方案;

  2. 和美团大牛普架讨论了解到的CAT存储方案;

说明:CAT是美团点评开源的APM,目前在Github上的star已经破万(Github地址:https://github.com/dianping/cat),比skywalking和pinpoint还快,如果你正在选型APM,而且能接受代码侵入,那么CAT是一个不错的选择。

CAT存储方案是按照写入时间顺序存储,假设每小时写入量是千万级别,那么分表就按照小时维度。也就是说,2019年7月18号10点数据写入到表tbcatdata2019071810中,2019年7月18号12点数据写入到表tbcatdata2019071812中,2019年7月20号14点数据写入到表tbcatdata2019072014中。这样做的优点如下:

  1. 历史数据不用迁移;

  2. 扩容非常简单;

缺点如下:

  1. 读写热点集中,所有写操作全部打在最新的表上。

有没有发现,这个方案的优点就是我们需要的。BINGO,要的就是这样的方案。那么对应到用户表上来具体的分表方案非常类似:按照range切分。需要说明的是,这个方案的前提是用户ID一定要趋势递增,最好严格递增。笔者给出3种用户ID递增的方案:

  • 自增ID

假设存量数据用户表的id最大值是960W,那么分表算法是这样的,表序号只需要根据user_id/10000000就能得到:

  1. 用户ID在范围[1, 10000000)中分到tbuser0中(需要将tbuser重命名为tbuser_0);

  2. 用户ID在范围[10000000, 20000000)中分到tbuser1中;

  3. 用户ID在范围[20000000, 30000000)中分到tbuser2中;

  4. 用户ID在范围[30000000, 40000000)中分到tbuser3中;

  5. 以此类推。

如果你的tbuser本来就有自增主键,那这种方案就比较好。但是需要注意几点,由于用户ID是自增的,所以这个ID不能通过HTTP暴露出去,否则可以通过新注册一个用户后,就能得到你的真实用户数,这是比较危险的。其次,存量数据在单表中可以通过自增ID生成,但是当切换分表后,用户ID如果还是用自增生成,需要注意在创建新表时设置AUTOINCREMENT,例如创建表tbuser2时,设置AUTO_INCREMENT=10000000,DDL如下:

CREATE TABLE if not exists `tb_user_2` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,    
  `username` varchar(16) NOT NULL COMMENT '用户名',    
  `remark` varchar(16) NOT NULL COMMENT '备注'    
) ENGINE=InnoDB AUTO_INCREMENT=10000000;

- 这样的话,当新增用户时,用户ID就会从10000000开始,而不会与之前的用户ID冲突   

insert into tb_user_2 values(null, 'afei', 'afei');
  • Redis incr

第二种方案就是利用Redis的incr命令。将之前最大的ID保存到Redis中,接下来新增用户的ID值都通过incr命令得到。然后insert到表tbuser中。这种方案需要注意Redis主从切换后,晋升为主的Redis节点中的ID可能由于同步时间差不是最新ID的问题。这样的话,可能会导致插入记录到tbuser失败。需要对这种异常特殊处理一下即可。

  • 利用雪花算法生成

采用类雪花算法生成用户ID,这种方式不太好精确掌握切分表的时机。因为没有高效获取tbuser表数据量的办法,也就不知道什么时候表数据量达到1000w级别,也就不知道什么时候需要往新表中插入数据(select count(*) from tbuser无论怎么优化性能都不会很高,除非是MyISAM引擎)。而且如果利用雪花算法生成用户ID,那么还需要一张表保存用户ID和分表关系:

image.png

笔者推荐第一种方案,即利用表自增ID生成用户ID:方案越简单,可靠性越高。其他两种方案,或者其他方案或多或少需要引入一些中间件或者介质,从而增加方案的复杂度。新方案效果图如下:

image.png

回顾总结

我们回头看一下这种用户表方案,满足了存量数据不需要做任何迁移(除非是存量数据远远超过单表承受能力)。而且,无论用户规模增长到多大量级,1亿,10亿,50亿,后面都不需要做数据迁移。而且也不再需要开发和运维介入。因为整个方案,会自己往新表中插入数据。我们唯一需要做的就是,根据硬件性能,约定一个库允许保存的用户表数量即可。假如一个库保存64张表,那么当扩容到第65张表时,程序会自动往第二个库的第一张表中写入。

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