39-分布式ID生成详解

2 阅读16分钟

分布式ID生成详解

一、知识概述

在分布式系统中,唯一ID的生成是一个基础且关键的问题。无论是订单号、用户ID、消息ID,还是数据库分片键,都需要一个全局唯一的标识符。一个好的分布式ID方案需要满足全局唯一、趋势递增、高性能、高可用等要求。

本文将深入讲解主流的分布式ID生成方案,包括UUID、数据库自增、号段模式、雪花算法、Leaf方案等,分析各种方案的优缺点和适用场景,帮助你在实际项目中做出正确的技术选型。

二、ID生成需求分析

2.1 核心需求

/**
 * 分布式ID核心需求
 */
public class IDRequirements {
    
    /**
     * 1. 全局唯一性
     * - 这是最基本的要求
     * - 在整个分布式系统中唯一
     * - 不能出现重复ID
     */
    
    /**
     * 2. 趋势递增
     * - 有利于数据库索引
     * - 减少页分裂
     * - 提高查询性能
     */
    
    /**
     * 3. 高性能
     * - 生成速度快
     * - 低延迟
     * - 高吞吐量
     */
    
    /**
     * 4. 高可用
     * - 单点故障不影响ID生成
     * - 容错能力强
     * - 99.99%以上可用性
     */
    
    /**
     * 5. 信息安全
     * - ID不暴露业务信息
     * - 不暴露系统规模
     * - 难以猜测
     */
    
    /**
     * 6. 可读性(可选)
     * - 某些场景需要人工识别
     * - 如订单号、发票号
     */
}

2.2 方案对比

/**
 * 分布式ID方案对比
 */
public class IDSchemesComparison {
    
    /*
    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                        分布式ID方案对比                                       │
    ├──────────────┬──────────┬──────────┬──────────┬──────────┬─────────────────┤
    │     方案     │  全局唯一 │  趋势递增 │   性能   │  可用性  │     适用场景    │
    ├──────────────┼──────────┼──────────┼──────────┼──────────┼─────────────────┤
    │ UUID         │    ✓     │    ✗     │   高     │   高     │ 非主键、无序场景│
    │ 数据库自增   │    ✓     │    ✓     │   低     │   低     │ 单库、小规模    │
    │ 号段模式     │    ✓     │    ✓     │   高     │   中     │ 中等规模        │
    │ 雪花算法     │    ✓     │    ✓     │   高     │   高     │ 大规模分布式    │
    │ Leaf         │    ✓     │    ✓     │   高     │   高     │ 大规模、高可用  │
    │ TinyID       │    ✓     │    ✓     │   极高   │   高     │ 超高并发        │
    └──────────────┴──────────┴──────────┴──────────┴──────────┴─────────────────┘
    */
}

三、UUID方案

3.1 UUID 简介

/**
 * UUID(Universally Unique Identifier)
 * 
 * 特点:
 * - 128位(16字节)
 * - 32个十六进制字符 + 4个连字符 = 36个字符
 * - 无需中心化协调
 * - 本地生成,高性能
 */
public class UUIDSolution {
    
    /**
     * UUID 版本
     */
    public void uuidVersions() {
        // UUID v1: 基于时间戳和MAC地址
        // 格式:时间戳 + 时钟序列 + 节点ID(MAC地址)
        // 优点:有序
        // 缺点:暴露MAC地址
        
        // UUID v3: 基于命名空间和MD5哈希
        // 格式:MD5(命名空间 + 名称)
        
        // UUID v4: 随机生成(最常用)
        // 格式:随机数
        // 优点:简单、安全
        // 缺点:无序
        
        // UUID v5: 基于命名空间和SHA-1哈希
        // 格式:SHA-1(命名空间 + 名称)
    }
    
    /**
     * Java 生成 UUID
     */
    public void generateUUID() {
        // Java 默认生成 v4 UUID
        UUID uuid = UUID.randomUUID();
        String uuidStr = uuid.toString();
        // 输出:550e8400-e29b-41d4-a716-446655440000
        
        // 去掉连字符
        String uuidNoDash = uuidStr.replace("-", "");
        // 输出:550e8400e29b41d4a716446655440000
        
        // 获取字节数组
        byte[] bytes = uuidToBytes(uuid);
    }
    
    /**
     * UUID 转字节数组
     */
    private byte[] uuidToBytes(UUID uuid) {
        ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
        bb.putLong(uuid.getMostSignificantBits());
        bb.putLong(uuid.getLeastSignificantBits());
        return bb.array();
    }
    
