目前正在测试中:完成后将开放 GitHub 代码。

工作原理

本页通过客户端与服务器(集群)之间实际的消息流来说明 Ticketing 的工作原理。 线路上传输字节的精确规格见协议 (API)协议 (集群)


整体结构

接收用户请求的服务(例如处理抢票开售的活动服务器)会针对同一个 key 向 Ticketing 集群排队,只有轮到的那台服务器才会继续执行诸如扣减库存或分配座位之类的工作。

groups
用户
您的服务(例如事件服务器)
dns 事件服务器 1
dns 事件服务器 2
dns 事件服务器 3
Ticketing 集群
confirmation_number ticketing-server 1
confirmation_number ticketing-server 2
confirmation_number ticketing-server 3

锁的获取与释放

最基本的流程。未被占用的 key 会立即被授予;已被占用的 key 会在 FIFO 队列中 排队等待。持有者释放后,锁会直接移交给队列最前面的等待者,无需重新竞争 —— 下一个等待者会立即带着新令牌继续,锁永远不会有空档期,也没有人能插队。

客户端 X
客户端 Y
服务器
A · 获取 (order-1234)
A · 已获取 (token 41)
A · 获取 (order-1234)
被占用 → 加入队列,响应暂缓
R · 释放
R · 已释放
A · 直接移交 (token 42)
请求响应
  • lease 是一道安全网。 如果客户端挂掉或忘记释放,lease 时长一到, 服务器就会回收该 key 并移交给下一个等待者。请把它设得宽裕一些 —— 比临界区最坏情况所需的时间还要长。v1 只接受 1–250 秒;超过 250 秒的工作 明确不受支持并会被拒绝。
  • wait 是客户端等待时长的上限。 如果在这段时间内仍未轮到,服务器会 返回 T(超时)而不是授予锁,因此锁永远不会泄漏。0 表示不进入队列、只做一次 即时尝试。不存在无限等待。
  • 如果同一个客户端尝试重新获取自己已经持有的 key,它会像其他等待者一样 排队 —— 这不是可重入锁

关于按 key 状态划分的服务器行为完整规则见请求 × Key 状态矩阵, 字段取值范围见字段约束

围栏令牌

再完美的锁服务器也无法控制客户端的时钟。仅靠 Ticketing 本身无法阻止过期持有者 的情况:客户端在持有锁期间因 GC 暂停或过载而卡住一段时间,随后醒来 —— 却不知道自己的 lease 早已过期 —— 继续完成工作。这就是为什么每次获取响应中的 token 都是一个保证每次授予都递增的 u64 整数。对金融场景或持久化数据库, 每个 key 的 fencing high-water 比较、更新必须与实际 business write 在同一个 DB transaction 或一次 conditional write 中原子执行。先只更新 fencing row、以后再执行 实际写入并不安全。

客户端 X
客户端 Y
服务器
受保护资源
A · 获取
A · 已获取 (token 7)
A · 获取 → 等待
因 GC / 过载而停滞
lease 到期 → 被回收
A · 直接移交 (token 8)
写入 (token 8)
记录 token 8 → 已接受
延迟醒来
写入 (token 7)
7 < 8 → 被拒绝
请求响应

单调性在整个集群范围内同样成立 —— 令牌计数器本身通过共识复制, 因此即使发生 leader 变更,新 leader 也始终从比之前更大的数字继续。 精确的字段格式见围栏令牌 (规格)

集群 — 基于 Raft 共识的无中断

在集群模式下,节点通过 Raft 共识共享同一份锁状态。只有 leader 处理客户端请求, 每一次锁状态变更(获取、释放、过期)都只有在过半数节点记录后才会最终生效。 连接到非 leader 节点会得到 M(已转移)响应,指向 leader。

客户端
跟随者 B
领导者 A
跟随者 C
A · 获取
M · 已转移(指向 leader)
A · 获取
复制提案
复制提案
确认
确认
过半确认 → 提交
A · 已获取 (token)
请求响应共识 RPC(Raft)
  • 如果 leader 挂掉,当前默认配置会在约 2.3–2.5 秒内选出新 leader。 只有在一个请求字节都未发送时,同一逻辑 acquire 才可在剩余 wait 预算内继续。 已发送但未确认响应的 acquire 是 Indeterminate,绝不自动重发。
  • 即使是 lease 过期也要经过共识。 只有当 leader 提交过期命令后,锁才会 真正消失,因此各节点间时钟的细微差异永远不会导致锁状态出现分歧。
  • 如果集群失去多数派(例如 3 个节点中有 2 个宕机),它会停止接受写入, 而不是冒险发放错误的锁(安全第一)。如果 3 个节点中的 2 个(或 5 个中的 3 个) 丢失了易失的 process state,不得恢复该 cluster/fencing domain,也不得以同一 identity 自动 bootstrap。

因为起决定作用的是多数派,所以只允许 3 或 5 个节点。 偶数节点只会增加成本, 却无法提升容错能力,因此服务器会拒绝以偶数节点启动。

节点数无中断可容忍的并发故障数备注
1token 单调性只在 process lifetime 内保证;不支持保护可重启的 persistent DB
31标准的无中断配置。 满足大多数场景
52即使有一个节点在维护中,冗余也依然充足

对端 RPC 的帧格式与端口规则见协议 (集群), 逐台替换的流程见无中断的新 NodeId learner 替换

客户端行为

官方客户端共同具备以下行为(各语言 API 见库列表)。

  • 后台为每个地址维持一条常驻连接。 对于单个地址,会保持两条连接到 同一节点,这样某个 socket 的短暂断开不会中断服务。
  • 优先选择 leader,其次轮询。 它会记住上次通过 M 被重定向到的 leader, 并将后续请求直接发往该节点。
  • 请求是流水线化的。 下一个请求无需等待上一个响应即可发送 —— 响应如何 与请求配对见响应匹配规则
  • 连接断开后会以指数退避方式持续重试(0.1 秒起,上限 3.2 秒)。 连接失效不会破坏 broker 对象 —— 它会自行恢复。
  • 只有一个请求字节都未发送时,客户端才会在内部重试同一逻辑 acquire。可关联的 B 是确定的 Busy/未获取结果,立即返回 caller,不做内部重试。M 只用作下一连接的 leader hint;它没有 owner/key,不能作为重发已发送 pending 请求的依据。已发送但未确认 响应的 acquire 会报告为 Indeterminate,客户端不得进入 critical section。
  • 显式 release 和已知 token 的补偿使用 owner/token/key 精确匹配及有界专用 queue。 重试以 request/enqueue 时刻起算的绝对 5 秒 deadline 为限,不会延长到剩余 lease。 未收到成功响应时,显式 release 不得报告或假定成功;server-side lease 是最终安全网。

连接与安全

每个连接(客户端或对端)都按 TCP → (TLS) → challenge-response 认证的顺序建立。 客户端将服务器的 nonce 与自己的令牌一起哈希后作答,因此令牌永远不会以明文形式 出现在线路上,每次连接都使用新的 nonce 可杜绝重放攻击。精确规格见 认证握手

一致性小结

  • 互斥性由共识保证。 由于每次授予都要经过过半数提交,即使在网络分区或 leader 变更期间,同一个 key 也绝不会同时发放给两个持有者。
  • 持久化数据库必须强制执行 fencing。 只有与受保护写入在同一个 transaction/ conditional write 中原子执行的每 key high-water 条件,才能阻止在 lease 过期后 延迟醒来的客户端。锁负责排序,DB fencing 阻断最后一次 stale write。