一、背景概述
在应用程序中,很多时候会用到全局唯一的id,比如用户ID,订单ID等等,因此在架构设计中,全局唯一的id生成服务作为公共基础服务是整个项目的基石之一。然而,设计一个满足不重复、高性能、高可用的分布式id服务并不容易:
- 不重复:需要保证在整个服务的生命周期内不会出现重复。
- 高性能:在满足自身业务需要的情况下,如果可以,尽量提供更高的性能,以利于业务的长期发展。
- 高可用:因为此服务是整个业务的基础服务,或者以模块的形式作为基础能力提供给其他服务,因此其高可用性直接影响到整个业务的高可用性,因此在必须保证服务的高可用。
二、设计方案
2.1 UUID 方案
UUID是通用唯一识别码(Universally Unique Identifier)的缩写,基于当前时间、计数器(counter)和硬件标识(通常为网卡的MAC地址)等数据计算生成的,因此可以做到全局唯一。Java本身提供UUID类,因此使用UUID可以满足不重复、高性能、高可用三方面的要求。
-
优点:
- 1)使用简单方便;
- 2)基于jdk的能力,且生成过程不存在阻塞,天然满足了高性能和高可用的要求;
-
缺点:
- 1)无序,如果业务中对id有顺序要求,如趋势递增,则无法满足;
- 2)id为数字和英文字母组合,对于只需要纯数字的场景,则无法满足;
- 3)长度为128bit,对长度有要求的场合也不适用;
因此,此方案非常适合对id无顺序、无长度和无纯数字要求的场景,比如在分布式应用场景中,为了排查问题方便,多个服务之间会使用一个唯一id串起来整个请求的链路,比如对于TCP长连接协议的服务端,需要一个连接id标识每个连接。
2.2 数据库自增
利用数据库的自增特性,以 MySQL 举例,利用给字段设置 auto_increment_increment 和auto_increment_offset 来保证ID自增,且全局唯一。
-
优点:
- 1)使用简单;
- 2)严格递增;
- 3)纯数字;
- 4)几乎任何业务都会使用数据库,因此等于未依赖第三方组件;
-
缺点:
- 1)依赖数据库,生成过程存在阻塞操作,且数据库本身性能不高,因此性能较差;
- 2)依赖数据库,因此高可用性需要数据库一起保证,需要做主从备份,异地容灾等;
因此,此方案仅适用对性能要求不高,且必须为纯数字递增的场景,如用户id。
2.3 类snowflake方案
snowflake是Twitter以划分命名空间来生成ID的一种算法,在实际应用中我们可以借鉴其算法模型生成满足自己要求的全局唯一id。
snowflake算法把时间戳,工作机器id,序列号组合在一起。总共64bit,设计如下:
image.png
- 63位:预留
- 62-22位:时间戳。使用了41bit。
- 21-12位:机器id。使用10位标识机器信息,如机房信息,节点id信息,进程号,端口号信息等。
- 11-0位:自增数。使用12bit作为自增数,范围为0~4098
snowflake 只是提供了一种算法思路,因此呢,我们可以根据业务的实际情况来调整各段所占的bit数,比如机器id不需要使用 10 位,可以预留更多的位数给自增数使用。因此应该灵活变通。
-
优点:
- 1)可以为纯数字,也可以使用16进制变为0~F组合;
- 2)由于生成过程不存在任何阻塞,因此性能极高;
- 3)不依赖第三方,与UUID一样,很好的满足了高可用性;
- 4)时间戳在高位,因此整体处于趋势递增的;
-
缺点:
- 1)需要业务自己开发。
- 2)由于机器时间可以回拨,因此可能存在重复的风险 (特别是某些国家夏令时和冬令时切换)
因此,此方案适用于在UUID不能满足的场景下,可以优先考虑的方案,如订单号,聊天消息id等等。
2.4 redis自增操作方案
利用 redis 的原子自增操作(incr命令)生成一个自增的序列,由于redis本身为单线程工作模式(6.0版本支持多线程,但是目前还属于beta版本,预计2020年5月才正式发布),因此可以天然的保证不会存在重复,而且redis本身的高性能,可以满足性能要求。
-
优点:
- 1)纯数字
- 2)使用简单
- 3)高性能
- 4)严格递增
-
缺点
- 1)依赖redis,因此必须通过redis的主从保证服务的高可用性。
- 2)因为redis是内存数据库,因此需要通过主从,持久化来保证服务宕机后,当前计数值不会丢失从而出现id重复。
- 3)redis是单线程,在与其他业务共用的情况下,会由于其他业务执行高时间复杂度命令导致线程阻塞,从而影响此服务性能。
- 4)即使使用持久化,当redis服务器的硬盘损坏,或者机房所在地出现自然灾害等,会导致数据丢失。
因此,此方案适用于生成QQ号这样纯数字,且递增,并对性能有较高要求的场景,但是依赖redis,需要考虑与其他业务共用redis存在的问题,比如持久化可能会影响redis性能。
2.5 类美团 Leaf方案
美团的 Leaf 方案使用本地缓存+MySQL实现高性能、高可用且全局唯一的纯数字id,在业务实现过程中可以借鉴其设计思路。
在第2种方案中,我们知道基于MySQL的方案最大问题是性能问题,因此美团Leaf进行了优化,应用服务到数据库取id的时候,每次不再是只取一个id,而是取一个范围的id,称之为步长,假如为1000,那么只有当这1000个值使用完后才会再去访问数据库,因此提升了性能。
数据库设计一张id_table的表,如下:
[图片上传失败...(image-6dafe0-1673276567734)]
- biz_tag:业务标识
- max_id:当前最大的id值
- step:步长(每次应用程序取值范围,根据业务实际情况而定)
- desc:描述信息
- create_time:创建时间
- update_time:更新时间
应用程序通过以下两条sql语句完成取值:
Begin
UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx
SELECT max_id, step FROM table WHERE biz_tag=xxx
Commit
由于应用程序是拿到一个步长范围内的id值,因此在应用程序中还需要进行id的分配,因此,应用程序需要通过锁/CAS的方式保证id分配时不因为并发而出现重复。所有对开发人员有较强的要求。为了避免每个开发人员/模块重复造轮子,因此可以将此服务独立出来。架构设计演变为如下:
image.png
Leaf服务可以通过集群提升服务的性能和增加可用性。
此外美团还提供了双buffer思想,进一步提升服务性能,即在Leaf服务内,当第一个步长值快要使用完毕之前,异步去数据库取下一个步长的值,这样不会存在当Leaf的id值分配完了,再去数据库取值出现性能问题。
目前,美团的Leaf已经开源,在我的业务使用时,此方案并没有开源,因此我个人参照此思路实现了我们业务的Leaf服务,主要需要考虑的是并发问题导致的id分配重复,另外双buffer思想实现起来非常繁琐,如果对性能要求不是非常极致,可以不考虑,而是通过3节点的Leaf服务即可满足。
- 优点:
1)纯数字
2)趋势递增
3)只依赖数据库,因为几乎所有服务都需要数据库,相当于未增加第三方依赖。
4)满足高性能和高可用的要求。
-
缺点:
- 1)应用服务需要自己实现id的分配,保证不会因为并发等问题出现重复。
- 2)因为应用程序每次取的是一个范围值到应用程序中,当服务发布时,未使用完的数据会丢失。所以,不是严格连续的。
因此,适用的场景是对性能要求很高,且为趋势递增的纯数字场景,而且不能因为任何原因而丢失数据导致ID重复的场景,如用户的id号。
三、总结
以上提供了5种最常见和实用的唯一id生成方案,可以根据自己的业务实际情况进行选择,总体原则是选实现最简单的,只要能满足业务需要即可,越复杂的方案越容易出错。
网友评论