号段表发号器如何设计?相比数据库自增主键有什么取舍?
简化版
号段表发号器是在数据库中维护业务号段,每次应用领取一段 ID 到内存里本地分配,减少频繁访问数据库。它比单次自增发号吞吐更高,也能按业务生成趋势递增 ID,但要处理号段浪费、并发领取和发号服务高可用。
详细版
数据库自增主键简单可靠,但在分库分表、多业务统一 ID、需要提前生成 ID 或跨库写入时会受限。号段模式通过一张表维护 biz_tag、max_id、step,应用每次把 max_id 增加一个步长并拿到区间。
例如当前 max_id=10000,步长 1000,服务领取后数据库更新到 11000,应用可在内存发放 10001 到 11000。这样 1000 次发号只需要 1 次数据库更新。
缺点是服务重启可能浪费未使用号段;如果步长太小,数据库压力仍大;步长太大,浪费和跳号更明显。
完整版教学
一、为什么需要号段模式
自增主键适合单库单表,但复杂系统常需要在插入前拿到 ID,或者多个服务、多个库共享一个业务 ID 规则。每次都访问数据库自增会成为瓶颈。
号段模式的思路是批发 ID。数据库不再每次发一个,而是每次发一段,应用在内存里零成本递增。
数据库发号:一次领取 [10001, 11000]
应用内存:10001, 10002, 10003 ... 11000
如果步长是 1000,数据库更新次数理论上下降 1000 倍。
二、号段表怎么设计
一个常见表结构如下:
CREATE TABLE id_segment (
biz_tag VARCHAR(64) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL,
updated_at DATETIME NOT NULL
);
领取号段时用乐观更新或行锁保证并发安全:
UPDATE id_segment
SET max_id = max_id + step
WHERE biz_tag = 'order';
更新成功后读取新的 max_id,区间就是 (old_max_id, new_max_id]。
三、并发领取怎么保证不重号
号段表的核心安全点是同一个 biz_tag 的更新必须原子。数据库行锁可以保证两个服务不会拿到同一段。
例如服务 A 和 B 同时领取订单号段。A 先把 10000 更新到 11000,B 之后看到的是 11000,再更新到 12000。两者区间不重叠。
| 服务 | 领取前 max_id | 领取后 max_id | 可用区间 |
|---|---|---|---|
| A | 10000 | 11000 | 10001-11000 |
| B | 11000 | 12000 | 11001-12000 |
这比应用自己查 max 再加一安全得多。
四、步长如何选择
步长太小,频繁访问数据库;步长太大,服务重启或实例下线会浪费大量 ID。选择要看发号 QPS 和可接受跳号程度。
假设订单发号峰值 5000/s,步长 1000,则每秒约领取 5 次号段;步长 10000,则每 2 秒领取一次,但单次浪费上限变大。
很多系统会用双 buffer:当前号段用到 70% 时异步加载下一段,避免号段耗尽时请求阻塞。
五、和自增主键、雪花算法怎么比较
自增主键最简单,强依赖单库;雪花算法不访问数据库,吞吐高,但依赖时钟和机器号;号段模式介于两者之间,趋势递增且实现可控。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自增主键 | 简单可靠 | 跨库不方便 |
| 号段模式 | 趋势递增、DB 压力低 | 可能跳号、依赖发号表 |
| 雪花算法 | 高吞吐、去中心化 | 时钟回拨和机器号管理 |
面试中要按业务约束选择,不要把某一种说成万能。
六、常见误区与追问
- 误区:号段模式不会跳号。 服务重启、缓存号段未用完都会造成跳号。
- 误区:先查 max_id 再 update 就安全。 并发下可能重复,必须用原子更新或锁。
- 误区:步长越大越好。 步长越大浪费和跳号越明显。
- 追问:如何减少号段耗尽时的延迟? 用双 buffer,在当前段使用到阈值时异步预取下一段。
- 追问:号段表挂了怎么办? 已领取号段还能发一段时间,但新号段不可领,需要主从、高可用和告警。
七、加强记忆
记忆钩子:号段模式像批发电影票,售票处一次给你 1000 张,你在门口慢慢发;速度快了,但没发完丢了就会跳号。
回答这题按“为什么要批量发号、表结构、并发安全、步长取舍、方案对比”来讲。它既是数据库设计题,也是高并发 ID 生成题。