在分布式系统中,网络波动可能导致请求重复发送,或者用户误操作导致重复提交。幂等性保证了无论调用多少次,对系统的影响都与调用一次相同。
以下是高并发场景下常见的幂等性实现方案:
1. 数据库唯一索引 (Unique Key) —— 最强兜底
利用数据库的唯一约束(Unique Constraint)来保证幂等。
- 原理: 建立一个去重表(或在业务表中建立唯一索引),将业务唯一 ID(如订单号、交易流水号)设为唯一索引。重复插入时会抛出
DuplicateKeyException,捕获该异常并直接返回成功或提示重复。 - 适用场景: 插入型操作(Insert),如防止重复注册、重复下单。
- 优点: 实现简单,数据库层面的强一致性保证。
- 缺点: 分库分表时需要保证唯一 ID 路由到同一张表,否则失效。
2. 状态机幂等 (State Machine) —— 业务层控制
利用业务状态的流转约束来保证幂等。
- 原理: 在更新数据时,带上当前状态作为条件。
如果第一次执行成功,状态变为UPDATE order SET status = 'PAID' WHERE id = 1001 AND status = 'UNPAID';PAID;第二次执行时,条件status = 'UNPAID'不满足,更新行数为 0,从而实现幂等。 - 适用场景: 更新型操作(Update),如订单状态流转、支付状态更新。
- 优点: 性能高(利用数据库行锁),无额外存储成本。
- 缺点: 仅适用于有状态流转的业务。
3. Token 机制 (Redis) —— 防止重复提交
经典的“一锁二判三更新”思想在 Web 层的应用。
- 原理:
- 申请 Token: 客户端操作前先向服务端申请一个全局唯一的 Token,存入 Redis。
- 提交请求: 客户端发起请求时带上该 Token。
- 校验销毁: 服务端收到请求后,校验 Redis 中是否存在该 Token。
- 若存在:删除 Token(原子操作),执行业务逻辑。
- 若不存在:说明是重复请求,直接拒绝。
- 关键点: 获取 Token 和删除 Token 必须是原子操作(使用 Lua 脚本或
DEL命令返回值判断)。 - 适用场景: 表单重复提交、非即时响应的按钮点击。
- 优点: 可以在业务处理前拦截重复请求,减轻数据库压力。
- 缺点: 需要多次交互(申请 Token),增加了系统复杂度。
4. 分布式锁 (Distributed Lock)
在业务执行前先获取锁,执行完释放锁。
- 原理: 使用 Redis (Redisson) 或 ZooKeeper 实现分布式锁。以业务唯一 ID 为 Key,获取锁成功的请求执行业务,获取失败的请求直接返回或等待。
- 适用场景: 并发极高且对数据一致性要求严格的场景。
- 优点: 可以控制并发,防止击穿数据库。
- 缺点: 引入了外部依赖(Redis/ZK),锁的粒度控制不好会影响性能。
5. 乐观锁 (Optimistic Lock)
利用版本号机制。
- 原理: 在表中增加
version字段。UPDATE account SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1; - 适用场景: 读多写少的更新场景。
- 优点: 无锁,性能较高。
- 缺点: 竞争激烈时重试次数多,影响性能。
6. 防重表 (Deduplication Table)
专门建立一张表用于记录已处理的请求 ID。
- 原理: 业务处理前/后,将请求 ID 插入防重表。如果插入失败(唯一索引冲突),则视为重复请求。
- 适用场景: 业务表无法建立唯一索引,或者业务逻辑复杂涉及多张表时。
总结
| 方案 | 适用场景 | 性能 | 复杂度 |
|---|---|---|---|
| 数据库唯一索引 | 插入防重 (Insert) | 中 | 低 |
| 状态机 | 状态流转 (Update) | 高 | 低 |
| Token 机制 | 页面重复提交 | 高 | 中 |
| 分布式锁 | 强一致性、高并发 | 中 | 高 |
最佳实践: 通常是 Token 机制(前端防抖/后端防重) + 数据库唯一索引/状态机(兜底) 结合使用。