用单库自增键来生成id了,后期怎么分库?哎,这个坑大!

星球水友 “写代码的” 提问:

沈老师,我们现在用户中心是单库单表,uid 使用数据库自增主键,uid 被很多业务关联,不能变化。

现在用户中心数据量逐步变大,有分库需求了,如何由单库升级为多库,保持历史 uid 不变,并且新生成的数据不冲突,有什么好办法么?

== 问题描述完 ==

应该有不少公司都会利用数据库 “插入数据自动自增 id” 来作为业务 id,这种方法会使得业务与 id 生成强耦合,导致 id 生成算法难以升级。

今天和大家一起简单探讨下,id 生成要考虑哪些要素。

画外音:别误会,不是说 “自增 id” 不好,是说它与业务耦合了,难以升级。

一、id 生成要考虑的技术点

几乎所有业务,都会有一个业务唯一标识:

  • 用户标识:uid(user-id)

  • 消息标识:mid(msg-id)

  • 订单标识:oid(order-id)

这个标识,在存储系统里通常是主键,主键使用聚集索引 (clustered-index),即在物理存储上以这个 id 排序。于是,对这个 id 有:唯一性趋势递增性的要求。

_画外音:_索引《1 分钟了解不同索引的差异》。

这个标识,也经常被用来做流量负载均衡,数据负载均衡的依据,即这个 id 必须在统计上必须是完全随机的。于是,对这个 id 有:随机性的要求。

同时,id 生成算法升级,理论上对业务系统是透明的。于是,对这个 id 的生成有:独立性需求。

为了保证 id 生成的上述特性,要有一个:

uint64_t GenID()

的独立方法(或者独立接口)来生成 id,生成 id 具体做什么用,该方法不关心,可以是用来做 uid,也可以是用来做 oid,甚至 log-id。

当然,id 生成的具体细节,业务也不用关心。即,GenID() 的内部实现,可以是利用数据库的自增 id,也可以使用时间递增,目前行业内最流行的,是仿照 snowflake 生成分布式 id。

这个封装,屏蔽了 id 生成的细节,保留方案升级的可能性,是系统设计中,解耦的体现。

如果使用了此类方法生成业务 id,数据库由单库扩展多库就很容易了:

(1)确定一个路由算法,例如 hash 取模;

(2)将单库中的数据,通过这个路由算法迁移到多库中去,以实现单库数据量的减少;

(3)通过这个路由算法寻找数据(读);

(4)通过这个路由算法插入数据(写);

假如架构设计前期没有提前考虑独立的 id 生成,后期又要实施单库拆多库,该怎么办呢?

二、针对星球水友提到的例子

历史的坑已经铸成,没有解耦 id 生成方法,而且也没法批量修改 id,该怎么办呢?

假设由单库拆分为 3 库,可以这么玩:

(1)做一个 1 主 2 从数据库集群,相当于每条数据复制成了 3 份;

(2)将路由算法,设为取模 hash 算法,%3;

(3)第一个库,%3=0,把余 1 和余 2 的 uid 删掉;

(4)第二个库,%3=1,把余 0 和余 2 的 uid 删掉;

(5)第三个库,%3=2,把余 0 和余 1 的 uid 删掉;

(6)将每个库的自增步长设置为 3,这样每个库的 id 生成就不会重复了;

(7)升级用户中心,按照路由算法查询 uid 数据;

搞定,拆库扩容达成:

(1)单库数据量下降为了原来的 1/3;

(2)读写实例个数扩充为了原来的 3 倍;

(3)并且 id 生成与查询都不会冲突;

希望这个取巧的方法对你有帮助。

但更希望,大伙提前考虑 id 生成的唯一性、随机性、趋势递增性、独立性。

系统性考虑问题,知其然,知其所以然

课后习题

单库拆多库,平滑扩容,不停止服务,怎么弄?