🚀 设计一个每秒生成百万ID的分布式ID生成器

面试官:如何设计一个每秒生成百万ID的系统?

候选人:用雪花算法...

面试官:雪花算法瓶颈在哪?如何突破?

候选人:😰💦(这...)

别慌!今天我们从零开始设计一个超高性能的分布式ID生成器!


🎬 第一章:需求分析

功能性需求

markdown 复制代码
1. 全局唯一 ✅
2. 趋势递增(有利于数据库B+树索引)✅
3. 高性能:100万/秒(单机)✅
4. 高可用:99.99%可用性 ✅
5. 可扩展:支持横向扩展 ✅

非功能性需求

markdown 复制代码
1. 低延迟:P99 < 1ms
2. 无单点:任何节点挂掉不影响服务
3. 易部署:不依赖复杂中间件
4. 易监控:提供metrics接口

💡 第二章:方案设计

架构选择:号段模式 + 双Buffer

arduino 复制代码
为什么选择号段模式?

对比:
┌────────────────────┬──────────────┬──────────────┐
│     方案           │    性能      │   复杂度     │
├────────────────────┼──────────────┼──────────────┤
│ 雪花算法           │ 400万/秒     │ ⭐⭐         │
│ 数据库自增         │ 1万/秒       │ ⭐           │
│ Redis原子递增      │ 10万/秒      │ ⭐⭐         │
│ 号段模式(单buffer)│ 500万/秒     │ ⭐⭐⭐       │
│ 号段模式(双buffer)│ 1000万/秒+   │ ⭐⭐⭐⭐     │
└────────────────────┴──────────────┴──────────────┘

结论:号段模式性能最高!

🎭 生活比喻:银行取号机进化史

第一代:实时生成(雪花算法)

复制代码
客户来了 → 取号机计算号码 → 打印号码
问题:计算慢,排队

第二代:批量领取(单buffer)

markdown 复制代码
取号机预先从总部领取100个号码
客户来了 → 直接发号(快!)

问题:号码快用完时,需要去领新号码
      → 期间无法发号(卡顿)

第三代:双buffer(最优)

less 复制代码
Buffer A: [1-100]   ← 正在使用
Buffer B: [101-200] ← 提前准备好

流程:
1. 发号:1、2、3...
2. 发到90号时(还剩10%)
   → 后台自动去领下一批 [201-300]
3. 发到100号时
   → 无缝切换到Buffer B
   → Buffer A重新加载新号段
   
优点:永远不会卡顿!✨

🏗️ 第三章:详细设计

3.1 整体架构

scss 复制代码
┌─────────────────────────────────────────┐
│         客户端应用                       │
│                                          │
│  ┌──────────────────────────────────┐   │
│  │   ID生成器客户端(SDK)           │   │
│  │  ┌────────┐  ┌────────┐          │   │
│  │  │Buffer A│  │Buffer B│ (双buffer)│   │
│  │  └────────┘  └────────┘          │   │
│  └──────────────────────────────────┘   │
│              ↓ (批量获取号段)            │
└─────────────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────┐
│         ID生成服务(HTTP/gRPC)          │
│                                          │
│  ┌─────────────────────────────────┐    │
│  │  号段分配器                      │    │
│  │  - 分配号段                      │    │
│  │  - 控制并发                      │    │
│  └─────────────────────────────────┘    │
│              ↓                           │
└─────────────────────────────────────────┘
                   ↓
┌─────────────────────────────────────────┐
│         MySQL数据库                      │
│                                          │
│  id_segment 表:                         │
│  - biz_tag (业务标识)                    │
│  - max_id (当前最大ID)                   │
│  - step (步长)                           │
└─────────────────────────────────────────┘

3.2 数据库表设计

