Java分布式ID案例如何生成

wen java案例 29

本文目录导读:

Java分布式ID案例如何生成

  1. 方案一:基于 UUID(简单但无序)
  2. 方案二:基于数据库自增 ID(经典但依赖 DB)🚀
  3. 方案三:基于 Redis INCR 命令(性能好,适合中小应用)
  4. 方案四:雪花算法(Snowflake,最主流推荐)
  5. 方案五:美团 Leaf(工业级方案)
  6. 总结:如何选择?

Java 分布式 ID 的生成是分布式系统设计中的一个经典问题,主要需要解决 全局唯一性趋势递增(便于数据库索引)、高可用高性能

以下是几种主流的 Java 分布式 ID 生成方案及案例代码,从简单到复杂依次介绍:

基于 UUID(简单但无序)

UUID 是 Java 内置的,生成简单,但缺点明显:无序过长无业务含义,不适合做数据库主键(B+树索引维护代价高)。

import java.util.UUID;
public class UUIDGenerator {
    // 生成无横线的32位字符串
    public static String generate() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    public static void main(String[] args) {
        System.out.println(generate());
        // 输出类似:a1b2c3d4e5f6789012345678abcdef01
    }
}

基于数据库自增 ID(经典但依赖 DB)🚀

利用数据库的 auto_incrementsequence,适用于小规模或对性能要求不高的场景。

1 单库步长模式(避免每次查询)

-- MySQL 示例:设置步长 10,起始值
-- 每台机器配置不同的起始值,避免冲突
CREATE TABLE id_generator (
    id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    stub CHAR(1) NOT NULL DEFAULT '' UNIQUE KEY
);
-- 每次插入获取一个区间
REPLACE INTO id_generator (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

2 号段模式(Leaf 方案)

这是最常见的数据库优化方案:一次获取一段 ID(如 1000 个),在内存中分发,避免了每次请求都访问数据库。

public class SegmentIdGenerator {
    private static final int STEP = 1000; // 步长
    private long currentId; // 当前ID
    private long maxId;      // 当前区段上限
    private DataSource dataSource;
    public SegmentIdGenerator(DataSource dataSource) {
        this.dataSource = dataSource;
        this.currentId = 0;
        this.maxId = 0;
    }
    // 核心方法:获取下一个ID
    public synchronized long nextId() {
        if (currentId >= maxId) {
            // 当前区间用完,申请新区间
            Segment segment = fetchNewSegment();
            currentId = segment.getStartId();
            maxId = segment.getMaxId();
        }
        return currentId++;
    }
    // 查询数据库,更新并获取新的步长区间
    private Segment fetchNewSegment() {
        // 这里省略 JDBC 具体连接代码,核心 SQL 如下:
        // UPDATE id_table SET max_id = max_id + ? WHERE business = 'order';
        // 获取更新后的 max_id
        long newMaxId = getUpdatedMaxIdFromDB();
        return new Segment(newMaxId - STEP, newMaxId);
    }
    static class Segment {
        private long startId;
        private long maxId;
        // 构造省略...
    }
}

优点:实现简单,可管理。
缺点:依赖 DB 性能,需要高可用。

基于 Redis INCR 命令(性能好,适合中小应用)

利用 Redis 的单线程特性保证原子唯一性。

import redis.clients.jedis.Jedis;
public class RedisIdGenerator {
    private Jedis jedis;
    private static final String KEY = "id_generator:order";
    public RedisIdGenerator(Jedis jedis) {
        this.jedis = jedis;
    }
    public long nextId() {
        // 自增并返回,原子操作
        return jedis.incr(KEY);
    }
    /**
     * 如果你想生成 时间戳+序列号 的ID(带趋势递增)
     * @param keyPrefix 业务前缀
     * @return 拼接后的ID
     */
    public long generateTimeBasedId(String keyPrefix) {
        long timestamp = System.currentTimeMillis();
        // 每天一个key,避免无限增长
        String dailyKey = keyPrefix + ":" + timestamp / (1000 * 60 * 60 * 24);
        long sequence = jedis.incr(dailyKey);
        // 组合:时间戳占高位,序列号占低位
        return (timestamp << 12) | (sequence & 0xFFF);
    }
    public static void main(String[] args) {
        RedisIdGenerator generator = new RedisIdGenerator(new Jedis("localhost"));
        for (int i = 0; i < 5; i++) {
            System.out.println(generator.generateTimeBasedId("order"));
        }
    }
}

优点:性能高(单机10万 QPS)。
缺点:依赖 Redis 高可用,Redis 崩溃且没持久化 ID可能重复。

雪花算法(Snowflake,最主流推荐)

这是 Twitter 开源的算法,java 实现广泛,ID 由 时间戳 + 机器ID + 序列号 组成,趋势递增,不依赖第三方中间件。

public class SnowflakeIdGenerator {
    private final long twepoch = 1288834974657L; // 起始时间戳,可自定义
    private final long workerIdBits = 5L;   // 机器ID位数
    private final long datacenterIdBits = 5L; // 数据中心ID位数
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);  // 最大31
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 最大31
    private final long sequenceBits = 12L;  // 序列号位数
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private final long sequenceMask = -1L ^ (-1L << sequenceBits); // 4095
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    // 构造函数:初始化机器ID和数据中心ID(从配置文件/环境变量读取)
    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("workerId 非法");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId 非法");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    public synchronized long nextId() {
        long timestamp = timeGen();
        // 时钟回拨处理(常见bug点)
        if (timestamp < lastTimestamp) {
            // 可以抛异常,也可以等待时钟追上,这里简化处理
            throw new RuntimeException("Clock moved backwards. Refusing to generate id for " +
                    (lastTimestamp - timestamp) + " milliseconds");
        }
        if (lastTimestamp == timestamp) {
            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();
    }
    public static void main(String[] args) {
        SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(1, 1);
        for (int i = 0; i < 5; i++) {
            long id = idWorker.nextId();
            System.out.println(id);
        }
    }
}

