什么是接口幂等性
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次
订单接口, 不能多次创建订单
支付接口, 重复支付同一笔订单只能扣一次钱
支付宝回调接口, 可能会多次回调, 必须处理重复回调
普通表单提交接口,多次点击提交, 只能成功一次,表中产生了两条重复的数据,只是id不一样
解决方法
1.唯一索引 -- 防止新增脏数据
2.token机制 -- 防止页面重复提交
3.悲观锁 -- 获取数据的时候加锁(锁表或锁行)
4.乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
5.分布式锁 -- redis(jedis、redisson)或zookeeper实现
6.状态机 -- 状态变更, 更新数据时判断状态(状态有一定的顺序)
实现
insert前先select (新增操作幂等性)
保存操作之间,进行数据查询,有就修改,没有就新增。
缺点:需要查询一次数据,麻烦
加悲观锁 (修改操作幂等性)
在支付场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的:
update user amount = amount-100 where id=123;
如果出现多次相同的请求,可能会导致用户A的余额变成负数。
加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得锁,更新数据,其他的请求则等待。
通常情况下通过如下sql锁住单行数据:
select * from user id=123 for update;
缺点:有性能问题,需要等待A操作完,这行数据 才能操作
加乐观锁 (修改操作幂等性)
乐观锁。需要在表中增加一个timestamp或者version字段,这里以version字段为例。
在更新数据之前先查询一下数据:
select id,amount,version from user id=123;
如果数据存在,假设查到的version等于1,再使用id和version字段作为查询条件更新数据:
update user set amount=amount+100,version=version+1where id=123 and version=1;
更新数据的同时version+1,然后判断本次update操作的影响行数,如果大于0,则说明本次更新成功,如果等于0,则说明本次更新没有让数据变更。
由于第一次请求version等于1是可以成功的,操作成功后version变成2了。这时如果并发的请求过来,再执行相同的sql
唯一索引
绝大数情况下,为了防止重复数据的产生,我们都会在表中加唯一索引,这是一个非常简单,并且有效的方案。
alter table order
add UNIQUE KEY un_code
(code
);
加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry '002' for key 'order.un_code异常,表示唯一索引有冲突。
建防重表
有时候表中并非所有的场景都不允许产生重复的数据,只有某些特定场景才不允许。这时候,直接在表中加唯一索引,显然是不太合适的。
针对这种情况,我们可以通过建防重表来解决问题。
该表可以只包含两个字段:id和唯一索引,唯一索引可以是多个字段比如:name、code等组合起来的唯一标识,例如:susan_0001。
防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。
根据状态值
比如订单表中有:1-下单、2-已支付、3-完成、4-撤销等状态。如果这些状态的值是有规律的,按照业务节点正好是从小到大,我们就能通过它来保证接口的幂等性。
update order
set status=3 where id=123 and status=2;
加分布式锁 + 获取token
redis/zk 实现 (传入的token需要验证,token是否是伪造的)
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可
用户访问页面时,浏览器自动发起获取token请求。
服务端生成token,保存到redis中,然后返回给浏览器。
用户通过浏览器发起请求时,携带该token。
在redis中查询该token是否存在,如果不存在,说明是第一次请求,做则后续的数据操作。
如果存在,说明是重复请求,则直接返回成功。
在redis中token会在过期时间之后,被自动删除。