系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,比如常见的基于数据库自增主键生成的id,随机生成的uuid,亦或者redis自增的计数器等都属于常见的解决方案;本文我们将会重点看一下业界内大名鼎鼎的雪花算法,是如何实现分布式id的
I. 雪花算法
1. 全局唯一id
雪花算法主要是为了解决全局唯一id,那么什么是全局唯一id呢?它应该满足什么属性呢
基本属性:
- 全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求;
- 趋势递增:在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能;
- 单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求;
- 信息安全:如果 ID 是连续的,恶意用户的爬取工作就非常容易做了,直接按照顺序下载指定 URL 即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则。
2. 雪花算法
雪花算法可以说是业界内生成全局唯一id的经典算法,其基本原理也比较简单

Snowflake 以 64 bit 来存储组成 ID 的4 个部分:
- 最高位占1 bit,值固定为 0,以保证生成的 ID 为正数;
- 中位占 41 bit,值为毫秒级时间戳;
- 中下位占 10 bit,值为机器标识id,值的上限为 1024;
- 末位占 12 bit,值为当前毫秒内生成的不同的自增序列,值的上限为 4096;
从上面的结构设计来看,雪花算法的实现可以说比较清晰了,我们重点看一下它的缺陷
- 时钟回拨问题:由于id的高位依赖于系统的时间戳,因此当服务器时间错乱或者出现时钟回拨,可能导致数据重复
- 集群规模1024台机器,每1ms单机4096个id最大限制
3. 实现与使用
目前雪花算法的实现方式较多,通常也不需要我们进行额外开发,如直接Hutool的Snowflake
看下它的核心实现
1 | public class Snowflake implements Serializable { |
关键实现在 nextId() 方法内,做了两个保护性兼容
- 记录上次生成id的时间戳,若当前时间戳小于上次产生的时间戳,则表示出现了时钟回拨,超过一定间隔,则直接抛异常

- 当前时间戳生成的id数量超过了4096最大值限制,则等待下一秒

接下来看一下实际的使用
1 | private static final Date EPOC = new Date(2023, 1, 1); |
输出如下:
1 | 1717380884565065728 |
- 生成的id:19位
- 单调递增,同一毫秒内,序号+1
4. 自定义雪花算法实现
在某些时候我们对雪花算法的实现有一些特殊的定制化场景,比如希望生成的id能一些更具有标识性,如以商城领域的订单数据模型为例
- 第一位:标记订单类型, 1: 普通订单 2: 换货订单 3: 退货订单 4: 退款订单
- 第二三位:标记订单所属年份,如 22xxx,表示22年的订单;23xxx,则表示23年的订单
再比如对订单的长度希望做一些限制,19位太多了,我希望16、7位的长度
再比如我希望调整workerId 与 datacenter之间的分配比例
基于以上等等原因,当我们面对需要修改雪花算法逻辑时,再知晓算法原理的基础上,完全可以自己手撸
1 | 4j |
注意上面的实现,相比较于前面HuTool的实现,有几个变更
- 时间戳从毫秒改为秒
- 生成id前五位:年 + 天
- workcenterId : dataCenterId = 3 : 7
- 当时钟回拨时,等待时间追上,而不是直接抛异常
- 自增序列的起始值,0/1互切
接下来再看下实际的使用输出
1 | public static void main(String[] args) throws InterruptedException { |
输出如下
1 | 23299055409901569 |
5. 小结
雪花算法本身的实现并不复杂,但是它的设计理念非常有意思;业界内也有不少基于雪花算法的变种实现,主要是为了解决时钟不一致及时钟回拨问题,如百度UIDGenerator,美团的Leaf-Snowflake方案
雪花算法其实是依赖于时间的一致性的,如果时间回拨,就可能有问题,其次机器数与自增序列虽然官方推荐是10位与12位,但正如没有万能的解决方案,只有最合适的解决方案,我们完全可以根据自己的实际诉求,对64个字节,进行灵活的分配
再实际使用雪花算法时,有几个注意事项
- 雪花算法生成的id,通常是长整形,对于前端使用时,对于超过16位的数字,会出现精度问题,需要转换成String的方式传递,否则就会出现各种预料之外的事情发生
- workId如何获取?
- 如:借助第三方服务(db/redis/zk),统一为每个实例分配唯一的workId
- 如:同一个局域网内的所有应用,借助ip的最后一段来定位
II. 不能错过的源码和相关知识点
0. 项目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源码:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/600-snowflake-id
1. 微信公众号: 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰Blog个人博客 https://blog.hhui.top
- 一灰灰Blog-Spring专题博客 http://spring.hhui.top
