基于注解的Redis分布式锁

基于之前用redis的lua脚本来实现安全的分布式锁,发现代码是加锁虽然灵活,但是非常的不便捷。每次需要加锁的时候,都要写出非常多的重复性代码。遂…………

  • 为什么使用基于注解的方式?

基于之前用redis的lua脚本来实现安全的分布式锁,发现代码是加锁虽然灵活,但是非常的不便捷。每次需要加锁的时候,都要写出非常多的重复性代码。遂考虑利用AOP的方式,完成这一重复性的工作。
在没利用注解之前加锁方式如下,基本每次都要这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
// 会话ID
String uuid = UUID.uuid();
try {
boolean getLock = RedisLockUtil.tryGetDistributedLock(key, uuid, 5000);
if (getLock) {
//如果获取锁,执行业务代码
// todo

}
} finally {
RedisLockUtil.releaseDistributedLock(key, uuid);
}

基于注解的使用放入如下,比较便捷

1
2
3
4
5
@Locker(key = RedisKeyEnum.POOL_ORDER_LOCK, paramExp = "0", noGetMsg = "老铁来晚了!")
public GrabAndAnswerVo grabOrderAnswer(String orderId, RedisKeyEnum poolType, User currentUser) {
Long workId = orderExist(poolType, orderId);
return doctorGrabOrderAnswer(poolType, orderId, currentUser);
}
  • 下面介绍以下代码
  1. 首先AOP的使用方式我定义为利用注解来判断是否需要加锁,类似事务的方式,我们定义一个Locker注解,这个注解的功能可以看代码;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 锁注解
*
* @author 625
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Locker {

/**
* 要锁定的Key
*
* @return
*/
RedisKeyEnum key();

/**
* 要锁定的参数
* 格式:
* 0 表示一个参数toString
* 0?payOrder 表示一个参数toString,且额外拼接锁定的Key为payOrder
* 0#name 表示第一个参数的name字段
* 0#name?payOrder 表示第一个参数的name字段,且额外拼接锁定的Key为payOrder
* 0#name+1#name?payOrder 表示第一个参数的name字段+第二个参数的name字段,且额外拼接锁定的Key为payOrder
*
* @return
*/
String paramExp();

/**
* 业务超时自动释放锁的时间,应该大于正常业务执行时间
*
* @return
*/
long expireTime() default 10000;

/**
* 最小持有锁的时间
*
* @return
*/
long limitTime() default 0;

/**
* 是否持续竞争锁,是则阻塞方法直至获取锁,或者达到最大竞争次数释放锁
*
* @return
*/
boolean continueGet() default false;

/**
* 最大竞争次数。默认0不限次
*
* @return
*/
int maxGetNum() default 0;

/**
* 拿不到锁,异常返回信息
*
* @return
*/
String noGetMsg() default "未获取锁";
}

  1. 实现AOP的拦截规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* 锁AOP拦截规则
*/
@Aspect
@Component
public class LockerAspect {

private static Logger LOGGER = LoggerFactory.getLogger(LockerAspect.class);

@Pointcut("@annotation(com.ym.common.utils.annotation.Locker)")
public void pointcut() {
}

@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) {
Object proceed = null;
long startTime = System.currentTimeMillis();
Locker locker = getAnnotation(joinPoint, Locker.class);
Object[] args = joinPoint.getArgs();
// 最大尝试次数
int maxGetNum = locker.maxGetNum();
// 会话标志
String uuid = UUID.uuid();
// 锁key
String lockFiled = getLockFiled(args, locker.paramExp());
String lockKey = RedisKeyUtil.keyBuilder(locker.key(), lockFiled);
// 过期时间
long expireTime = locker.expireTime();
boolean lock = RedisLockUtil.tryGetDistributedLock(lockKey, uuid, expireTime);
int getNum = 0;
while (!lock && locker.continueGet() && (maxGetNum == 0 || getNum < maxGetNum)) {
// 如果获取失败,且持续获取,且尝试次数小于最大次数
Threads.sleep(100);
lock = RedisLockUtil.tryGetDistributedLock(lockKey, uuid, expireTime);
}
if (!lock) {
throw new BusinessException(locker.noGetMsg());
}
// -------------------------------before-------------------------
try {
proceed = joinPoint.proceed();
// -------------------------------after-------------------------
// 如果业务时间小于最小持有锁时间,休眠一会
long sleepTime = locker.limitTime() - (System.currentTimeMillis() - startTime);
if (sleepTime > 0) {
Threads.sleep(sleepTime);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
// 释放锁
RedisLockUtil.releaseDistributedLock(lockKey, uuid);
}
return proceed;
}

/**
* 根据表达式获取要锁的字段
*
* @param args
* @param expression 表达式
* @return
*/
private String getLockFiled(Object[] args, String expression) {
if (args == null || args.length == 0 || StringUtils.isBlank(expression)) {
throw new UnsupportedOperationException("Locker所在方法参数为空! 请使用代码锁");
}
String[] extraParams = expression.split("\\?");
String extraKey = null;
if (extraParams.length > 1) {
extraKey = extraParams[1];
expression = extraParams[0];
}
String[] commboExpression = expression.split("\\+");
StringBuilder field = new StringBuilder();
for (String commbo : commboExpression) {
String[] split = commbo.split("#");
int argsNum = 0;
try {
if (split.length == 1) {
argsNum = Integer.parseInt(split[0]);
field.append(String.valueOf(args[argsNum]));
} else {
argsNum = Integer.parseInt(split[0]);
Object fieldValue = ReflectUtils.getFieldValue(args[argsNum], split[1]);
field.append(String.valueOf(fieldValue));
}
} catch (Exception e) {
throw new UnsupportedOperationException("Locker表达式paramExp不正确!");
}
}
if (extraKey != null) {
field.append(extraKey);
}
return field.toString();
}

/**
* 是否存在注解,如果存在就获取
*/
private <T> T getAnnotation(JoinPoint joinPoint, Class<? extends Annotation> t) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return (T) method.getAnnotation(t);
}
return null;
}
}


基于注解的Redis分布式锁
https://kanchai.club/2022/12/10/基于注解的Redis分布式锁/
作者
625
发布于
2022年12月10日
许可协议