【架构实战】分布式ID生成方案(雪花/Leaf/美团)

一、为什么需要分布式ID

在分布式系统中,数据库自增ID已经无法满足需求:

自增ID的问题:

  • 分库分表后ID冲突:不同库的自增ID会重复
  • 暴露业务信息:连续ID暴露订单量等敏感信息
  • 单点瓶颈:依赖数据库生成ID,性能受限
  • 无法排序:无法通过ID判断创建时间

分布式ID的要求:

  • 全局唯一:不同机器生成的ID不重复
  • 趋势递增:便于数据库索引优化
  • 高性能:每秒生成百万级ID
  • 高可用:不依赖单点
  • 信息安全:不暴露业务信息

二、雪花算法(Snowflake)

1. 算法原理

复制代码
64位ID结构:
┌─────────────────────────────────────────────────────────────────┐
│ 0 │ 41位时间戳 │ 10位机器ID │ 12位序列号 │
└─────────────────────────────────────────────────────────────────┘
  1位    41位         10位          12位
符号位  毫秒时间戳   机器标识      序列号

各部分说明:

  • 1位符号位:固定为0,保证ID为正数
  • 41位时间戳:毫秒级,可用约69年
  • 10位机器ID:支持1024台机器(5位数据中心+5位机器)
  • 12位序列号:同一毫秒内最多生成4096个ID

理论最大QPS: 4096 × 1000 = 4,096,000/秒

2. Java实现

java 复制代码
public class SnowflakeIdGenerator {
    
    // 开始时间戳(2020-01-01 00:00:00)
    private static final long START_TIMESTAMP = 1577836800000L;
    
    // 各部分占用位数
    private static final long SEQUENCE_BITS = 12L;
    private static final long MACHINE_BITS = 5L;
    private static final long DATACENTER_BITS = 5L;
    
    // 最大值
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);       // 4095
    private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_BITS);      // 31
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_BITS); // 31
    
    // 位移量
    private static final long MACHINE_SHIFT = SEQUENCE_BITS;
    private static final long DATACENTER_SHIFT = SEQUENCE_BITS + MACHINE_BITS;
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS + DATACENTER_BITS;
    
    private final long datacenterId;
    private final long machineId;
    
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId超出范围");
        }
        if (machineId > MAX_MACHINE_ID || machineId < 0) {
            throw new IllegalArgumentException("machineId超出范围");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }
    
    public synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();
        
        // 时钟回拨检测
        if (currentTimestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format("时钟回拨,拒绝生成ID,回拨时间:%d毫秒", 
                    lastTimestamp - currentTimestamp));
        }
        
        if (currentTimestamp == lastTimestamp) {
            // 同一毫秒内,序列号递增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            
            if (sequence == 0) {
                // 序列号溢出,等待下一毫秒
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒,序列号重置
            sequence = 0L;
        }
        
        lastTimestamp = currentTimestamp;
        
        // 组装ID
        return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
                | (datacenterId << DATACENTER_SHIFT)
                | (machineId << MACHINE_SHIFT)
                | sequence;
    }
    
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
    
    // 解析ID
    public static Map<String, Long> parseId(long id) {
        Map<String, Long> result = new HashMap<>();
        result.put("timestamp", (id >> TIMESTAMP_SHIFT) + START_TIMESTAMP);
        result.put("datacenterId", (id >> DATACENTER_SHIFT) & MAX_DATACENTER_ID);
        result.put("machineId", (id >> MACHINE_SHIFT) & MAX_MACHINE_ID);
        result.put("sequence", id & MAX_SEQUENCE);
        return result;
    }
}

Spring Boot集成:

java 复制代码
@Configuration
public class SnowflakeConfig {
    
    @Value("${snowflake.datacenter-id:1}")
    private long datacenterId;
    
    @Value("${snowflake.machine-id:1}")
    private long machineId;
    
    @Bean
    public SnowflakeIdGenerator snowflakeIdGenerator() {
        return new SnowflakeIdGenerator(datacenterId, machineId);
    }
}

@Service
public class OrderService {
    
