【◐系统架构】全局唯一ID生成方案
日常开发中,我们需要对系统中的各种数据使用 ID 唯一表示,比如用户 ID 对应且仅对应一个人,商品 ID 对应且仅对应一件商品,订单 ID 对应且仅对应一个订单。
分布式 ID 是分布式系统下的 ID。在分库分表的架构下,尤其常见。
分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候就需要生成分布式 ID了。
分布式 ID 需要满足哪些要求?
一个最基本的分布式 ID 需要满足下面这些要求:
- 全局唯一:ID 的全局唯一性肯定是首先要满足的!
- 高性能:分布式 ID 的生成速度要快,对本地资源消耗要小。
- 高可用:生成分布式 ID 的服务要保证可用性无限接近于 100%。
- 方便易用:拿来即用,使用方便,快速接入。
除了这些之外,一个比较好的分布式 ID 还应保证:
- 安全:ID 中不包含敏感信息。
- 有序递增:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。
- 有具体的业务含义:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
- 独立部署:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。
分布式 ID 常见解决方案
雪花算法
雪花算法优点:
- 系统环境ID不重复:能满足高并发分布式系统环境ID不重复,比如大家熟知的分布式场景下的数据库表的ID生成。
- 生成效率极高:在高并发,以及分布式环境下,除了生成不重复 id,每秒可生成百万个不重复 id,生成效率极高。
- 保证基本有序递增:基于时间戳,可以保证基本有序递增,很多业务场景都有这个需求。
- 不依赖第三方库:不依赖第三方的库,或者中间件,算法简单,在内存中进行。
- 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。
雪花算法原理
雪花算法的原理:就是生成一个的 64 位的 long 类型的唯一 id,主要分为如下4个部分组成:
1)1位保留 (基本不用)
1位标识:由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0,所以这第一位都是0。
2)41位时间戳
接下来 41 位存储毫秒级时间戳,41位可以表示2^41-1个毫秒的值,转化成单位年则是:(2^41−1)/(1000∗60∗60∗24∗365)=69年 。
41位时间戳 :也就是说这个时间戳可以使用69年不重复,大概可以使用 69 年。
注意:41位时间截不是存储当前时间的时间截,而是存储时间截的差值“当前时间截 – 开始时间截”得到的值。
这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的,一般设置好后就不要去改变了,切记!
因为,雪花算法有如下缺点:依赖服务器时间,服务器时钟回拨时可能会生成重复 id。
10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId,最多可以部署 2^10=1024 台机器。
这里的5位可以表示的最大正整数是2^5−1=31,即可以用0、1、2、3、….31这32个数字,来表示不同的datecenterId,或workerId。
用来记录同毫秒内产生的不同id,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号。
理论上雪花算法方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
代码实现
@Slf4j
public class Snowflake {
// ==============================Fields===========================================
/**
* 开始时间戳 (2000-01-01 00:00:00)
*/
private static final long TWEPOCH = 946656000000L;
/**
* 机器id所占的位数 5
*/
private static final long WORKER_ID_BITS = 5L;
/**
* 数据标识id所占的位数 5
*/
private static final long DATA_CENTER_ID_BITS = 5L;
/**
* 支持的最大机器id,结果是 31
*/
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
/**
* 支持的最大数据标识id,结果是 31
*/
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
/**
* 序列在id中占的位数
*/
private static final long SEQUENCE_BITS = 12L;
/**
* 机器ID向左移12位
*/
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
/**
* 数据标识id向左移17位(12+5)
*/
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
/**
* 时间戳向左移22位(5+5+12)
*/
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
/**
* 步长 1024
*/
private static final long STEP_SIZE = 1024;
/**
* unsigned int max value
*/
private static final long UINT_MAX_VALUE = 0xffffffffL;
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 工作机器ID 计数器
*/
private long workerIdFlags = 0L;
/**
* 数据中心ID(0~31)
*/
private long dataCenterId;
/**
* 数据中心ID 计数器
*/
private long dataCenterIdFlags = 0L;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 毫秒内序列基数[0|1024|2048|3072]
*/
private long basicSequence = 0L;
/**
* 上次生成ID的时间戳
*/
private long lastTimestamp = -1L;
/**
* 工作模式
*/
private final WorkMode workMode;
public enum WorkMode { NON_SHARED, RATE_1024, RATE_4096; }
//==============================Constructors=====================================
public Snowflake() {
this(0, 0, WorkMode.RATE_4096);
}
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
*/
public Snowflake(long workerId, long dataCenterId) {
this(workerId, dataCenterId, WorkMode.RATE_4096);
}
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
* @param workMode 工作模式
*/
public Snowflake(long workerId, long dataCenterId, WorkMode workMode) {
this.workMode = workMode;
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(MessageFormat.format("worker Id can't be greater than {0} or less than 0", MAX_WORKER_ID));
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(MessageFormat.format("datacenter Id can't be greater than {0} or less than 0", MAX_DATA_CENTER_ID));
}
this.workerId = workerId;
this.workerIdFlags = setSpecifiedBitTo1(this.workerIdFlags, this.workerId);
this.dataCenterId = dataCenterId;
this.dataCenterIdFlags = setSpecifiedBitTo1(this.dataCenterIdFlags, this.dataCenterId);
}
// ==============================Methods==========================================
/**
* 获取机器id
*
* @return 所属机器的id
*/
public long getWorkerId() {
return workerId;
}
/**
* 获取数据中心id
*
* @return 所属数据中心id
*/
public long getDataCenterId() {
return dataCenterId;
}
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < this.lastTimestamp) {
if (timestamp > TWEPOCH) {
if (WorkMode.NON_SHARED == this.workMode) {
nonSharedClockBackwards(timestamp);
} else if (WorkMode.RATE_1024 == this.workMode) {
rate1024ClockBackwards(timestamp);
} else {
throw new RuntimeException(MessageFormat.format("Clock moved backwards. Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
}
} else {
throw new RuntimeException(MessageFormat.format("Clock moved backwards. Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
}
}
//如果是同一时间生成的,则进行毫秒内序列
if (this.lastTimestamp == timestamp) {
this.sequence = (this.sequence + 1) & SEQUENCE_MASK;
//毫秒内序列溢出
if (this.sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(this.lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
this.sequence = this.basicSequence;
}
//上次生成ID的时间戳
this.lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (this.dataCenterId << DATA_CENTER_ID_SHIFT)
| (this.workerId << WORKER_ID_SHIFT)
| this.sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间戳
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp0;
do {
timestamp0 = timeGen();
} while (timestamp0 <= lastTimestamp);
return timestamp0;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
/**
* 尝试解决时钟回拨<br>【* 仅用于 单机生成不对外 的情况 *】
*
* @param timestamp 当前时间戳
* @return void
*/
private void nonSharedClockBackwards(long timestamp) {
if (this.dataCenterIdFlags >= UINT_MAX_VALUE && this.workerIdFlags >= UINT_MAX_VALUE) {
throw new RuntimeException(MessageFormat.format("Clock moved backwards. Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
} else {
//如果仅用于生成不重复的数值,尝试变更 dataCenterId 或 workerId 修复时钟回拨问题
log.warn("Clock moved backwards. Refusing to generate id for {} milliseconds", lastTimestamp - timestamp);
//先尝试变更 dataCenterId,当 dataCenterId 轮询一遍之后,尝试变更 workerId 并重置 dataCenterId
if (this.dataCenterIdFlags >= UINT_MAX_VALUE) {
if (++this.workerId > MAX_WORKER_ID) { this.workerId = 0L; }
this.workerIdFlags = setSpecifiedBitTo1(this.workerIdFlags, this.workerId);
// 重置 dataCenterId 和 dataCenterIdFlags
this.dataCenterIdFlags = this.dataCenterId = 0L;
} else {
if (++this.dataCenterId > MAX_DATA_CENTER_ID) { this.dataCenterId = 0L; }
}
this.dataCenterIdFlags = setSpecifiedBitTo1(this.dataCenterIdFlags, this.dataCenterId);
this.lastTimestamp = -1L;
log.warn("Try to fix the clock moved backwards. timestamp : {}, worker Id : {}, datacenter Id : {}", timestamp, workerId, dataCenterId);
}
}
/**
* 尝试解决时钟回拨<br>【* 仅用于每毫秒生成量 不大于 1024 的情况 *】
*
* @param timestamp 当前时间戳
* @return void
*/
private void rate1024ClockBackwards(long timestamp) {
if (this.basicSequence > (SEQUENCE_MASK - STEP_SIZE)) {
throw new RuntimeException(MessageFormat.format("Clock moved backwards. Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
} else {
log.warn("Clock moved backwards. Refusing to generate id for {} milliseconds", lastTimestamp - timestamp);
this.basicSequence += STEP_SIZE;
this.lastTimestamp = -1L;
log.warn("Try to fix the clock moved backwards. timestamp : {}, basicSequence : {}", timestamp, basicSequence);
}
}
/**
* Set the specified bit to 1
*
* @param value raw long value
* @param index bit index (From 0~31)
* @return long value
*/
private long setSpecifiedBitTo1(long value, long index) {
return value |= (1L << index);
}
/**
* Set the specified bit to 0
*
* @param value raw long value
* @param index bit index (From 0~31)
* @return long value
*/
private long setSpecifiedBitTo0(long value, long index) {
return value &= ~(1L << index);
}
/**
* Get the specified bit
* @param value raw long value
* @param index bit index(From 0-31)
* @return 0 or 1
*/
private int getSpecifiedBit(long value, long index) {
return (value & (1L << index)) == 0 ? 0 : 1;
}
}
数据库号段模式
数据库主键自增这种模式,每次获取 ID 都要访问一次数据库,ID 需求比较大的时候,肯定是不行的。
如果我们可以批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就舒服了!这也就是我们说的 基于数据库的号段模式来生成分布式 ID。
数据库的号段模式也是目前比较主流的一种分布式 ID 生成方式。
以 MySQL 举例。
1)创建一个数据库表
CREATE TABLE `sequence_id_generator` (
`id` int(10) NOT NULL,
`current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(10) NOT NULL COMMENT '号段的长度',
`version` int(20) NOT NULL COMMENT '版本号',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
current_max_id 字段和step字段主要用于获取批量 ID,获取的批量 id 为: current_max_id ~ current_max_id+step。
version 字段主要用于解决并发问题(乐观锁),biz_type 主要用于表示业务类型。
2)先插入一行数据
INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES(1, 0, 100, 0, 101);
3)通过 SELECT 获取指定业务下的批量唯一 ID
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101
4)不够用的话,更新之后重新 SELECT 即可
UPDATE sequence_id_generator SET current_max_id = 0+100, version=version+1 WHERE version = 0 AND `biz_type` = 101;
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101;
相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。
另外,为了避免单点问题,你可以从使用主从模式来提高可用性。
数据库号段模式的优缺点:
- 优点 :ID 有序递增、存储消耗空间小
- 缺点 :存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、ID 没有具体业务含义、安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊!
redis实现分布式ID
我们生成的订单号一般需要存在Long类型中,正好Long类型是64位,所以将第一位永远设置成0,表示正数。后面31位表示时间戳,可以表示的数字为2的31次方(0-2147483648),单位秒,再后面的32位可以表示成2的32次方的订单号(0-4294967296)。这种思想主要是借鉴雪花算法的原理。

- 符号位:1bit,永远为0,表示正数
- 时间戳:31bit,最大2147483648秒,大概69年
- 序列号:32bit,最大4294967296,表示一秒中内能生成的不同的订单数(接近43亿)。一般一秒中能产生43亿个不一样的订单号,基本满足各种电商场景了。
java代码实现
@Component
public class RedisIdMaker {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 时间戳开始时间,从2022年1月1号0点0时0分开始
*/
private static final Long START_TIME = 1640995200L;
/**
* 订单生成数量 每天最多2的31次方个订单数量
*/
private static final int COUNT_BITS = 32;
private static final String ORDER_COUNT_KEY = "order:";
/**
* 根据redis生成唯一订单号
*
* @return
*/
public Long generateNextId() {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
long currentStamp = now.toEpochSecond(ZoneOffset.UTC);
// 获取当前时间戳(秒)
long timeStamp = currentStamp - START_TIME;
// 组装成key=order:2022:01:01(组装成这种形式方便日后根据日期统计当天的订单数量)
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm"));
String redisKey = ORDER_COUNT_KEY + date;
// 订单自增长
long orderCount = stringRedisTemplate.opsForValue().increment(redisKey);
// 返回唯一订单号(拼接而来的)
return timeStamp << COUNT_BITS | orderCount;
}
/**
* 获取2022年1月1号0点0时0分的时间戳
* @param args
*/
public static void main(String[] args) {
LocalDateTime startLocalTime = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
long startTime = startLocalTime.toEpochSecond(ZoneOffset.UTC);
System.out.println(startTime);
LocalDateTime now = LocalDateTime.now();
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd:HH:mm"));
System.out.println(date);
}
}
说明:
时间戳开始是从1970年开始的,当31位时间戳全部是0的情况下,那么就是最开始,当当31位时间戳全部是1的情况下那么就是2039年,也就是说只能用到2039年
图中后32位表示2022年1月1号0点0时0分的秒时间戳。
timeStamp << COUNT_BITS
这行代码表示将上图中的时间戳往前位移32位,就变成了下面的:
最后就是这段代码:
timeStamp << COUNT_BITS |orderCount
| 是把某两个数中, 只要其中一个的某一位为1,则结果的该位就为1。
由于我们现在表示成二进制,只有0和1,所以这样运算后变成了
批量生成测试
CountDownLatch countDownLatch = new CountDownLatch(300);
// 定义任务
Runnable task = ()->{
for (int i = 0; i < 100; i++) {
long id = redisIdMaker.generateNextId();
System.out.println(id);
}
countDownLatch.countDown();
};
for (int i = 0; i < 300; i++) {
es.submit(task);
}
countDownLatch.await();
美团Leaf
- Leaf依赖了数据库或者ZK(这里依赖ZK实现的Snowflake是对Snowflake原有的方式的一种增强)。
- Leaf提供两种模式:Leaf-segment数据库模式和Leaf-snowflake模式两种。
- 对snowflake时间回拨做了优化。解决了时间回拨的问题。
在Github上美团有实现代码,下面我们跟进代码搭建一个本地项目跑起来
1)clone代码
git clone https://github.com/Meituan-Dianping/Leaf.git
2)进入leaf-server/src/main/resources/leaf.properties配置文件中设置
我们开启 leaf.segment.enable=true 。而 leaf.snowflake.enable 由于需要使用到zk暂时不开起。leaf.properties设置:
leaf.name=mxsm
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf?useUnicode=true&characterEncoding=utf-8
leaf.jdbc.username=root
leaf.jdbc.password=sys123456
3)执行脚本
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '', -- your biz unique name
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id');
4)启动项目
我使用的是mysql8.0,所以修改了 mysql的connector版本以及druid的版本,不修改会报错。
5)测试
#segment
curl http://localhost:8080/api/segment/get/leaf-segment-test
#snowflake
curl http://localhost:8080/api/snowflake/get/test
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2022-04-05 【Windows/Linux】xxl-job部署及使用
2022-04-05 【Windows】部署ELK
2022-04-05 【Windows/Linux】安装Sentinel
2022-04-05 【Windows】安装Zipkin
2022-04-05 【Windows】安装skywalking
2022-04-05 【Windows】Redis集群部署
2022-04-05 【Windows】Redis单机部署