题目:详细介绍下号段模式生成分布式 ID 的原理和优缺点?
难度:high
标签:arch/distributed/id, distributed/id-segment, distributed/id, difficulty/high
评分:⭐⭐⭐☆☆(3/5)
生成分布式 ID 这个号段模式的生成方案。首先要了解什么是号段模式的生成方案。它一般是有一个中心的发号的服务,一次性的发给不同的服务一批号段,然后每个服务自己去申请,根据申请的这个号段去使用它的 ID。这基本是它的整个流程。当它使用完或者使用到一定的量级,比如说 30% 的时候,它就去再申请下一个号段。这就是它的整个流程了。
从流程上看,首先第一个问题,它的发号中心是一个单点的。如果发号中心坏了,或者是出现业务异常,然后各个服务自身的号段使用光了,然后中心坏了,就导致整个业务瘫痪。
另外它的一个问题就是,由于一次性分一批号段,所以导致几个服务生成的数据是在服务内单调递增的,但服务间并不是,可能会出现服务 ID 跳转。一会是,比如以一次性分 1000 个号段为例,服务一分了 1000 个,服务二分了 1000 个,服务三分了 1000 个。服务一生成的服务 ID,数据库的那个接受的 ID 是 1001,服务二创建的一条数据,这时候它是 2001。这样大家其实是服务内单调递增,但是不同服务之间并不是单调递增的;
它的优点好处方面,首先是实现简单,其次是它的性能还是很不错的,另外它能保证一定程度的 id 递增性,虽然不完全。但是如果不能接受服务 ID 完全不完全自增,就无法使用这个模式。
评分:⭐⭐⭐☆☆(3/5)
优点:
不足:
UPDATE max_value 用乐观锁(版本号)还是悲观锁(SELECT FOR UPDATE),是号段模式最核心的技术点补充:
id_segment(biz_tag, max_id, step, version),UPDATE 时 WHERE version=? 用乐观锁INCRBY key step 原子操作,性能更好但 Redis 是单点核心考点定位:考察对号段模式(Segment Pattern)的存储层实现 + 高可用设计 + 与相关方案的权衡。相比原理层面,工程实现细节是本题的重点。
知识点拆解:
CREATE TABLE id_segment (
biz_tag VARCHAR(64) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL DEFAULT 1000,
version BIGINT NOT NULL
);
-- 取号:事务内两行更新
BEGIN;
SELECT max_id, version FROM id_segment WHERE biz_tag='order';
-- 计算新段:[max_id+1, max_id+step]
UPDATE id_segment SET max_id=max_id+step, version=version+1
WHERE biz_tag='order' AND version=?; -- 乐观锁
COMMIT;
INCRBY id_segment:order step
Redis 单线程原子,性能更高;但 Redis 单点、持久化丢失风险
| 号段大小 | 请求中心频率 | 单点压力 | 号浪费 | 跳跃幅度 |
|---|---|---|---|---|
| 100 | 高 | 高 | 少 | 小 |
| 1000(典型) | 中 | 中 | 中 | 中 |
| 10000 | 低 | 低 | 多 | 大 |
| 维度 | 号段 | 雪花 | UUID |
|---|---|---|---|
| 严格递增 | 趋势递增 | 趋势递增 | 无序 |
| 时钟依赖 | 无 | 强依赖 | 无 |
| 中心依赖 | 有 | 无 | 无 |
| 性能 | 高 | 极高 | 极高 |
| B+ 树友好 | 是 | 是 | 否 |
| 典型场景 | 金融账务 | 互联网 | 日志埋点 |
递进:核心思想 → 存储层实现 → 双 Buffer 机制 → 号段大小权衡 → 高可用设计 → 方案对比 → 选型建议
考察方向:
延伸递进面试题:
面试官您好,号段模式的核心思想是用空间换性能——从中心服务一次拉取一批 ID(号段),本地顺序分配,用完再拉,这样大部分时间都不需要访问中心,接近本地生成的性能。
存储层实现有两种主流方案。第一种是 MySQL,用一张 id_segment 表存 biz_tag / max_id / step / version,取号时用乐观锁更新:UPDATE ... SET max_id=max_id+step, version=version+1 WHERE biz_tag=? AND version=?。乐观锁保证多个服务实例同时取号时不会拿到相同号段。第二种是 Redis INCRBY id_segment:order step,Redis 单线程原子操作,性能更高但有单点风险。美团 Leaf 默认用 MySQL 方案,兼顾可靠和性能。
号段模式的核心工程技巧是双 Buffer 机制。本地缓存两份号段:当前段和备用段。当前段剩余低于 20% 时,异步从中心拉下一段填充备用段;当前段用完时切换到备用段。这样避免"号段用完时同步拉取阻塞服务"的问题,把拉取延迟从用户请求路径中剥离出去。
号段大小需要权衡。太小时请求中心频繁、压力大;太大时服务下线浪费的号多,跳跃幅度也大。美团 Leaf 典型值是 1000,需要根据业务 QPS 计算——如果 1000 QPS,1 个号段只能撑 1 秒,就需要 10000 以上的号段。
双 Buffer 的 20% 阈值也不是拍脑袋定的。假设拉号段 P99 是 5ms,QPS 是 1000,那么在拉取期间最多消耗 5ms × 1000 = 5 个 ID。20% 意味着 200 个 ID 的 buffer,远大于 5 个,所以不会阻塞。
号段的两个主要风险:第一是中心单点——可以通过中心服务多实例 + DB/Redis 分片解决;第二是跨服务非严格递增——比如 A 服务拿 1000-1999、B 服务拿 2000-2999,A 用完切到 3000-3999 时,B 可能还在用 2500,A 新生成的 ID 会"跳"到 B 前面。这个是号段模式的固有代价。
与相关方案对比:号段 vs 雪花——号段无时钟依赖、严格趋势递增、性能接近雪花,但强依赖中心服务;雪花无中心依赖但依赖时钟,且时钟回拨需要额外处理。金融账务选号段(严格递增 + 可审计),互联网业务选雪花(性能优先 + 无中心),日志埋点选 UUID(去中心化)。
整体来说号段模式是严格趋势递增和高性能两个诉求的最佳折中,代价是引入了中心服务和跨服务跳跃。工程上双 Buffer + MySQL 乐观锁 + 多实例部署基本能覆盖大部分场景,美团、滴滴这类大厂的核心业务都基于类似思路。