惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Security Blog
Microsoft Security Blog
罗磊的独立博客
大猫的无限游戏
大猫的无限游戏
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
aimingoo的专栏
aimingoo的专栏
腾讯CDC
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
Engineering at Meta
Engineering at Meta
博客园_首页
B
Blog RSS Feed
D
Docker
M
MIT News - Artificial intelligence
爱范儿
爱范儿
I
InfoQ

博客园 - 幽州散人

SQL优化案例 synchronized 为啥会让虚拟线程卸载失效 主线任务和使命 温州3日之旅 CompletableFuture + 异步Servlet:同步的姿势、异步的效果 windows Java 幽灵转发壳进程 也谈MySQL limit offset深翻页问题 SHOW SESSION STATUS 与 Handler 变量 MySQL 5.7 Profiles 执行性能分析 MySQL临时表与文件排序 MySQL 5.7 MRR多范围读优化 MySQL 5.7 ICP索引条件下推优化 MySQL符合索引与最左前缀原则 从I/O 的物理成本理解回表和索引覆盖 第4个电瓶以及补漆 Flink原理:并行度与keyGroup桶 什么是数字批发银行 Java虚拟线程(三)实现原理 Java虚拟线程(二) Java虚拟线程(一) 大模型推理层服务化架构 传奇调查员之路:理智值-99,但我必须听懂深渊的语言 js里调用智能合约读/写函数的方法的区别 字节与其16进制字符表示转换的bug Redisson分布式锁 交易心得 DexScreener接口初探 某安全软件跑飞了。。 Ed25519算法签名与验签的Java实现 Tendermint拜占庭容错引擎
Snowflake雪花算法与发号器的应用
幽州散人 · 2026-08-28 · via 博客园 - 幽州散人

雪花算法、发号器及其应用场景

Snowflake雪花算法是一种全局ID生成方案,比如分库分表场景下,不能用自增主键了,全局会重复,就需要用全局ID方案作为分库分表下的唯一标识,可以认为是唯一业务标识,因为自增主键这时候分表了已经都是重复的了。这时候业务应用需要向发号器(用来生成全局ID的独立应用或集群)拿号子。
这是一个完全不依赖数据库的算法,由Twitter开源。它生成的是一个64位的长整型ID。

  • ID结构
    • 1位符号位:固定为0,保证ID为正数。
    • 41位时间戳:记录从固定纪元开始到现在的毫秒级时间。默认是2010-11-04 01:42:54 GMT
    • 10位机器ID:支持最多1024个节点。分centerId,workerId两段,分别是数据中心ID和服务器ID
    • 12位序列号:同一毫秒内可生成4096个不同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调这个发号集群取号。

snowflake时钟回拨问题

前面提到“保证集群时钟一致且准确”,这确实是雪花算法的命门。
需要在生产环境强制部署 NTP(网络时间协议) 服务,让所有机器定时对时。
但即使有NTP,网络抖动也可能导致系统时间“往回跳”几毫秒。 这会导致生成重复ID,危害很大。
这个需要做2个事情,一是如何识别时钟回拨了,二是识别到之后的处理策略。

识别时钟回拨就是在本地内存里边存一下最后发出去的ID的时间戳, 发ID的时候用当前时钟的时间戳跟这个保存的上一次的时间戳做一下比较。判断是否发生了时钟回拨。
如果发生了回拨,先sleep一下然后自旋重试一下,看看新时钟是否追上来,超过了上一次发出去的ID的时间。
另外为了防止回拨时间较长(比如人为改时间),需要设置一下sleep的最大时间,防止长时间等待和自旋。

不造轮子

https://tech.meituan.com/2017/04/21/mt-leaf.html 《Leaf——美团点评分布式ID生成系统》