4. 常见幂等方案二:状态机与乐观锁(版本号机制)

在支付系统中,订单或交易记录通常具有明确的生命周期状态,例如:待支付 → 支付中 → 支付成功 / 支付失败。状态机与乐观锁的组合方案,正是利用数据库行锁与 CAS(Compare And Swap)思想,确保支付状态在并发环境下只按预定义方向流转一次,从根本上杜绝重复支付或状态回跳。

4.1 核心原理:状态机 + 版本号

该方案将支付记录视为一个有限状态机(FSM),每个状态变更必须满足两个条件:

  • 状态方向性:状态只能沿预定义方向前进(如:待支付 → 支付中 → 支付成功),不允许回退或跳跃。
  • 版本号递增:每条记录维护一个整型版本号(version),每次更新时版本号 +1。更新 SQL 中同时校验当前版本号与期望版本号,实现乐观锁。
关键公式: UPDATE payment_order SET status = ?, version = version + 1 WHERE order_id = ? AND version = ? AND status = ?

上述 SQL 中的 AND version = ? 即为 CAS 中的“比较”步骤,version = version + 1 为“交换”步骤。数据库行锁保证同一时刻只有一个事务能成功执行该更新。

状态机 + 乐观锁(版本号)协作流程 待支付 version=1 支付中 version=2 支付成功 version=3 CAS: version=1 CAS: version=2 并发冲突场景:两个线程同时读取 version=1 线程A 更新成功(version→2),线程B 更新失败(version≠1),保证状态只流转一次 支付失败 version=2 超时/异常

4.2 数据库行锁与 CAS 的协同机制

该方案依赖数据库的行级锁(Row Lock)与乐观锁(Optimistic Locking)双重保障:

机制 作用层次 说明
数据库行锁 事务隔离 UPDATE 语句在索引上获取行锁,保证同一时刻只有一个事务能修改该行记录
乐观锁(版本号) 业务逻辑 通过 WHERE version = ? 条件实现 CAS,防止“读-改-写”过程中的并发覆盖
状态机约束 业务规则 WHERE status = ? 确保状态只能从当前合法状态流转到下一状态
实践建议:在支付核心表中,建议将 status 和 version 字段设为 NOT NULL 并建立联合索引(order_id, version),可进一步提升 UPDATE 性能与锁粒度。

4.3 标准实现步骤

  1. 读取当前记录:SELECT status, version FROM payment_order WHERE order_id = ?
  2. 校验状态合法性:判断当前 status 是否允许流转到目标状态(如:只有“待支付”才能流转到“支付中”)
  3. 执行 CAS 更新:
    UPDATE payment_order
    SET status = '支付中', version = version + 1
    WHERE order_id = ?
      AND version = ?
      AND status = '待支付'
  4. 检查影响行数:若影响行数 = 1,则更新成功;若影响行数 = 0,则说明版本号或状态已被其他事务修改,需重试或返回失败

4.4 方案优势与适用场景

维度 评价
并发安全 高——行锁 + CAS 双重保障,无死锁风险
实现复杂度 低——仅需在 SQL 中增加 version 和 status 条件
性能开销 低——乐观锁无锁等待,仅在提交时检测冲突
适用场景 支付状态流转、订单状态机、账户余额变更等高频更新场景
局限性 高并发下冲突率上升时,重试次数增加,需配合重试策略

4.5 避坑指南

常见错误:部分开发者只校验 version 而不校验 status,导致状态回跳。例如:支付成功(version=3)被错误更新为支付中(version=4),虽然 version 递增,但状态方向错误。必须同时校验 status 与 version。
  • 版本号初始值:建议从 1 开始,避免与 NULL 混淆
  • 重试策略:CAS 更新失败后,建议使用指数退避(Exponential Backoff)重试,避免打满数据库连接
  • 事务边界:UPDATE 语句应放在事务内,且事务尽量短小,减少行锁持有时间
  • 索引优化:WHERE 条件中的 order_id、version、status 字段应建立联合索引,避免全表扫描
CAS 更新成功 vs 失败对比 ✅ 更新成功 线程A:version=1 → version=2 影响行数 = 1,提交事务 状态正常流转,幂等性得到保证 ❌ 更新失败 线程B:期望 version=1,实际 version=2 影响行数 = 0,回滚事务 触发重试策略或返回幂等拒绝 并发

4.6 总结

状态机与乐观锁(版本号机制)是支付系统中最成熟、最轻量的幂等方案之一。它利用数据库自身的行锁与 CAS 语义,以极低的代码侵入成本解决了并发状态覆盖问题。在实际生产环境中,该方案通常作为第一道防线,与后续的分布式锁、消息去重等方案组合使用,形成多层幂等防护体系。