实现思路
1.自定义注解@NoRepeatSubmit 标记所有Controller中提交的请求
2.通过AOP对所有标记了@NoRepeatSubmit 的方法进行拦截
3.在业务方法执行前,获取当前用户的token或者JsessionId+当前请求地址,作为一个唯一的key,去获取redis分布式锁,如果此时并发获取,只有一个线程能获取到。
4.业务执行后,释放锁。
Pom引入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
application.properties配置
spring.redis.port=6379
spring.redis.host=localhost
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.database=0
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000ms
自定义注解
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 {
/**
* 设置请求锁定时间
*
* @return
*/
int lockTime() default 10;
}
Redis分布式锁
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
* Redis 分布式锁实现
*/
@Component
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
// 当前设置 过期时间单位, EX秒,PX毫秒
private static final String SET_WITH_EXPIRE_TIME = "EX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate redisTemplate;
/**
*加锁
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param expireTime 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, long expireTime) {
return 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;
});
}
/**
* 解锁
* @param lockKey
* @param clientId
* @return
*/
public boolean unLock(String lockKey, String clientId) {
return 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;
});
}
}
AOP
import com.zx.zhuangxiu.dto.BaseResult;
import io.jsonwebtoken.lang.Assert;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
@Aspect
@Component
public class RepeatSubmitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
@Autowired
RedisLock redisLock;
@Pointcut("@annotation(noRepeatSubmit)")
public void pointCut(NoRepeatSubmit noRepeatSubmit) {
}
@Around("pointCut(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
int lockSeconds = noRepeatSubmit.lockTime();
System.out.println(lockSeconds);
ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ra.getRequest();
Assert.notNull(request, "request can not null");
// 此处可以用token或者JSessionId
// String token = request.getHeader("Authorization");
String jsessionid=request.getSession().getId();
String path = request.getServletPath();
String key = getKey(jsessionid, path);
String clientId = getClientId();
boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);
if (isSuccess) {
LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功
Object result;
try {
// 执行进程
result = pjp.proceed();
} finally {
// 解锁
redisLock.unLock(key, clientId);
LOGGER.info("unLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
LOGGER.info("tryLock fail, key = [{}]", key);
return new BaseResult<Object>(false, "重复请求,请稍后再试");
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
统一返回结果类
import com.fasterxml.jackson.annotation.JsonInclude;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResult<T> implements Serializable {
private static final long serialVersionUID = -4185151304730685014L;
private boolean success;
private T data;
private String error;
public BaseResult(boolean success, T data) {
this.success = success;
this.data = data;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
@Override
public String toString() {
return "BaseResult [success=" + success + ", data=" + data + ", error=" + error + "]";
}
}
Controller
import com.zx.zhuangxiu.dto.BaseResult;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
public class SubmitController {
@RequestMapping("/submit")
@NoRepeatSubmit(lockTime = 30)
public BaseResult submit(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
// 模拟业务场景
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new BaseResult<Object>(true, "请求成功");
}
}