    /**
     * UUID 优缺点分析
     */
    public void prosAndCons() {
        // 优点:
        // 1. 本地生成,无网络开销
        // 2. 全局唯一
        // 3. 无需中心化协调
        // 4. 性能极高
        
        // 缺点:
        // 1. 无序,不利于数据库索引
        // 2. 字符串过长(36字符)
        // 3. 作为主键时索引性能差
        // 4. 无法体现业务含义
        // 5. v1版本可能暴露MAC地址
    }
    
    /**
     * UUID 作为主键的问题
     */
    public void uuidPrimaryKeyProblem() {
        // MySQL InnoDB 主键是聚簇索引
        // 数据按主键顺序存储
        
        // UUID 无序导致:
        // 1. 插入时需要大量随机IO
        // 2. 频繁的页分裂
        // 3. 索引碎片化
        // 4. 空间利用率低
        
        // 解决方案:
        // 1. 使用有序UUID(UUID v1或改写v4)
        // 2. 使用自增主键,UUID作为业务键
        // 3. 使用COMB(UUID + 时间戳)
    }
}

3.2 有序UUID实现

/**
 * 有序UUID实现
 */
public class OrderedUUID {
    
    /**
     * COMB(UUID + 时间戳)
     * 将时间戳作为UUID的一部分,使其有序
     */
    public static UUID createCOMB() {
        // 生成随机UUID
        UUID uuid = UUID.randomUUID();
        
        // 获取当前时间戳(精确到毫秒)
        long timestamp = System.currentTimeMillis();
        
        // 将时间戳嵌入UUID的后64位
        long mostSigBits = uuid.getMostSignificantBits();
        long leastSigBits = timestamp << 16 | 
            (uuid.getLeastSignificantBits() & 0xFFFF);
        
        return new UUID(mostSigBits, leastSigBits);
    }
    
    /**
     * 时间有序UUID(模拟v1)
     */
    public static UUID createTimeOrderedUUID() {
        // 时间戳(100纳秒精度,从1582-10-15开始)
        long timestamp = System.currentTimeMillis();
        long uuidTime = (timestamp - UUID_EPOCH_START) * 10000;
        
        // 时钟序列
        int clockSeq = ThreadLocalRandom.current().nextInt(0, 16384);
        
        // 节点ID(可以用机器ID替代MAC地址)
        long node = getNodeId();
        
        // 组装UUID
        long mostSigBits = (uuidTime << 32) | 
            (0x1000L << 48) |  // 版本1
            (clockSeq << 48 >>> 16);
        
        long leastSigBits = (0x8000000000000000L) |  // 变体
            node;
        
        return new UUID(mostSigBits, leastSigBits);
    }
    
    private static final long UUID_EPOCH_START = 
        GregorianCalendar.getInstance().getTimeInMillis();
    
    private static long getNodeId() {
        // 可以使用机器ID或随机数
        return ThreadLocalRandom.current().nextLong(0, 281474976710656L);
    }
    
    /**
     * 性能测试
     */
    public static void main(String[] args) {
        // 生成100万个UUID
        long start = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) {
            UUID.randomUUID();
        }
        long end = System.nanoTime();
        System.out.println("UUID生成100万耗时: " + 
            (end - start) / 1_000_000 + "ms");
    }
}

四、数据库自增方案

4.1 单机自增

/**
 * 数据库自增ID
 */
public class DatabaseAutoIncrement {
    
    /**
     * MySQL 自增主键
     */
    /*
    CREATE TABLE `id_generator` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT,
        `biz_type` varchar(64) NOT NULL COMMENT '业务类型',
        `max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
        `step` int(11) NOT NULL COMMENT '步长',
        `version` int(11) NOT NULL COMMENT '版本号',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_biz_type` (`biz_type`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    */
    
    /**
     * 单机自增的问题
     */
    public void singleMachineProblems() {
        // 1. 单点故障
        // - 数据库宕机后无法生成ID
        
        // 2. 性能瓶颈
        // - 单机QPS有限
        // - 写入压力集中
        
        // 3. 扩展困难
        // - 无法水平扩展
    }
    
    /**
     * 主从模式
     */
    // 主库:auto_increment_increment=2, auto_increment_offset=1
    // 从库:auto_increment_increment=2, auto_increment_offset=2
    // 结果:主库生成1,3,5,7... 从库生成2,4,6,8...
}

4.2 号段模式

