十、分布式数据库之自增主键
1、自增主键的理想特征
- 首先是唯一性,这是必须保证的,否则还能叫主键吗?
- 其次是单调递增,也就是后插入记录的自增主键值一定比先插入记录要大。
- 最后就是连续递增,自增主键每次加 1。有些应用系统甚至会基于自增主键的“连续递增”特性来设计业务逻辑。
但很多情况下,这些特性无法同时满足。
2、单体数据库的自增主键
只能真正的满足唯一性的约束,单调和连续性无法得到满足。
2.1、无法连续递增
在多数情况下,自增主键确实表现为连续递增。但是当事务发生冲突时,主键就会跳跃,留下空洞。
如下图,ID表示主键,两个事物T1和T2,T1回滚后,其对应的ID就被跳过了
为什么不支持连续递增呢?这是因为自增字段所依赖的计数器并不是和事务绑定的。如果要做到连续递增,就要保证计数器提供的每个主键都被使用。
怎么确保每个主键都被使用呢?那就要等待使用主键的事务都提交成功。这意味着,必须前一个事务提交后,计数器才能为后一个事务提供新的主键,这个计数器就变成了一个表级锁。
2.2、无法单调递增
对于单体数据库自身来说,自增主键确实是单调递增的。但使用自增主键也是有前提的,那就是主键生成的速度要能够满足应用系统的并发需求。而在高并发量场景下,每个事务都要去申请主键,数据库如果无法及时处理,自增主键就会成为瓶颈。那么,这时只用自增主键已经不能解决问题了,往往还要在应用系统上做些优化。
比如,对于 Oracle 数据库,常见的优化方式就是由 Sequence 负责生成主键的高位,由应用服务器负责生成低位数字,拼接起来形成完整的主键。
这个方案虽然使用了 Sequence,但也只能保证全局唯一,数据表中最终保存的主键不再是单调递增的了。
3、分布式数据库下自增主键的问题
具体来说是两个问题,一是在自增主键的产生环节,二是在自增主键的使用环节。
首先,产生自增主键难点就在单调递增。单调递增这个要求和全局时钟中的 TSO 是很相似的。你现在已经知道,TSO 实现起来比较复杂,也容易成为系统的瓶颈,如果再用作主键的发生器,显然不大合适。
其次,使用单调递增的主键,也会给分布式数据库的写入带来问题。这个问题是在 Range 分片下发生的,我们通常将这个问题称为 “尾部热点”。
3.1、尾部热点
在使用Range 分片的情况下,测试程序的生成主键是单调递增的,所以新写入的数据往往集中在一个 Range 范围内,而 Range 又是数据调度的最小单位,只能存在于单节点,那么这时集群就退化成单机的写入性能,不能充分利用分布式读写的扩展优势了。当所有写操作都集中在集群的一个节点时,就出现了我们常说的数据访问热点(Hotspot)。
性能问题的根因已经找到了,就是同时使用自增主键和 Range 分片。主流产品的默认方案是保持 Range 分片,放弃自增主键,转而用随机主键来代替。