接口幂等性

含义:接口可重复调用后,在调用方多次调用的情况下,接口最终得到的结果是一致的。
有些接口天然具备幂等性,如查询接口。
除查询外,增加、更新、删除都要保证幂等性。
如何保证幂等性?
1.全局唯一ID(防止新增脏数据)
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在来判断这个操作是否已经执行,如果不存在则把全局唯一ID存储到存储系统中,比如数据库,redis等,如果存在则表示该方法已经执行。
举例:创建履约单的过程
1)response是根据订单ID和订单类型唯一构建的。
2)创建履约单时,先到表中根据订单ID和订单类型查询response,如果response已经存在,则表示已经创建过履约单,不能再创建,否则创建履约单。


2.去重表
这种方法适用于在业务中有唯一标识的插入场景中。比如在支付场景中,如果一个订单只会支付一次,所以订单ID可以作为唯一标识。这时,我们就可以创建一张去重表,并且把唯一标识作为唯一索引,在我们实现时,把创建支付单据写入去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。
3.插入或更新
插入或有唯一索引的情况,比如关联某商品,其中商品的ID和品类的ID可以构成唯一索引,并且在数据表中也增加了唯一索引。这时候就可以使用insertOrUpdate操作
insert into goods_category (goods_id,category_id,create_time,update_time)
values(#{goodsId},#{categoryId},now(),now())
on DUPLICATE KEY UPDATE
update_time=now()


4.多版本控制
适用于更新的场景,比如更新商品的名字,可以在更新的接口中增加一个版本号,做幂等。
boolean updateGoodsName(int id,String newName,intversion);
实现代码
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}


5.状态机控制
适用于有状态机流转的情况下,比如订单的创建和付款,订单的付款肯定在前,这时可以通过在设计状态字段时,使用int类型,并且通过值类型的大小做幂等,比如订单的创建为0,付款成功为100,付款失败为99.
update `order` set status=#{status} where id=#{id} andstatus<#{status}


6.token机制:防止页面重复提交
原理上通过session token来实现(也可以通过redis实现)。当客户端请求页面时,服务器会生成一个随机数token,并且将token放到session中,然后将token发给客户端(一般通过构造hidden表单).
下次客户端提交请求时,token会随着表单一起提交到服务器端。
服务器端第一次验证相同之后,会将session中的token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的token没变,但是服务器端session中token的值已经改变了。
5、悲观锁
获取数据的时候加锁获取。select * from table_xxx where id='xxx' for update; 注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;
6、乐观锁——乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:
1. 通过版本号实现update table_xxx set name=#name#,version=version+1 where version=#version#如下图(来自网上);
2. 通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
7、分布式锁
如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);
8、select + insert
【接口幂等性】并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;
9、状态机幂等
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助
10、对外提供接口的api如何保证幂等
如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号;source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。
重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。
三、总结
幂等与你是不是分布式高并发还有JavaEE都没有关系。关键是你的操作是不是幂等的。一个幂等的操作典型如:把编号为5的记录的A字段设置为0这种操作不管执行多少次都是幂等的。一个非幂等的操作典型如:把编号为5的记录的A字段增加1这种操作显然就不是幂等的。要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。譬如说需求是:当用户点击赞同时,将答案的赞同数量+1。改为:当用户点击赞同时,确保答案赞同表中存在一条记录,用户、答案。赞同数量由答案赞同表统计出来。总之幂等性应该是合格程序员的一个基因,在设计系统时,是首要考虑的问题,尤其是在像支付宝,银行,互联网金融公司等涉及的都是钱的系统,既要高效,数据也要准确,所以不能出现多扣款,多打款等问题,这样会很难处理,用户体验也不好。


参考:
https://blog.csdn.net/jks456/article/details/71453053
https://www.cnblogs.com/linjiqin/p/9678022.html

    推荐阅读