/**
 * 号段模式(Flickr方案)
 * 
 * 原理:
 * - 每次从数据库获取一个号段
 * - 在内存中分配ID
 * - 号段用完再获取新号段
 */
public class SegmentMode {
    
    /**
     * 号段表设计
     */
    /*
    CREATE TABLE `id_segment` (
        `biz_type` varchar(64) NOT NULL COMMENT '业务类型',
        `max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
        `step` int(11) NOT NULL COMMENT '步长',
        `version` int(11) NOT NULL COMMENT '版本号(乐观锁)',
        PRIMARY KEY (`biz_type`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    */
    
    /**
     * 号段服务
     */
    @Service
    public class SegmentIDService {
        
        @Autowired
        private JdbcTemplate jdbcTemplate;
        
        // 内存中的号段
        private final Map<String, Segment> segments = new ConcurrentHashMap<>();
        
        /**
         * 获取ID
         */
        public synchronized long nextId(String bizType) {
            Segment segment = segments.get(bizType);
            
            // 如果号段为空或已用完,获取新号段
            if (segment == null || !segment.hasNext()) {
                segment = loadSegment(bizType);
                segments.put(bizType, segment);
            }
            
            return segment.nextId();
        }
        
        /**
         * 从数据库加载号段
         */
        private Segment loadSegment(String bizType) {
            // 使用乐观锁更新
            String sql = "UPDATE id_segment SET max_id = max_id + step, " +
                        "version = version + 1 " +
                        "WHERE biz_type = ? AND version = ?";
            
            while (true) {
                // 先查询当前状态
                Map<String, Object> current = jdbcTemplate.queryForMap(
                    "SELECT * FROM id_segment WHERE biz_type = ?", bizType);
                
                long maxId = (Long) current.get("max_id");
                int step = (Integer) current.get("step");
                int version = (Integer) current.get("version");
                
                // 更新
                int updated = jdbcTemplate.update(sql, bizType, version);
                
                if (updated > 0) {
                    // 更新成功,返回新号段
                    return new Segment(maxId, maxId + step);
                }
                
                // 乐观锁冲突,重试
            }
        }
        
        /**
         * 号段对象
         */
        @Data
        @AllArgsConstructor
        private static class Segment {
            private long startId;
            private long endId;
            private AtomicLong currentId;
            
            public Segment(long startId, long endId) {
                this.startId = startId;
                this.endId = endId;
                this.currentId = new AtomicLong(startId);
            }
            
            public boolean hasNext() {
                return currentId.get() < endId;
            }
            
            public long nextId() {
                return currentId.getAndIncrement();
            }
        }
    }
    
    /**
     * 双Buffer优化
     */
    @Service
    public class DoubleBufferSegmentService {
        
        private final Map<String, SegmentBuffer> buffers = 
            new ConcurrentHashMap<>();
        
        /**
         * 获取ID
         */
        public long nextId(String bizType) {
            SegmentBuffer buffer = buffers.computeIfAbsent(
                bizType, k -> new SegmentBuffer());
            
            return buffer.nextId();
        }
        
        /**
         * 双Buffer
         */
        private class SegmentBuffer {
            private Segment[] segments = new Segment[2];
            private volatile int currentPos = 0;  // 当前使用的buffer
            private volatile boolean nextReady = false;  // 下一个buffer是否准备好
            private volatile boolean isLoading = false;  // 是否正在加载
            
            public synchronized long nextId() {
                Segment current = segments[currentPos];
                
                // 当前buffer使用超过10%,异步加载下一个
                if (current != null && 
                    current.getUsedPercent() > 0.1 && 
                    !nextReady && !isLoading) {
                    
                    isLoading = true;
                    asyncLoadNext();
                }
                
                // 当前buffer用完,切换到下一个
                if (current == null || !current.hasNext()) {
                    if (!nextReady) {
                        // 同步加载
                        loadNext();
                    }
                    
                    // 切换
                    currentPos = 1 - currentPos;
                    current = segments[currentPos];
                    nextReady = false;
                }
                
                return current.nextId();
            }
            
            private void asyncLoadNext() {
                CompletableFuture.runAsync(() -> {
                    loadNext();
                    nextReady = true;
                    isLoading = false;
                });
            }
            
            private void loadNext() {
                // 加载新号段
                int nextPos = 1 - currentPos;
                segments[nextPos] = loadSegmentFromDB();
            }
        }
    }
}

五、雪花算法

5.1 雪花算法原理

