06. 如何实现分布式ID生成器
分布式ID生成器的内容跟订单号生成内容完全一致,仅提供设计方案内容。
更详细的分析思路可以查看 如何实现分布式ID生成器
设计方案
接下来会阐述一些常见的方案,再谈论一下这种解决方案的优缺点。
方案一:数据库自增ID
所谓数据库自增,意思是在数据库中给某个列设置为自增列,并且给该列设置一个初始值,代码层面无需任何特殊处理,以 Mysql 的用户表 ID 列为例,可以通过如下方式在创建表的时候生产。
CREATE TABLE `tb_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
优点:
- 实现简单
- 保证唯一性
缺点:
- 数据库存在性能瓶颈,特别是高并发情况下
- 分布式数据库,在多个实例情况下难以保证全局唯一、
- 安全性低,自增ID容易暴露信息
- 无业务含义
适用场景:
如果是单体服务下,并且是初始业务情况下可以使用,但需要对数据库进行分库分表会出现重复ID。不建议直接使用这种方式,上限较低。
方案二:UUID
UUID 是Universally Unique Indentifier
的缩写,翻译为通用唯一识别码,顾名思义 UUID 是一个用于记录唯一标识一条的数据,其按照开放软件基金会(OSF)指定的标准进行计算,用到了以太网卡地址(MAC)、纳秒级时间、芯片 ID 码和许多可能的数字。
总的来说,UUID 码由以下三部分组成:
- 当前日期和时间
- 时钟序列
- 全局唯一的 IEEE 机器识别码(如果有网卡从网卡获得,没有网卡则通过其他方式获得)
UUID 的标准形式包含 32 个 16 进制数字,以连字号分为五段,示例:00000191-adc6-4314-8799-5c3d737aa7de
。
以java
为例,通过以下方式即可生成:
String uuid = UUID.randomUUID().toString();
优点:
- 全局唯一性
- 简单易用
- 安全性: UUID的随机性使得它不容易被猜测或预测,增加了数据的安全性
- 无需集中管理:由于UUID是本地生成的,不需要依赖于中心化的ID生成服务,减少了单点故障的风险。
- 可扩展性:UUID的生成过程是分布式的,可以在多个节点上并行生成,适合大规模分布式系统。
缺点:
- 长度较长
- 可读性差: 无业务含义,难以理解
- 索引效率低: UUID的随机性会导致插入时的索引分裂和碎片化,从而降低写入性能,查询效率也低
- **排序问题:**UUID不具备自然的时间顺序,因此不适合用于需要按时间顺序进行排序的场景
适用场景:
如果是在分布式系统中,对订单号有着特别高的要求,并且不需要长期持久化存储以及不需要频繁查询那么就可以推荐使用。
分布式文件存储系统,该系统需要处理大量的文件上传、下载和管理操作。这些文件可能来自不同的用户和设备,并且需要在多个服务器之间进行分布存储和访问。为了确保每个文件都有一个唯一的标识符,并且能够在全球范围内保持唯一性,UUID是一个非常适合的选择。
方案三: 雪花算法
Snowflake(中文简称:雪花算法) 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的 ID 号码。Snowflake把 64-bit分别划分成多段,分开来标识机器ID、时间等。其核心思想是:使用 41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。SnowFlake 的结构图如下所示:

可以很清晰的看出,Snowflake 由 4个部分组成:
- 第一部分:bit 值,为未使用的符号位
- 第二部分:由 41 位的时间戳(毫秒)构成,它的取值是当前时间相对于某一时间的偏移
- 第三部分:表示工作机器 id,由服务节点 id 和数据中心 id 组合而成
- 第四部分:表示每个工作机器每毫秒生成的序列号 ID,同一毫秒内最多可生成生产 4095 个 ID。
由于在 Java 中 64bit 的整数是 long 类型,因此在 Java 中 SnowFlake 算法生成的 id 就是 long 来存储的。
SnowFlake 算法可以保证:
- 1.所有生成的 id 按时间趋势递增
- 2.整个分布式系统内不会产生重复id(因为有服务节点 id 和数据中心 id 来做区分)
需要注意的是:
- 在分布式环境中,5 个 bit 位的 datacenter 和 worker 表示最多能部署 31 个数据中心,每个数据中心最多可部署 31 台节点。
- 41 位的二进制长度最多能表示
2^41 -1
毫秒即 69 年,所以雪花算法最多能正常使用 69 年,为了能最大限度的使用该算法,在使用的时候,应该为其指定一个开始时间,不然会发生重复!
优点
- 高并发下的唯一性:由于算法设计考虑到了时间戳、机器标识等因素,因此即使是在大规模分布式系统中也能保证生成的ID是唯一的。
- 趋势递增:生成的ID通常是按照时间顺序递增的,这有利于数据库索引性能优化。
- 信息量丰富:ID中包含了时间戳、工作节点ID等信息,使得每个ID都携带了额外的信息价值。
- 不依赖数据库:相比于传统基于数据库自增ID的方式,雪花算法不需要访问数据库即可生成ID,减少了数据库的压力。
- 高效:计算效率高,适合快速生成大量ID的需求。
缺点
- 时钟回拨问题:如果服务器时钟出现回拨,则可能产生重复ID的问题。不过,可以通过等待时钟追赶上来或者拒绝服务来解决这个问题。
- 有限的空间:虽然64位足够大,但理论上还是存在上限,对于某些极端情况可能不够用。
- 可读性差:生成的ID对人类来说不易于阅读和理解。
- 安全性较低:因为包含时间戳,所以可能泄露一些关于数据创建时间的信息,这在某些安全敏感的应用场景下可能是不利的。
适用场景
- 分布式系统:特别适用于需要跨多个服务器或数据中心工作的应用程序,如电商网站、社交平台等。
- 高并发应用:当系统需要处理大量的并发请求并要求快速响应时,比如在线游戏、实时消息传递服务等。
- 需要唯一标识符的服务:任何需要生成全局唯一标识符的服务都可以采用此方法,例如订单管理系统、用户账号注册等。
- 大数据分析:由于ID带有时间信息,有助于进行时间序列数据分析。
方案四:借助Redis,分布式组件
要想在分布式环境下生成一个唯一的订单编号,我们可以通过分布式组件的方式,来帮忙我们生成全局唯一的订单号,例如我们可以采用 redis 分布式缓存组件中的incr
命令,来帮我们生成一个全局自增长的序列号。
实现某个Key实现自增的代码如下:
// 基于某个key实现自增长
String res = jedis.get(key);
if (StringUtils.isBlank(res)) {
// 设置初始值,INIT_ID 是初始值
jedisClient.set(key, INIT_ID);
// 设置过期时间,seconds 是多少秒过期
jedisClient.expire(key, seconds);
}
//存在就生成+1的订单号
long orderId = jedis.incr(key);
这种方式生成的自增长序列号,非常的快,可以很好的满足大流量环境下的编号要求唯一的特性!
案例分析:
在互联网几个大厂的订单号分析一下:
京东商城订单号格式:157444499
苏宁易购订单号格式:2000839647
凡客诚品订单号格式:213052230059
小米订单号格式:1111218032345170
凡客诚品和银泰网订单号都含有 0522,这是因为这 2 张订单都是2013年5月22号下的订单。
基本猜测一下,凡客的订单规则是:业务编码+年的后2位+月+日+订单数;泰网的订单号规则:年的第三位数+业务编码+年的后1位+月+日+订单数;而京东商城和苏宁易购的订单号看不出规则。
再来分析一下小米订单编号:
1211218032345170(16位)
//拆解成为四个部分
1——211218—03234—5170
- 第一部分,1 表示购买,2 表示退货。
- 第二部分,表示 2021 年 12 月 18 日下的单,前面两位省掉了。
- 第三部分,时间戳对应
00:53:54
,换算成秒是03234
秒。 - 最后一部分,表示在同一秒内下的第 5170 单,也就是说,小米认为,在一秒内不会超过一万个订单。
总结
通过上面的示例演示,下面针对这几种情况做一个分析与总结。尽可能的选择一种合理的方式。
实现方案 | 优势 | 劣势 |
---|---|---|
数据库自增 | 代码层面无需任何特殊处理;利用MySQL特点实现数据递增 | 并发性能差;MySQL负担重 |
UUID | 实现简单、方便;重复性低 | 可读性低;过于冗长;数据库查询效率低 |
雪花算法 | 基于内存、速度快;性能高;不会产生额外的网络开销;数据依次成递增 | 依赖于服务器时间,如变动服务器时间则存在重复的情况 |
Redis | 基于内存、速度库;使用简单;可分布数据、扩展性强 | 需要独立搭建一套服务、增加了维护成本;跨应用调用、存在网络开销 |
总体上来说,优先选择使用Redis进行分布式的处理方式,如果在没有Redis的情况下,那就优先选用雪花算法。