    @Autowired
    private SnowflakeIdGenerator idGenerator;
    
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setId(idGenerator.nextId());  // 生成分布式ID
        order.setUserId(request.getUserId());
        // ...
        return order;
    }
}

3. 时钟回拨问题

问题: 服务器时钟回拨会导致ID重复

解决方案:

java 复制代码
public synchronized long nextId() {
    long currentTimestamp = System.currentTimeMillis();
    
    if (currentTimestamp < lastTimestamp) {
        long offset = lastTimestamp - currentTimestamp;
        
        if (offset <= 5) {
            // 回拨时间较短,等待时钟追上
            try {
                Thread.sleep(offset << 1);
                currentTimestamp = System.currentTimeMillis();
                if (currentTimestamp < lastTimestamp) {
                    throw new RuntimeException("时钟回拨,无法生成ID");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("等待时钟恢复被中断");
            }
        } else {
            // 回拨时间较长,直接报错
            throw new RuntimeException(
                String.format("时钟回拨超过5ms,回拨时间:%d ms", offset));
        }
    }
    
    // ... 其余逻辑
}

三、美团Leaf

1. Leaf-Segment(号段模式)

原理:

复制代码
数据库存储号段信息:
┌──────────────┬──────────┬──────────┬──────────┐
│ biz_tag      │ max_id   │ step     │ version  │
├──────────────┼──────────┼──────────┼──────────┤
│ order        │ 1000000  │ 1000     │ 1        │
│ user         │ 500000   │ 500      │ 1        │
└──────────────┴──────────┴──────────┴──────────┘

每次从数据库取一段号段(如1000个),在内存中分配
当号段用完时,再从数据库取下一段

数据库表:

sql 复制代码
CREATE TABLE leaf_alloc (
    biz_tag VARCHAR(128) NOT NULL DEFAULT '',
    max_id BIGINT NOT NULL DEFAULT 1,
    step INT NOT NULL,
    description VARCHAR(256) DEFAULT NULL,
    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (biz_tag)
) ENGINE=InnoDB;

-- 初始化数据
INSERT INTO leaf_alloc(biz_tag, max_id, step, description) 
VALUES ('order', 1, 2000, '订单ID');

Java实现:

java 复制代码
@Service
public class LeafSegmentService {
    
    @Autowired
    private LeafAllocMapper leafAllocMapper;
    
    private Map<String, SegmentBuffer> cache = new ConcurrentHashMap<>();
    
    public long nextId(String bizTag) {
        SegmentBuffer buffer = cache.computeIfAbsent(bizTag, 
            k -> new SegmentBuffer(k));
        
        return buffer.nextId();
    }
    
    private class SegmentBuffer {
        private final String bizTag;
        private volatile Segment current;
        private volatile Segment next;
        private volatile boolean isPreloading = false;
        
        public SegmentBuffer(String bizTag) {
            this.bizTag = bizTag;
            this.current = loadSegment();
        }
        
        public synchronized long nextId() {
            // 当前号段使用超过10%,预加载下一个号段
            if (!isPreloading && current.getUsagePercent() > 0.1) {
                isPreloading = true;
                CompletableFuture.runAsync(() -> {
                    next = loadSegment();
                    isPreloading = false;
                });
            }
            
            // 当前号段用完,切换到下一个
            if (current.isExhausted()) {
                if (next != null) {
                    current = next;
                    next = null;
                } else {
                    current = loadSegment();
                }
            }
            
            return current.nextId();
        }
        
        private Segment loadSegment() {
            // 从数据库获取号段
            LeafAlloc alloc = leafAllocMapper.updateMaxIdAndGetLeafAlloc(bizTag);
            long min = alloc.getMaxId() - alloc.getStep() + 1;
            long max = alloc.getMaxId();
            return new Segment(min, max);
        }
    }
}

2. Leaf-Snowflake(雪花模式)

改进点: 使用Zookeeper解决机器ID分配问题

java 复制代码
@Component
public class LeafSnowflakeService {
    
    @Autowired
    private ZookeeperClient zkClient;
    
    private long workerId;
    
    @PostConstruct
    public void init() {
        // 从Zookeeper获取workerId
        String path = "/leaf/snowflake/" + getLocalIp();
        
        if (zkClient.exists(path)) {
            // 已注册,读取workerId
            workerId = Long.parseLong(zkClient.getData(path));
        } else {
            // 新注册,分配workerId
            workerId = zkClient.createSequential("/leaf/snowflake/worker-");
        }
        
        log.info("Leaf-Snowflake workerId: {}", workerId);
    }
    
    public long nextId() {
        return snowflake.nextId(workerId);
    }
}

四、百度UidGenerator

1. 原理

复制代码
64位ID结构(可配置):
┌──────────────────────────────────────────────────────────────────┐
│ 1位 │ 28位时间戳 │ 22位机器ID │ 13位序列号 │
└──────────────────────────────────────────────────────────────────┘

特点:

  • 时间戳精度可配置(秒级)
  • 机器ID通过数据库分配
  • 支持RingBuffer缓存,性能极高

2. 集成

xml 复制代码
<dependency>
    <groupId>com.baidu.fsg</groupId>
    <artifactId>uid-generator</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private UidGenerator uidGenerator;
    
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setId(uidGenerator.getUID());
        // ...
        return order;
    }
}

