分布式ID生成方案实战:雪花算法与百度Leaf部署方案

分布式系统中,全局唯一ID生成是分库分表、消息追踪、订单编号等场景的基础需求。数据库自增ID在分库分表后会冲突,UUID无序且占用空间大。雪花算法(Snowflake)通过时间戳+机器ID+序列号的组合,生成趋势递增的64位ID,兼顾有序性和分布式唯一性。百度开源的Leaf系统在雪花算法基础上增加了ZooKeeper协调和Segment号段模式,解决了时钟回拨和机器ID分配问题。本文从原理到部署,给出完整的分布式ID生成方案。

雪花算法原理与结构设计

Snowflake算法生成的ID为64位整数,各bit位含义如下:1bit符号位 + 41bits时间戳 + 10bits机器ID + 12bits序列号。

public class SnowflakeIdGenerator {
    private static final long EPOCH = 1704067200000L; // 2024-01-01 UTC

    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 must be between 0 and " + MAX_MACHINE_ID);
        }
        this.machineId = machineId;
    }

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

        // 时钟回拨检测
        if (currentTimestamp < lastTimestamp) {
            throw new RuntimeException(
                "Clock moved backwards by " +
                (lastTimestamp - currentTimestamp) + "ms");
        }

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

        lastTimestamp = currentTimestamp;

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

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

时钟回拨问题与解决方案

时钟回拨是雪花算法最棘手的问题。NTP时间同步可能导致系统时钟向后跳转,此时生成的ID可能与之前重复。常见的解决方案包括等待策略和扩展位策略:

// 时钟回拨容忍策略
public synchronized long nextIdWithBackwardTolerance() {
    long currentTimestamp = System.currentTimeMillis();
    long offset = lastTimestamp - currentTimestamp;

    if (offset <= 5 && offset > 0) {
        // 回拨5ms以内,等待
        try {
            Thread.sleep(offset + 1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        currentTimestamp = System.currentTimeMillis();
    } else if (offset > 5) {
        // 回拨超过容忍范围,使用扩展位标记
        return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
                | (machineId << MACHINE_ID_SHIFT)
                | sequence
                | (1L << 63);
    }

    // 正常生成逻辑...
    return 0L;
}

百度Leaf系统架构与部署

百度Leaf提供两种模式:Segment号段模式和Snowflake模式。Segment模式通过数据库批量获取ID号段,减少数据库访问频率;Snowflake模式通过ZooKeeper分配机器ID,解决手工配置问题。

-- Leaf Segment模式数据库表结构
CREATE TABLE leaf_alloc (
    biz_tag varchar(128) NOT NULL COMMENT 'business tag',
    max_id bigint(20) NOT NULL DEFAULT '1',
    step int(11) NOT NULL DEFAULT '1000',
    description varchar(256) DEFAULT NULL,
    update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
        ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (biz_tag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO leaf_alloc (biz_tag, max_id, step, description)
VALUES ('order', 1, 2000, 'order ID');
INSERT INTO leaf_alloc (biz_tag, max_id, step, description)
VALUES ('user', 1, 1000, 'user ID');
# Leaf配置文件 leaf.properties
leaf.name=leaf-server
leaf.segment.enable=true
leaf.snowflake.enable=true
leaf.segment.url=jdbc:mysql://127.0.0.1:3306/leaf
leaf.segment.username=root
leaf.segment.password=your_password
leaf.snowflake.zk.address=127.0.0.1:2181
leaf.snowflake.port=2181

Leaf Segment模式的核心逻辑是双Buffer预加载。当前号段使用到10%时,异步加载下一个号段,实现ID获取的无阻塞:

// Leaf Segment双Buffer核心逻辑(简化版)
public class SegmentBuffer {
    private volatile Segment segment;
    private volatile Segment nextSegment;
    private volatile boolean isLoadingNext;
    private final String bizTag;

    public synchronized long getId() {
        if (segment == null) {
            segment = loadSegmentFromDB();
        }

        long id = segment.getAndIncrement();
        if (id > segment.getMaxId()) {
            if (nextSegment != null) {
                segment = nextSegment;
                nextSegment = null;
                id = segment.getAndIncrement();
            } else {
                segment = loadSegmentFromDB();
                id = segment.getAndIncrement();
            }
        }

        // 使用量超过10%时异步预加载
        if (nextSegment == null && !isLoadingNext
                && segment.getUsage() > 0.1) {
            isLoadingNext = true;
            CompletableFuture.runAsync(() -> {
                synchronized (this) {
                    nextSegment = loadSegmentFromDB();
                    isLoadingNext = false;
                }
            });
        }
        return id;
    }

    private Segment loadSegmentFromDB() {
        // UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = ?
        // SELECT max_id, step FROM leaf_alloc WHERE biz_tag = ?
        // return [max_id - step + 1, max_id]
        return null;
    }
}

Leaf Snowflake模式与ZooKeeper协调

Leaf Snowflake模式通过ZooKeeper的持久顺序节点自动分配workerId:

// ZK节点结构
// /leaf/snowflake/
//   worker-0000000000  (workerId=0)
//   worker-0000000001  (workerId=1)
//   worker-0000000002  (workerId=2)

public class SnowflakeZKHolder {
    private final String zkPath = "/leaf/snowflake";
    private final CuratorFramework zkClient;
    private int workerId;

    public void init() throws Exception {
        // 创建持久顺序节点
        String nodePath = zkClient.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.PERSISTENT_SEQUENTIAL)
            .forPath(zkPath + "/worker-");

        // 从节点路径解析workerId
        String nodeSeq = nodePath.substring(
            nodePath.lastIndexOf('-') + 1);
        workerId = Integer.parseInt(nodeSeq);

        // 写入IP和端口
        zkClient.setData().forPath(nodePath,
            (NetUtils.getLocalIp() + ":" + port).getBytes());

        // 注册临时子节点用于心跳
        zkClient.create()
            .withMode(CreateMode.EPHEMERAL)
            .forPath(nodePath + "/alive");
    }

    public int getWorkerId() {
        return workerId;
    }
}

分布式ID方案选型与性能对比

三种方案各有适用场景。原生Snowflake实现简单、性能最高,但需要手动管理机器ID和处理时钟回拨。Leaf Segment模式ID有序、对数据库依赖低,适合对性能要求中等但对稳定性要求高的场景。Leaf Snowflake模式通过ZK自动协调,适合大规模机器部署。

性能参考数据(单节点QPS):原生Snowflake约400万/s,Leaf Segment约100万/s,Leaf Snowflake约300万/s。多节点部署时QPS线性扩展。

选型建议:百台以下机器规模,原生Snowflake配合手动ID分配足够使用;千台规模以上,Leaf Snowflake的ZK协调机制更可靠。对ID连续性有要求的场景(如订单号),Leaf Segment的号段模式生成的ID更紧凑。无论选择哪种方案,时钟回拨检测和workerId唯一性保障都是必须实现的防护措施。

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

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

相关推荐