sql 复制代码
CREATE TABLE `id_segment` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `biz_tag` VARCHAR(64) NOT NULL COMMENT '业务标识',
    `max_id` BIGINT(20) NOT NULL COMMENT '当前最大ID',
    `step` INT(11) NOT NULL DEFAULT 1000 COMMENT '步长',
    `description` VARCHAR(256) DEFAULT NULL COMMENT '描述',
    `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_biz_tag` (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ID号段表';

-- 初始化数据
INSERT INTO id_segment (biz_tag, max_id, step, description)
VALUES 
('order_id', 0, 10000, '订单ID'),
('user_id', 0, 5000, '用户ID'),
('product_id', 0, 5000, '商品ID');

3.3 核心代码实现

双Buffer实现

java 复制代码
/**
 * 双Buffer号段生成器
 */
public class SegmentIdGenerator {
    
    private static final double LOADING_PERCENT = 0.1; // 10%触发加载
    
    private final String bizTag;
    private final SegmentService segmentService;
    
    // 双buffer
    private volatile Segment currentSegment;
    private volatile Segment nextSegment;
    
    // 加载状态
    private volatile boolean isLoadingNext = false;
    private final Object loadLock = new Object();
    
    // 监控指标
    private final AtomicLong generatedCount = new AtomicLong(0);
    private final AtomicLong loadSegmentCount = new AtomicLong(0);
    
    public SegmentIdGenerator(String bizTag, SegmentService segmentService) {
        this.bizTag = bizTag;
        this.segmentService = segmentService;
        // 初始化:加载第一个号段
        this.currentSegment = loadSegment();
    }
    
    /**
     * 生成下一个ID(核心方法)
     */
    public Result<Long> nextId() {
        while (true) {
            // 1. 检查是否需要加载下一个号段
            if (needLoadNext() && !isLoadingNext && nextSegment == null) {
                // 异步加载下一个号段
                asyncLoadNext();
            }
            
            // 2. 尝试从当前号段获取ID
            Long id = currentSegment.nextId();
            if (id != null) {
                generatedCount.incrementAndGet();
                return Result.success(id);
            }
            
            // 3. 当前号段用完,切换到下一个
            synchronized (this) {
                if (currentSegment.isExhausted()) {
                    if (nextSegment == null) {
                        // 下一个号段还没准备好,同步加载
                        log.warn("[{}] 下一个号段未准备好,同步加载", bizTag);
                        nextSegment = loadSegment();
                    }
                    
                    // 切换
                    currentSegment = nextSegment;
                    nextSegment = null;
                    isLoadingNext = false;
                }
            }
        }
    }
    
    /**
     * 是否需要加载下一个号段
     */
    private boolean needLoadNext() {
        return currentSegment.getRemainPercent() < LOADING_PERCENT;
    }
    
    /**
     * 异步加载下一个号段
     */
    private void asyncLoadNext() {
        synchronized (loadLock) {
            if (isLoadingNext || nextSegment != null) {
                return; // 已经在加载或已经加载好了
            }
            isLoadingNext = true;
        }
        
        // 异步执行
        CompletableFuture.runAsync(() -> {
            try {
                long startTime = System.currentTimeMillis();
                Segment segment = loadSegment();
                long costTime = System.currentTimeMillis() - startTime;
                
                nextSegment = segment;
                log.info("[{}] 加载下一个号段成功,耗时: {}ms", bizTag, costTime);
            } catch (Exception e) {
                log.error("[{}] 加载下一个号段失败", bizTag, e);
                isLoadingNext = false;
            }
        });
    }
    
    /**
     * 从数据库加载号段
     */
    private Segment loadSegment() {
        try {
            loadSegmentCount.incrementAndGet();
            SegmentRange range = segmentService.getNextSegment(bizTag);
            return new Segment(range.getStart(), range.getEnd());
        } catch (Exception e) {
            throw new RuntimeException("加载号段失败: " + bizTag, e);
        }
    }
    
    /**
     * 获取监控指标
     */
    public Metrics getMetrics() {
        return Metrics.builder()
            .bizTag(bizTag)
            .generatedCount(generatedCount.get())
            .loadSegmentCount(loadSegmentCount.get())
            .currentSegmentRemain(currentSegment.getRemainCount())
            .nextSegmentLoaded(nextSegment != null)
            .build();
    }
}

/**
 * 号段
 */
@Data
public class Segment {
    private final long start;
    private final long end;
    private final AtomicLong current;
    
    public Segment(long start, long end) {
        this.start = start;
        this.end = end;
        this.current = new AtomicLong(start);
    }
    
    /**
     * 获取下一个ID
     * @return ID,如果号段用完返回null
     */
    public Long nextId() {
        long id = current.getAndIncrement();
        if (id > end) {
            return null; // 号段用完
        }
        return id;
    }
    
    /**
     * 是否用完
     */
    public boolean isExhausted() {
        return current.get() > end;
    }
    
    /**
     * 剩余百分比
     */
    public double getRemainPercent() {
        long total = end - start + 1;
        long used = current.get() - start;
        long remain = total - used;
        return remain * 1.0 / total;
    }
    
    /**
     * 剩余数量
     */
    public long getRemainCount() {
        long remain = end - current.get() + 1;
        return Math.max(0, remain);
    }
}

号段分配服务

java 复制代码
@Service
public class SegmentService {
    
    @Autowired
    private IdSegmentMapper segmentMapper;
    
    /**
     * 获取下一个号段(线程安全)
     */
    @Transactional
    public SegmentRange getNextSegment(String bizTag) {
        // 1. 加行锁,更新max_id
        IdSegmentDO segment = segmentMapper.selectForUpdate(bizTag);
        
        if (segment == null) {
            throw new IllegalArgumentException("业务标识不存在: " + bizTag);
        }
        
        // 2. 计算新的max_id
        long oldMaxId = segment.getMaxId();
        long newMaxId = oldMaxId + segment.getStep();
        
        // 3. 更新数据库
        segment.setMaxId(newMaxId);
        segmentMapper.updateById(segment);
        
        // 4. 返回号段范围 [oldMaxId + 1, newMaxId]
        return SegmentRange.builder()
            .start(oldMaxId + 1)
            .end(newMaxId)
            .build();
    }
}

@Mapper
public interface IdSegmentMapper {
    
    /**
     * 查询并加行锁
     */
    @Select("SELECT * FROM id_segment WHERE biz_tag = #{bizTag} FOR UPDATE")
    IdSegmentDO selectForUpdate(@Param("bizTag") String bizTag);
    
    /**
     * 更新最大ID
     */
    @Update("UPDATE id_segment SET max_id = #{maxId}, " +
            "update_time = CURRENT_TIMESTAMP WHERE id = #{id}")
    int updateById(IdSegmentDO segment);
}

对外API服务

java 复制代码
@RestController
@RequestMapping("/api/id")
public class IdGeneratorController {
    
    @Autowired
    private IdGeneratorManager generatorManager;
    
    /**
     * 生成单个ID
     */
    @GetMapping("/next")
    public Result<Long> nextId(@RequestParam String bizTag) {
        return generatorManager.nextId(bizTag);
    }
    
    /**
     * 批量生成ID
     */
    @GetMapping("/batch")
    public Result<List<Long>> batchNextId(
            @RequestParam String bizTag,
            @RequestParam(defaultValue = "100") int count) {
        
        if (count > 1000) {
            return Result.error("批量数量不能超过1000");
        }
        
        List<Long> ids = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            Result<Long> result = generatorManager.nextId(bizTag);
            if (!result.isSuccess()) {
                return Result.error(result.getMessage());
            }
            ids.add(result.getData());
        }
        
        return Result.success(ids);
    }
    
    /**
     * 监控指标
     */
    @GetMapping("/metrics")
    public Result<List<Metrics>> metrics() {
        return Result.success(generatorManager.getAllMetrics());
    }
}

