前言
这个文章汇聚了很多。参考了很多文章和知识,最后实践汇总而成的。这里着重声明感谢 等待的萝卜,AOP这块就是借鉴他的文章去做的。但是由于Jedis的频频错误,我选择RedisTemplate配合lua脚本去做。这篇文章的目的,最终希望给大家一个正确的方式来实现。避免出现其他一些坑。
附上 等待的萝卜 文章地址:https://blog.csdn.net/a992795427/article/details/92834286
直接上代码:
Redis 分布式锁实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
/**
* Redis 分布式锁实现
* lua 表达式为了保持数据的原子性。
*
* @see <a href="https://redis.io/commands/setnx">SETNX key value</a>
* @see <a href="https://www.cnblogs.com/linjiqin/p/8003838.html">Redis分布式锁的正确实现方式</a>
*/
@Component
public class RedisLock {
@Autowired
private RedisTemplate redisTemplate;
/**
* redis 锁成功标识常量
*/
private static final Long RELEASE_SUCCESS = 1L;
/**
* 加锁 Lua 表达式。
*/
private static final String RELEASE_TRY_LOCK_LUA =
"if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";
/**
* 解锁 Lua 表达式.
*/
private static final String RELEASE_RELEASE_LOCK_LUA =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 加锁
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
* <p>
* 支持重复,现成安全
*
* @param lockKey 加锁键
* @param userId 加锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 String 类型)
* @param expireTime 锁过期时间
* @return 1 如果key被设置了 0 如果key没有被设置
*/
public boolean tryLock(String lockKey, String userId, long expireTime) {
try {
RedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_TRY_LOCK_LUA, Long.class);
Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), userId, expireTime);
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 解锁
* 与 tryLock 相对应,用作释放锁
* 解锁必须与加锁是同一人,其他人拿到锁也不可以解锁 (解铃还须系铃人)
*
* @param lockKey 加锁键
* @param userId 解锁客户端唯一标识(采用用户id, 需要把用户 id 转换为 String 类型)
* @return
*/
public boolean releaseLock(String lockKey, String userId) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_RELEASE_LOCK_LUA, Long.class);
Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), userId);
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
自定义注解:
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 ControlRepeatSubmit {
/**
* 设置重复请求请求锁定时间 单位秒
*
* @return
*/
int lockTime() default 3;
}
自定义注解切面实现:
import com.test.utils.RedisLock;
import com.test.utils.RequestUtils;
import com.test.utils.annotation.ControlRepeatSubmit;
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.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 切面类
*/
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(controlRepeatSubmit)")
public void pointCut(ControlRepeatSubmit controlRepeatSubmit) {
}
@Around("pointCut(controlRepeatSubmit)")
public Object around(ProceedingJoinPoint point, ControlRepeatSubmit controlRepeatSubmit) throws Throwable {
// 获得注解锁时间 单位秒
int lockTime = controlRepeatSubmit.lockTime();
// 从 HttpServletRequest 里获取登陆用户 id
HttpServletRequest request = RequestUtils.getRequest();
Assert.notNull(request, "request can not null");
// 从消息头获取用户id
String userIdStr = (String) Objects.requireNonNull(request).getAttribute("id");
// 获取请求url路径
String servletPath = request.getServletPath();
String key = getKey(userIdStr, servletPath);
boolean isSuccess = redisLock.tryLock(key, userIdStr, lockTime);
if (isSuccess) {
// 获取锁成功
Object result;
try {
result = point.proceed();
} finally {
// 解锁
redisLock.releaseLock(key, userIdStr);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
throw new RuntimeException("重复请求");
}
}
private String getKey(String userId, String path) {
return userId + path;
}
}
HttpRequestUtils
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 获取 HttpServletRequest 工具类
*
*/
public class RequestUtils {
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getRequest();
}
}
使用
/**
* 在controller上,使用 @ControlRepeatSubmit 注解即可
*/
@GetMapping("/test")
@ControlRepeatSubmit(lockTime = 1)
public void test() {
// 你的业务
}