🎫 设计一个分布式唯一ID生成器:给每个人发身份证!

80 阅读15分钟

📖 开场:身份证号码

想象全国人民的身份证号码 🆔:

单机生成(自增ID)

第1个人:ID = 1
第2个人:ID = 2
第3个人:ID = 3
...

简单,但只能在一个地方办理 ❌

分布式生成(多地办理)

北京办事处:生成 110开头的ID
上海办事处:生成 310开头的ID
广州办事处:生成 440开头的ID

问题:如何保证全国唯一?如何保证有序?🤔

这就是分布式唯一ID生成器要解决的问题!

需求

  • 全局唯一:不能重复
  • 趋势递增:有序(方便MySQL索引)
  • 高性能:QPS > 10万
  • 高可用:不能单点故障

🤔 分布式ID的要求

核心要求

要求说明重要性
全局唯一不能有重复ID⭐⭐⭐ 必须
趋势递增新ID比旧ID大⭐⭐⭐ 重要(MySQL B+树索引)
高性能QPS > 10万⭐⭐⭐ 重要
高可用不能单点故障⭐⭐⭐ 重要
信息安全不暴露业务信息⭐⭐ 可选

为什么需要趋势递增?

MySQL的B+树索引

自增ID(有序):
插入ID=1 → 插入ID=2 → 插入ID=3
    ↓
顺序插入B+树叶子节点
    ↓
性能好 ✅(顺序IO)

随机ID(无序):
插入ID=999 → 插入ID=2 → 插入ID=500
    ↓
随机插入B+树,可能导致页分裂
    ↓
性能差 ❌(随机IO,页分裂)

结论:趋势递增的ID对数据库友好 ⭐⭐⭐


🎯 五种分布式ID生成方案

方案1:数据库自增ID 🗄️

原理

单机版

CREATE TABLE id_generator (
    id BIGINT NOT NULL AUTO_INCREMENT,
    stub CHAR(1) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY (stub)
) ENGINE=InnoDB;

-- 获取ID
INSERT INTO id_generator (stub) VALUES ('a');
SELECT 0;

分布式版(多实例,不同步长):

数据库1(步长2,初始值1):
1, 3, 5, 7, 9, ...

数据库2(步长2,初始值2):
2, 4, 6, 8, 10, ...

结果:所有ID唯一 ✅

配置

-- 数据库1配置
SET @@auto_increment_increment = 2;  -- 步长
SET @@auto_increment_offset = 1;     -- 初始值

-- 数据库2配置
SET @@auto_increment_increment = 2;  -- 步长
SET @@auto_increment_offset = 2;     -- 初始值

代码实现

@Service
@Slf4j
public class DatabaseIdGenerator {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 生成ID
     */
    public Long generateId() {
        // 插入记录
        jdbcTemplate.update("INSERT INTO id_generator (stub) VALUES ('a')");
        
        // 获取自增ID
        Long id = jdbcTemplate.queryForObject("SELECT 0", Long.class);
        
        log.info("生成ID: {}", id);
        return id;
    }
}

优缺点

优点 ✅:

  • 实现简单
  • 趋势递增(满足MySQL索引要求)
  • 数据库天然保证唯一性

缺点 ❌:

  • 性能瓶颈(数据库IO)
  • 单点故障(主库挂了)
  • 扩展性差(增加数据库需要重新配置步长)

优化:号段模式(见下文)


方案2:号段模式(Leaf-segment)🍃

原理

核心思想:一次性从数据库获取一批ID,缓存在内存中

数据库:
┌─────────────────────────────────────┐
 biz_tag  | max_id | step | version 
 order    | 1000   | 1000 | 1       
 user     | 5000   | 1000 | 1       
└─────────────────────────────────────┘

应用服务器:
1. 从数据库获取号段:[1001, 2000]
2. 更新数据库:max_id = 2000
3. 内存分配:1001, 1002, 1003, ..., 2000
4. 号段用完,再次获取:[2001, 3000]

数据库设计

