面试官:如何设计一个每秒生成百万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-10000),在内存中分配
- 双Buffer:Buffer A使用中,Buffer B提前加载好,无缝切换
- 异步加载:当前号段用到90%时,异步加载下一个号段
- 自适应步长:根据QPS动态调整号段大小
关键代码:
javaif (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. 降级:实在不行,本地时间戳+随机数(牺牲唯一性)
🎁 总结
核心要点
- 号段模式 = 批量获取 + 内存分配
- 双Buffer = 无缝切换 + 异步加载
- 性能关键 = 减少DB访问
一句话记住
号段模式就像银行取号机,提前领一叠号码,现场直接发,快!🚀
祝你面试顺利!💪✨