原生写法
平常在用 Redisson 的时候都是怎么写分布式锁的呢?
1 2 3 4 5 6 7
| RLock lock = redissonClient.getLock(key); boolean lockSuccess = lock.tryLock(waitTime, timeUnit); if (lockSuccess) { 执行业务代码... } final { lock.unlock(); }
|
是不是都用的这样子的模板,那既然是模板,我们就可以把他抽出来,不用每次都去写这么一大串了。
分布式锁工具类
我们可以把模板抽出来放到一个工具类 LockService
中,每次要加锁的时候只需要传入锁的一些参数,以及需要加锁的代码(通过函数式接口传入)。
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
| @Service @Slf4j public class LockService {
public <T> T executeWithLock(String key, int waitTime, TimeUnit timeUnit, Supplier<T> supplier) { RLock lock = redissonClient.getLock(key); boolean lockSuccess = lock.tryLock(waitTime, timeUnit); AssertUtil.isTrue(lockSuccess, SystemCommonErrorEnum.LOCK_LIMIT); try { return supplier.get(); } finally { if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } } } }
|
在 Java 8 中,Supplier
是一个函数式接口,属于 java.util.function
包。它表示一个不接受任何参数并且返回一个结果的函数。简单来说,可以把一个函数通过这个参数传入,并通过 supplier.get()
调用,
使用:
1 2 3 4 5
| lockService.executeWithLock(key, 10, TimeUnit.SECONDS, ()->{ 。。。。。 return null; });
|
需要注意的是,由于 Supplier
有返回值,如果业务逻辑代码没有返回,也需要写一个 return null
。

也可以通过重载方法,编写一个默认不等待的锁,更少了两个参数:
1 2 3 4 5 6 7 8 9
|
public <T> T executeWithLock(String key, Supplier<T> supplier) { return executeWithLock(key, -1, TimeUnit.MILLISECONDS, supplier); }
|
有时我们希望业务代码中只包含业务逻辑,加锁显得代码格式有点乱,是否还有更简便的方法?当然有,使用 Spring 提供的 AOP 进行切面处理。
注解实现分布式锁
上述的分布式锁其实已经是核心功能了,使用注解只是为了让使用更加方便。
并且锁的 key
一般都是由入参组成的,我们就可以使用到 Spring EL 直接解析入参,将拼装 key
的操作放在业务逻辑之外。
Spring Expression Language (Spring EL) 是一个功能强大的表达式语言,用于在 Spring Framework 中动态地操作对象图、查询属性、调用方法等。Spring EL 主要用于在 Spring 配置文件、注解、或者 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
|
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RedissonLock {
String prefixKey() default "";
String key();
int waitTime() default -1;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS; }
|
秉持着 Spring 约定大于配置的思想,一些参数我们设置默认值。
并且大多数时候,锁是针对于某个特定的方法的,那么锁键就可以由两部分组成:
prefixKey
:前缀,通常为方法全限定名,用于表示该锁属于哪个方法
全限定名:类名#方法名
切面类
切面类用于拦截打上了 @RedissonLock
注解的方法,通过动态代理执行加锁。
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
|
@Aspect @Component @Order(0) public class RedissonLockAspect {
@Autowired private LockService lockService;
@Around("@annotation(com.ershi.hichat.common.common.annotation.RedissonLock)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); RedissonLock redissonLock = method.getAnnotation(RedissonLock.class); String prefix = StrUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey(); String key = SpElUtils.parseMethodArgsSpEl(method, joinPoint.getArgs(), redissonLock.key()); return lockService.executeWithLockThrows(prefix + ":" + key, redissonLock.waitTime(), redissonLock.timeUnit(), joinPoint::proceed); } }
|
分布式锁要在事务外执行,不然就是失去了意义。
可以通过@Order
指定运行运行顺序,越小越优先
这里处理 SpringEL 表达式的方法往下看。
需要注意的是 joinPoin.proceed()
方法会抛出一个异常,而我们接收的 Supplier
不抛出异常,那传参就传不进去。

我们可以自定义一个函数式接口,抛出异常,就可以接收这个参数了。

再把工具类中的参数替换:
1
| public <T> T executeWithLockThrows(String key, int waitTime, TimeUnit timeUnit, SupplierThrow<T> supplier) throws Throwable
|
SpringEL 表达式处理类
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
|
public class SpElUtils {
private static final ExpressionParser parser = new SpelExpressionParser(); private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public static String parseMethodArgsSpEl(Method method, Object[] args, String spEl) { String[] params = Optional.ofNullable(parameterNameDiscoverer.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(spEl); return expression.getValue(context, String.class); }
public static String getMethodKey(Method method) { return method.getDeclaringClass() + "#" + method.getName(); } }
|
关于 SpringEL 表达式不懂得可以自己找下教程,这里就不赘述了。
使用
现在使用就非常方便了,只需要在需要加锁的方法上打上注解@RedissonLock
,切面类就会自动拦截方法开启锁。
1
| @RedissonLock(key = "#idempotentId", waitTime = 5000)
|

切面失效情况
通常我们会通过切分代码,来达到锁操作去锁最精准位置,这就避免不了类内调用方法,比如:

其实这样我们切面拦截方法 doAcquireItem
并没有生效。因为 Spring AOP 的原理是通过在加载 Bean 的时候,检测到需要切面的方法时,会为该类生成一个动态代理类,通过代理类去执行切面方法。
如果在内类调用,相当于使用 this.doAcquireItem()
,是通过本类调用的,而不是通过代理类调用的,切面自然就不会生效。
Spring 只有在执行需要用到切面的方法时,才会使用代理类,平常使用本类。
解决方法
(1)自己注入自己,通过 Spring 注入的 Bean 进行调用

使用 @Lazy
懒加载解决循环依赖。

(2)通过 Spring 上下文获取代理类
这也是我比较推荐的一个做法,更加简单:

使用该方法的话,需要去启动类设置开启获取 Proxy 对象:

总结
通过抽象组件可以极大化的增加开发效率。那有没有现成的分布式锁注解框架呢?有,baomidou 的 lock4j,非常灵活。但也因为太过灵活,很多扩展有时候用不到,还要花时间去学习,不如自己写一个。
而且我们这个还支持函数式调用。