CREATE TABLE leaf_alloc (
    biz_tag VARCHAR(128) NOT NULL COMMENT '业务标识',
    max_id BIGINT NOT NULL COMMENT '当前最大ID',
    step INT NOT NULL COMMENT '步长(号段大小)',
    description VARCHAR(256) DEFAULT NULL COMMENT '描述',
    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, 1000, '订单ID');

代码实现

@Component
@Slf4j
public class LeafSegmentIdGenerator {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 缓存号段
    private Map<String, Segment> segmentMap = new ConcurrentHashMap<>();
    
    /**
     * 生成ID
     */
    public Long generateId(String bizTag) {
        // 获取号段
        Segment segment = segmentMap.computeIfAbsent(bizTag, k -> new Segment());
        
        // 从号段中获取ID
        Long id = segment.getId();
        
        if (id == null) {
            // 号段用完,重新获取
            synchronized (segment) {
                // 双重检查
                if (segment.getId() == null) {
                    loadSegment(bizTag, segment);
                    id = segment.getId();
                }
            }
        }
        
        return id;
    }
    
    /**
     * ⭐ 从数据库加载号段
     */
    private void loadSegment(String bizTag, Segment segment) {
        // ⭐ 更新数据库,获取号段
        String sql = 
            "UPDATE leaf_alloc " +
            "SET max_id = max_id + step " +
            "WHERE biz_tag = ?";
        
        jdbcTemplate.update(sql, bizTag);
        
        // 查询号段范围
        String querySql = 
            "SELECT max_id, step FROM leaf_alloc WHERE biz_tag = ?";
        
        Map<String, Object> row = jdbcTemplate.queryForMap(querySql, bizTag);
        Long maxId = (Long) row.get("max_id");
        Integer step = (Integer) row.get("step");
        
        // 计算号段范围:[current, max]
        long current = maxId - step + 1;
        long max = maxId;
        
        // 设置号段
        segment.setCurrent(current);
        segment.setMax(max);
        
        log.info("加载号段成功: bizTag={}, range=[{}, {}]", bizTag, current, max);
    }
    
    /**
     * 号段类
     */
    static class Segment {
        private AtomicLong current = new AtomicLong(0);
        private long max = 0;
        
        public Long getId() {
            long val = current.incrementAndGet();
            if (val <= max) {
                return val;
            }
            return null;  // 号段用完
        }
        
        public void setCurrent(long current) {
            this.current.set(current - 1);  // -1是因为getId会先+1
        }
        
        public void setMax(long max) {
            this.max = max;
        }
    }
}

双Buffer优化

问题:号段用完,需要等待数据库IO

优化:双Buffer,提前加载下一个号段

static class Segment {
    private SegmentBuffer buffer = new SegmentBuffer();
    
    static class SegmentBuffer {
        private Segment current;  // 当前号段
        private Segment next;     // 下一个号段
        private volatile boolean nextReady = false;  // 下一个号段是否就绪
    }
    
    public Long getId() {
        // 从当前号段获取ID
        Long id = buffer.current.getId();
        
        // ⭐ 当前号段使用到10%时,异步加载下一个号段
        if (buffer.current.getUsagePercent() > 0.9 && !buffer.nextReady) {
            loadNextSegmentAsync();
        }
        
        // ⭐ 当前号段用完,切换到下一个号段
        if (id == null && buffer.nextReady) {
            buffer.current = buffer.next;
            buffer.nextReady = false;
            id = buffer.current.getId();
        }
        
        return id;
    }
}

优缺点

优点 ✅:

  • 性能高(内存分配,减少数据库访问)
  • 数据库压力小(批量获取)
  • 趋势递增
  • 可扩展(支持多业务)

缺点 ❌:

  • 号段浪费(服务重启,未使用的号段丢失)
  • 不是严格递增(多机器并发分配)

适用场景

  • 大部分业务场景 ⭐⭐⭐
  • 美团Leaf-segment方案

方案3:雪花算法(Snowflake)❄️

原理

64位Long类型ID

