最新【安全】Java幂等性校验解决重复点击(6种实现方式)_幂等校验,阿里P8架构师网络安全大厂面试题总结

给大家的福利

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

在这里插入图片描述

因篇幅有限,仅展示部分资料

网络安全面试题

绿盟护网行动

还有大家最喜欢的黑客技术

网络安全源码合集+工具包

所有资料共282G,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

三、实现方式

3.1 数据库层面,主键/唯一索引冲突

日常开发中,为了实现接口幂等性校验,可以这样实现:

  1. 提前在数据库中为唯一存在的字段(如:唯一流水号 bizSeq 字段)添加唯一索引,或者直接设置为主键。
  2. 请求过来,直接将数据插入、更新到数据库中,并进行 try-catch 捕获。
  3. 如果抛出异常,说明为重复请求,可以直接返回成功,或提示请求重复。

补充: 也可以新建一张 防止重复点击表,将唯一标识放到表中,存为主键或唯一索引,然后配合 tra-catch 对重复点击的请求进行处理。

伪代码如下:

/\*\*
 \* 幂等处理
 \*/
Rsp idempotent(Request req){
  
    try {
        insert(req);
    } catch (DuplicateKeyException e) {
        //拦截是重复请求,直接返回成功
        log.info("主键冲突,是重复请求,直接返回成功,流水号:{}",bizSeq);
        return rsp;
    }

    //正常处理请求
    dealRequest(req);

    return rsp;
}

3.2 数据库层面,乐观锁

乐观锁:乐观锁在操作数据时,非常乐观,认为别人不会同时在修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下,在此期间是否有人修改了数据。

乐观锁的实现:

就是给表多加一列 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 的情况。

3.3 数据库层面,悲观锁(select for update)【不推荐】

悲观锁:通俗点讲就是很悲观,每次去操作数据时,都觉得别人中途会修改,所以每次在拿数据的时候都会上锁。官方点讲就是,共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其它资源。

悲观锁的实现:

在订单业务场景中,假设先查询出订单,如果查到的是处理中状态,就处理完业务,然后再更新订单状态为完成。如果查到订单,并且不是处理中的状态,则直接返回。

可以使用数据库悲观锁(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.提交事务

注意:

  • 这里的 order_id 需要是主键或索引,只用行级锁锁住这条数据即可,如果不是主键或索引,会锁住整张表。
  • 悲观锁在同一事务操作过程中,锁住了一行数据。这样 别的请求过来只能等待,如果当前事务耗时比较长,就很影响接口性能。所以一般 不建议用悲观锁的实现方式
3.4 数据库层面,状态机

很多业务表,都是由状态的,比如:转账流水表,就会有 0-待处理,1-处理中,2-成功,3-失败的状态。转账流水更新的时候,都会涉及流水状态更新,即涉及 状态机(即状态变更图)。我们可以利用状态机来实现幂等性校验。

状态机的实现:

比如:转账成功后,把 处理中 的转账流水更新为成功的状态,SQL 如下:

update transfor_flow set status = 2 where biz_seq='666' and status = 1;

流程图如下:

在这里插入图片描述

  • 第1次请求来时,bizSeq 流水号是 666,该流水的状态是处理中,值是 1,要更新为 2-成功的状态,所以该 update 语句可以正常更新数据,sql 执行结果的影响行数是 1,流水状态最后变成了 2。
  • 第2次请求也过来了,如果它的流水号还是 666,因为该流水状态已经变为 2-成功的状态,所以更新结果是0,不会再处理业务逻辑,接口直接返回。

伪代码实现如下:

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:
}

3.5 应用层面,token令牌【不推荐】

token 唯一令牌方案一般包括两个请求阶段:

  1. 客户端请求申请获取请求接口用的token,服务端生成token返回;
  2. 客户端带着token请求,服务端校验token。

流程图如下:

在这里插入图片描述

  1. 客户端发送请求,申请获取 token。
  2. 服务端生成全局唯一的 token,保存到 redis 中(一般会设置一个过期时间),然后返回给客户端。
  3. 客户端带着 token,发起请求。
  4. 服务端去 redis 确认 token 是否存在,一般用 redis.del(token) 的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败,则直接返回结果。

补充: 这种方式个人不推荐,说两方面原因:

  1. 需要前后端联调才能实现,存在沟通成本,最终效果可能与设想不一致。
  2. 如果前端多次获取多个 token,还是可以重复请求的,如果再在获取 token 处加分布式锁控制,就不如直接用分布式锁来控制幂等性了,即下面这种解决方式。
3.6 应用层面,分布式锁【推荐】

分布式锁 实现幂等性的逻辑就是,请求过来时,先去尝试获取分布式锁,如果获取成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。

流程图如下:

  • 分布式锁可以使用 Redis,也可以使用 Zookeeper,不过 Redis 相对好点,比较轻量级。
  • Redis 分布式锁,可以使用 setIfAbsent() 来实现,注意分布式锁的 key 必须为业务的唯一标识。
  • Redis 执行设置 key 的动作时,要设置过期时间,防止释放锁失败。这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,请求量多的话会占用存储空间。

四、Java 代码实现

4.1 @NotRepeat 注解

@NotRepeat 注解用于修饰需要进行幂等性校验的类。

NotRepeat.java

import java.lang.annotation.\*;

/\*\*
 \* 幂等性校验注解
 \*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotRepeat {

}

4.2 AOP 切面

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);
        }
    }
}

4.3 RedisUtils 工具类

RedisUtils.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;


**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值