用户连续点击提交按钮、网关超时后自动重试、消息队列重复投递,都可能让同一业务动作执行多次。前端按钮置灰只能改善体验,无法构成服务端保障。真正的幂等必须由能够决定业务结果的服务负责。
先定义“同一次业务动作”
幂等设计的第一步不是上 Redis 锁,而是找到稳定的业务标识。例如支付请求可以使用支付流水号,创建订单可以使用客户端生成的请求号,消费消息可以使用消息 ID。
一个好的幂等键应满足:在一次业务动作内保持不变,不同业务动作之间不会冲突,并且能够被数据库唯一约束保护。
首选数据库唯一约束
数据库是订单最终落点,因此唯一约束通常是最可靠的最后一道防线。
1 | CREATE UNIQUE INDEX uk_order_request_no |
接口接收由调用方生成的 requestNo,创建订单时一并写入。两个并发请求即使同时通过应用层检查,也只有一个能够插入成功。
1 | public record CreateOrderRequest( |
1 |
|
示例表达的是核心结构。生产代码捕获唯一键冲突时,不应仅凭异常文本判断;应按项目使用的数据库和持久层识别约束,并重新查询已有结果。
幂等令牌适合什么场景
当业务本身没有天然唯一键时,可以先申请一次性令牌。客户端提交时携带令牌,服务端以原子操作把令牌从“未使用”改为“处理中”,处理成功后保存结果。
令牌状态不能只有“存在”和“不存在”。如果业务执行到一半进程崩溃,简单删除令牌会让请求永久失败,简单保留令牌又可能让请求永久卡住。更完整的状态通常包括 PROCESSING、SUCCEEDED 和 FAILED,同时记录结果引用、过期时间和失败原因。
分布式锁不是最终答案
Redis 锁可以减少重复执行,但无法替代数据库约束。锁可能过期,持锁进程可能暂停,Redis 与数据库之间也不存在天然事务。更稳妥的组合是:Redis 用于削峰和快速拦截,数据库唯一约束负责最终一致性。
锁值必须包含请求拥有者的随机标识,释放时通过 Lua 脚本比较后删除,避免误删其他请求后来获得的锁。锁过期时间也必须覆盖合理的业务执行时间,长任务则需要续期或改用状态机。
返回相同结果,而不只是拒绝重复请求
幂等不等于第二次请求统一返回“请勿重复提交”。如果第一次已经成功,后续相同请求最好返回相同的业务结果,这样调用方才能在网络超时后可靠重试。
需要同时校验幂等键和请求内容。如果相同 requestNo 对应了不同商品或数量,应返回参数冲突,而不是复用旧结果。可以保存请求摘要,例如对规范化后的关键字段计算哈希,并在重复请求时比较。
HTTP 层如何表达
创建成功可返回 201 Created;相同请求再次到达并复用已有结果时,可以继续返回成功响应,并携带相同订单信息。相同幂等键对应不同参数时,返回 409 Conflict 更容易让调用方理解问题。
不要对所有 POST 请求套用同一种切面。不同业务的幂等范围、结果保存时间和失败恢复规则不同,通用注解只能封装令牌读取、状态存储等机械逻辑,业务唯一性仍应明确写在领域服务中。
总结
可靠的接口幂等通常由四部分组成:稳定的幂等键、数据库唯一约束、可恢复的处理状态,以及重复请求返回原结果的语义。先把业务唯一性定义清楚,再决定是否增加 Redis,远比先写一个分布式锁注解更重要。