┌───────────┬──────┬──────┬────────────┐
│  41位     │ 10位 │ 12位 │            │
│  时间戳   │机器ID│ 序列号│    64位    │
└───────────┴──────┴──────┴────────────┘
     ↑         ↑      ↑
     │         │      └─ 同一毫秒内的序列号(0-4095)
     │         └──────── 机器ID(0-1023)
     └────────────────── 时间戳(毫秒级)

结构:
- 1位符号位:0(正数)
- 41位时间戳:精确到毫秒,可用69年
- 10位机器ID:支持1024台机器
  - 5位数据中心ID(0-31)
  - 5位机器ID(0-31)
- 12位序列号:同一毫秒内最多生成4096个ID

特点:
- 全局唯一 ✅
- 趋势递增 ✅(时间戳递增)
- 高性能 ✅(内存生成,无IO)
- 去中心化 ✅(每台机器独立生成)

代码实现

@Component
@Slf4j
public class SnowflakeIdGenerator {
    
    // ⭐ 起始时间戳(2020-01-01)
    private static final long START_TIMESTAMP = 1577808000000L;
    
    // ⭐ 各部分的位数
    private static final long DATACENTER_ID_BITS = 5L;  // 数据中心ID位数
    private static final long WORKER_ID_BITS = 5L;      // 机器ID位数
    private static final long SEQUENCE_BITS = 12L;      // 序列号位数
    
    // ⭐ 各部分的最大值
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);  // 31
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);          // 31
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);            // 4095
    
    // ⭐ 各部分的左移位数
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;                              // 12
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;         // 17
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;  // 22
    
    // ⭐ 机器标识
    private long datacenterId;  // 数据中心ID(0-31)
    private long workerId;      // 机器ID(0-31)
    
    // ⭐ 序列号
    private long sequence = 0L;
    
    // ⭐ 上次生成ID的时间戳
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator() {
        // 默认:数据中心ID=1,机器ID=1
        this(1, 1);
    }
    
    public SnowflakeIdGenerator(long datacenterId, long workerId) {
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId超出范围");
        }
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("workerId超出范围");
        }
        
        this.datacenterId = datacenterId;
        this.workerId = workerId;
        
        log.info("Snowflake初始化: datacenterId={}, workerId={}", datacenterId, workerId);
    }
    
    /**
     * ⭐ 生成ID(线程安全)
     */
    public synchronized long generateId() {
        long timestamp = System.currentTimeMillis();
        
        // ⭐ 时钟回拨检查
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format("时钟回拨,拒绝生成ID: lastTimestamp=%d, current=%d", 
                    lastTimestamp, timestamp)
            );
        }
        
        // ⭐ 同一毫秒内
        if (timestamp == lastTimestamp) {
            // 序列号+1
            sequence = (sequence + 1) & MAX_SEQUENCE;
            
            if (sequence == 0) {
                // 序列号溢出(同一毫秒生成超过4096个ID)
                // 等待下一毫秒
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒,序列号重置为0
            sequence = 0L;
        }
        
        // 更新上次时间戳
        lastTimestamp = timestamp;
        
        // ⭐ 组装ID
        long id = ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)  // 时间戳部分
                | (datacenterId << DATACENTER_ID_SHIFT)               // 数据中心ID部分
                | (workerId << WORKER_ID_SHIFT)                       // 机器ID部分
                | sequence;                                           // 序列号部分
        
        return id;
    }
    
    /**
     * 等待下一毫秒
     */
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
    
    /**
     * 解析ID(测试用)
     */
    public String parseId(long id) {
        long timestamp = ((id >> TIMESTAMP_SHIFT) & ~(-1L << 41)) + START_TIMESTAMP;
        long datacenterId = (id >> DATACENTER_ID_SHIFT) & MAX_DATACENTER_ID;
        long workerId = (id >> WORKER_ID_SHIFT) & MAX_WORKER_ID;
        long sequence = id & MAX_SEQUENCE;
        
        return String.format(
            "ID=%d, timestamp=%d (%s), datacenterId=%d, workerId=%d, sequence=%d",
            id, timestamp, new Date(timestamp), datacenterId, workerId, sequence
        );
    }
}

使用示例

@SpringBootTest
public class SnowflakeTest {
    