/**
 * 雪花算法(Snowflake)
 * 
 * Twitter开源的分布式ID生成算法
 * 64位Long类型ID
 */
public class SnowflakeAlgorithm {
    
    /*
    雪花算法ID结构(64位):
    
    0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
    │   │                                                   │       │       │
    │   │                                                   │       │       └─ 12位序列号(毫秒内序列)
    │   │                                                   │       └───────── 5位机器ID(0-31)
    │   │                                                   └───────────────── 5位数据中心ID(0-31)
    │   └───────────────────────────────────────────────────────────────────────── 41位时间戳(毫秒级)
    └───────────────────────────────────────────────────────────────────────────────── 1位符号位(始终为0)
    
    总计:1 + 41 + 5 + 5 + 12 = 64位
    
    时间戳:41位,可用约69年
    数据中心ID:5位,最大32个数据中心
    机器ID:5位,每个数据中心最多32台机器
    序列号:12位,每毫秒最多4096个ID
    
    理论QPS:409.6万/秒
    */
    
    /**
     * 雪花算法实现
     */
    public class SnowflakeIdGenerator {
        
        // 起始时间戳(2024-01-01)
        private final long twepoch = 1704038400000L;
        
        // 位数分配
        private final long workerIdBits = 5L;
        private final long datacenterIdBits = 5L;
        private final long sequenceBits = 12L;
        
        // 最大值
        private final long maxWorkerId = ~(-1L << workerIdBits);          // 31
        private final long maxDatacenterId = ~(-1L << datacenterIdBits);  // 31
        private final long sequenceMask = ~(-1L << sequenceBits);         // 4095
        
        // 位移
        private final long workerIdShift = sequenceBits;
        private final long datacenterIdShift = sequenceBits + workerIdBits;
        private final long timestampLeftShift = 
            sequenceBits + workerIdBits + datacenterIdBits;
        
        // 工作机器ID
        private final long workerId;
        private final long datacenterId;
        
        // 序列号和上次时间戳
        private long sequence = 0L;
        private long lastTimestamp = -1L;
        
        public SnowflakeIdGenerator(long workerId, long datacenterId) {
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(
                    "worker Id can't be greater than %d or less than 0");
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(
                    "datacenter Id can't be greater than %d or less than 0");
            }
            
            this.workerId = workerId;
            this.datacenterId = datacenterId;
        }
        
        /**
         * 生成ID(线程安全)
         */
        public synchronized long nextId() {
            long timestamp = timeGen();
            
            // 时钟回拨检测
            if (timestamp < lastTimestamp) {
                throw new RuntimeException(
                    "Clock moved backwards, refusing to generate id");
            }
            
            // 同一毫秒内
            if (timestamp == lastTimestamp) {
                sequence = (sequence + 1) & sequenceMask;
                
                // 序列号溢出,等待下一毫秒
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                // 新毫秒,序列号重置
                sequence = 0L;
            }
            
            lastTimestamp = timestamp;
            
            // 组装ID
            return ((timestamp - twepoch) << timestampLeftShift) |
                   (datacenterId << datacenterIdShift) |
                   (workerId << workerIdShift) |
                   sequence;
        }
        
        /**
         * 等待下一毫秒
         */
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
        
        /**
         * 获取当前时间戳
         */
        private long timeGen() {
            return System.currentTimeMillis();
        }
        
        /**
         * 解析ID
         */
        public IdInfo parseId(long id) {
            long timestamp = (id >>> timestampLeftShift) + twepoch;
            long datacenter = (id >>> datacenterIdShift) & maxDatacenterId;
            long worker = (id >>> workerIdShift) & maxWorkerId;
            long seq = id & sequenceMask;
            
            return new IdInfo(timestamp, datacenter, worker, seq);
        }
        
        @Data
        @AllArgsConstructor
        public static class IdInfo {
            private long timestamp;
            private long datacenterId;
            private long workerId;
            private long sequence;
        }
    }
    
    /**
     * 使用示例
     */
    public void usage() {
        // 创建生成器
        SnowflakeIdGenerator generator = 
            new SnowflakeIdGenerator(1, 1);
        
        // 生成ID
        long id = generator.nextId();
        System.out.println("ID: " + id);
        
        // 解析ID
        IdInfo info = generator.parseId(id);
        System.out.println("解析结果: " + info);
    }
}

5.2 时钟回拨问题

/**
 * 时钟回拨问题及解决方案
 */
public class ClockBackwardSolution {
    
