Redisson 分布式锁的使用
按照下面的形式使用:
1 2 3 4 5 6 7 8 9 10 11 12
| 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()
方法中需要的参数 time
、timeUtil
- `业务的执行代码

分布式锁工具类的编写
对于传递的业务代码, 可以使用 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) { 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 {
String prefixKey() default "";
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(); 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和普通自定义切面执行顺序的思考