    @Test
    public void testGenerate() {
        SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
        
        // 生成10个ID
        for (int i = 0; i < 10; i++) {
            long id = generator.generateId();
            System.out.println(generator.parseId(id));
        }
    }
    
    @Test
    public void testPerformance() {
        SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
        
        int count = 1000000;  // 100万
        long start = System.currentTimeMillis();
        
        for (int i = 0; i < count; i++) {
            generator.generateId();
        }
        
        long end = System.currentTimeMillis();
        
        System.out.println("生成" + count + "个ID,耗时: " + (end - start) + "ms");
        System.out.println("QPS: " + (count * 1000 / (end - start)));
    }
}

输出

ID=123456789012345, timestamp=1609459200000 (2021-01-01 00:00:00), datacenterId=1, workerId=1, sequence=0
ID=123456789012346, timestamp=1609459200000 (2021-01-01 00:00:00), datacenterId=1, workerId=1, sequence=1
...

生成1000000个ID,耗时: 500ms
QPS: 2000000  ← ⭐ 200万QPS

时钟回拨问题

问题:服务器时钟回拨,导致生成重复ID

10:00:00  生成ID(timestamp=10:00:00)
10:00:05  【时钟回拨到09:59:00】💀
10:00:06  生成ID(timestamp=09:59:00)
    ↓
可能生成重复ID ❌

解决方案1:拒绝生成

if (timestamp < lastTimestamp) {
    throw new RuntimeException("时钟回拨,拒绝生成ID");
}

缺点:服务不可用


解决方案2:等待时钟追上 ⚠️

if (timestamp < lastTimestamp) {
    long offset = lastTimestamp - timestamp;
    if (offset > 5000) {  // 回拨超过5秒,拒绝
        throw new RuntimeException("时钟回拨过大");
    }
    
    // 等待时钟追上
    Thread.sleep(offset);
    timestamp = System.currentTimeMillis();
}

缺点:等待时间长


解决方案3:使用扩展位 ✅(推荐)

// 预留2位扩展位
// 时钟回拨时,扩展位+1

