最近在本地开发测试的时候,遇到一个表单重复提交的现象。其实原因很简单,因为网络延迟的问题,我点击了两次提交按钮,数据库里生成了两条记录。其实这种现象以前也有遇到过,一般都是提交后把按钮置灰,无法再次提交,这是很常见的客户端处理的方式。
但是这真的有从根本上解决问题吗,虽然客户端解决了多次提交的问题,但是接口中依旧存在着问题。假设我们不是从客户端提交,而是被其他的系统调用,当遇到网络延迟,系统补偿的时候,是否也会遇到这种问题呢。看了下网上关于这类问题的解决方案,需要实现接口的幂等性。概念很高大上,结合我的实际的理解其实幂等性就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
以我的实际案例来讲,就是无论我点击提交按钮多少次,数据库应该只有一条记录才对。可能这个案例还不太符合幂等性的定义,再举个我们都很切身的案例,当我们去参加一些电商的抢购活动,假设网络卡顿,这时候很多人肯定会多次点击支付按钮,假设支付接口没有做幂等性校验,这时候会发生什么情况,肯定会发生多次扣款的情况。
什么时候需要实现幂等性接口?
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。既然是这样我们的查询和删除不就是多次执行的结果和一次执行的相同吗。是的,查询和删除拥有天然的幂等性,当然删除这个第一次执行和后面执行的返回值可能会有所不同,但是最终的效果是一致的。所以需要我们额外实现的幂等性接口主要是新增和更新操作。
实现幂等性的技术方案
1、token机制,防止页面重复提交
这种方法也是我目前在表单重复提交服务端的解决方案,技术原理很简单。
这种方式分成两个阶段:申请token阶段和执行新增操作。
第一阶段,在进入到新增页面之前,需要服务端发起一次申请token的请求,服务端一定的逻辑得出Token,并将token保存到Redis缓存中并设置生效时间,为第二阶段使用,注意保证token的唯一性。
第二阶段,新增页面拿着申请到的token发起新增请求,服务端执行删除token操作,如果返回0表示Token不存在,为非法请求,如果返回1则为第一次请求,发起新增请求。
注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
2、唯一索引,防止新增脏数据
比如:以上面抢购案例为例,我们点击支付按钮,向支付系统提交支付申请,这时候支付系统会将一条记录插入到支付状态表,这个表将支付的订单号设为唯一索引,第一次请求将支付订单记录插入表中并设置为未支付同时返回给支付系统完成实际支付操作,后续重复请求就会因为唯一索引导致插入失败而不会再走后续的实际支付操作。就好像一种另类的锁机制。
3、悲观锁乐观锁机制
悲观锁乐观锁的用法,在我这些年的开发中应用还是比较广泛的。
悲观锁,就是悲观的认为数据会被改变,在数据修改的过程中始终是加锁的。其他线程无论是读还是写都无法拿到数据。
select * from t_goods where id=1 for update;
与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。使用悲观锁的原理就是,当我们在查询出goods信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为goods被锁定了,就不会出现有第三者来对其进行修改了。
乐观锁,相对悲观锁来讲更为广泛一些,因为乐观锁不依赖数据库,只会在update的一瞬间加锁,其余处理过程中并不加锁。
UPDATE t_goodsSET STATUS = #{status},name=#{name},version=version+1 WHERE id = #{id} and version=#{version}
乐观锁每次只会有一个线程执行成功,其他线程因为条件发生了改变,而执行失败,这样就避免了数据覆盖的可能性。
乐观锁悲观锁虽然很好的解决了数据不一致的问题,但是也要学会善用。因为数据库的锁机制,条件字段一定要是主键或者唯一索引,不然会造成锁表或者锁无限的可能,可以说印象深刻,我就曾因为这个被狠批过。o(╥﹏╥)o
如果有同学不了解悲观锁和乐观锁的,可以看下这两篇深入了解下。后面我也会梳理下mysql的锁机制总结分享出来,目前自己对数据库锁机制也是一知半解。
4、 分布式锁
可以用redis或zookeeper实现分布式锁。以电商支付为案例,订单发起支付请求,支付系统首先查询订单是否已经支付,如果已经支付,直接返回已支付。如果未支付去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号,已存在返回重复操作。再次查询是否完成支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这笔订单支付请求完成,下次请求才能进来。相比唯一索引,将并发放到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。
要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁。
分布式锁我并没有亲手实践过,相关的原理也是从大佬那里偷师来的,后续亲身实践过后会做不定期更新。
站在大佬的肩膀上,才能更快的撬动地球~
参考:
原文出处:https://www.cnblogs.com/laoyeye/p/9557269.html