SpringBoot如何使用AOP技术和redis分布式锁防止用户重复发起请求呢?
下文笔者讲述SpringBoot中使用AOP技术防止用户重复提交的方法及示例分享,如下所示
Pom引入依赖:
防止用户重复提交
是每一个开发人员都必须注意的事项,那么如何避免用户重复提交
下文笔者将一一道来,如下所示
防止用户重复提交的实现思路:
当页面生成时,放入标识,并缓存至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){}
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。


