如果业务要求分布式 ID 必须是严格递增的(不仅是趋势递增),该如何设计?
在分布式系统中,将 ID 生成从“趋势递增”(如 Snowflake)提升到“严格递增”(Strictly Increasing / Monotonic),是一个巨大的挑战。
核心矛盾在于: 严格递增意味着全局有序。为了保证全局有序,必须将所有生成 ID 的请求串行化(Serialize)到一个单点或共识组中处理。这必然会牺牲性能(吞吐量)和可用性(存在单点故障风险)。
如果业务强制要求严格递增(例如:银行流水号、强一致性事务排序、特定证券交易场景),通常有以下几种设计方案,按推荐程度和复杂度排序:
方案一:Redis 原子递增(最推荐,性能与一致性的平衡)
利用 Redis 的单线程特性和 INCR 命令,是实现分布式严格递增 ID 最常见的高性能方案。
- 原理:
- 所有应用节点都向同一个 Redis 实例(或主节点)发送
INCR key命令。 - Redis 单线程处理命令,天然保证了操作的原子性和严格的先后顺序。
- 所有应用节点都向同一个 Redis 实例(或主节点)发送
- 优点:
- 性能高: 内存操作,远快于数据库。
- 严格有序: 绝对保证 。
- 缺点与挑战:
- 单点故障(SPOF): 如果 Redis 挂了,整个 ID 生成服务不可用。
- 持久化风险(RDB/AOF): 如果 Redis 宕机且数据未完全刷盘(AOF 存在 1秒延迟或异步复制),重启后可能会回退,导致 ID 重复。
- 改进策略(高可用设计):
- 利用 Lua 脚本 + 预分配: 每次请求不只拿 1 个,而是拿一批(例如
INCRBY key 1000),但这会退化为“趋势递增”(因为客户端拿到范围后,不同客户端之间无法保证严格的时间序)。如果要严格递增,必须每次取 1 个。 - 双写/多实例轮询(复杂): 设置 2 个 Redis 实例,A 生成奇数(1,3,5...),B 生成偶数(2,4,6...)。
- 注意: 这种方式只能保证唯一性,不能保证严格递增(如果 A 负载高,B 负载低,可能出现 ID 4 比 ID 3 先生成的情况)。
- 强一致性保障: 必须使用 Redis Sentinel 或 Cluster,并且在应用层处理好主从切换期间可能的数据丢失(通常业务层需要去重校验)。
- 利用 Lua 脚本 + 预分配: 每次请求不只拿 1 个,而是拿一批(例如
方案二:集中式数据库表(Flicker / Ticket Server 模式)
这是最传统也最可靠的方案,利用关系型数据库(MySQL/PostgreSQL)的事务锁机制。
- 原理:
- 利用数据库的
AUTO_INCREMENT或一张专门的计数表。 - 使用
REPLACE INTO或UPDATE ... RETURNING语法获取下一个 ID。
sqlCREATE TABLE ids ( id bigint(20) unsigned NOT NULL auto_increment, stub char(1) NOT NULL default '', PRIMARY KEY (id), UNIQUE KEY stub (stub) ); REPLACE INTO ids (stub) VALUES ('a'); SELECT LAST_INSERT_ID(); - 利用数据库的
- 优点:
- 严格递增: 数据库 ACID 保证。
- 持久化可靠: 不容易丢数据。
- 缺点:
- 性能瓶颈: 数据库写锁是最大的瓶颈,并发 TPS 有限(通常几千到一万)。
- 单点故障: 数据库宕机则服务不可用。
- 注意: 千万不要使用“步长(Step)”设置(如机器 A 产 1,4,7...),因为这会破坏“严格递增”的语义(先请求的可能拿到更大的 ID)。
方案三:TiKV / TSO (Timestamp Oracle) 模式(分布式强一致)
如果你使用的是 TiDB 或者类似的 NewSQL 架构,或者愿意引入 Etcd,可以采用 TSO(授时中心) 方案。这是 Google Spanner 和 TiDB 处理分布式事务排序的核心机制。
- 原理:
- 建立一个高可用的集群(如 Etcd 或专门的 TSO 服务),通过 Raft/Paxos 协议选出一个 Leader。
- 所有客户端向 Leader 请求 ID。
- Leader 在内存中维护一个计数器(通常结合物理时间 + 逻辑计数),分配 ID 并持久化。
- 优点:
- 高可用: 自动选主,解决了单点故障问题。
- 严格有序: 通过共识算法保证全局线性一致性。
- 缺点:
- 网络开销: 每次获取 ID 都要经过一次网络 RPC。
- 系统复杂度: 需要维护 Etcd 或 ZooKeeper 集群。
方案四:ZooKeeper 顺序节点
利用 ZooKeeper 的持久顺序节点(Persistent Sequential ZNode)特性。
- 原理:
- 客户端在 ZK 的某个目录下创建节点,ZK 会自动在节点名后追加一个严格递增的序列号。
- 客户端解析这个序列号作为 ID。
- 优点:
- 严格递增,强一致性。
- 缺点:
- 性能极差: ZK 的写性能不支持高并发 ID 生成,且会产生大量垃圾节点需要清理。不推荐用于高频业务,仅适用于低频、配置类的 ID 生成。
关键决策点:你说的“严格递增”到底是指什么?
在设计前,必须再次确认业务对“严格递增”的定义,这决定了方案的选择:
定义 A:全局绝对连续(No Gaps)
- 含义: ID 必须是 1, 2, 3, 4... 中间不能有空洞。
- 方案: 只能选 方案二(数据库单表) 或 方案一(Redis + AOF Everysec)。
- 代价: 吞吐量极低,无法扩展。一旦系统崩溃,为了保证不跳号,恢复极其麻烦。
定义 B:全局单调递增(Monotonically Increasing)
- 含义: ,允许中间有空洞(例如 1, 2, 5, 6...)。比如事务失败了,ID 就消耗掉了。
- 方案: Redis 或 TSO。
- 推荐: 这是大多数“严格递增”业务的实际需求。
定义 C:线性一致性(Linearizability)
- 含义: 如果操作 A 在操作 B 之前完成(Wall clock time),那么 。
- 方案: 必须使用 单点序列化(Redis/DB) 或 TSO(授时中心)。
- 注意: 任何基于“号段模式”(客户端批量申请 ID)的方案都无法满足此要求,因为客户端 A 申请了 [1-100],客户端 B 申请了 [101-200],如果 B 先用掉了 101,而 A 后用掉 1,就违反了时间上的严格递增。
总结建议
- 如果并发量 < 1000 TPS: 直接用 MySQL
auto_increment。简单、可靠、不仅严格递增还连续。 - 如果并发量高(万级 TPS)且允许 ID 不连续: 使用 Redis
INCR。需要配置好 AOF 策略,并接受极端宕机情况下的少量 ID 丢失或重复(业务层做幂等)。 - 如果是分布式数据库/事务场景: 参照 TiDB 的 PD (Placement Driver) / TSO 模型,使用 Etcd 实现一个高可用的发号器。
避坑指南: 绝对不要使用 Snowflake 或美团 Leaf 的 Segment 模式,它们只能做到“趋势递增”,无法满足“严格递增”。