优点

  • 不依赖外部服务(DB、Redis),高性能(每秒可生成十几万)。
  • ID 趋势递增,对 MySQL B+树索引友好。

缺点

  • 需要配置 workerId,避免机器ID冲突。
  • 强依赖系统时间,如果时钟回拨,可能出现重复。

美团 Leaf(工业级方案)

Leaf 是美团开源的 ID 生成系统,集成了 雪花算法(Leaf-segment)号段模式(Leaf-snowflake),它解决了雪花算法需要手动分配机器 ID、时钟回拨等问题。

用法(引入 Maven 依赖):

<dependency>
    <groupId>com.sankuai.inf.leaf</groupId>
    <artifactId>leaf-boot-starter</artifactId>
    <version>1.0.0-RELEASE</version>
</dependency>

配置文件 application.properties

# 号段模式
leaf.name=com.sankuai.leaf
leaf.segment.enable=true
leaf.segment.url=jdbc:mysql://localhost:3306/leaf
leaf.segment.username=root
leaf.segment.password=123456

推荐生产环境使用美团 Leaf 或百度 Uidgenerator。

如何选择?

方案 优点 缺点 适用场景
UUID 无依赖,生成快 无序,过长 不关心顺序,不适合DB主键
DB自增 简单,有序 性能瓶颈,单点 小规模,低并发
DB号段 性能好,可定制 依赖DB高可用 中大型应用,需更优性能
Redis 高并发,简单 依赖Redis,持久化问题 中小型应用,已有Redis
雪花算法 高性能,不依赖第三方 需管理机器ID,时钟问题 最常见,适配合大部分场景
美团Leaf 成熟,解决时钟回拨 需搭建Leaf服务 生产环境推荐

最推荐的做法:直接使用开源组件(如 Leaf 或 百度 UidGenerator),如果不想引入额外中间件,自实现雪花算法 + 通过Redis或Zookeeper分配WorkerId是最稳妥的方式。

抱歉,评论功能暂时关闭!