1.背景
在复杂分布式系统中,往往需要对大量的数据进行唯一标识。一般情况下,我们用的都是数据库的自增主键id,但是当数据量大了之后,需要进行分库分表,每个表维护自己的自增id,无法做到唯一。这时候就需要一个全局的id发号器,生成一个唯一标识。看看我们对id生成一般有以下三个期望:
全局唯一:最基本的要求,标识唯一的记录
递增
-
单调递增:有些场景,需要根据生成的id进行排序,需要严格递增,比如IM消息,不能乱序,靠id递增。微信为此做了个单调递增的发号服务,阅读算法篇,容灾篇。
-
趋势递增:在分布式场景下,保证本地单调递增,全局趋势递增。是考虑实现复杂度和高性能的一个取舍后的结果。为什么需要趋势递增呢?因为我们数据库的索引比如Innodb是顺序的,如果新增的数据不递增,不是在末尾加入,而是插入索引的中间。会造成页分裂问题。
信息安全:如果我们的分布式id有一定的新增规律,比如连续增加,就能被竞争对手猜出,业务的增长情况。比如每天产生了多少订单,在一些特殊的场景下,id的生成需要做到无规律。
页分裂问题可能会导致数据存储结构的调整和重新平衡,以及对索引的维护和更新。这种操作可能会影响数据库的性能,特别是在频繁进行插入操作的情况下。因此,在数据库设计和性能优化中,需要考虑如何有效地管理页分裂问题,以减少对数据库性能的影响。
趋势递增和单调递增都可以有助于减少页分裂问题。当索引键按照递增顺序排列时,无论是趋势递增还是单调递增,新数据通常会被追加到已有数据的末尾,而不会插入到已有数据中间。这种方式可以避免数据页频繁地进行分裂操作,因为新数据的插入不会导致现有数据页的重组或重新平衡。
2.递增类型
2.1 全局递增
消息在整个IM系统都是唯一且递增的。一般对于单表来说主键就自然保证了递增。但是如果消息量大了,省不了分库分表,分库分表后的消息递增,通常采用分布式id。但是分布式id通常保证的是趋势递增,而不是单调递增。
比如说,雪花id就不适用于IM这种严格时序性的系统。事实上,严格的单调递增,意味着严重的单点竞争问题。对于一个都需要分库分表的系统,是很难实现这样的方案的。
2.2 会话级别递增
上面也说到,全局的单调递增,意味着严重的单点竞争。话说回来,我们为啥需要递增呢?不就是为了消息的顺序性展示吗?只需要保证单个群组内的消息id是有序的且唯一的,就足够了。QQ就是这样的架构。
那么如何保证会话级别的递增呢?一个简单的做法分库分表以会话id分表。这样相同的会话必定在同一张表,又重新用回了主键自增。但是,这种方案很难支持之后的扩容,比如4扩8。所以一般分表就不用主键自增了,都是用分布式id。
用分布式id保证会话级别的单调递增:单调递增,同时意味着单点问题。两者是不可两全的。分布式id之所以没有单点问题,所以大多都是趋势递增。这里面涉及的可用性,以及单调递增这种一致性的取舍。非常有意思。最终实现的效果,类似mysql的主从,单点的mysql用于自增id,以及主从保证高可用的切换。实现参考:微信序列号生成器架构设计。
2.3 收信箱递增
会话级别的递增,更多的适用于读扩散的场景。所有人拉取消息列表的时候,都去会话的消息表拉取。
收信箱的递增,适用于写扩散的场景。所有人都有自己的一个收信箱,维护自己的时间线即可。
收信箱的时间线的单调递增和uid相关。实现的方式和上面都一样,一个是以会话为key,一个是以uid为key。实现参考:微信序列号生成器架构设计
微信就是典型的写扩散场景,所以他的群聊最多只能500人。