    /**
     * 时钟回拨原因
     */
    // 1. NTP时间同步
    // 2. 人工调整时间
    // 3. 虚拟机时间漂移
    // 4. 硬件时钟问题
    
    /**
     * 方案1:等待时间追上
     */
    public class WaitSolution extends SnowflakeIdGenerator {
        
        private static final long MAX_BACKWARD_MS = 5;  // 最大容忍回拨时间
        
        @Override
        public synchronized long nextId() {
            long timestamp = timeGen();
            
            if (timestamp < lastTimestamp) {
                long offset = lastTimestamp - timestamp;
                
                if (offset <= MAX_BACKWARD_MS) {
                    // 小幅回拨,等待
                    try {
                        Thread.sleep(offset);
                        timestamp = timeGen();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    // 大幅回拨,拒绝
                    throw new RuntimeException("Clock moved backwards");
                }
            }
            
            // 正常生成逻辑...
            return generateId(timestamp);
        }
    }
    
    /**
     * 方案2:使用预留序号
     */
    public class ReservedSequenceSolution extends SnowflakeIdGenerator {
        
        // 上一次时间戳对应的最大序列号
        private long lastMaxSequence = 0;
        
        @Override
        public synchronized long nextId() {
            long timestamp = timeGen();
            
            if (timestamp < lastTimestamp) {
                // 时钟回拨,使用之前预留的序列号
                if (lastMaxSequence < sequenceMask) {
                    sequence = lastMaxSequence + 1;
                    return generateId(timestamp);
                }
                
                // 序列号用完,抛异常
                throw new RuntimeException("Clock moved backwards and sequence exhausted");
            }
            
            long id = generateId(timestamp);
            
            // 正常情况下,预留当前时间戳的序列号范围
            if (timestamp > lastTimestamp) {
                lastMaxSequence = 0;
            }
            
            return id;
        }
    }
    
    /**
     * 方案3:动态调整机器ID
     */
    public class DynamicWorkerIdSolution {
        
        private final WorkerIdAllocator workerIdAllocator;
        private volatile long workerId;
        
        public synchronized long nextId() {
            long timestamp = timeGen();
            
            if (timestamp < lastTimestamp) {
                // 时钟回拨,重新分配workerId
                workerId = workerIdAllocator.allocate();
                // 重置lastTimestamp
                lastTimestamp = timestamp;
            }
            
            // 正常生成...
            return generateId(timestamp);
        }
    }
    
    /**
     * 方案4:Baidu UidGenerator方案
     * 使用时间戳秒级而非毫秒,增加序列号位数
     */
    public class UidGeneratorSolution {
        
        // 时间戳改为秒级,节省位数
        // 序列号从12位增加到22位
        // 可承受1秒的时钟回拨
    }
}

5.3 机器ID分配

/**
 * 机器ID分配方案
 */
public class WorkerIdAllocation {
    
    /**
     * 方案1:配置文件
     */
    // 每台机器配置不同的workerId
    // 缺点:运维成本高,容易冲突
    
    /**
     * 方案2:数据库分配
     */
    @Service
    public class DatabaseWorkerIdAllocator implements WorkerIdAllocator {
        
        @Autowired
        private JdbcTemplate jdbcTemplate;
        
        /*
        CREATE TABLE `worker_id_allocator` (
            `id` bigint(20) NOT NULL AUTO_INCREMENT,
            `ip` varchar(64) NOT NULL COMMENT '机器IP',
            `worker_id` int(11) NOT NULL COMMENT '分配的workerId',
            `create_time` datetime NOT NULL,
            `expire_time` datetime NOT NULL COMMENT '过期时间',
            PRIMARY KEY (`id`),
            UNIQUE KEY `uk_worker_id` (`worker_id`),
            UNIQUE KEY `uk_ip` (`ip`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        */
        
        @Override
        public long allocate() {
            String ip = getLocalIp();
            
            // 尝试获取已分配的
            Long workerId = getExistingWorkerId(ip);
            if (workerId != null) {
                renewWorkerId(ip);
                return workerId;
            }
            
            // 分配新的
            return allocateNewWorkerId(ip);
        }
        
        private Long getExistingWorkerId(String ip) {
            try {
                return jdbcTemplate.queryForObject(
                    "SELECT worker_id FROM worker_id_allocator " +
                    "WHERE ip = ? AND expire_time > NOW()",
                    Long.class, ip);
            } catch (EmptyResultDataAccessException e) {
                return null;
            }
        }
        
