基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如果业务要求分布式 ID 必须是严格递增的(不仅是趋势递增),该如何设计?

知识点图片

在分布式系统中,将 ID 生成从“趋势递增”(如 Snowflake)提升到“严格递增”(Strictly Increasing / Monotonic),是一个巨大的挑战。

核心矛盾在于: 严格递增意味着全局有序。为了保证全局有序,必须将所有生成 ID 的请求串行化(Serialize)到一个单点或共识组中处理。这必然会牺牲性能(吞吐量)和可用性(存在单点故障风险)。

如果业务强制要求严格递增(例如:银行流水号、强一致性事务排序、特定证券交易场景),通常有以下几种设计方案,按推荐程度和复杂度排序:


方案一:Redis 原子递增(最推荐,性能与一致性的平衡)

利用 Redis 的单线程特性和 INCR 命令,是实现分布式严格递增 ID 最常见的高性能方案。

  • 原理:
    • 所有应用节点都向同一个 Redis 实例(或主节点)发送 INCR key 命令。
    • Redis 单线程处理命令,天然保证了操作的原子性和严格的先后顺序。
  • 优点:
    • 性能高: 内存操作,远快于数据库。
    • 严格有序: 绝对保证 IDt+1>IDtID_{t+1} > ID_t
  • 缺点与挑战:
    • 单点故障(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,并且在应用层处理好主从切换期间可能的数据丢失(通常业务层需要去重校验)。

方案二:集中式数据库表(Flicker / Ticket Server 模式)

这是最传统也最可靠的方案,利用关系型数据库(MySQL/PostgreSQL)的事务锁机制。

  • 原理:
    • 利用数据库的 AUTO_INCREMENT 或一张专门的计数表。
    • 使用 REPLACE INTOUPDATE ... RETURNING 语法获取下一个 ID。
    sql
    CREATE 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)

  • 含义: IDnext>IDprevID_{next} > ID_{prev},允许中间有空洞(例如 1, 2, 5, 6...)。比如事务失败了,ID 就消耗掉了。
  • 方案: RedisTSO
  • 推荐: 这是大多数“严格递增”业务的实际需求。

定义 C:线性一致性(Linearizability)

  • 含义: 如果操作 A 在操作 B 之前完成(Wall clock time),那么 IDA<IDBID_A < ID_B
  • 方案: 必须使用 单点序列化(Redis/DB)TSO(授时中心)
  • 注意: 任何基于“号段模式”(客户端批量申请 ID)的方案都无法满足此要求,因为客户端 A 申请了 [1-100],客户端 B 申请了 [101-200],如果 B 先用掉了 101,而 A 后用掉 1,就违反了时间上的严格递增。

总结建议

  1. 如果并发量 < 1000 TPS: 直接用 MySQL auto_increment。简单、可靠、不仅严格递增还连续。
  2. 如果并发量高(万级 TPS)且允许 ID 不连续: 使用 Redis INCR。需要配置好 AOF 策略,并接受极端宕机情况下的少量 ID 丢失或重复(业务层做幂等)。
  3. 如果是分布式数据库/事务场景: 参照 TiDB 的 PD (Placement Driver) / TSO 模型,使用 Etcd 实现一个高可用的发号器。

避坑指南: 绝对不要使用 Snowflake 或美团 Leaf 的 Segment 模式,它们只能做到“趋势递增”,无法满足“严格递增”。

00:00
00:00