┌───────────┬──────┬──────┬──┬──────────┐
│  39位     │ 10位 │ 2位  │12位       │
│  时间戳   │机器ID│扩展位│序列号     │
└───────────┴──────┴──────┴───────────┘
                      ↑
                扩展位(0-3

优缺点

优点 ✅:

  • 全局唯一
  • 趋势递增(时间戳递增)
  • 高性能(200万QPS)
  • 去中心化(无数据库依赖)
  • 包含时间信息(可解析)

缺点 ❌:

  • 依赖系统时钟(时钟回拨问题)
  • 机器ID需要配置(手动或自动分配)
  • 不是严格递增(多机器并发)

适用场景

  • 高性能场景 ⭐⭐⭐
  • 分布式系统
  • 大部分互联网公司的选择(Twitter、美团等)

方案4:Redis生成ID 🔴

原理

利用Redis的INCR命令(原子操作)

INCR key

每次调用返回递增的值:
INCR order_id → 1
INCR order_id → 2
INCR order_id → 3
...

代码实现

@Component
@Slf4j
public class RedisIdGenerator {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 生成ID
     */
    public Long generateId(String bizTag) {
        String key = "id:" + bizTag;
        
        // ⭐ INCR命令(原子操作)
        Long id = redisTemplate.opsForValue().increment(key);
        
        log.info("生成ID: bizTag={}, id={}", bizTag, id);
        return id;
    }
    
    /**
     * 批量生成ID(性能优化)
     */
    public List<Long> generateIds(String bizTag, int count) {
        String key = "id:" + bizTag;
        
        // ⭐ INCRBY命令(一次获取多个)
        Long maxId = redisTemplate.opsForValue().increment(key, count);
        
        // 返回 [maxId - count + 1, maxId]
        List<Long> ids = new ArrayList<>();
        for (long i = maxId - count + 1; i <= maxId; i++) {
            ids.add(i);
        }
        
        return ids;
    }
}

优化:加入时间戳

/**
 * 生成ID(包含时间戳)
 * 
 * 格式:yyyyMMddHHmmss + 6位序号
 * 例如:20210101120000000001
 */
public String generateIdWithTimestamp(String bizTag) {
    // 时间戳部分
    String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
    
    // 序号部分(Redis INCR)
    String key = "id:" + bizTag + ":" + timestamp;
    Long seq = redisTemplate.opsForValue().increment(key);
    
    // 设置过期时间(1天后过期)
    redisTemplate.expire(key, 1, TimeUnit.DAYS);
    
    // 拼接ID
    String id = timestamp + String.format("%06d", seq);
    
    return id;
}

高可用方案

问题:Redis单点故障

解决方案1:Redis集群

Redis Master-Slave集群
    ↓
Master挂了 → Slave升级为Master
    ↓
可能丢失部分ID(主从同步延迟)⚠️

解决方案2:Redis Cluster + 步长

Redis1:生成 1, 4, 7, 10, ...(步长3,初始值1)
Redis2:生成 2, 5, 8, 11, ...(步长3,初始值2)
Redis3:生成 3, 6, 9, 12, ...(步长3,初始值3)

优缺点

优点 ✅:

  • 实现简单
  • 性能好(Redis内存操作)
  • 严格递增(单Redis实例)

缺点 ❌:

  • 依赖Redis(单点故障)
  • 网络IO(比Snowflake慢)
  • 需要持久化(AOF/RDB)

适用场景

  • 已有Redis集群
  • 对ID严格递增有要求
  • 并发量不是特别高

方案5:UUID 🆔

原理

UUID = Universally Unique Identifier(通用唯一识别码)

格式8-4-4-4-12,共36个字符

550e8400-e29b-41d4-a716-446655440000
    ↑      ↑    ↑    ↑         ↑
  时间戳  版本 变体      随机数

长度:32个十六进制字符 + 4个连字符 = 36个字符

代码实现

import java.util.UUID;

@Component
public class UUIDGenerator {
    
    /**
     * 生成UUID
     */
    public String generateId() {
        return UUID.randomUUID().toString();
    }
    
    /**
     * 生成UUID(去掉连字符)
     */
    public String generateIdWithoutDash() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

优缺点

优点 ✅:

  • 实现简单(JDK内置)
  • 全局唯一
  • 去中心化(无依赖)

缺点 ❌:

  • 不是趋势递增(随机,对MySQL索引不友好)❌❌❌
  • 太长(36个字符,占用存储空间大)
  • 无序(不适合做主键)

适用场景

  • 不需要趋势递增的场景
  • 文件名、临时ID等
  • 不适合做数据库主键

📊 五种方案对比

方案性能趋势递增依赖适用场景
数据库自增⭐ 低✅ 是数据库低并发
号段模式⭐⭐⭐ 高✅ 是数据库通用 ⭐⭐⭐
雪花算法⭐⭐⭐ 极高✅ 是高并发 ⭐⭐⭐
Redis⭐⭐ 中✅ 是Redis中等并发
UUID⭐⭐⭐ 高❌ 否不适合主键

🎓 面试题速答

Q1: 设计一个分布式ID生成器,有哪些方案?

A: 五种方案

  1. 数据库自增ID:简单,但性能差
  2. 号段模式:批量获取,性能好(美团Leaf)⭐
  3. 雪花算法:高性能,去中心化(Twitter)⭐⭐⭐
  4. Redis INCR:简单,依赖Redis
  5. UUID:全局唯一,但不是趋势递增 ❌

推荐:雪花算法(高并发)或号段模式(通用)


Q2: 雪花算法的原理是什么?

A: 64位Long类型ID

┌───────────┬──────┬──────┬────────────┐
│  41位     │ 10位 │ 12位 │   64位     │
│  时间戳   │机器ID│序列号│            │
└───────────┴──────┴──────┴────────────┘

组成

  • 41位时间戳:精确到毫秒,可用69年
  • 10位机器ID:支持1024台机器
  • 12位序列号:同一毫秒最多4096个ID

特点

  • 全局唯一 ✅
  • 趋势递增 ✅
  • 高性能(200万QPS)✅

Q3: 雪花算法的时钟回拨问题如何解决?

A: 三种方案

  1. 拒绝生成

    if (timestamp < lastTimestamp) {
        throw new RuntimeException("时钟回拨");
    }
    

    缺点:服务不可用

  2. 等待时钟追上

    Thread.sleep(lastTimestamp - timestamp);
    

    缺点:等待时间长

  3. 使用扩展位(推荐):

    • 预留2位扩展位
    • 时钟回拨时,扩展位+1
    • 最多支持4次回拨

Q4: 为什么UUID不适合做数据库主键?

A: 三个原因

  1. 不是趋势递增

    • UUID是随机的
    • 插入MySQL时,随机位置插入
    • 导致页分裂,性能差 ❌
  2. 太长

    • 36个字符(包含连字符)
    • 占用存储空间大
    • 索引占用空间大
  3. 无序

    • 不方便排序
    • 不方便分页

结论:UUID适合文件名等场景,不适合数据库主键 ❌


Q5: 号段模式的原理是什么?

A: 核心思想:批量获取ID,缓存在内存中

1. 从数据库获取号段:[1001, 2000]
2. 更新数据库:max_id = 2000
3. 内存分配:1001, 1002, ..., 2000
4. 号段用完,再次获取:[2001, 3000]

优点

  • 减少数据库访问(批量获取)
  • 性能高(内存分配)
  • 趋势递增

优化:双Buffer

  • 当前号段使用到10%时,异步加载下一个号段
  • 避免号段用完时的等待

Q6: 如何选择分布式ID方案?

A: 根据场景选择

场景推荐方案理由
高并发(秒杀)雪花算法性能极高(200万QPS)⭐⭐⭐
通用业务号段模式性能好,实现简单 ⭐⭐⭐
低并发数据库自增简单 ⭐
已有RedisRedis INCR利用现有资源 ⭐⭐
文件名等UUID不需要趋势递增 ⭐

推荐

  • 高并发:雪花算法 ⭐⭐⭐
  • 通用:号段模式 ⭐⭐⭐

🎬 总结

     分布式ID生成方案对比

┌────────────────────────────────────┐
│ 数据库自增:简单,性能差 ⭐         │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 号段模式:批量获取,性能好 ⭐⭐⭐   │
│ (美团Leaf)                       │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ 雪花算法:高性能,去中心化 ⭐⭐⭐   │
│ (Twitter Snowflake)              │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ Redis:简单,依赖Redis ⭐⭐         │
└────────────────────────────────────┘

┌────────────────────────────────────┐
│ UUID:全局唯一,但不趋势递增 ❌    │
└────────────────────────────────────┘

  高并发用雪花,通用用号段!✅

🎉 恭喜你!

你已经完全掌握了分布式ID生成器的设计!🎊

核心要点

  1. 雪花算法:高性能,去中心化 ⭐⭐⭐
  2. 号段模式:批量获取,性能好 ⭐⭐⭐
  3. UUID不适合做主键:不是趋势递增 ❌

下次面试,这样回答

"分布式ID生成器有5种方案,最常用的是雪花算法和号段模式。

雪花算法生成64位Long类型ID,包含41位时间戳、10位机器ID和12位序列号。优点是全局唯一、趋势递增、高性能(200万QPS)、去中心化。缺点是依赖系统时钟,有时钟回拨问题,可以通过预留扩展位解决。

号段模式是一次性从数据库获取一批ID(如1000个),缓存在内存中分配,减少数据库访问。可以用双Buffer优化,当前号段使用到10%时异步加载下一个号段,避免等待。

我们项目的订单系统使用雪花算法生成订单号,QPS达到50万,机器ID通过Zookeeper自动分配,避免手动配置。"

面试官:👍 "很好!你对分布式ID生成器理解很深刻!"


本文完 🎬

上一篇: 198-分布式事务的解决方案.md
下一篇: 200-设计一个亿级用户的社交关系链存储.md

作者注:写完这篇,我都想去公安局办身份证了!🆔
如果这篇文章对你有帮助,请给我一个Star⭐!