文章目录
1.什么是幂等
程序无论执行多少次,其产生的结果均与一次执行相同,不会因为重复执行会对系统造成改变.
之所以强调幂等,原因在于接口不幂等时,在某些场景下会引发严重的问题:支付、退款、结算等场景时,由于重复点击/操作后,进行了二次处理,导致重复扣款,重复退款,错误结算问题。这时候凡事碰到此问题的用户恐怕心情瞬间崩溃了
2.幂等产生原因
引发的原因都有一个共性:短时间内,重复操作。以下场景下可能会发生支付异常导致重复支付:
- 网络延迟。请求到后端服务后,后端处理后返回结果的时候网络抖动/延迟。这时前端超时等待结束后再发起请求会导致重复操作行为。
- 服务异常。后端因各种异常原因导致服务处理缓慢,前端提示超时后再次发起请求,这时两次请求同时受理会导致重复问题。
- 第三方服务异常情况。比如支付场景下,支付时,支付回调超时导致DB没有变,当用户再次发起支付时导致重复支付问题了。
- 其他。所依赖服务不稳定(比如第三方支付接口)、服务超时,这时前端如果再次发起支付请求时会导致重复支付。
3.如何实现幂等
1.前端提交限制
一个按钮,点击一次后,后端接口返回前不允许按钮点击第二次(如置灰);不过这种方案虽然简单,但存在一定的风险,如置灰按钮前再次点击,或直接调用接口
2.数据库查询判断 (数据库,低并发)
并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了
注意:核心高并发流程不要用这种方法
3.唯一索引,防止新增脏数据(数据库)
当表存在唯一索引,并发时新增报错时,再查询一次如果数据存在旧不用新增了。
通过索引只支持唯一值的方式来实现报错告知系统请求已在处理
4.悲观锁(数据库)
获取数据的时候加锁获取:select * from table_xxx where id='xxx' for update;
注意:id字段一定是主键或者唯一索引,不然是锁表
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用
5.多版本控制(数据库)
这种方法适合在更新的场景中,比如我们要更新商品的名字,这时我们就可以在更新的接口中增加一个版本号,来做幂等boolean updateGoodsName(int id,String newName,int version);
在实现时可以如下`update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
6.状态机控制(数据库)
这种方法适合在有状态机流转的情况下,比如就会订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用int类型,并且通过值类型的大小来做幂等,比如订单的创建为0,付款成功为100。付款失败为99
在做状态机更新时,我们就这可以这样控制update
orderset status=#{status} where id=#{id} and status<#{status}
7.token机制(交互)
-
1.前端提交数据前,先和后端服务申请一个token;
-
2.提交数据时连同token一起提交;
实现token要求:唯一性,不重复,并重点实现token的产生、存储机制(Map、redis缓存.)
8.全局唯一ID(交互)
如果使用全局唯一ID,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。
从工程的角度来说,使用全局ID做幂等可以作为一个业务的基础的微服务存在,在很多的微服务中都会用到这样的服务,在每个微服务中都完成这样的功能,会存在工作量重复。另外打造一个高可靠的幂等服务还需要考虑很多问题,比如一台机器虽然把全局ID先写入了存储,但是在写入之后挂了,这就需要引入全局ID的超时机制。
使用全局唯一ID是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。