分布式ID生成实战:Snowflake雪花算法与Leaf发号器方案

分布式系统中生成全局唯一ID是微服务架构的基础需求。订单号、用户ID、消息流水号等业务场景都要求ID全局唯一、趋势递增、高性能生成。Snowflake雪花算法和美团Leaf是目前主流的两种分布式ID方案,在高并发设计场景下各有适用场景。

Snowflake雪花算法原理与实现

Snowflake由Twitter开源,生成64位Long型ID。结构为:1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号。41位时间戳支持约69年(以毫秒为单位),10位机器ID支持1024个节点,12位序列号单节点每毫秒可生成4096个ID。

public class SnowflakeIdGenerator {
    private static final long EPOCH = 1704067200000L;
    private static final long MACHINE_ID_BITS = 10L;
    private static final long SEQUENCE_BITS = 12L;
    private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
    private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;

    private final long machineId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long machineId) {
        if (machineId < 0 || machineId > MAX_MACHINE_ID) {
            throw new IllegalArgumentException("machineId range: 0-" + MAX_MACHINE_ID);
        }
        this.machineId = machineId;
    }

    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();

        // 时钟回拨检测
        if (currentTimestamp < lastTimestamp) {
            long offset = lastTimestamp - currentTimestamp;
            if (offset <= 5) {
                try { Thread.sleep(offset); } catch (InterruptedException e) {}
                currentTimestamp = System.currentTimeMillis();
            } else {
                throw new RuntimeException("Clock moved backwards " + offset + "ms");
            }
        }

        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                currentTimestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = currentTimestamp;

        return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
             | (machineId << MACHINE_ID_SHIFT)
             | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

Snowflake的核心优势是本地生成、无网络依赖、性能极高(单线程每秒百万级)。但存在时钟回拨问题——NTP同步可能导致系统时钟跳变,需要代码层处理。上面的实现采用了小范围等待、大范围抛异常的策略。

时钟回拨问题的多种解决方案

时钟回拨是Snowflake最棘手的问题。除了等待策略,还有几种方案:

方案一:借用未来时间。记录最大时间戳,回拨时使用最大时间戳继续生成,相当于预支了时间。缺点是ID时间戳不精确。

方案二:引入序列号段缓存。每次从Zookeeper/etcd获取机器ID时同时获取一个可用时间偏移量,回拨时使用偏移量补偿。

方案三:使用数据中心+机器双5位划分。将10位机器ID拆分为5位数据中心+5位机器,通过Zookeeper分配唯一标识,降低冲突概率。

美团Leaf发号器架构与部署

Leaf是美团开源的分布式ID生成系统,提供两种模式:Leaf-segment(号段模式)和Leaf-snowflake(雪花模式)。

Leaf-segment通过数据库批量分配号段,每次获取一批ID缓存在本地,减少数据库访问频率。核心表结构:

CREATE TABLE leaf_alloc (
    biz_tag     VARCHAR(128) PRIMARY KEY,
    max_id      BIGINT NOT NULL DEFAULT 1,
    step        INT NOT NULL DEFAULT 1000,
    description VARCHAR(256) DEFAULT NULL,
    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
// Leaf-segment核心逻辑
public class SegmentIDGenImpl implements IDGen {
    private final Map cache = new ConcurrentHashMap<>();

    public Result get(final String bizTag) {
        SegmentBuffer buffer = cache.get(bizTag);
        if (buffer == null) {
            buffer = new SegmentBuffer();
            cache.putIfAbsent(bizTag, buffer);
            buffer = cache.get(bizTag);
        }

        Segment segment = buffer.getCurrent();
        if (!buffer.isInitialized() || segment.getIdle() < segment.getStep() * 0.2) {
            updateSegmentFromDb(bizTag, buffer.getNext());
            buffer.setNextReady(true);
        }

        long value = segment.getAtomicId().incrementAndGet();
        if (value < segment.getMaxId()) {
            return new Result(value, Status.SUCCESS);
        }

        synchronized (buffer) {
            if (buffer.isInitialized() && buffer.getNextReady()) {
                buffer.switchSegment();
                buffer.setNextReady(false);
                return get(bizTag);
            }
        }
        return new Result(0, Status.EXCEPTION);
    }
}

双Buffer策略是Leaf-segment的关键设计。当前号段使用到80%时,异步加载下一个号段到备用Buffer,号段切换时无阻塞。这解决了单Buffer方案在号段耗尽时需要同步等待数据库的问题。

Snowflake与Leaf方案对比与选型

维度 Snowflake Leaf-segment Leaf-snowflake
依赖 MySQL Zookeeper
性能 百万QPS 十万QPS 百万QPS
ID特征 趋势递增 严格递增 趋势递增
时钟回拨 需自行处理 无此问题 框架处理
运维复杂度
扩展性 机器ID管理 biz_tag扩展 ZK管理

选型建议:单机场景用Snowflake足够,无需额外依赖;对ID严格递增有要求的业务(如数据库分库分表按ID范围查询)选Leaf-segment;多机房部署选Leaf-snowflake,Zookeeper管理机器ID避免冲突。

生产环境部署注意事项

Snowflake方案部署时,机器ID分配需要保证全局唯一。推荐通过启动参数传入或配置中心管理。以下是基于Zookeeper的机器ID自动分配:

public class ZkMachineIdAllocator {
    private final String zkPath = "/snowflake/machine-ids";
    private final CuratorFramework zkClient;

    public long allocateMachineId() {
        String nodePath = zkClient.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(zkPath + "/node-");

        String nodeNum = nodePath.substring(nodePath.lastIndexOf("-") + 1);
        long machineId = Long.parseLong(nodeNum) % 1024;

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try { zkClient.delete().forPath(nodePath); }
            catch (Exception e) { }
        }));

        return machineId;
    }
}

临时顺序节点在服务重启时自动释放旧ID,避免机器ID泄漏。取模1024保证ID在合法范围内。生产环境建议配合监控告警,当可用机器ID低于100时触发扩容评估。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-id-sheng-cheng-shi-zhan-snowflake-xue-hua-suan/

(0)
小编小编
上一篇 7小时前
下一篇 7小时前

相关推荐