/**
 * ID生成器管理器
 */
@Component
public class IdGeneratorManager {
    
    @Autowired
    private SegmentService segmentService;
    
    // 缓存:bizTag -> Generator
    private final ConcurrentMap<String, SegmentIdGenerator> generatorMap = 
        new ConcurrentHashMap<>();
    
    public Result<Long> nextId(String bizTag) {
        try {
            SegmentIdGenerator generator = generatorMap.computeIfAbsent(
                bizTag,
                k -> new SegmentIdGenerator(k, segmentService)
            );
            
            return generator.nextId();
        } catch (Exception e) {
            log.error("[{}] 生成ID失败", bizTag, e);
            return Result.error("生成ID失败: " + e.getMessage());
        }
    }
    
    public List<Metrics> getAllMetrics() {
        return generatorMap.values().stream()
            .map(SegmentIdGenerator::getMetrics)
            .collect(Collectors.toList());
    }
}

🚀 第四章:性能优化

优化1:减少数据库访问

java 复制代码
// ❌ 不好:每次都访问数据库
public long nextId() {
    return segmentService.getNextId(); // 每次DB调用
}

// ✅ 好:批量获取,内存分配
public long nextId() {
    if (segment.isExhausted()) {
        segment = loadNewSegment(); // 1万次才1次DB调用
    }
    return segment.nextId();
}