        private long allocateNewWorkerId(String ip) {
            // 寻找未使用的workerId
            for (int i = 0; i < 32; i++) {
                try {
                    jdbcTemplate.update(
                        "INSERT INTO worker_id_allocator " +
                        "(ip, worker_id, create_time, expire_time) " +
                        "VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 1 DAY))",
                        ip, i);
                    return i;
                } catch (DuplicateKeyException e) {
                    // workerId已被占用,继续尝试
                    continue;
                }
            }
            throw new RuntimeException("No available workerId");
        }
        
        private void renewWorkerId(String ip) {
            jdbcTemplate.update(
                "UPDATE worker_id_allocator SET expire_time = " +
                "DATE_ADD(NOW(), INTERVAL 1 DAY) WHERE ip = ?", ip);
        }
    }
    
    /**
     * 方案3:Redis分配
     */
    @Service
    public class RedisWorkerIdAllocator implements WorkerIdAllocator {
        
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
        
        private static final String WORKER_ID_KEY = "snowflake:worker_id";
        private static final int MAX_WORKER_ID = 31;
        
        @Override
        public long allocate() {
            String ip = getLocalIp();
            String key = WORKER_ID_KEY + ":" + ip;
            
            // 尝试获取已有的
            String existing = redisTemplate.opsForValue().get(key);
            if (existing != null) {
                // 续期
                redisTemplate.expire(key, 1, TimeUnit.DAYS);
                return Long.parseLong(existing);
            }
            
            // 分配新的
            for (int i = 0; i <= MAX_WORKER_ID; i++) {
                Boolean success = redisTemplate.opsForValue()
                    .setIfAbsent(WORKER_ID_KEY + ":slot:" + i, ip, 
                                 1, TimeUnit.DAYS);
                if (Boolean.TRUE.equals(success)) {
                    // 绑定IP
                    redisTemplate.opsForValue()
                        .set(key, String.valueOf(i), 1, TimeUnit.DAYS);
                    return i;
                }
            }
            
            throw new RuntimeException("No available workerId");
        }
    }
    
    /**
     * 方案4:Zookeeper分配
     */
    @Service
    public class ZkWorkerIdAllocator implements WorkerIdAllocator {
        
        @Autowired
        private CuratorFramework zkClient;
        
        private static final String PATH = "/snowflake/worker_id";
        
        @Override
        public long allocate() {
            try {
                // 创建临时顺序节点
                String node = zkClient.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(PATH + "/worker-");
                
                // 获取序号
                String workerIdStr = node.substring(node.lastIndexOf('-') + 1);
                long workerId = Long.parseLong(workerIdStr) % 32;
                
                return workerId;
            } catch (Exception e) {
                throw new RuntimeException("Failed to allocate workerId", e);
            }
        }
    }
}

六、Leaf方案

6.1 Leaf 简介

/**
 * Leaf - 美团分布式ID生成系统
 * 
 * 提供两种模式:
 * 1. Leaf-segment:号段模式,适合对ID连续性有要求的场景
 * 2. Leaf-snowflake:雪花算法模式,适合对性能要求高的场景
 */

6.2 Leaf-segment 实现

/**
 * Leaf-segment 号段模式实现
 */
@Service
public class LeafSegmentService {
    
    /**
     * 号段表设计
     */
    /*
    CREATE TABLE `leaf_segment` (
        `biz_tag` varchar(128) NOT NULL COMMENT '业务标识',
        `max_id` bigint(20) NOT NULL COMMENT '当前最大ID',
        `step` int(11) NOT NULL COMMENT '步长',
        `description` varchar(256) DEFAULT NULL,
        `update_time` datetime NOT NULL,
        PRIMARY KEY (`biz_tag`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    */
    
    /**
     * 双Buffer号段
     */
    @Data
    public class SegmentBuffer {
        
        private String key;                    // 业务标识
        private Segment[] segments = new Segment[2];  // 双buffer
        private volatile int currentPos = 0;   // 当前使用的buffer索引
        private volatile boolean nextReady = false;  // 下一个buffer是否准备好
        private volatile boolean loading = false;    // 是否正在加载
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        
        public Segment getCurrent() {
            return segments[currentPos];
        }
        
        public Segment getNext() {
            return segments[1 - currentPos];
        }
        
        public void switchBuffer() {
            currentPos = 1 - currentPos;
        }
    }
    
    @Data
    public class Segment {
        private AtomicLong value;    // 当前值
        private long max;            // 最大值
        private long step;           // 步长
        private volatile boolean initialized = false;
        
