Java接口幂等性的几种实现方法
Java接⼝幂等性的⼏种实现⽅法优秀少先队辅导员事迹材料
⽬录
0- 什么是幂等性
举个例⼦:
投资理财的案例,⽤户可以充值、投资、提现,使⽤第三⽅⽀付进⾏充值,过程如下:
step1:⽤户⽹站中输⼊充值⾦额
step2:后端创建充值订单⼊库,此时订单是待⽀付状态
step3:跳转到第三⽅⽀付页⾯,输⼊银⾏卡,然后确认⽀付
step4:第三⽅⽀付通过我⽅提供的回调接⼝异步将充值结果告知我⽅
问题出在了step4,逻辑如下:
//返回通知处理结果,true:处理成功;false:处理失败,第三⽅会继续重试
public boolean rechargeNotice(第三⽅⽀付充值结果){
try{
//第三⽅充值结果中包含了我⽅的订单id,从db中获取充值订单信息
OrderModel order =OrderById(订单id);//@1
//判断订单状态是否是待⽀付状态
if(订单状态==待⽀付状态){//@2
//将订单状态置为充值成功
order.status(充值成功);
orderService.update(order);
//⽤户账户可⽤余额增加
this.accountService.incrBalance(⽤户id,充值⾦额);
return true;
}else{
//订单已处理过,返回true
return true;
}
}catch(Exception e){
//记录异常信息,返回通知失败
return false;
}
}
并发情况时,上⾯逻辑是有问题的,同⼀笔订单,同时进⾏2次通知,此时都会⾛到@1,此时看到or
der的状态都是待⽀付状态,然后都会进⼊@2,最后导致账户余额重复增加了,最后导致,充值1000,账户余额增加2000,这个问题,就是我们常说的幂等性的问题,是⾮常⾮常重要的⼀个技术点。
什么是幂等性?
对于同⼀笔业务操作,不管调⽤多少次,得到的结果都是⼀样的。
幂等性设计
我们以对接⽀付宝充值为例,来分析⽀付回调接⼝如何设计?
如果我们系统中对接过⽀付宝充值功能的,我们需要给⽀付宝提供⼀个回调接⼝,⽀付宝回调信息中会携带(out_trade_no【商户订单号】,trade_no【⽀付宝交易号】),trade_no在⽀付宝中是唯⼀的,out_trade_no在商户系统中是唯⼀的。
1- ⽅式1(普通⽅式,不推荐)
过程如下:
1.接收到⽀付宝⽀付成功请求
2.根据trade_no查询当前订单是否处理过
3.如果订单已处理直接返回,若未处理,继续向下执⾏
4.开启本地事务
5.本地系统给⽤户加钱
6.将订单状态置为成功
7.提交本地事务
上⾯的过程,对于同⼀笔订单,如果⽀付宝同时通知多次,会出现什么问题?当多次通知同时到达第2步时候,查询订单都是未处理的,会继续向下执⾏,最终本地会给⽤户加两次钱。
此⽅式适⽤于单机其,通知按顺序执⾏的情况,只能⽤于⾃⼰写着玩玩。
2- ⽅式2(jvm加锁⽅式)
⽅式1中由于并发出现了问题,此时我们使⽤java中的Lock加锁,来防⽌并发操作,过程如下:
1.接收到⽀付宝⽀付成功请求
2.调⽤java中的Lock加锁
3.根据trade_no查询当前订单是否处理过
4.如果订单已处理直接返回,若未处理,继续向下执⾏
5.开启本地事务
6.本地系统给⽤户加钱
7.将订单状态置为成功
8.提交本地事务
9.释放Lock锁
分析问题:
Lock只能在⼀个jvm中起效,如果多个请求都被同⼀套系统处理,上⾯这种使⽤Lock的⽅式是没有问题的,不过互联⽹系统中,多数是采⽤集⽅式部署系统,同⼀套代码后⾯会部署多套,如果⽀付宝同时发来多个通知经过负载均衡转发到不同的机器,上⾯的锁就不起效了。此时对于多个请求相当于
⽆锁处理了,⼜会出现⽅式1中的结果。此时我们需要分布式锁来做处理。
3- ⽅式3(悲观锁⽅式)
使⽤数据库中悲观锁实现。悲观锁类似于⽅式⼆中的Lock,只不过是依靠数据库来实现的。数据中悲观锁使⽤for update来实现,过程如下:
1.接收到⽀付宝⽀付成功请求
2.打开本地事物
3.查询订单信息并加悲观锁
select*from t_order where order_id = trade_no for update;
4.判断订单是已处理
5.如果订单已处理直接返回,若未处理,继续向下执⾏
6.给本地系统给⽤户加钱
7.将订单状态置为成功
8.提交本地事物
重点在于for update,对for update,做⼀下说明:
1.当线程A执⾏for update,数据会对当前记录加锁,其他线程执⾏到此⾏代码的时候,会等待线程A释放锁之后,才可以获取锁,继续后续操作。
2.事物提交时,for update获取的锁会⾃动释放。
⽅式3可以正常实现我们需要的效果,能保证接⼝的幂等性,不过存在⼀些缺点:
1.如果业务处理⽐较耗时,并发情况下,后⾯线程会长期处于等待状态,占⽤了很多线程,让这些线程处于⽆效等待状态,我们的web服务中的线程数量⼀般都是有限的,如果⼤量线程由于获取for update锁处于等待状态,不利于系统并发操作。
4- ⽅式4(乐观锁⽅式)
年终总结2018
依靠数据库中的乐观锁来实现。
1.接收到⽀付宝⽀付成功请求
2.查询订单信息
select*from t_order where order_id = trade_no;
3.判断订单是已处理
4.如果订单已处理直接返回,若未处理,继续向下执⾏
5.打开本地事物
6.给本地系统给⽤户加钱
7.将订单状态置为成功,注意这块是重点,伪代码:
update t_order set status =1 where order_id = trade_no where status =0;
安全生产先进材料//上⾯的update操作会返回影响的⾏数num
防灾减灾标语if(num==1){
//表⽰更新成功
提交事务;
}else{
//表⽰更新失败
回滚事务;
}
5- ⽅式5(唯⼀约束⽅式)
依赖数据库中唯⼀约束来实现。
我们可以创建⼀个表:
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'保证业务唯⼀性'
);
对于任何⼀个业务,有⼀个业务类型(ref_type),业务有⼀个全局唯⼀的订单号,业务来的时候,先查询t_uq_dipose表中是否存在相关记录,若不存在,继续放⾏。
过程如下:
1.接收到⽀付宝⽀付成功请求
2.查询t_uq_dipose(条件ref_id,ref_type),可以判断订单是否已处理
select*from t_uq_dipose where ref_type ='充值订单'and ref_id = trade_no;
3.判断订单是已处理
4.如果订单已处理直接返回,若未处理,继续向下执⾏
5.打开本地事物
6.给本地系统给⽤户加钱
7.将订单状态置为成功
最牛签名8.向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,使⽤上⾯的⽅式即可实现消息消费的幂等性。
6- 总结
1.实现幂等性常见的⽅式有:悲观锁(for update)、乐观锁、唯⼀约束
2.⼏种⽅式,按照最优排序:乐观锁 > 唯⼀约束 > 悲观锁

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。