幂等性实现 -接口幂等性
接口幂等性
1.什么是幂等性
对于同一笔业务操作,不管调用多少次,得到的结果都是一样的。
也就是方法调用一次和调用多次产生的额外效果是相同的,他就具有幂等性
2.为什么需要幂等性
在系统高并发的环境下,很有可能因为网络,阻塞等等问题导致客户端或者调用方并不能及时的收到服务端的反馈甚至是调用超时的问题。总之,就是请求方调用了你的服务,但是没有收到任何的信息,完全懵逼的状态。比如订单的问题,可能会遇到如下的几个问题:
1.创建订单时,第一次调用服务超时,再次调用是否产生两笔订单?
2.订单创建成功去减库存时,第一次减库存超时,是否会多扣一次?
3.订单支付时,服务端扣钱成功,但是接口反馈超时,此时再次调用支付,是否会多扣一笔呢?
作为消费者,前两种能接受,第三种情况就MMP了,哈哈哈!!!这种情况一般有如下两种解决方式:
1.服务方提供一个查询操作是否成功的api,第一次超时之后,调用方调用查询接口,如果查到了就走成功的流程,失败了就走失败的流程。
2.服务方需要使用幂等的方式保证一次和多次的请求结果一致。
3.产生幂等性的场景
幂等性问题在我们的开发中,分布式、微服务架构中随处可见;
- 因网络波动,可能会引起重复请求;
- 用户重复操作,用户在使用产品时可能会无意的触发多次下单多次交易,甚至没有响应而有意触发多笔交易;
- 应用使用了失败或超时重试机制(如nginx重试、RPC重试或业务层重试等)
- 第三方平台的接口:(如:支付成功回调接口),因为异常导致多次异步回调;
- 中间件/应用服务根据自身的特性,也有可能进行重试
- 用户双击提交按钮;
- 页面重复刷新;
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单;
- 使用浏览器历史记录重复提交表单;
- 浏览器重复的HTTP请求;
- 定时任务重复执行;
3.RESTFUL HTTP的幂等性
- GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。
- HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。
- OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。
- DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调 用方可以调用N次而不用担心引起的错误(根据业务需求而变)。
- Put和Post:都可以用于新增/修改,使用区别就说Put请求是幂等的,Post不是幂等性的,使用时不应该简单的却别与做新增还是修改,根据业务是否需要幂等来进行选择。
4.幂等性的实现方式
- 前端实现:第5节
- 后端实现:第6节
5.前端幂等性
5.1 按钮只可点击一次
对于客户端交互的接口,可以在前端拦截一部分,例如防止表单重复提交,按钮置灰,隐藏,不可点击等方式。但是前端进行拦截器显然是针对普通用户,懂点技术的都可以模拟请求调用接口,所以后端幂等性很重要。
5.2 Token机制(前端+后端)
- 针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用Token的机制实现防止重复提交。
- TOKEN机制的实现:简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(TOKEN),请求的时候携带这个全局ID一起请求,后端需要对这个全局ID校验来保证幂等操作
主要的流程步骤如下:
- 客户端进入业务操作之前(例如进入提交页面时),先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端;
- 客户端调用业务请求的时候必须携带这个token,一般放在请求头上;
- 服务端操作redis做del删除token,如果删除成功证明是第一次请求,执行后续操作;
- 如果删除失败,则表示重复操作,直接返回指定的结果给客户端。
- 通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性,但是工程量和实现难度比较大,UUID的数据量相对有些大,此处可以选择雪花算法生成全局唯一ID。
- token机制缺点:
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。(当然redis性能很好,耗时不会太明显)
6.后端幂等性实现
6.1 普通方式
例子过程如下:
- 接收到支付宝支付成功请求
- 根据trade_no查询当前订单是否处理过
- 如果订单已处理直接返回,若未处理,继续向下执行
- 开启本地事务
- 本地系统给用户加钱
- 将订单状态置为成功
- 提交本地事务上面的过程,对于同一笔订单,如果支付宝同时通知多次,会出现什么问题?当多次通知同时到达第2步时候,查询订单都是未处理的,会继续向下执行,最终本地会给用户加两次钱。此方式适用于单机其,通知按顺序执行的情况,只能用于自己写着玩玩。
6.2 JVM加锁的方式
方式1中由于并发出现了问题,此时我们使用java中的Lock加锁,来防止并发操作,过程如下:1. 接收到支付宝支付成功请求
- 调用java中的Lock加锁
- 根据trade_no查询当前订单是否处理过
- 如果订单已处理直接返回,若未处理,继续向下执行
- 开启本地事务
- 本地系统给用户加钱
- 将订单状态置为成功
- 提交本地事务
- 释放Lock锁分析问题:
Lock只能在一个jvm中起效,如果多个请求都被同一套系统处理,上面这种使用Lock的方式是没有问题的,不过互联网系统中,多数是采用集群方式部署系统,同一套代码后面会部署多套,如果支付宝同时发来多个通知经过负载均衡转发到不同的机器,上面的锁就不起效了。此时对于多个请求相当于无锁处理了,又会出现方式1中的结果。此时我们需要分布式锁来做处理。
6.3 Token机制(同5.2)
6.4 唯一索引
可以限制重复插入数据,当数据重复时,插入数据库会抛出异常,保证不会出现脏数据。
对于insert操作,当我们插入数据的时候会出现两种情况?
1.自增主键
如果是自增主键,多次插入一定会出现重复插入问题
2.业务主键(唯一索引)
如果是业务主键,假设我们对订单id做唯一索引,前提是我们可以保证这一笔订单的id是唯一的,即便多次提交也是唯一的。
6.5 悲观锁的实现(查询语句加锁for update)
- 接收到支付宝支付成功请求
- 打开本地事物
- 查询订单信息并加悲观锁select * from t_order where order_id = trade_no for update;
- 判断订单是已处理
- 如果订单已处理直接返回,若未处理,继续向下执行
- 给本地系统给用户加钱
- 将订单状态置为成功
- 提交本地事物
重点在于for update,对for update,做一下说明:
1.当线程A执行for update,数据会对当前记录加锁,其他线程执行到此行代码的时候(也要有for update如果没有会出现问题),会等待线程A释放锁之后,才可以获取锁,继续后续操作。
2.事物提交时,for update获取的锁会自动释放。
问题:for update可以正常实现我们需要的效果,能保证接口的幂等性,不过存在一些缺点:
如果业务处理比较耗时,并发情况下,后面线程会长期处于等待状态,占用了很多线程,让这些线程处于无效等待状态,我们的web服务中的线程数量一般都是有限的,如果大量线程由于获取for update锁处于等待状态,不利于系统并发操作。
6.6 有限状态机实现
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候如果状态机已经处于下一个状态,却来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
针对更新操作,如果业务上需要修改订单状态,订单有待支付、支付中、支付成功、支付失败、订单超时等,在设计时最好只支持单项改变(不可逆),这样在更新的时候where条件里可以加上status=我期望的上一个status,多次调用的话实际上也指挥执行一次。
update xx set status = “支付中” where status = ‘待支付’ and id=xx (注:update是当前读,多次update会等待)
6.7 乐观锁实现
如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,比如通过加version字段来做乐观锁,这样既能保证执行效率,又能保证幂等性。乐观锁的version版本在更新业务数据要自增。
也可以采用更新带条件,实现乐观锁,通过version或者其他条件来实现乐观锁
例子主要流程如下:
- 接收到支付宝支付成功请求
- 查询订单信息
select * from t_order where order_id = trade_no;
- 判断订单是否已经处理
- 如果处理直接返回,未处理,继续执行
- 打开本地事务
- 给本地系统用户加钱
将订单状态置为成功(这里要判断update返回的结果)
update t_order set status = 1 where order_id = trade_no where status = 0;
- 返回为1表示更新成功,提交事务
- 返回为其他表示更新失败,回滚事务
因为update是当前读,也就是多个事务去更新同一行数据,update会锁定数据上了排他锁,直到事务提交。
- 例子:
比如,表名A,字段名为 number,如下的SQL语句:
语句1:update A set number=number+ 5 where id=1;
语句2:update A set number=number+ 7 where id=1;
假设这两条SQL语句同时被mysql执行,id=1的记录中number字段的原始值为 10,那么是否有可能出现这种情况:
语句1和2因为同时执行,他们得到的number的值都是10,都是在10的基础上分别加5和7,导致最终number被更新为15或17,而不是22?
这个其实就是 关系型数据库本身就需要解决的问题。首先,他们同时被MySQL执行,你的意思其实就是他们是并发执行的,而并发执行的事务在关系型数据库中是有专门的理论支持的- ACID,事务并行等理论,所有关系型数据库实现,包括Oracle, MySQL都需要遵循这个原理。简单一点理解就是锁的原理。这个时候第一个update会持有id=1这行记录的 排它锁,第二个update需要持有这个记录的排它锁的才能对他进行修改,正常的话, 第二个update会阻塞,直到第一个update提交成功,他才会获得这个锁,从而对数据进行修改。也就是说,按照关系型数据库的理论,这两个update都成功的话,id=1的number一定会被修改成22。
6.8 防重表+唯一约束实现
需要增加一个表,防止数据重复的表
CREATE TABLE `t_uq_dipose` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '关联对象类型',
`ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '关联对象id',
PRIMARY KEY (`id`),
UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保证业务唯一性'
) ENGINE=InnoDB;
对于任何一个业务,有一个业务类型(ref_type),业务有一个全局唯一的订单号,业务来的时候,先查询t_uq_dipose表中是否存在相关记录,若不存在,继续放行。
过程如下:
- 接收到支付宝支付成功请求
- 查询t_uq_dipose(条件ref_id,ref-type),可以判断订单是否已经处理
select * from t_uq_dipose where ref_type = '充值订单' and ref_id = trade_no;
- 判断订单是否已经处理
- 如果订单已经处理直接返回,若未处理,继续放行
- 打开本地事务
- 给本地系统给用户加钱
- 将订单状态置为成功
向t_uq_dipose插入数据,插入成功,提交本地事务,插入失败,回滚本地事务:
try{
insert into t_uq_dipose (ref_type,ref_id) values ('充值订单',trade_no);
提交本地事务:
}catch(Exception e){
回滚本地事务;
}
说明:
对于同一个业务,ref_type是一样的,当并发时,插入数据只会有一条成功,其他的会违法唯一约束,进入catch逻辑,当前事务会被回滚,最终最有一个操作会成功,从而保证了幂等性操作。
关于这种方式可以写成通用的方式,不过业务量大的情况下,t_uq_dipose插入数据会成为系统的瓶颈,需要考虑分表操作,解决性能问题。
上面的过程中向t_uq_dipose插入记录,最好放在最后执行,原因:插入操作会锁表,放在最后能让锁表的时间降到最低,提升系统的并发性。关于消息服务中,消费者如何保证消息处理的幂等性?
每条消息都有一个唯一的消息id,类似于上面业务中的trade_no,使用上面的方式即可实现消息消费的幂等性。
参考
幂等性
- https://www.cnblogs.com/Leo\_wl/p/12640651.html
- https://www.cnblogs.com/itsoku123/p/10860527.html
- https://zhuanlan.zhihu.com/p/151438657
- https://www.bilibili.com/video/BV1YJ411V7aj?p=15
数据库: - https://blog.csdn.net/zmemorys/article/details/104814110
- https://blog.csdn.net/silyvin/article/details/79294508?tdsourcetag=s\_pctim\_aiomsg
- https://blog.csdn.net/winy\_lm/article/details/49718193
- https://blog.csdn.net/sinat\_27143551/article/details/89968902
还没有评论,来说两句吧...