之前博文中介绍过token 机制处理 接口幂等性问题,这种方式一个问题对代码的入侵比较多,相对书写代码来讲就比较麻烦,本文介绍使用 redis 分布式锁机制解决接口幂等性问题
- 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ide{
/**
* 设置请求锁定时间,超时后自动释放锁
*
* @return
*/
int lockTime() default 10;
}
- AOP 实现 注解
@Ide
的拦截处理
/**
* 接口幂等性的 -- 分布式锁实现
*/
@Slf4j
@Aspect
@Component
public class ReqSubmitAspect {
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(com.laiease.common.annotation.Ide)")
public void idePointCut() {
}
@Around("idePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 使用分布式锁 机制-实现
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Ide ide = method.getAnnotation(Ide.class);
int lockSeconds = ide.lockTime();
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
AssertUtils.notNull(request, "request can not null");
// 获取请求的凭证,本项目中使用的JWT,可对应修改
String token = request.getHeader("Token");
String requestURI = request.getRequestURI();
String key = getIdeKey(token, requestURI);
String clientId = CmUtil.getUUID();
// 获取锁
boolean lock = redisLock.tryLock(key, clientId, lockSeconds);
log.info("tryLock key = [{}], clientId = [{}]", key, clientId);
if (lock) {
log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功
Object result;
try {
// 执行进程
result = joinPoint.proceed();
} finally {
// 解锁
redisLock.releaseLock(key, clientId);
log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
log.info("tryLock fail, key = [{}]", key);
throw new RuntimeException("重复请求,请稍后再试!");
}
}
private String getIdeKey(String token, String requestURI) {
return token + requestURI;
}
}
- 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 = seconds; PX = milliseconds
private static final String SET_WITH_EXPIRE_TIME = "EX";
//lua
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;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
* <p>
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, long seconds) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
// Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object nativeConnection = redisConnection.getNativeConnection();
RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
byte[] keyByte = stringRedisSerializer.serialize(lockKey);
byte[] valueByte = stringRedisSerializer.serialize(clientId);
// lettuce连接包下 redis 单机模式
if (nativeConnection instanceof RedisAsyncCommands) {
RedisAsyncCommands connection = (RedisAsyncCommands) nativeConnection;
RedisCommands commands = connection.getStatefulConnection().sync();
String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
if (LOCK_SUCCESS.equals(result)) {
return true;
}
}
// lettuce连接包下 redis 集群模式
if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
RedisAdvancedClusterAsyncCommands connection = (RedisAdvancedClusterAsyncCommands) nativeConnection;
RedisAdvancedClusterCommands commands = connection.getStatefulConnection().sync();
String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
if (LOCK_SUCCESS.equals(result)) {
return true;
}
}
if (nativeConnection instanceof JedisCommands) {
JedisCommands jedis = (JedisCommands) nativeConnection;
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
}
return false;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(RELEASE_LOCK_SCRIPT);
redisScript.setResultType(Integer.class);
// Integer execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), clientId);
Object execute = redisTemplate.execute((RedisConnection connection) -> connection.eval(
RELEASE_LOCK_SCRIPT.getBytes(),
ReturnType.INTEGER,
1,
lockKey.getBytes(),
clientId.getBytes()));
if (RELEASE_SUCCESS.equals(execute)) {
return true;
}
return false;
}
}