        public Segment(long start, long step) {
            this.value = new AtomicLong(start);
            this.max = start + step;
            this.step = step;
            this.initialized = true;
        }
        
        public boolean hasNext() {
            return value.get() < max;
        }
        
        public long nextId() {
            return value.getAndIncrement();
        }
        
        public double getUsedPercent() {
            return (value.get() - (max - step)) * 1.0 / step;
        }
    }
    
    private final Map<String, SegmentBuffer> buffers = 
        new ConcurrentHashMap<>();
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 获取ID
     */
    public long nextId(String key) {
        SegmentBuffer buffer = buffers.computeIfAbsent(
            key, k -> initBuffer(k));
        
        return getIdFromBuffer(buffer);
    }
    
    private long getIdFromBuffer(SegmentBuffer buffer) {
        while (true) {
            buffer.lock.readLock().lock();
            try {
                Segment current = buffer.getCurrent();
                
                if (!current.initialized) {
                    // 当前buffer未初始化,等待
                    continue;
                }
                
                // 当前buffer有剩余
                if (current.hasNext()) {
                    long id = current.nextId();
                    
                    // 使用超过10%,异步加载下一个buffer
                    if (!buffer.nextReady && 
                        !buffer.loading && 
                        current.getUsedPercent() > 0.1) {
                        
                        buffer.loading = true;
                        asyncLoadNextBuffer(buffer);
                    }
                    
                    return id;
                }
                
                // 当前buffer用完,检查下一个buffer是否准备好
                if (buffer.nextReady) {
                    buffer.lock.readLock().unlock();
                    buffer.lock.writeLock().lock();
                    try {
                        // 切换buffer
                        buffer.switchBuffer();
                        buffer.nextReady = false;
                        buffer.loading = false;
                    } finally {
                        buffer.lock.readLock().lock();
                        buffer.lock.writeLock().unlock();
                    }
                    continue;
                }
                
                // 下一个buffer也未准备好,同步加载
                return loadAndGetId(buffer);
                
            } finally {
                buffer.lock.readLock().unlock();
            }
        }
    }
    
    /**
     * 异步加载下一个buffer
     */
    private void asyncLoadNextBuffer(SegmentBuffer buffer) {
        CompletableFuture.runAsync(() -> {
            try {
                Segment next = loadSegment(buffer.getKey());
                buffer.lock.writeLock().lock();
                try {
                    buffer.segments[1 - buffer.currentPos] = next;
                    buffer.nextReady = true;
                } finally {
                    buffer.lock.writeLock().unlock();
                }
            } catch (Exception e) {
                // 加载失败,下次重试
                buffer.loading = false;
            }
        });
    }
    
    /**
     * 同步加载并获取ID
     */
    private long loadAndGetId(SegmentBuffer buffer) {
        buffer.lock.readLock().unlock();
        buffer.lock.writeLock().lock();
        try {
            // 再次检查
            Segment current = buffer.getCurrent();
            if (current.hasNext()) {
                return current.nextId();
            }
            
            // 加载新号段
            Segment segment = loadSegment(buffer.getKey());
            buffer.segments[buffer.currentPos] = segment;
            
            return segment.nextId();
        } finally {
            buffer.lock.readLock().lock();
            buffer.lock.writeLock().unlock();
        }
    }
    
    /**
     * 从数据库加载号段
     */
    private Segment loadSegment(String key) {
        // 更新数据库
        jdbcTemplate.update(
            "UPDATE leaf_segment SET max_id = max_id + step, " +
            "update_time = NOW() WHERE biz_tag = ?", key);
        
        // 读取新号段
        Map<String, Object> result = jdbcTemplate.queryForMap(
            "SELECT max_id, step FROM leaf_segment WHERE biz_tag = ?", key);
        
        long maxId = (Long) result.get("max_id");
        int step = (Integer) result.get("step");
        
        return new Segment(maxId - step, step);
    }
    
    private SegmentBuffer initBuffer(String key) {
        SegmentBuffer buffer = new SegmentBuffer();
        buffer.setKey(key);
        buffer.segments[0] = loadSegment(key);
        return buffer;
    }
}

6.3 Leaf-snowflake 实现

/**
 * Leaf-snowflake 雪花算法实现
 */
@Service
public class LeafSnowflakeService {
    
    // 使用Zookeeper管理workerId
    
    @Autowired
    private CuratorFramework zkClient;
    
    private static final String ZK_PATH = "/leaf/snowflake";
    
