





幂等性原本是数学中的概念,用在接口设计上,指的是同一个请求被重复执行一次和执行多次,对系统产生的最终影响完全一致。简单说,就是调用方不管点一次还是点十次,结果都只能是一次。很多大连企业的线上事故都与幂等缺失有关:用户连点两次支付按钮扣了两次款、消息队列重投递导致订单重复创建、定时任务重跑发了两次优惠券。这些问题单靠前端禁用按钮无法根治,必须在接口层面从设计上保证幂等。对大连技术团队而言,幂等性是支付、下单、退款这类写操作接口的基本功,不是锦上添花,而是底线要求。
第一类是用户侧:网络慢时用户反复点击提交按钮,或者页面刷新后浏览器重新提交表单。第二类是中间链路:网关超时后客户端自动重试、负载均衡切换连接、消息队列At-Least-Once投递语义导致同一消息被推送多次。第三类是系统侧:定时任务调度异常重复触发、异步任务执行一半失败后回滚重跑、发布流程中任务被重复拉起。这三类来源决定了幂等问题无法靠某一端单独解决,必须从接口契约层面统一设计,让任何一次重复请求在服务端都被自然拦截。
读操作天然幂等,查多少次结果都一样,不需要额外处理。真正需要重点设计的是写操作:POST创建类接口如下单、创建工单;支付与退款接口;消息回调接口如支付结果通知;批量任务接口如定时结算。其中涉及资金和库存的接口要求最高,一次重复就可能造成直接损失。建议在接口评审阶段就把写接口分成两类:必须幂等和允许重复,分别标注在接口文档中,开发和测试据此安排对应的校验方案,避免上线后才发现问题。
利用数据库唯一约束是最可靠的方案之一。在业务表上为业务唯一键建唯一索引,例如订单表的out_trade_no字段,重复插入时数据库直接抛出DuplicateKeyException,业务捕获后返回已存在的结果。这种方案的优点是强一致、实现简单、性能开销极低,缺点是依赖数据库、不适合跨多个写操作的复杂流程。对大连技术团队来说,凡是能加唯一索引的场景,都应优先用这个方案,因为它是数据库层面的硬约束,比任何应用层判断都更不容易出错。
针对更新操作,在表中增加version字段,更新时带上版本号作为条件:update account set balance=100, version=version+1 where id=1 and version=5。如果更新影响行数为0,说明记录已被其他请求修改过,本次请求失败并返回冲突。乐观锁不需要加锁,并发性能好,适合库存扣减、余额变动这类场景。需要注意的是,乐观锁只能保证并发修改不互相覆盖,不能防止同一个请求被重复执行,因此通常与其他方案配合使用。
在分布式系统中,用Redis实现分布式锁:以业务唯一键为锁key,请求到来时先尝试加锁,加锁成功才执行业务,执行完释放锁;加锁失败说明已有请求在处理,直接返回重复提示。Redis锁要设置合理的过期时间,防止持锁进程宕机后死锁;释放锁时要用Lua脚本校验value,避免误删其他请求的锁。分布式锁适合无法用数据库唯一索引表达的跨服务流程,例如一个下单接口要同时写订单、库存、积分三个库,此时用锁把整个流程串行化。
Token机制适合表单提交场景:进入页面时服务端先下发一个一次性Token存入Redis,用户提交时把Token一起带来,服务端校验通过后立即删除该Token。第一次请求带Token进来校验通过并删除,后续重复请求因为Token已被删除而校验失败。整个过程用Redis的原子操作完成,例如用Lua脚本实现getAndDel。这种方案体验好,适合用户主动触发的表单,缺点是对消息队列、定时任务这类无用户上下文的场景不适用。
有明确生命周期的业务适合用状态机做幂等:订单只有待支付状态才能转为已支付,已支付状态再次收到支付回调时直接忽略。在更新语句里带上状态条件:update order set status=2 where id=1 and status=1。这类方案天然契合业务语义,支付回调、退款、审批流都适用。它的优点是不需要额外存储,判断条件就是业务状态本身;缺点是只能覆盖状态迁移,不能防止同一状态被重复写入。
去重表是独立于业务表的一张轻量表,专门记录已处理过的请求唯一ID:每次请求先查去重表,存在则拒绝,不存在则插入并执行业务。配合数据库唯一索引,插入冲突即视为重复。去重表方案灵活,不侵入业务表结构,适合复杂多步骤流程的统一幂等入口,例如网关层对所有外部回调做统一去重。缺点是要维护额外表,需要定期清理历史记录避免膨胀。

选型原则是能靠数据库就不依赖中间件:单表单行写入首选唯一索引;更新库存余额首选乐观锁;用户表单提交首选Token机制;支付退款回调首选状态机控制;跨服务编排首选分布式锁;外部回调统一入口首选去重表。实际项目中往往组合使用,例如支付接口既用状态机防止重复回调,又用唯一索引防止重复创建流水。大连企业在技术评审时应把幂等方案写进接口设计文档,明确每个写接口用哪种方案,避免开发人员各自为政。
第一个坑是先查后判而非原子操作:先查Redis有没有这个key,没有就执行业务,这在并发下两个请求可能同时查到没有,从而双双通过,必须用原子操作或数据库唯一约束兜底。第二个坑是锁过期时间过短,业务还没执行完锁就释放,导致并发穿透。第三个坑是幂等Key设计不合理:把会变化的字段当作幂等Key,例如用户ID加时间戳,每次都不同,等于没做幂等。幂等Key必须是业务上真正唯一且稳定的标识,例如订单号、支付流水号。
前端虽然不能替代服务端幂等,但可以减少重复请求:提交按钮在请求返回前置灰禁用,避免用户连点;表单提交后跳转页面或清空表单,避免浏览器刷新重新POST。服务端返回重复请求时,前端要给出友好提示,例如订单已提交请勿重复操作,而不是直接报错。前后端约定好幂等重复的错误码,前端据此做差异化处理,用户体验会明显提升。
测试阶段要专门设计幂等用例:用同一参数连续发起十次请求,断言最终数据库只有一条记录;模拟消息队列重复投递,断言业务只执行一次;模拟网络超时后客户端重试,断言接口返回与首次一致。压测时还要关注幂等方案自身的性能,例如去重表在高并发下的查询耗时、Redis锁的命中率,避免为了解决一个问题又引入新的性能瓶颈。对大连技术团队来说,幂等用例应作为写接口的标准测试项纳入回归测试集。
接口幂等性设计是一个从选型到落地再到测试的完整闭环。建议大连技术团队先盘点现有写接口,识别出资金、库存、回调三类高风险接口,逐个补全幂等方案,再把幂等校验纳入新接口评审和测试的标准流程。把重复请求当作系统的常态而不是异常来设计,线上因重复提交导致的事故就会大幅减少。