接口幂等:多次请求,结果一致。
同样的请求参数,多次去访问同一个接口,得到的结果是一致的。且服务端(针对于数据入库或数据修改)只处理一次。通俗点讲就是:防止重复提交。
以下演示相关案例
- 案例1: 数据表添加数据
- 案例2:完成任务,领取奖励
- 案例3:修改了其他线程已经修改的数据
- 案例4:接口回调,且存在重试
- 总结
案例1: 数据表添加数据
往数据库添加数据
如下数据库(user),和服务端添加(user)数据接口。
假如用同样的参数去调用添加(user)接口,那么每次都会往数据库插入一条数据,那么数据库就会有多条重复的数据。但是该数据库(user)并没有指明某个字段是唯一的,所以对于服务端来说,每一次请求都是合理的(即使是相同的参数)。
假如将 user表中的 user_name
字段设定为不能重复
。那么如何去处理幂等性呢?
- 添加数据入库之前,查询
user_name
是否存在。 - 数据表(user)的
user_name
设置唯一索引。推荐
1:添加数据入库之前,查询user_name
是否存在。
以上代码可以解决吗?可以,但是必须是建立在单线程,或没有并发的情况。
为什么必须建立在单线程基础上,或没有并发的情况呢?
假设有多个请求同时到达添加用户接口(save),又同时去数据库中查询(user_name)是否存在,如果user_name不存在,则就会同时添加多条数据,造成(user_name)重复。
如何在以上代码上改进解决?在接口方法上加synchronized
或者使用Lock
锁。
这种方案一定不可取,这会导致该接口变成单线程。同时也存在一个的问题,那就是服务器集群环境下,锁只针对当前服务器,如果多个线程分发给多个服务器去处理,那么同样又重复数据。
那么分布式锁可以解决吗?可以的
注意:如果非要用分布式锁解决的话,一定要将锁的粒度细化,用
user_name
作为锁的key。
2:数据表(user)的user_name
设置唯一索引。推荐
代码修改成以下这样
为什么要将user_name
设置唯一索引
在最终数据持久化(数据库)中,能够非常好的保证数据唯一,同时也能在编码中减少许多代码判断。
案例2:完成任务,领取奖励
相信有不少朋友做过完成活动,然后领取积分,红包,啥的,不知道有没有碰到过多次领取,都领取成功的情况。
假设现在有个需求:用户完成任务,领取1
元现金红包
- 用户在任务列表领取任务,领取后状态是
待完成
。 - 用户完成任务后,状态是
领取奖励
。 - 用户领取完奖励后,状态是
已领取
。
现在要实现的是:
- 用户领取任务,只能领取一次,且重复领取 数据记录表中只能有一条记录。
- 用户完成任务后,领取奖励,重复领取情况下,只能发放奖励一次。
先建立表,以下表揭是模拟表
任务表:
用户任务记录表:
1:用户领取任务,只能领取一次,且重复领取 数据记录表中只能有一条记录
这个实现方式与案例1
一致,需求可以得知 用户id
和 任务id
在用户任务记录表中
是唯一的,所以可以设置一个组合字段的唯一索引。
领取任务的先查询下任务是否领取,然后再添加记录。
2:用户完成任务后,领取奖励,重复领取情况下,只能发放奖励一次
- 查询任务状态是否
已完成
或者已领取
。 - 发放奖励前,采用用户id+任务id组合获取分布式锁。
- 检查下数据表,任务状态是否
已领取
,双重检查。如果不想双重检查可以在接口处获取分布式锁。重要 - 修改任务状态为
已领取
。重要 - 发放奖励后,必须写入至
用户奖励流水表
,并备注因何获得。重要
伪代码 就不贴了。
为什么要使用分布式锁?
因为是涉及的金额之类的,如果出错,就会导致财产损失,以后对账都不好对。加了分布式锁之后,就会变成单线程去执行发放奖励,且锁的粒度也小,不会导致整个接口变成单线程。
前端可以控制重复提交吗? 可以
用户在点击
领取奖励按钮
后,在未返回结果的情况下,按钮不可再点击。
前端控制了重复提交,服务端可以不用去控制吗? 不可以
服务端是整个数据添加的入口,前端只是调用服务端接口的一个应用而已。前端控制了,还有其他渠道也可以访问服务端接口。
案例3:修改了其他线程已经修改的数据
说明:
- 线程A 读取了数据库一行数据。
- 线程B 读取了线程A同一行数据。
- 线程A 把数据修改了。
- 线程B 把数据也修改了,或者改回去了。
- 导致线程A 白忙活了。
此问题我并未有遇到过实际情况,所以不好举例。
因为是修改数据,我都认为每个线程都是合理的,最终数据以最后修改为准。
如何解决:数据表中 每行数据版本
机制
- 线程A 读取到了数据 假设当前行数据的版本是 version = 1。
- 线程B 读取的数据同线程A一致。
- 线程A 修改数据的Sql = set version = version + 1 where version = 1。
- 线程B 修改数据的Sql = set version = version + 1 where version = 1。
- 因为两条Sql的 where条件都是 version = 1,所以只有一个线程修改成功。谁先修改,谁成功。
案例4:接口回调,且存在重试
做过三方支付,或者提供接口给别人对接,应该都知道这个幂等性问题。
举个例子:
假如用户支付成功后,就加相应的积分。用三方支付,支付完后,都会被三方进行回调,假如三方回调我们的接口,出错了,或超时了,三方会继续重试,那就会导致多次调用接口。这也是重复提交。
伪代码:假设我们的服务器应该某些原因,没有及时响应给三方调用者,那么就会触发重试,然后又给用户增加了积分。
如何改进与设计
- 获取此次接口请求的订单号,这是唯一的。
- 查询该订单是否已经处理,如果处理了,响应成功。
- 记录该订单号已经处理了。
- 异步去处理给用户添加积分。
- 响应给三方成功。
伪代码
为什么已经处理该订单 也要响应成功
这可以说是幂等性的核心理念吧,多次请求,结果一致,响应结果也是一致。
总结
幂等性就是在对一些唯一的数据,做校验,当服务器多次接收到这些唯一数据时,做合适的处理。但是响应是同样的结果,都是成功。
重点就是在唯一的数据上做处理。