    private SnowflakeIdGenerator generator;
    
    @PostConstruct
    public void init() throws Exception {
        // 获取workerId
        long workerId = getWorkerId();
        
        // 初始化生成器
        generator = new SnowflakeIdGenerator(workerId, 0);
    }
    
    /**
     * 从Zookeeper获取workerId
     */
    private long getWorkerId() throws Exception {
        // 获取本机IP
        String ip = getLocalIp();
        
        // 创建临时节点
        String node = zkClient.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(ZK_PATH + "/" + ip + "-");
        
        // 解析workerId
        String workerIdStr = node.substring(node.lastIndexOf('-') + 1);
        return Long.parseLong(workerIdStr) % 32;
    }
    
    /**
     * 生成ID
     */
    public long nextId() {
        return generator.nextId();
    }
}

七、最佳实践与选型

7.1 选型建议

/**
 * 分布式ID选型建议
 */
public class IDSelectionGuide {
    
    /**
     * 选型决策树
     */
    public String selectIdScheme(Requirement req) {
        
        // 1. 是否需要作为数据库主键?
        if (req.isPrimaryKey()) {
            // 需要趋势递增
            if (req.getQps() < 1000) {
                // QPS较低,使用号段模式
                return "Segment";
            } else {
                // QPS较高,使用雪花算法
                if (req.canTolerateClockBackward()) {
                    return "Snowflake";
                } else {
                    // 对时钟敏感,使用Leaf
                    return "Leaf-Segment";
                }
            }
        } else {
            // 不作为主键
            if (req.needSort()) {
                // 需要排序
                return "Snowflake";
            } else {
                // 不需要排序,UUID即可
                return "UUID";
            }
        }
    }
    
    /**
     * 各方案适用场景
     */
    /*
    ┌─────────────────────────────────────────────────────────────────────────┐
    │                        分布式ID方案适用场景                               │
    ├──────────────┬──────────────────────────────────────────────────────────┤
    │     方案     │                      适用场景                             │
    ├──────────────┼──────────────────────────────────────────────────────────┤
    │ UUID         │ 非主键、无需排序、简单场景                                 │
    │ 数据库自增   │ 单机、小规模、已有数据库依赖                               │
    │ 号段模式     │ 中等规模、需要趋势递增、对可用性有一定要求                  │
    │ 雪花算法     │ 大规模分布式、高并发、能容忍一定程度的时钟回拨              │
    │ Leaf         │ 大规模、高可用、需要完善的监控和管理                        │
    └──────────────┴──────────────────────────────────────────────────────────┘
    */
}

7.2 注意事项

/**
 * 分布式ID使用注意事项
 */
public class IDBestPractices {
    
    /**
     * 1. 时钟同步
     */
    // 使用雪花算法时,必须保证时钟同步
    // 配置NTP服务
    // 监控时钟偏移
    
    /**
     * 2. 机器ID管理
     */
    // 确保机器ID不重复
    // 支持动态分配和回收
    // 容器环境需要特别注意
    
    /**
     * 3. 监控告警
     */
    // ID生成QPS监控
    // 号段使用率监控
    // 时钟回拨告警
    
    /**
     * 4. 容灾设计
     */
    // 多机房部署
    // 故障自动切换
    // 数据持久化
    
    /**
     * 5. 性能优化
     */
    // 本地缓存
    // 批量获取
    // 异步加载
}

六、思考与练习

思考题

  1. 基础题:UUID作为主键存在哪些性能问题?为什么InnoDB引擎对UUID主键不友好?

  2. 进阶题:雪花算法的时间戳只有41位,可用约69年。如果系统需要运行更长时间,有哪些解决方案?

  3. 实战题:在Kubernetes容器环境中部署使用雪花算法的服务,workerId如何动态分配和管理?请设计一个完整方案。

编程练习

练习:实现一个完整的雪花算法ID生成器,包含:(1) 时钟回拨检测与处理;(2) workerId自动分配(基于Redis或Zookeeper);(3) ID解析工具,能从ID中提取时间戳、机器ID等信息。

章节关联

  • 前置章节:CAP与BASE理论详解
  • 后续章节:分布式锁实现详解
  • 扩展阅读:Twitter Snowflake源码、美团Leaf技术博客

📝 下一章预告

当多个进程同时访问共享资源时,如何保证数据一致性?下一章将深入讲解分布式锁的实现原理,包括数据库锁、Redis锁、Zookeeper锁等主流方案。


本章完


参考资料: