首页>安全>正文

四种防重复提交方案实测:从Token到数据库唯一索引

Feng 52 阅读 安全

支付接口被重复触发,一天跑出两千多笔异常交易——这不是段子,是真实事故。防重复提交不是加个按钮禁用就完事,服务端得有几层硬防线。

请求指纹:用SETNX做原子占位

配图

配图

配图

思路很直接:把用户ID、设备指纹、时间戳等要素拼起来,算出唯一特征码,丢进Redis占个坑。

关键在SETNX的原子性——同一指纹并发进来,只有一个能写成功。过期时间别设太长,5到30秒足够覆盖一次提交窗口,设久了正常重试会被误杀。

物流订单场景用得最多。运单创建接口加这层,重复运单率能压到很低。

这里有个坑:MD5产出32位特征码,但别把时间戳精确到毫秒直接拼进去,否则同一用户快速点两次会生成两个指纹,等于没防。时间戳应该取一个粗粒度窗口,或者干脆用业务参数本身做指纹。

配图

异步队列:把并发变成串行

即时通讯这类场景,请求先落持久化队列,用唯一ID去重,工作线程顺序消费。

好处是顺带扛住了流量洪峰——队列本身就是缓冲层。坏处是引入中间件后链路变长,排查问题多一个环节。

RabbitMQ、Kafka都能做,但要注意:去重逻辑必须在消费端做,不能只靠生产端。生产端重复投递的情况太常见了,网络抖动、客户端重试都会导致同一条消息进队列两次。

数据库唯一索引:最后一道硬防线

库存扣减、资金操作这种场景,前面几层都可能被绕过,数据库约束是兜底的。

做法是把业务流水号、操作类型等字段组合建联合唯一索引。插入时数据库直接拒绝重复记录,不依赖应用层判断。

某电商大促期间用“订单号+操作类型”的联合索引,拦下了大量重复库存变更。

注意:唯一索引必须和事务配合。如果先查再插,中间有并发窗口,照样会重复。要么直接insert靠数据库报错,要么用insert ignore/on duplicate key。

不建议在生产用“先select判断再insert”的写法,并发下必然出问题。

Token机制:页面级的经典拦截

Web端防重复提交,Token还是最稳的。服务端渲染页面时生成加密令牌,客户端提交时带上,服务端校验后立即删除。

一次性是核心——校验和删除必须原子完成。用Redis的GETDEL或者Lua脚本,别用先get再delete两步操作,中间挂了令牌就泄漏了。

上面这段是简化示例,实际生产建议用Lua脚本把校验和删除合并成一次原子操作。

怎么选:按场景分层

读多写少的页面提交,Token机制够用。

资金类强一致性业务,数据库唯一索引是底线,不能省。

瞬时高并发场景,队列缓冲更合适,削峰的同时天然去重。

实际系统里通常是组合拳:Token拦页面重复点击,请求指纹防接口重放,数据库唯一索引保资金安全。三层各管一段,谁也别指望单靠一层能兜住所有情况。

验证手段别偷懒

防重复逻辑写完不算完,得验证。

并发压测用JMeter或wrk,同一请求发N次,看最终业务表里是不是只有一条记录。

异常注入也重要:模拟Redis挂掉、队列积压、数据库连接超时,看防护逻辑降级后会不会漏。

判定标准很简单:无论中间件怎么抖,最终数据不能重复。做不到这一点,方案就是纸糊的。

配图

几个容易翻车的点

Redis过期时间设太短,用户正常网络慢导致重试被拒,体验很差。设太长,内存占用上去了。5到30秒是个经验区间,具体看业务。

唯一索引字段选错,比如只用了订单号没加操作类型,同一订单的不同操作会被误拦。

Token存在客户端localStorage里,刷新页面就丢了,用户得重新操作。存session或者服务端渲染时注入更稳。

队列去重的唯一ID如果由客户端生成,恶意用户能伪造。必须服务端生成或者校验。

防重复提交没有银弹,核心原则是:至少有一层是原子的、不可绕过的。数据库唯一索引通常扮演这个角色,其他层都是优化体验和减轻数据库压力的。

Feng
这位作者很神秘,还没有填写简介。