前言
在日常开发中,为了防止高并发,在不依赖过多的中间件的情况下,最常使用的分布式锁之一是 Redis锁。使用Redis锁就不得不面临一个问题,就是在业务代码中要控制Redis加锁、释放锁等等,对代码的侵入性较强。本文采用注解的方式为方法体增加分布式锁,唯一标识从方法参数中动态获取。
优点
-
无侵入
。通过注解实现加锁和释放锁,代码中只需关注业务实现,无须关心“锁”问题,避免代码侵入。 -
无死锁
。即使某一线程中断没能释放锁,在到达指定的时间后,程序会自动释放锁。 -
锁唯一独有
。加锁和释放锁必须由同一线程执行,不会出现A线程加锁后,B线程将锁释放。 -
支持多种方式传参做key
。通过注解指定参数名,通过反射,动态获得key。 -
线程间锁互斥
。在同一时间内,仅有一个线程持有锁,避免多个线程同时执行逻辑,出现并发情况。
代码解释
- 通过AOP切面对方法执行前和方法执行后加锁和释放锁
- 注解参数传入key保证唯一,通过key加锁保证次key不会重复
- 默认释放锁时间30分钟,可以通过注解参数设置不同方法的默认释放锁时间防止死锁
上代码
分布式锁注解,适用于方法 通过key定义请求唯一值,防止业务重复提交, 业务重复提交判断需要在方法内判断,注解能拦截正在提交中不能再次请求唯一值。 key中禁止使用 ” p # + . “字符 这几个字符作为解析key的关键字
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LocalLock {
/**请求唯一标识*/
String key() default "";
/**默认30分钟*/
long expireTime() default 1800L;
/**redis保存的值*/
String value() default "";
}
复制代码
AOP切面对注解的方法加锁和释放锁
@Resource
private RedissonClient redissonClient;
@Resource
private RedisLockUtil redisUtil;
@Pointcut("@annotation(com.cdgas.hallsite.config.lock.LocalLock)")
public void annotationPointcut() {
}
@Around("annotationPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获得当前访问的class
Class<?> className = joinPoint.getTarget().getClass();
// 获得访问的方法名
String methodName = joinPoint.getSignature().getName();
// 得到方法的参数的类型
Class<?>[] argClass = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
Object[] args = joinPoint.getArgs();
String key = "";
long expireTime = 1800L;
String value = "local_lock";
//是否执行锁操作
boolean lock = true;
try {
// 得到访问的方法对象
Method method = className.getMethod(methodName, argClass);
method.setAccessible(true);
// 判断是否存在@LocalLock注解
if (method.isAnnotationPresent(LocalLock.class)) {
LocalLock annotation = method.getAnnotation(LocalLock.class);
key = getRedisKey(args, annotation);
expireTime = annotation.expireTime();
if(StrUtil.isNotBlank(annotation.value())){
value = annotation.value();
}
}
} catch (Exception e) {
log.info("分布式锁方法注解生成key失败,方法不执行分布式锁。错误信息:{}", ExceptionUtil.getMsg(e));
lock = false;
}
key = methodName+":local_lock:" + key;
//返回结果
RLock redissonLock = redissonClient.getLock(key);
try {
//分布式锁
if (lock && StrUtil.isNotBlank(key)) {
//加锁 操作很类似Java的ReentrantLock机制
redissonLock.lock();
final boolean locker = redisUtil.lock(key, value);
if (!locker) {
throw new AppException("请求频繁");
}
}
Object res = joinPoint.proceed();
return res;
} catch (Exception e) {
log.info("分布式锁中方法执行错误释放锁,错误信息:{}", ExceptionUtil.getMsg(e));
throw e;
} finally {
//释放锁
redissonLock.unlock();
redisUtil.unlock(key, value);
}
}
复制代码
解析key方法
/**
* 解析key对应的参数
*
* @param args
* @param annotation
* @return
* @throws Exception
*/
private String getRedisKey(Object[] args, LocalLock annotation) throws Exception {
String primalKey = annotation.key();
String redisKey = "";
String[] splitAdd = primalKey.split("\+");
for (int i = 0; i < splitAdd.length; i++) {
String param = splitAdd[i];
String[] split = param.split("#p");
for (int j = 0; j < split.length; j++) {
String p = split[j];
if(StrUtil.isBlank(p)){
continue;
}
//判断是直接获取,还是获取对象中的参数
if(p.contains(".")){
String[] splitField = p.split("\.");
int idx = Integer.parseInt(splitField[0]);
String field = splitField[1];
Object parValueByObject = getFieldValueByObject(args[idx], field);
redisKey += parValueByObject;
}else {
if(StrUtil.isLowerCase(p)){
redisKey += p;
}else {
int idx = Integer.parseInt(p);
redisKey += args[idx];
}
}
}
}
log.info("分布式锁注解获取到的key值:{}",redisKey);
return redisKey;
}
/**
* 获取对象中指定字段的值
*
* @param obj
* @param field
* @return
* @throws Exception
*/
private Object getFieldValueByObject(Object obj, String field) throws Exception {
Class cls = obj.getClass();
Method method = cls.getMethod("get" + StrUtil.upperFirst(field));
method.setAccessible(true);
log.info(method.getName());
return method.invoke(obj);
}
复制代码
测试
@LocalLock("#p0+#p1.name+#p1.gender")
public Result<String> test(Long flag, Order order) {
}