24

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

 4 years ago
source link: https://www.tuicool.com/articles/jQnAveM
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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

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

  3. 存量数据迁移;

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

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

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

zi6ruuY.jpg!web 扩容-1

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

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

  • 表的选择算法为:hash(user_id)%2的结果为1则选tb_user_1表,hash(user_id)%2的结果为0则选tb_user_2表。

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

IbQ3a2u.jpg!web 扩容-2

即使这样方案,还是避免不了分表时的存量数据迁移,以及分表后业务发展到一定时期后的繁琐扩容。那么有没有一种很好的方案,能够一劳永逸,分表时不需要存量数据迁移,用户量无论如何增长,扩容时都不需要迁移存量数据,只需要新增一个数据库示例,修改一下配置即可。软件开发行业,一个方案能撑过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点数据写入到表tb_catdata_2019071810中,2019年7月18号12点数据写入到表tb_catdata_2019071812中,2019年7月20号14点数据写入到表tb_catdata_2019072014中。这样做的优点如下:

  1. 历史数据不用迁移;

  2. 扩容非常简单;

缺点如下:

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

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

  • 自增ID

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

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

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

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

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

  5. 以此类推。

如果你的tb_user本来就有自增主键,那这种方案就比较好。但是需要注意几点,由于用户ID是自增的,所以这个ID不能通过HTTP暴露出去,否则可以通过新注册一个用户后,就能得到你的真实用户数,这是比较危险的。其次,存量数据在单表中可以通过自增ID生成,但是当切换分表后,用户ID如果还是用自增生成,需要注意在创建新表时设置AUTO_INCREMENT,例如创建表tb_user_2时,设置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到表tb_user中。这种方案需要注意Redis主从切换后,晋升为主的Redis节点中的ID可能由于同步时间差不是最新ID的问题。这样的话,可能会导致插入记录到tb_user失败。需要对这种异常特殊处理一下即可。

  • 利用雪花算法生成

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

iu2I7nB.png!web 关系表

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

qMVjMvV.jpg!web 新方案效果图

回顾总结

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

公众号二维码

↓↓↓↓

分库分表技术演进&最佳实践-修订篇

【阿飞的博客】 公众号二维码

↓↓↓↓

UZ7B7n7.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK