分布式锁工具类

MallChat 项目过程中学习到的对于分布式锁工具类的抽离

Redisson 分布式锁的使用

按照下面的形式使用:

1
2
3
4
5
6
7
8
9
10
11
12
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
RLock lock = redissonClient.getLock("myLock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}

可以看到, 除了业务代码以及获取锁时参数的设定, 其他地方都是固定的格式
执行的流程都是先获取锁, 然后执行业务, 最后解锁。因此在编写工具类时, 需要传递的参数内容有

  • getLock 方法需要的锁的键名
  • tryLock() 方法中需要的参数 timetimeUtil
  • `业务的执行代码
    tryLock() 方法的参数

分布式锁工具类的编写

对于传递的业务代码, 可以使用 Supplier<T> , Supplier<T> 是一个函数式接口,它的 get() 方法没有任何参数,并且返回一个类型为 T 的对象。
以下为几种函数式接口的区别:

接口名称 输入参数类型 返回值类型 主要方法 用途
Consumer T void void accept(T t) 接收一个参数,执行操作,无返回值
Function<T, R> T R R apply(T t) 接收一个参数,返回一个结果
Runnable void void run() 不接受参数,执行操作,无返回值
Supplier T T get() 不接受参数,返回一个结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class LockService {
@Autowired
private RedissonClient redissonClient;

@SneakyThrows
public <T> T executeWithLock(String key, int waitTime, TimeUnit timeUnit, Supplier<T> supplier) {//Supplier: 无入参,
RLock lock = redissonClient.getLock(key);
boolean success = lock.tryLock(waitTime, timeUnit);
// 抛出获取锁太频繁的异常
if (!success){
throw new BusinessException(CommonErrorEnum.LOCK_LIMIT);
}
try {
return supplier.get();// 此处执行业务
} finally {
lock.unlock();
}
}
}

executeWithLock 方法进行重载, 编写一个 waitTime 以及 timeUnit 为默认的方法:

1
2
3
public <T> T executeWithLock(String key, Supplier<T> supplier){
return executeWithLock(key, -1, TimeUnit.MILLISECONDS, supplier);
}

这样 LockService 的工具类就封装好了, 使用Lambda表达式传递业务代码:

1
2
3
4
5
lockService.executeWithLock(key, 10, TimeUnit.SECONDS, ()->{
// 执行业务逻辑
...
return null;
});

如果不需要排队等锁,还能重载方法减少两个参数:

1
2
3
4
5
lockService.executeWithLock(key, ()->{
//执行业务逻辑
。。。。。
return null;
});

注解实现分布式锁

编写自定义注解类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Retention(RetentionPolicy.RUNTIME) // 指定注解保留到运行时
@Target(ElementType.METHOD) // 指定注解作用于方法上
public @interface RedissonLock {
/**
* key的前缀, 默认去方法全限定名, 可以自己指定
*/
String prefixKey() default "";

/**
* 支持SpringEl表达式的Key (即可输入字符串, 也可使用参数名 #value)
*/
String key();

/**
* 等待锁的排队时间, 默认快速失败
*/
int waiteTime() default -1;

/**
* 等待时间单位, 默认毫秒
*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
}

处理自定义注解

需要注意的地方有两点

  • 在使用分布式锁时, 如果与事务一起执行, 要确保锁在事务外
  • 需要实现el表达式组装key

编写一个 SpElUtils 工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SpringElUtils {
private static final ExpressionParser PARSER = new SpelExpressionParser();
private static final DefaultParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

public static String getMethodKey(Method method){
return method.getDeclaringClass() + "#" + method.getName();
}

public static String parseSpEl(Method method, Object[] args, String key) {
String[] params = Optional.ofNullable(PARAMETER_NAME_DISCOVERER.getParameterNames(method)).orElse(new String[]{});
EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
}
Expression expression = PARSER.parseExpression(key);
return expression.getValue(context, String.class);
}
}

对于自定义注解的优先级, 参考博客
编写切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
@Aspect
@Order(0)// 确保比事务注解先执行, 分布式锁在事务外
public class RedissonLockAspect {
@Autowired
private LockService lockService;

@Around("@annotation(redissonLock)")
public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable{
// 获取方法名
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
String prefix = StringUtils.isBlank(redissonLock.prefixKey())
? SpringElUtils.getMethodKey(method)
: redissonLock.prefixKey();
// 使用SpElUtils工具类获取参数
String key = SpringElUtils.parseSpEl(method, joinPoint.getArgs(), redissonLock.key());
return lockService.executeWithLock(prefix + ":" + key, redissonLock.waiteTime(), redissonLock.unit(), joinPoint::proceed);
}
}

使用注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(key = "#uid")
public void modifyName(Long uid, String name) {
User valid = userDao.getByName(name);
AssertUtil.isEmpty(valid, "名字已被抢占, 请换一个");
UserBackpack modifyNameItem = userBackpackDao.getFirstValidItem(uid, ItemEnum.MODIFY_NAME_CARD.getId());
AssertUtil.isNotEmpty(modifyNameItem, "改名卡不够了! ");
// 使用改名卡
boolean success = userBackpackDao.useItem(modifyNameItem);
if (success) {
//改名
userDao.modifyName(uid, name);
}
}

测试接口:

切面类获取参数
分布式锁添加

参考资料

Mallchat项目
@Transactional和普通自定义切面执行顺序的思考


分布式锁工具类
http://example.com/2024/10/17/分布式锁工具类/
作者
Emberff
发布于
2024年10月17日
许可协议