性能提升:1000倍+

优化2:自适应步长

java 复制代码
/**
 * 根据QPS动态调整步长
 */
public class AdaptiveStepStrategy {
    
    private static final int MIN_STEP = 1000;
    private static final int MAX_STEP = 100000;
    
    private final AtomicLong lastSecondCount = new AtomicLong(0);
    private volatile long currentStep = MIN_STEP;
    
    @Scheduled(fixedRate = 1000) // 每秒调整一次
    public void adjustStep() {
        long qps = lastSecondCount.getAndSet(0);
        
        if (qps > 10000) {
            // 高QPS,增大步长
            currentStep = Math.min(currentStep * 2, MAX_STEP);
        } else if (qps < 1000) {
            // 低QPS,减小步长(避免浪费)
            currentStep = Math.max(currentStep / 2, MIN_STEP);
        }
        
        log.info("QPS: {}, 调整步长为: {}", qps, currentStep);
    }
}

优点:
- 高峰期:大步长,减少DB访问
- 低谷期:小步长,避免ID跳跃太大

优化3:号段预加载阈值调优

java 复制代码
// 根据加载耗时动态调整阈值
public class DynamicThresholdStrategy {
    
    private volatile double threshold = 0.1; // 默认10%
    
    public void onSegmentLoaded(long loadTimeMs) {
        if (loadTimeMs > 100) {
            // 加载慢,提前触发
            threshold = Math.min(0.3, threshold + 0.05);
        } else if (loadTimeMs < 10) {
            // 加载快,晚点触发
            threshold = Math.max(0.05, threshold - 0.01);
        }
        log.info("加载耗时: {}ms, 调整阈值为: {}", loadTimeMs, threshold);
    }
}

📊 第五章:监控与运维

监控指标

java 复制代码
@Data
@Builder
public class Metrics {
    private String bizTag;              // 业务标识
    private long generatedCount;        // 累计生成数量
    private long loadSegmentCount;      // 累计加载号段次数
    private long currentSegmentRemain;  // 当前号段剩余
    private boolean nextSegmentLoaded;  // 下一号段是否已加载
    private long avgLoadTimeMs;         // 平均加载耗时
    private long maxLoadTimeMs;         // 最大加载耗时
    private double qps;                 // 当前QPS
}

// Prometheus监控
@Component
public class MetricsExporter {
    
    @Autowired
    private IdGeneratorManager manager;
    
    private final Counter generatedCounter = Counter.build()
        .name("id_generated_total")
        .help("Total generated IDs")
        .labelNames("biz_tag")
        .register();
    
    private final Histogram loadTimeHistogram = Histogram.build()
        .name("segment_load_duration_seconds")
        .help("Segment load duration")
        .labelNames("biz_tag")
        .register();
    
    @Scheduled(fixedRate = 5000)
    public void export() {
        List<Metrics> metricsList = manager.getAllMetrics();
        for (Metrics metrics : metricsList) {
            generatedCounter.labels(metrics.getBizTag())
                .inc(metrics.getGeneratedCount());
            
            loadTimeHistogram.labels(metrics.getBizTag())
                .observe(metrics.getAvgLoadTimeMs() / 1000.0);
        }
    }
}

