SpringBoot如何使用AOP技术和redis分布式锁防止用户重复发起请求呢?

欣喜 SpringBoot 发布时间:2024-01-18 10:05:13 阅读数:12012 1
下文笔者讲述SpringBoot中使用AOP技术防止用户重复提交的方法及示例分享,如下所示
 防止用户重复提交
        是每一个开发人员都必须注意的事项,那么如何避免用户重复提交
        下文笔者将一一道来,如下所示

防止用户重复提交的实现思路:   
        当页面生成时,放入标识,并缓存至redis中
        当页面运行后,当redis缓存清除,后期的提交都视为重复提交

防止用户重复提交的代码实现
        1.自定义注解@NoRepeatSubmit 标记所有Controller中提交的请求
        2.通过AOP对所有标记了@NoRepeatSubmit 的方法进行拦截
        3.在业务方法执行前
               获取当前用户的token或者JsessionId+当前请求地址
               作为一个唯一的key
              去获取redis分布式锁
              如果此时并发获取,只有一个线程能获取到
        4.业务执行后,释放锁      
例:SpringBoot实现AOP拦截重复请求
Pom引入依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <!-- 因为springboot2.0中默认是使用 Lettuce来集成Redis服务,spring-boot-starter-data-redis默认只引入了 Lettuce包,并没有引入 jedis包支持。所以在我们需要手动引入jedis的包,并排除掉lettuce的包,
         如果不排除可能会出现错误:io.lettuce.core.RedisAsyncCommandsImpl cannot be cast to redis.clients.jedis.Jedis -->
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 依赖 Springboot aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 防止订单重复提交
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.Runtime)
public @interface NoRepeatSubmit {

    // 设置请求的锁定时间
    int lockTime() default 5;
}
Redis分布式锁
private  final Long RELEASE_SUCCESS = 1L;
private  final String LOCK_SUCCESS = "OK";
private  final String SET_IF_NOT_EXIST = "NX";
// 当前设置 过期时间单位, EX秒,PX毫秒
private  final String SET_WITH_EXPIRE_TIME = "EX";
private  final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

/**
 *加锁
 * @param lockKey   加锁键
 * @param clientId  加锁客户端唯一标识(采用UUID)
 * @param expireTime   锁过期时间
 * @return
 */
public boolean tryLock(String lockKey, String clientId, long expireTime) {
    return (boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
        Jedis jedis = (Jedis) redisConnection.getNativeConnection();
        String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    });
}
代码解析
 加锁代码
     jedis.set(String key, String value, String nxxx, String expx, int time)

set()方法一共有五个形参:
  第一个为key
     我们使用key来当锁,因为key是唯一的。
  第二个为value
     我们传的是requestId

  第三个为nxxx, 
       NX,意思是SET IF NOT EXIST
       即当key不存在时,我们进行set操作;
       若key已经存在,则不做任何操作;
  第四个为expx
       PX,意思是我们要给这个key加一个过期的设置
       具体时间由第五个参数决定。
  第五个为time
       与第四个参数相呼应
       代表key过期时间 

运行set()方法可能会出现两种情况:
     1.当前没有锁(key不存在) ,则进行加锁操作,并对锁设置个有效期
         同时value表示加锁的客户端
     2. 已有锁存在,不做任何操作
/**
 * 解锁
 * @param lockKey
 * @param clientId
 * @return
 */
public boolean unLock(String lockKey, String clientId) {
    return (boolean) redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
        Jedis jedis = (Jedis) redisConnection.getNativeConnection();
        Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonlist(lockKey),
                Collections.singletonList(clientId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    });
}
解锁代码解析
将Lua代码传到jedis.eval()方法里
   使参数KEYS[1]赋值为lockKey
    ARGV[1]赋值为requestId
   eval()方法是将Lua代码交给Redis服务端执行

AOP

为符合条件的代码增强
import com.java265.core.base.BoxOut;
import com.java265.core.model.BDic;
import com.java265.zystoreservice.annotation.NoRepeatSubmit;
import com.java265.zystoreservice.service.redis.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

/**
 * 防止订单重复提交
 */
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    RedisService redisService;

    @Pointcut("@annotation(noRepeatSubmit)")
    public void pointcut(NoRepeatSubmit noRepeatSubmit) {}

    @Around("pointcut(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
        int lockSeconds = noRepeatSubmit.lockTime();
        ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ra.getRequest();
        Assert.notNull(request, "request can not null");
        //此处可以用token或者JSessionId
   //String jsessionid=request.getSession().getId();
   String token = request.getHeader("Authorization");
        String path = request.getServletPath();
        String key = getKey(token, path);
        String clientId = getClientId();
        boolean isSuccess = redisService.tryLock(key, clientId, lockSeconds);
        log.info("tryLock key = [{}], clientId = [{}]", key, clientId);
        if (isSuccess) {
            // 获取锁成功
            Object result;
            try {
                // 执行进程
                result = pjp.proceed();
            } finally {
                // 解锁
                redisService.unLock(key, clientId);
                log.info("unLock success, key = [{}], clientId = [{}]", key, clientId);
            }
            return result;

        } else {
            // 获取锁失败,认为是重复提交的请求,返回
            log.info("tryLock fail, key = [{}]", key);
            return BoxOut.build(BDic.FAIL,"重复请求,请稍后再试");
        }
    }

    private String getKey(String token, String path) {
        return token + path;
    }

    private String getClientId() {
        return UUID.randomUUID().toString();
    }

}

controller调用

/**
 * 需要防止重复请求的接口
 */
@NoRepeatSubmit
@PostMapping(value = "/userPayTemplateOrderByCombo")
public BaseOut userPayTemplateOrderByCombo(@RequestBody ContractTemplatePayIn templatePayIn){}
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

本文链接: https://www.Java265.com/JavaFramework/SpringBoot/202401/7695.html

最近发表

热门文章

好文推荐

Java265.com

https://www.java265.com

站长统计|粤ICP备14097017号-3

Powered By Java265.com信息维护小组

使用手机扫描二维码

关注我们看更多资讯

java爱好者