分布式下如何解决多个数据中重复的主键ID
分布式下带来的数据库ID问题
在分布式下每个服务可能有至少大于 1 的台的部署,这些服务如果都拥有独立的数据库,或者N个微服务拥有N-1个数据库(有两个微服务使用了同一个数据库)。大多数公司都是使用 MySql 数据库, 在分库分表后,由于数据库 ID 的唯一性且自增,会发生 ID 冲突的情况。因此需要某个服务在多台部署后每台的 ID 与其他服务中的 ID 不重复,即使用全局 ID。
解决方案
UUID
在插入新的行时不使用主键自增策略,而手动指定ID,在插入前生成一个UUID添加到SQL语句中,使用UUID作为主键,但此方法有几个缺点。
- 因为其长度原因占用多余的空间
- 难以进行排序
- 无规律可循,某些需要根据ID计算的逻辑难以完成
数据库自增
第一种方案仍然还是基于数据库的自增ID,需要单独使用一个数据库实例,在这个实例中新建一个单独的表:
CREATE DATABASE `SQLID`;
CREATE TABLE SEQUENCE_ID (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(10) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM;
可以使用下面的语句生成并获取到一个自增ID
begin;
replace into SEQUENCE_ID (stub) VALUES ('anyword');
select last_insert_id();
commit;
stub字段只是为了方便的去插入数据,只有能插入数据才能产生自增id。而对于插入我们用的是replace
,replace
会先看是否存在stub指定值一样的数据,如果存在则先delete
再insert
,如果不存在则直接insert
。
这种生成分布式ID的机制,需要一个单独的 Mysq l实例,虽然可行,但是基于性能与可靠性来考虑的话都不够,业务系统每次需要一个ID时,都需要请求数据库获取,性能低,并且如果此数据库实例下线了,那么将影响所有的业务系统。
号段模式
我们可以使用号段的方式来获取自增ID,号段可以理解成批量获取,比如某个服务从数据库获取ID时,批量获取多个ID并缓存在本地的话,那样将大大提供业务应用获取ID的效率。
比如服务A每次从数据库获取ID时,就获取一个号段,比如 [1,1000],这个范围表示了1000个ID,业务应用在请求服务AID时,服务A只需要在本地从1开始自增并返回即可,而不需要每次都请求数据库,一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库重新获取下一号段。
所以,我们需要对数据库表进行改动,如下:
CREATE TABLE id_generator (
id int(10) NOT NULL,
current_max_id bigint(20) NOT NULL COMMENT '当前最大id',
increment_step int(10) NOT NULL COMMENT '号段的长度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这个数据库表用来记录自增步长以及当前自增ID的最大值(也就是当前已经被申请的号段的最后一个值)
这种方案不再强依赖数据库,就算数据库不可用,那么服务A也能继续支撑一段时间。但是如果服务A重启,会丢失一段ID,导致ID空洞。
为了提高服务A的高可用,需要做一个集群,业务在请求服务A集群获取ID时,会随机的选择某一个服务A节点进行获取,对每一个服务A节点来说,数据库连接的是同一个数据库,那么可能会产生多个服务A节点同时请求数据库获取号段,那么这个时候需要利用乐观锁来进行控制,比如在数据库表中增加一个version字段,在获取号段时使用如下SQL:
update id_generator set current_max_id=#{newMaxId}, version=version+1 where version = #{version}
因为newMaxId
是服务A中根据oldMaxId
+步长算出来的,只要上面的update
更新成功了就表示号段获取成功了。
为了提供数据库层的高可用,需要对数据库使用多主模式进行部署,对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长,比如如果现在是两台Mysql,那么
mysql1将生成号段(1,1001],自增的时候序列为1,3,4,5,7…
mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10…
Redis
使用 Redis 的原子性机制,使用 incr
和 increby
自增原子命令,在插入之前获取一个ID,使用此ID作为数据库中的唯一ID
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回
(integer) 2
127.0.0.1:6379> incr seq_id // 增加1,并返回
(integer) 3
但却有如下缺点:
- 增加了程序的复杂度,和硬件资源
- 如果 Rdeis 宕机,则服务不可用,
- 可能达到Rdeis的性能瓶颈,则需要部署多台,使用步长方式
- 持久化问题,若使用RDB持久化策略则可能在最后一次持久化之前发生宕机则恢复后可能发生
ID重复问题,若使用AOF持久化策略则在恢复Redis时需要较长时间。
雪花算法-snowflake
算法产生的是一个long型 64 比特位的值。
- 第一个bit位是标识部分,在java中由于long的最高位是符号位,正数是0,负数是1,一般生成的ID为正数,所以固定为0。
- 时间戳部分占
41bit
,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年 - 工作机器ID占
10bit
,这里比较灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点 - 序列号部分占
12bit
,支持同一毫秒内同一个节点可以生成4096个ID
我们可以对其进行封装实现,作为工具类使用,也有现成的开源框架。
例如 美团 Leaf 和 百度 uid-generator 请点击连接查看。
[参考]大型互联网公司分布式ID方案总结