分布式架构下,唯一序列号生成是我们在设计一个系统,尤其是数据库使用分库分表的时候常常会遇见的问题。当数据分成若干个sharding表后,如何能够快速拿到一个唯一序列号,是我们需要解决的问题。
一、唯一ID特性需求
-
全局唯一;
-
支持高并发;
-
高可靠,容错单点故障;
-
高性能;
二、业内方案
生成ID的方法有很多,来适应不同的场景、需求以及性能要求。
业内常见解决方案有:
1、利用数据库id递增
优点:简单易用,实现过程简单。
缺点:单库单表,数据库压力大。
2、UUID唯一识别码(Universally Unique Identifier)
生成的是length=32的16进制格式的字符串。标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以连字号分为五段形式的36个字符。
优点:不依赖于数据库,实现简单。Java标准类库中已经提供了UUID的API。
缺点:UUID的缺陷在于生成的结果串会比较长,占用过多DB存储空间,不方便业务使用,排序也比较费时。
3、twitter开源的全局唯一ID生成算法Snowflake
(1) 41位的时间序列(精确到毫秒,41位的长度可以使用69年)。
(2)10位的机器标识(10位的长度最多支持部署1024个节点)。
(3)12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
(4)最高位是符号位,始终为0(正数)。
优点:高性能,低延迟;独立的应用;按时间有序。
缺点:需要独立的开发和部署。
4、利用Redis生成ID
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作INCR和INCRBY来实现。
可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。
优点:
不依赖于数据库,灵活方便,且性能优于数据库。
数字ID天然排序,对分页或者需要排序的结果很有帮助。
使用Redis集群也可以防止单点故障的问题。
缺点:
如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
需要编码和配置的工作量比较大,多环境运维很麻烦。
5、百度开源的UidGenerator
UidGenerator是Java实现的,基于Snowflake算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于docker等虚拟化环境下实例自动重启、漂移等场景。
在实现上,UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题。最终单机QPS可达600万。
依赖版本:Java8及以上版本, MySQL(内置WorkerID分配器, 启动阶段通过DB进行分配; 如自定义实现, 则DB非必选依赖)
伪共享False Sharing可以理解为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
关于作者
王硕,十年软件开发经验,业余产品经理,精通Java/Python/Go等,喜欢研究技术,著有《PyQt 5 快速开发与实战》《Python 3.* 全栈开发》,多个业余开源项目托管在GitHub上,欢迎微博交流: