再次抛出笔者的观点, 在能满足业务场景的情况下, 单表>分区>单库分表>分库分表, 推荐优先级从左到右逐渐降低.
本篇文章主要讲用户表 (或者类似这种业务属性的表) 的分表方案, 至于订单表, 流水表等, 本文的方案可能不是很合适, 可以参考笔者另一篇文章《 分库分表技术演进 & 最佳实践 - 修订篇 》.
我们首先来看一下分表时主要需要做的事情:
选定分片键: 既然是用户表那分片键非用户 ID 莫属;
修改代码: 以 sharding-jdbc 这种 client 模式的中间件为例, 主要是引入依赖, 然后新增一些配置. 业务代码并不怎么需要改动.
存量数据迁移;
业务发展超过容量评估后需要开发和运维介入扩容;
做过分库分表的都知道, 第 3 步最麻烦, 而且非常不好验证迁前后数据一致性(目前业界主流的迁移方案是存量数据迁移 + 利用 binlog 进行增量数据同步, 待两边的数据持平后, 将业务代码中的开关切到分表模式).
第 4 步同样麻烦, 业务增长完全超过当初分表设计的容量评估是很常见的事情, 这也成为业务高速发展的一个隐患. 而且互联网类型的业务都希望能做到 7x24 小时不停服务, 这样就给扩容带来了更大的挑战. 笔者看过比较好的方案就是 58 沈剑 提出的成倍扩容方案. 如下图所示, 假设现在已经有 2 张表: tb_user_1,tb_user_2. 且有两个库是主备关系, 并且分表算法是 hash(user_id)%2:
扩容 - 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.... 将这些冗余数据删掉后, 库, 表, 数据示意图如下所示:
扩容 - 2
即使这样方案, 还是避免不了分表时的存量数据迁移, 以及分表后业务发展到一定时期后的繁琐扩容. 那么有没有一种很好的方案, 能够一劳永逸, 分表时不需要存量数据迁移, 用户量无论如何增长, 扩容时都不需要迁移存量数据, 只需要新增一个数据库示例, 修改一下配置即可. 软件开发行业, 一个方案能撑过 3~5 年就是一个很优秀的方案, 我们现在 YY 的是整个生命周期内都不用改动的完美的方案. 没错, 我们在寻找银弹 .
这个方案笔者在两个地方都接触到了:
某 V 厂面试时, 部门老大提出的方案;
和美团大牛普架讨论了解到的 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 中. 这样做的优点如下:
历史数据不用迁移;
扩容非常简单;
缺点如下:
读写热点集中, 所有写操作全部打在最新的表上.
有没有发现, 这个方案的优点就是我们需要的. BINGO, 要的就是这样的方案. 那么对应到用户表上来具体的分表方案非常类似: 按照 range 切分 . 需要说明的是, 这个方案的前提是用户 ID 一定要趋势 递增, 最好严格递增. 笔者给出 3 种用户 ID 递增 的方案 :
自增 ID
假设存量数据用户表的 id 最大值是 960W, 那么分表算法是这样的, 表序号只需要根据 user_id/10000000 就能得到:
用户 ID 在范围 [1, 10000000) 中分到 tb_user_0 中(需要将 tb_user 重命名为 tb_user_0);
用户 ID 在范围 [10000000, 20000000) 中分到 tb_user_1 中;
用户 ID 在范围 [20000000, 30000000) 中分到 tb_user_2 中;
用户 ID 在范围 [30000000, 40000000) 中分到 tb_user_3 中;
以此类推.
如果你的 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 和分表关系:
关系表
笔者推荐第一种方案, 即利用表自增 ID 生成用户 ID: 方案越简单, 可靠性越高 . 其他两种方案, 或者其他方案或多或少需要引入一些中间件或者介质, 从而增加方案的复杂度. 新方案效果图如下:
新方案效果图
回顾总结
我们回头看一下这种用户表方案, 满足了存量数据不需要做任何迁移(除非是存量数据远远超过单表承受能力). 而且, 无论用户规模增长到多大量级, 1 亿, 10 亿, 50 亿, 后面都不需要做数据迁移. 而且也不再需要开发和运维介入. 因为整个方案, 会自己往新表中插入数据. 我们唯一需要做的就是, 根据硬件性能, 约定一个库允许保存的用户表数量即可. 假如一个库保存 64 张表, 那么当扩容到第 65 张表时, 程序会自动往第二个库的第一张表中写入.
公众号二维码
↓↓↓↓
分库分表技术演进 & 最佳实践 - 修订篇
[阿飞的博客] 公众号二维码
↓↓↓↓
来源: http://www.tuicool.com/articles/jQnAveM