重复提交防不住?接口幂等的四种落地做法
用户点一下提交按钮,网络抖了一下,前端自动重试了一次,后端就收到了两条一模一样的下单请求。结果是重复扣款、重复发货,还是只执行了一次?这取决于接口是不是幂等的。幂等不是一个可有可无的加分项,而是所有会产生副作用的接口都应该具备的基本属性。本文讲四种落地做法,以及各自适合的场景。
幂等键:让客户端带上唯一标识
最通用的做法是幂等键。客户端每次发起请求时生成一个唯一键,随请求一起发过来,服务端用这个键做去重:第一次执行并把结果存下来,之后带着同一个键再来,直接返回第一次的结果,不再重复执行。
幂等键通常放在请求头里,服务端用 Redis 之类的存储记录键和处理结果,并设置一个合理的过期时间。过期时间要比客户端可能重试的时间窗口长,否则键过期后重试又会重复执行。这个做法的好处是与业务解耦,几乎所有写接口都能套用;缺点是需要客户端配合生成和传递唯一键,老旧的调用方改造起来有成本。
唯一约束:让数据库替你挡住重复
如果业务本身存在天然的唯一标识,可以直接利用数据库的唯一约束。比如订单号、流水号这类字段,建表时加上唯一索引,重复插入时数据库会直接报错,应用层捕获这个错误后按“已处理”返回即可。
这种做法简单可靠,不依赖额外的存储,数据库本身就是去重的最后防线。它的适用面相对窄一些:只有存在天然唯一键的场景才用得上,而且要处理好并发下两个请求同时插入时的竞争。唯一约束适合作为兜底防线,即使上层幂等键失效,它也能挡住最后一次重复。
状态机:只允许合法的状态流转
很多业务对象本身就有状态,比如订单从待支付到已支付,再到已发货。幂等的另一种思路是把操作写成条件更新:只有当前状态符合预期时才执行,否则直接返回当前状态。
例如支付回调处理时,更新语句写成“只有状态还是待支付时才改为已支付”,重复的回调第二次执行时条件不满足,影响行数为零,应用层据此判断为重复请求,不再重复发券、记账。状态机做法的好处是不需要额外的去重存储,坏处是要求业务建模时就把状态流转想清楚,事后补会很痛苦。
乐观锁:用版本号挡住并发重复
乐观锁是在记录上加一个版本号,更新时检查版本号是否与读取时一致,一致才更新并把版本号加一。两个并发请求同时读到同一版本,只有一个能更新成功,另一个会因为版本不匹配而失败,应用层把失败按重复或冲突处理即可。
乐观锁适合并发修改同一条记录的场景,比如库存扣减、余额变更。它和状态机常常配合使用:状态机管业务流转是否合法,乐观锁管并发下谁先得手。需要注意的是,乐观锁失败后的重试策略要想好,无脑重试可能把一次重复执行变成多次。
四种做法怎么选
没有一种做法能包打天下,实际项目里往往是组合使用。幂等键适合对外暴露的通用写接口,唯一约束适合有天然唯一标识的写操作,状态机适合有明确生命周期的业务对象,乐观锁适合高并发下对同一记录的修改。
一个实用的组合是:对外接口用幂等键做第一层去重,数据库层用唯一约束做最后兜底,核心业务对象再用状态机约束流转。三层叠加之后,重复提交、网络重试、回调重发这几种最常见的重复来源,基本都能挡住。
最后提醒一点:幂等要从设计阶段就放进接口契约里,而不是等线上出现重复数据再补。补幂等本身不难,难的是已经产生的脏数据清理和对账。早做,成本最低。
- 点赞
- 收藏
- 关注作者
评论(0)