












Snowflake雪花算法是一种全局ID生成方案,比如分库分表场景下,不能用自增主键了,全局会重复,就需要用全局ID方案作为分库分表下的唯一标识,可以认为是唯一业务标识,因为自增主键这时候分表了已经都是重复的了。这时候业务应用需要向发号器(用来生成全局ID的独立应用或集群)拿号子。
这是一个完全不依赖数据库的算法,由Twitter开源。它生成的是一个64位的长整型ID。
举例,具体场景:
订单表:上亿,我们要分表,orderid和userid都需要用全局唯一ID了。
查询主要是,按用户分页查订单,以及按照订单号精确查订单。
我们按照userid来进行分表,理由是翻页的时候可以落在1个分表内。
按订单号查询,要有一个基因机制,把userid分表规则藏在orderid里,这样根据orderid可以确定去哪个分表,然后直接用主键查。
比如订单表我们按照userid后两位0 - 99来分100张表,这两位记为xx。
orderid用雪花算法生成全局唯一ID, 为了不破坏趋势递增,我们用snowflakeid + xx作为orderid
我们可以用hutool简单的使用snowflake,如下,包括生成snowflakeId和对这个Id的解析:
用IdUtil.getSnowflake(workerId,datacenterId)获取snowflakeId,用SnowflakeParser.parse(id)把id里的时间、数据中心ID、工作服务器ID、序列号,这些都按照可读的格式解析出来,封装成 SnowflakeIdInfo 。
import lombok.Data;
import java.time.Instant;
@Data
public class SnowflakeIdInfo {
// 原始ID
private long id;
// 时间戳差值(相对于纪元的毫秒数)
private long timestampDelta;
// 实际时间(纪元 + 差值)
private Instant actualTime;
// 格式化的时间字符串(本地时区)
private String formattedTime;
// 整体机器ID(10位)
private long machineId;
// 数据中心ID(高5位)
private long datacenterId;
// 工作机器ID(低5位)
private long workerId;
// 序列号(12位)
private long sequence;
}
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class SnowflakeParser {
// 默认纪元(Twitter 风格):2010-11-04 01:42:54 GMT
public static final long DEFAULT_EPOCH = 1288834974657L;
/**
* 解析雪花ID(使用默认纪元)
*/
public static SnowflakeIdInfo parse(long id) {
return parse(id, DEFAULT_EPOCH);
}
/**
* 解析雪花ID(可指定自定义纪元)
* @param id 64位雪花ID
* @param epoch 自定义纪元毫秒数
*/
public static SnowflakeIdInfo parse(long id, long epoch) {
// 1. 提取时间戳(41位),右移22位(机器ID 10位 + 序列号 12位)
long timestampDelta = id >> 22;
// 2. 提取序列号(12位),直接取低12位
long sequence = id & 0xFFF; // 0xFFF = 4095
// 3. 提取机器ID(10位),先右移12位再取低10位
long machineId = (id >> 12) & 0x3FF; // 0x3FF = 1023
// 4. 拆分 datacenterId(高5位)和 workerId(低5位)
long datacenterId = (machineId >> 5) & 0x1F; // 0x1F = 31
long workerId = machineId & 0x1F;
// 5. 计算实际时间
Instant actualTime = Instant.ofEpochMilli(epoch + timestampDelta);
// 6. 格式化为可读时间(使用系统默认时区,也可指定)
String formattedTime = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
.withZone(ZoneId.systemDefault())
.format(actualTime);
// 7. 组装POJO
SnowflakeIdInfo info = new SnowflakeIdInfo();
info.setId(id);
info.setTimestampDelta(timestampDelta);
info.setActualTime(actualTime);
info.setFormattedTime(formattedTime);
info.setMachineId(machineId);
info.setDatacenterId(datacenterId);
info.setWorkerId(workerId);
info.setSequence(sequence);
return info;
}
}
测试
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
public class SnowFlakeTest {
public static void main(String[] args) {
long workerId = 1L;
long datacenterId = 1L;
Snowflake snowflake = IdUtil.getSnowflake(workerId,datacenterId);
long id = snowflake.nextId();
System.out.println(id);
System.out.println(Snowflake.DEFAULT_TWEPOCH); //固定纪元 1288834974657 2010-11-04 01:42:54 GMT
SnowflakeIdInfo snowflakeIdInfo = SnowflakeParser.parse(id);
System.out.println(JSON.toJSONString(snowflakeIdInfo));
}
}
运行结果
2093162895612448768
1288834974657
{"actualTime":"2026-08-28T02:24:58.057Z","datacenterId":1,"formattedTime":"2026-08-28 10:24:58.057","id":2093162895612448768,"machineId":33,"sequence":0,"timestampDelta":499048923400,"workerId":1}
实际生产上用的时候,如果是自己分配workerid和centerid,
那么需要保证集群里边时钟一致,且准确。 centerid如果是一个vpc那么可以算同一个数据中心,写死成1好了。
架构上,比如2个发号器应用组成1个集群,前面用ng做反向代理负载均衡、ng与发号器节点之间用keepalive探活。 应用配置文件里配置自己的workerid ,比如分别是1和2。
业务应用通过ng调这个发号集群取号。
前面提到“保证集群时钟一致且准确”,这确实是雪花算法的命门。
需要在生产环境强制部署 NTP(网络时间协议) 服务,让所有机器定时对时。
但即使有NTP,网络抖动也可能导致系统时间“往回跳”几毫秒。 这会导致生成重复ID,危害很大。
这个需要做2个事情,一是如何识别时钟回拨了,二是识别到之后的处理策略。
识别时钟回拨就是在本地内存里边存一下最后发出去的ID的时间戳, 发ID的时候用当前时钟的时间戳跟这个保存的上一次的时间戳做一下比较。判断是否发生了时钟回拨。
如果发生了回拨,先sleep一下然后自旋重试一下,看看新时钟是否追上来,超过了上一次发出去的ID的时间。
另外为了防止回拨时间较长(比如人为改时间),需要设置一下sleep的最大时间,防止长时间等待和自旋。
https://tech.meituan.com/2017/04/21/mt-leaf.html 《Leaf——美团点评分布式ID生成系统》
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。