告警规则

yaml 复制代码
# Prometheus告警规则
groups:
  - name: id_generator
    rules:
      # ID生成QPS过低
      - alert: IdGeneratorLowQps
        expr: rate(id_generated_total[1m]) < 100
        for: 5m
        annotations:
          summary: "ID生成QPS过低"
          
      # 号段加载耗时过长
      - alert: SegmentLoadTooSlow
        expr: segment_load_duration_seconds > 0.5
        for: 1m
        annotations:
          summary: "号段加载耗时超过500ms"
          
      # 下一号段未准备好
      - alert: NextSegmentNotReady
        expr: next_segment_loaded == 0
        for: 1m
        annotations:
          summary: "下一号段未准备好,可能影响性能"

🎓 第六章:面试高分回答

问题:如何设计一个百万级ID生成器?

标准回答(STAR法则):

S(场景):"我们的业务需要一个高性能的分布式ID生成器,要求每秒生成100万个ID。"

T(挑战):"单纯的雪花算法只能达到400万/秒,而且依赖时钟。数据库自增只有1万/秒,完全不够用。"

A(方案):"我们采用了号段模式+双Buffer的设计:

  1. 号段模式:从数据库批量获取号段(如1-10000),在内存中分配
  2. 双Buffer:Buffer A使用中,Buffer B提前加载好,无缝切换
  3. 异步加载:当前号段用到90%时,异步加载下一个号段
  4. 自适应步长:根据QPS动态调整号段大小

关键代码

java 复制代码
if (currentSegment.getRemainPercent() < 0.1) {
    // 剩余10%时,异步加载
    asyncLoadNext();
}

R(结果):"

  • 性能:单机1000万/秒,远超100万目标
  • 可用性:99.99%,数据库故障期间仍能使用当前号段
  • 延迟:P99 < 1ms
  • 数据库压力:从100万QPS降低到100QPS(步长10000)"

常见追问

Q1:号段模式有什么缺点?

markdown 复制代码
A:
1. ID不是严格递增,而是趋势递增
   - 例如:服务器A发放1-10000,服务器B发放10001-20000
   - 可能A的9999比B的10001晚生成
   
2. 服务器重启会浪费号段
   - 解决:号段不要太大,或者持久化到本地文件

3. 依赖数据库
   - 解决:数据库做主从,号段内不依赖DB

Q2:如何保证高可用?

markdown 复制代码
A:
1. 数据库:主从+读写分离
2. 应用层:多实例部署
3. 号段:Buffer A/B任意一个都能提供服务
4. 降级:实在不行,本地时间戳+随机数(牺牲唯一性)

🎁 总结

核心要点

  1. 号段模式 = 批量获取 + 内存分配
  2. 双Buffer = 无缝切换 + 异步加载
  3. 性能关键 = 减少DB访问

一句话记住

号段模式就像银行取号机,提前领一叠号码,现场直接发,快!🚀


祝你面试顺利!💪✨

相关推荐
我命由我123455 小时前
Spring Cloud - Spring Cloud 负载均衡(Ribbon 负载均衡概述、Ribbon 使用)
java·后端·spring·spring cloud·ribbon·java-ee·负载均衡
xyy1235 小时前
使用 SQLite 实现 CacheHelper
后端
Lear5 小时前
SpringBoot启动流程分析
后端
Lear5 小时前
SpringMVC之拦截器(Interceptor)
后端
Lear5 小时前
SpringBoot之自动装配
后端
Lear5 小时前
SpringMVC之监听器(Listener)
后端
karry_k5 小时前
Redis如何搭建搭建一主多从?
后端·面试
用户5975653371105 小时前
【Java多线程与高并发系列】第2讲:核心概念扫盲:进程 vs. 线程
后端
Lear5 小时前
SpringBoot异步编程
后端