在后端开发领域,接口幂等性问题不算罕见,但往往要等到数据出现异常,才会真正引起重视。我负责的订单系统上线半年后,运维同事在每周例行日志审计中,发现支付回调接口存在少量重复请求记录,对应的数据库里还出现了12笔重复订单。虽然数量不多、涉及金额不大,但这类问题若不及时处理,到大促等高并发场景下,很可能引发批量数据异常。更关键的是,排查后发现,问题根源并非支付网关的恶意重试,而是我们的接口本身缺乏完整的幂等性防护机制。
接到运维反馈后,我先梳理了支付回调接口的业务流程:用户支付完成后,支付网关会向我们的回调接口推送支付结果,接口接收数据后做校验,接着更新订单状态并写入支付记录。从流程上看似乎没什么问题,但查看接口日志和数据库记录后,很快定位到关键细节:12笔重复订单对应的回调请求,时间间隔都在500ms以内,且请求参数完全一致。进一步跟支付网关技术人员确认得知,他们的系统有超时重试机制,当接口响应超过500ms时会自动重试,而我们的接口在高峰期偶尔会因数据库锁等待导致响应延迟,进而触发了重试。
这个结论让我意识到,之前的开发存在明显疏漏。当时为了赶上线进度,只在接口里做了基础的参数格式校验,完全没考虑幂等性问题。虽然平时流量不大时问题不明显,但隐患一直存在。接下来的核心任务,就是选择一套合适的幂等性解决方案——既要解决当前问题,又要兼顾系统性能和后续可维护性。
结合团队技术栈和系统现状,我们初步筛选了三种常见解决方案,并分别做了测试验证:
第一种是“数据库唯一索引”方案,这也是最基础的做法:给订单号字段添加唯一索引,当重复请求过来时,数据库会抛出主键冲突异常,我们在代码里捕获异常后返回成功。这种方案开发成本极低,运维同事配合加索引只需几分钟,但测试时发现问题:若重复请求频率过高,大量主键冲突异常会增加数据库日志开销,且异常处理逻辑不够优雅。
第二种是“Redis分布式锁”方案,这是高并发场景下的常用方案。具体思路是:收到回调请求后,先以订单号为key向Redis发起加锁请求,加锁成功再执行后续业务逻辑,执行完成后释放锁;为避免死锁,还需给锁设置过期时间。这个方案防护效果很好,但缺点也很明显:我们的Redis集群主要用于缓存热点数据,之前曾因网络波动出现过短暂不可用的情况,引入分布式锁会增加系统依赖风险;而且加锁、解锁代码需要考虑锁超时、释放别人的锁等异常场景,开发和测试成本都比较高。
第三种是“WAF请求去重+接口状态校验”方案,这是结合现有技术储备想到的。之前为做基础安全防护,服务器已部署雷池WAF,它自带“请求去重”功能,可通过配置拦截短时间内的重复请求。我们的思路是:先用WAF做第一层过滤,拦截大部分重复请求,再在接口层做第二层校验,形成双重防护。这样既能降低接口处理压力,又能避免单一方案的局限性。
确定方案后,落地过程比预期顺利。第一步是配置WAF:登录WAF控制台,找到“请求控制”模块,选择“按参数去重”策略,指定“orderNo”作为去重参数,设置10秒时间窗口——也就是说,10秒内针对同一个订单号的重复请求会被直接拦截。这个配置不用写代码,运维同事按文档操作,20分钟就完成了。第二步是修改接口代码,增加状态校验逻辑:收到请求后,先根据订单号查询数据库,若订单已存在且状态为“已支付”,就直接返回成功响应,不执行后续更新和插入操作;若订单不存在或状态未支付,再执行正常业务流程。这部分代码改动很小,只增加了四五行查询和判断逻辑,加上测试总共花了1个小时。
为验证效果,我们用JMeter做了针对性压测:模拟支付网关在1秒内对同一个订单号发起5次回调请求,持续10分钟。压测结果显示,WAF成功拦截了80%的重复请求,剩下20%的请求在接口层被状态校验拦截,数据库里只生成了1条订单记录和1条支付记录,完全符合预期。而且接口平均响应时间仅增加15ms,对系统性能几乎没有影响。
方案上线后,我们持续监控了一个月,期间经历了一次小型促销活动,支付回调接口请求量达到平时的3倍,但没有再出现一笔重复订单。这次经历也让团队意识到,接口幂等性防护不应是“事后补救”,而应是“事前规划”。随后,我牵头整理了《接口幂等性设计规范》,明确不同场景下的解决方案:对于支付、订单等核心接口,采用“WAF去重+接口校验+唯一索引”的三重防护;对于查询类接口,因本身天然幂等,只需做基础参数校验;对于新增类接口,若没有唯一标识,采用“令牌桶”方案。
这里给同行提两个实际开发中的小建议:一是做幂等性设计时,要结合业务场景选择方案,不要盲目追求复杂的分布式方案,适合自己系统的才是最好的;二是尽量利用现有工具降低开发成本,比如我们用到的WAF,原本是做安全防护的,没想到在幂等性防护上也能发挥作用。如果大家在实际开发中遇到过不同的幂等性问题,或者有更轻量的解决方案,欢迎在评论区交流,互相借鉴学习。