在开发项目时,我们需要给每一个”角色“都打上唯一的标签,比如用户、订单、消息等都需要唯一的标识,以此来区分不同的用户、订单及消息。单体项目中使用数据库的自增主键作为唯一标识可能就绰绰有余了,但分布式系统下可以使用这种方案吗?显然不行,此时就需要用到分布式id了。
(1)全局唯一:最基本的要求,必须保证全局不会出现重复ID。
(2)有序:id作为唯一标识肯定是字段之一,要随业务数据写入数据库的;如果id乱序,将不利于数据库的写入和排序操作。
(3)高可用:分布式ID生成系统需要具有较高的可用性,因为业务数据的唯一标识都由该系统分配,若系统不可用,将直接影响用户、订单这些强依赖于ID生成系统的模块。
(4)安全:不会暴露业务信息。
1、UUID:由32个十六进制数组成的字符串,并用-分割成了五个部分,如:b86d54e5-0452-4e0b-b15a-94af36d68022这种形式。
常用的UUID版本:
1.1、基于随机数的UUID:基于随机数或伪随机数,实现较为简单,但存在id重复的可能性(java中的randomUUID就是该版本)。
1.2、基于名字空间的UUID:基于指定的命名空间生成SHA1散列,命名空间下唯一,但计算耗时。
UUID方案优点:在本地生成,没有网络开销,性能好。
UUID方案缺点:总共36个字符,长度较长;没有递增趋势,作为主键插入数据时可能会造成大量的页分裂现象产生。
2、数据库自增ID:数据库插入数据时,若创建表时主键设置了auto_increment,那么主键会自动递增,我们可以用递增的主键作为分布式id。
具体实现:
2.1、创建id生成表:
DROP TABLE IF EXISTS `id_generator`;
CREATE TABLE `id_generator` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`mark` char(1) not null,
PRIMARY KEY (`id`),
UNIQUE KEY `mark` (`mark`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2、开启事务,获取唯一id:
begin;
replace into id_generator (mark) VALUES ('x');
select last_insert_id();
commit;
数据库方案优点:实现简单,id递增且唯一性由数据库保证。
数据库方案缺点:多个线程并发获取id时,性能不好;数据库压力较大。
2.3、高并发场景的解决方案:
将数据库进行水平拆分,每个数据库设置不同的初始值、相同的步长,如下图所示。
3、Redis:基于redis单线程执行命令的特性,使用原子命令INCR或INCRBY命令来获取全局唯一并且递增的序列号作为分布式id。
redis方案优点:基于内存、不依赖于数据库,性能较好;能保证id唯一且递增。
redis方案缺点:id生成强依赖于redis,redis挂了会导致服务不可用;redis在宕机时,如果没有将id持久化到磁盘,重启后可能会生成重复id。
4、Snowflake算法:该算法生成的是一个64位的long型id。其核心思想是使用 41bit 作为时间戳(当前时间戳-起始时间戳),10bit 作为机器ID,12bit 作为自增序列号(意味着每个节点在每毫秒可以产生 2^12 = 4096 个 ID),最高位是符号位0(id不可能为负数)。
雪花算法方案优点:id是趋势递增的;不依赖于数据库、redis等第三方服务,本地生成,性能较好;可根据业务调整bit,灵活性好。
雪花算法方案缺点:在分布式环境下由于每个机器的时钟无法做到完全一致,id不一定是全局递增的;依赖于机器时钟,如果时钟回拨,会生成重复id或非递增id。
4.1、时钟回拨问题的解决方案:
a、只让少量服务器生成id,并关闭这些服务器的时钟回拨。
b、当发生时钟回拨时直接报错,交给上层业务处理。
c、如果时钟回拨时间较短,在容忍范围内,可以等待回拨时间后再生成id。
5、号段Segment:从数据库批量获取id,将 id 缓存在本地,以此来提高业务获取 id 的效率。例如,每次从数据库获取 id 时,获取一个号段,如[1,2999],这个范围表示3000 个 ID,业务应用在请求获取 id 时,只需要在本地从 1 开始自增并返回(注意线程安全问题,使用原子类来做自增操作),而不用每次去请求数据库,一直到本地自增到 2999 时,再去数据库重新获取新的号段,后续流程循环往复。
号段方案优点:即使数据库不可用了,在本地号段用尽之前,仍能够正常获取id;id是递增;只需操作一次数据库就可以获取多个id,同时业务可以在本地获取id,性能好。
号段方案缺点:还是依赖于数据库。