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 为“交换”步骤。数据库行锁保证同一时刻只有一个事务能成功执行该更新。
4.2 数据库行锁与 CAS 的协同机制
该方案依赖数据库的行级锁(Row Lock)与乐观锁(Optimistic Locking)双重保障:
| 机制 | 作用层次 | 说明 |
|---|---|---|
| 数据库行锁 | 事务隔离 | UPDATE 语句在索引上获取行锁,保证同一时刻只有一个事务能修改该行记录 |
| 乐观锁(版本号) | 业务逻辑 | 通过 WHERE version = ? 条件实现 CAS,防止“读-改-写”过程中的并发覆盖 |
| 状态机约束 | 业务规则 | WHERE status = ? 确保状态只能从当前合法状态流转到下一状态 |
实践建议:在支付核心表中,建议将
status 和 version 字段设为 NOT NULL 并建立联合索引(order_id, version),可进一步提升 UPDATE 性能与锁粒度。
4.3 标准实现步骤
- 读取当前记录:SELECT status, version FROM payment_order WHERE order_id = ?
- 校验状态合法性:判断当前 status 是否允许流转到目标状态(如:只有“待支付”才能流转到“支付中”)
- 执行 CAS 更新:
UPDATE payment_order SET status = '支付中', version = version + 1 WHERE order_id = ? AND version = ? AND status = '待支付' - 检查影响行数:若影响行数 = 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 字段应建立联合索引,避免全表扫描
4.6 总结
状态机与乐观锁(版本号机制)是支付系统中最成熟、最轻量的幂等方案之一。它利用数据库自身的行锁与 CAS 语义,以极低的代码侵入成本解决了并发状态覆盖问题。在实际生产环境中,该方案通常作为第一道防线,与后续的分布式锁、消息去重等方案组合使用,形成多层幂等防护体系。