# 如何保证接口的幂等性?

# 介绍
在分布式系统和微服务架构中,接口的幂等性是指同一个接口,使用相同的参数重复执行多次,其结果和仅执行一次的效果完全相同,不会因为重复提交而导致数据混乱(比如重复扣款、重复创建订单)
# 前端保证幂等性的方法
# 按钮只能点击一次
用户点击按钮后将按钮置灰,或者显示loading状态
# RPG模式
即Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,转到提交成功页面。避免用户按F5刷新导致的重复提交,也能消除按浏览器后退键导致的重复提交问题。目前绝大多数公司都是这样做的,比如淘宝,京东等
# Token 机制(防重令牌)
这是最常用的前后端交互防重方案,专门解决用户“手抖”连续点击提交按钮的问题
- 获取 Token:进入表单页面前,前端先向后端请求一个全局唯一的 Token(通常用 UUID,存入 Redis 并设置过期时间)。
- 提交请求:前端提交表单时,将这个 Token 随请求一起发送给后端。
- 校验与删除:后端收到请求后,在 Redis 中检查该 Token 是否存在。如果存在,先删除 Token(用 Lua 脚本保证查询和删除的原子性),再执行业务逻辑;如果 Token 不存在,说明是重复提交,直接拦截
# 后端保证幂等性的方法
# 使用唯一索引
对业务唯一的字段加上唯一索引,这样当数据重复时,插入数据库会抛异常
# 状态机幂等
如果你的业务数据有明确的状态流转(如:待支付 -> 已支付 -> 已发货 -> 已签收),可以通过状态机来保证幂等
update order_table set status = 'PAID' where status = 'UNPAID' and order_no = 100
# 悲观锁
在查询时直接锁行
SELECT * FROM table WHERE id = 1 FOR UPDATE;
在当前事务结束前,其他连接无法修改这一行,但并发性能较低
# 乐观锁
不依赖数据库的锁机制,通过代码逻辑来实现
- 查询数据获得版本号
- 通过版本号去更新,版本号匹配则更新,版本号不匹配则不更新
-- 假如查询出的version为1
select version from table_name where userid = 10;
-- 给用户的账户加10
update table_name set money = money + 10, version = version + 1 where userid = 10 and version = 1
也可以通过条件来实现乐观锁,如库存不能超卖,数量不能小于0
update table_name set num = num - 10 where num - 10 >= 0
# 防重表
增加一个防重表,业务唯一的id作为唯一索引,如订单号,当想针对订单做一系列操作时,可以向防重表中插入一条记录,插入成功,执行后续操作,插入失败,则不执行后续操作
本质上可以看成是基于MySQL实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据
# 分布式锁
执行方法时,先根据业务唯一的id获取分布式锁,获取成功,则执行,失败则不执行。分布式锁可以基于redis,zookeeper,mysql来实现,分布式锁的细节就不介绍了