五、各方案对比

方案 性能 依赖 时钟回拨 趋势递增 适用场景
雪花算法 极高 有风险 通用
Leaf-Segment MySQL 号段分配
Leaf-Snowflake 极高 ZK 有处理 通用
UidGenerator 极高 MySQL 高性能
Redis自增 Redis 简单场景
UUID 不推荐

六、Redis实现分布式ID

java 复制代码
@Service
public class RedisIdGenerator {
    
    @Autowired
    private RedisTemplate<String, Long> redisTemplate;
    
    public long nextId(String bizTag) {
        String key = "id:" + bizTag + ":" + LocalDate.now().toString();
        
        // 原子自增
        Long id = redisTemplate.opsForValue().increment(key);
        
        // 设置过期时间(防止key无限增长)
        redisTemplate.expire(key, 1, TimeUnit.DAYS);
        
        // 拼接时间戳,保证全局唯一
        long timestamp = System.currentTimeMillis();
        return timestamp * 1000000 + id;
    }
}

七、总结

分布式ID是分布式系统的基础组件:

  • 雪花算法:无依赖,高性能,注意时钟回拨
  • Leaf-Segment:号段模式,依赖MySQL,稳定可靠
  • Leaf-Snowflake:雪花改进版,依赖ZK,解决机器ID问题
  • UidGenerator:百度开源,性能极高

选型建议:

  1. 简单场景:雪花算法
  2. 需要号段分配:Leaf-Segment
  3. 高性能场景:UidGenerator

思考题:你们系统用的什么分布式ID方案?有没有遇到过ID冲突的问题?


个人观点,仅供参考

相关推荐
清水白石0087 小时前
Python 编程实战全景:从基础语法到插件架构、异步性能与工程最佳实践
开发语言·python·架构
ting94520008 小时前
HunyuanOCR 全方位深度解析
人工智能·架构
heimeiyingwang11 小时前
【架构实战】CQRS架构模式实战
架构
技术传感器11 小时前
Hermes为什么开始像基础设施:11万星、RCE修复与生态接入
人工智能·安全·架构·aigc
执于代码11 小时前
智能客服的agent 的架构和作用以及源码分析
架构
AI创界者13 小时前
【独家解析】Ernie-Image-AIO-Rapid一键部署本地运行整合包:深度融合架构如何重塑AI绘图效率?4K超分与硬件适配全指南
人工智能·架构
weisian15114 小时前
Java并发编程--45-分布式一致性协议入门:Raft、Paxos与ZAB的核心思想
java·分布式·raft·paxos·zab
BullSmall14 小时前
Redis 双机部署 完整方案(两种架构,适配两台机器)
java·redis·架构
juniperhan15 小时前
Flink 系列第17篇:Flink Table&SQL 核心概念、原理与实战详解
大数据·数据仓库·分布式·sql·flink
SamDeepThinking16 小时前
适合中小型企业的出口入口网关微服务
java·后端·架构