赞
踩
日常开发中,为了实现接口幂等性校验,可以这样实现:
try-catch
捕获。补充: 也可以新建一张 防止重复点击表,将唯一标识放到表中,存为主键或唯一索引,然后配合 tra-catch 对重复点击的请求进行处理。
伪代码如下:
/\*\* \* 幂等处理 \*/ Rsp idempotent(Request req){ try { insert(req); } catch (DuplicateKeyException e) { //拦截是重复请求,直接返回成功 log.info("主键冲突,是重复请求,直接返回成功,流水号:{}",bizSeq); return rsp; } //正常处理请求 dealRequest(req); return rsp; }
乐观锁
:乐观锁在操作数据时,非常乐观,认为别人不会同时在修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下,在此期间是否有人修改了数据。
乐观锁的实现:
就是给表多加一列 version 版本号,每次更新数据前,先查出来确认下是不是刚刚的版本号,没有改动再去执行更新,并升级 version(version=version+1)。
比如,我们更新前,先查一下数据,查出来的版本号是 version=1。
select order_id,version from order where order_id='666';
然后使用 version=1 和 订单ID 一起作为条件,再去更新:
update order set version = version +1,status='P' where order_id='666' and version =1
最后,更新成功才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。
流程图如下:
为什么版本号建议自增呢?
因为乐观锁存在 ABA 的问题,如果 version 版本一直是自增的就不会出现 ABA 的情况。
悲观锁
:通俗点讲就是很悲观,每次去操作数据时,都觉得别人中途会修改,所以每次在拿数据的时候都会上锁。官方点讲就是,共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其它资源。
悲观锁的实现:
在订单业务场景中,假设先查询出订单,如果查到的是处理中状态,就处理完业务,然后再更新订单状态为完成。如果查到订单,并且不是处理中的状态,则直接返回。
可以使用数据库悲观锁(select … for update)解决这个问题:
begin; # 1.开始事务
select \* from order where order_id='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_id='666' # 更新完成
commit; # 5.提交事务
注意:
很多业务表,都是由状态的,比如:转账流水表,就会有 0-待处理,1-处理中,2-成功,3-失败的状态。转账流水更新的时候,都会涉及流水状态更新,即涉及 状态机(即状态变更图)。我们可以利用状态机来实现幂等性校验。
状态机的实现:
比如:转账成功后,把 处理中 的转账流水更新为成功的状态,SQL 如下:
update transfor_flow set status = 2 where biz_seq='666' and status = 1;
流程图如下:
伪代码实现如下:
Rsp idempotentTransfer(Request req){ String bizSeq = req.getBizSeq(); int rows= "update transfr\_flow set status=2 where biz\_seq=#{bizSeq} and status=1;" if(rows==1){ log.info(“更新成功,可以处理该请求”); //其他业务逻辑处理 return rsp; } else if(rows == 0) { log.info(“更新不成功,不处理该请求”); //不处理,直接返回 return rsp; } log.warn("数据异常") return rsp: }
token 唯一令牌方案一般包括两个请求阶段:
流程图如下:
redis.del(token)
的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败,则直接返回结果。补充: 这种方式个人不推荐,说两方面原因:
- 需要前后端联调才能实现,存在沟通成本,最终效果可能与设想不一致。
- 如果前端多次获取多个 token,还是可以重复请求的,如果再在获取 token 处加分布式锁控制,就不如直接用分布式锁来控制幂等性了,即下面这种解决方式。
分布式锁
实现幂等性的逻辑就是,请求过来时,先去尝试获取分布式锁,如果获取成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。
流程图如下:
setIfAbsent()
来实现,注意分布式锁的 key 必须为业务的唯一标识。@NotRepeat 注解用于修饰需要进行幂等性校验的类。
NotRepeat.java
import java.lang.annotation.\*;
/\*\*
\* 幂等性校验注解
\*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotRepeat {
}
AOP切面监控被 @Idempotent 注解修饰的方法调用,实现幂等性校验逻辑。
IdempotentAOP.java
import com.demo.util.RedisUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.concurrent.TimeUnit; /\*\* \* 重复点击校验 \*/ @Slf4j @Aspect @Component public class IdempotentAOP { /\*\* Redis前缀 \*/ private String API\_IDEMPOTENT\_CHECK = "API\_IDEMPOTENT\_CHECK:"; @Resource private HttpServletRequest request; @Resource private RedisUtils redisUtils; /\*\* \* 定义切面 \*/ @Pointcut("@annotation(com.demo.annotation.NotRepeat)") public void notRepeat() { } /\*\* \* 在接口原有的方法执行前,将会首先执行此处的代码 \*/ @Before("notRepeat()") public void doBefore(JoinPoint joinPoint) { String uri = request.getRequestURI(); // 登录后才做校验 UserInfo loginUser = AuthUtil.getLoginUser(); if (loginUser != null) { assert uri != null; String key = loginUser.getAccount() + "\_" + uri; log.info(">>>>>>>>>> 【IDEMPOTENT】开始幂等性校验,加锁,account: {},uri: {}", loginUser.getAccount(), uri); // 加分布式锁 boolean lockSuccess = redisUtils.setIfAbsent(API\_IDEMPOTENT\_CHECK + key, "1", 30, TimeUnit.MINUTES); log.info(">>>>>>>>>> 【IDEMPOTENT】分布式锁是否加锁成功:{}", lockSuccess); if (!lockSuccess) { if (uri.contains("contract/saveDraftContract")) { log.error(">>>>>>>>>> 【IDEMPOTENT】文件保存中,请稍后"); throw new IllegalArgumentException("文件保存中,请稍后"); } else if (uri.contains("contract/saveContract")) { log.error(">>>>>>>>>> 【IDEMPOTENT】文件发起中,请稍后"); throw new IllegalArgumentException("文件发起中,请稍后"); } } } } /\*\* \* 在接口原有的方法执行后,都会执行此处的代码(final) \*/ @After("notRepeat()") public void doAfter(JoinPoint joinPoint) { // 释放锁 String uri = request.getRequestURI(); assert uri != null; UserInfo loginUser = SysUserUtil.getloginUser(); if (loginUser != null) { String key = loginUser.getAccount() + "\_" + uri; log.info(">>>>>>>>>> 【IDEMPOTENT】幂等性校验结束,释放锁,account: {},uri: {}", loginUser.getAccount(), uri); redisUtils.del(API\_IDEMPOTENT\_CHECK + key); } } }
RedisUtils.java
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.concurrent.TimeUnit; 本人从事网路安全工作12年,曾在2个大厂工作过,安全服务、售后服务、售前、攻防比赛、安全讲师、销售经理等职位都做过,对这个行业了解比较全面。 最近遍览了各种网络安全类的文章,内容参差不齐,其中不伐有大佬倾力教学,也有各种不良机构浑水摸鱼,在收到几条私信,发现大家对一套完整的系统的网络安全从学习路线到学习资料,甚至是工具有着不小的需求。 最后,我将这部分内容融会贯通成了一套282G的网络安全资料包,所有类目条理清晰,知识点层层递进,需要的小伙伴可以点击下方小卡片领取哦!下面就开始进入正题,如何从一个萌新一步一步进入网络安全行业。 ![](https://img-blog.csdnimg.cn/img_convert/311903982dea1d8a5d2c98fc271b5b41.jpeg) ### 学习路线图 其中最为瞩目也是最为基础的就是网络安全学习路线图,这里我给大家分享一份打磨了3个月,已经更新到4.0版本的网络安全学习路线图。 相比起繁琐的文字,还是生动的视频教程更加适合零基础的同学们学习,这里也是整理了一份与上述学习路线一一对应的网络安全视频教程。 ![](https://img-blog.csdnimg.cn/img_convert/1ddfaf7dc5879b1120e31fafa1ad4dc7.jpeg) #### 网络安全工具箱 当然,当你入门之后,仅仅是视频教程已经不能满足你的需求了,你肯定需要学习各种工具的使用以及大量的实战项目,这里也分享一份**我自己整理的网络安全入门工具以及使用教程和实战。** ![](https://img-blog.csdnimg.cn/img_convert/bcd1787ce996787388468bb227d8f959.jpeg) #### 项目实战 最后就是项目实战,这里带来的是**SRC资料&HW资料**,毕竟实战是检验真理的唯一标准嘛~ ![](https://img-blog.csdnimg.cn/img_convert/35fc46df24091ce3c9a5032a9919b755.jpeg) #### 面试题 归根结底,我们的最终目的都是为了就业,所以这份结合了多位朋友的亲身经验打磨的面试题合集你绝对不能错过! **网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。** **[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/topics/618540462)** **一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。