
平常在用 Redisson 的时候都是怎么写分布式锁的呢?
RLock lock = redissonClient.getLock(key);
boolean lockSuccess = lock.tryLock(waitTime, timeUnit);
if (lockSuccess) {
执行业务代码...
} final {
lock.unlock();
}是不是都用的这样子的模板,那既然是模板,我们就可以把他抽出来,不用每次都去写这么一大串了。
我们可以把模板抽出来放到一个工具类 LockService 中,每次要加锁的时候只需要传入锁的一些参数,以及需要加锁的代码(通过函数式接口传入)。
@Service
@Slf4j
public class LockService {
/**
* 使用分布式锁执行给定的操作
*
* @param key 锁的键
* @param waitTime 等待锁的时间
* @param timeUnit 时间单位
* @param supplier 执行的操作
* @param <T> 操作返回的类型
* @return 操作的结果
*/
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()调用,
使用:
lockService.executeWithLock(key, 10, TimeUnit.SECONDS, ()->{
//执行业务逻辑
。。。。。
return null;
});需要注意的是,由于 Supplier 有返回值,如果业务逻辑代码没有返回,也需要写一个 return null。

也可以通过重载方法,编写一个默认不等待的锁,更少了两个参数:
/**
* 使用分布式锁执行给定操作,默认不重试
* @param key
* @param supplier
* @return {@link T}
*/
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 中动态地计算值。
首先编写一个注解,用于设置参数:
/**
* 分布式锁注解
* @author Ershi
* @date 2024/12/08
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedissonLock {
/**
* key的前缀,默认取当前方法的全限定名,除非希望在不同方法上对同一个资源做分布式锁,就自己指定
*
* @return key的前缀
*/
String prefixKey() default "";
/**
* 锁的主要key值,使用springEl表达式
*
* @return 表达式
*/
String key();
/**
* 等待锁的时间,默认-1,不等待
*
* @return 单位秒
*/
int waitTime() default -1;
/**
* 等待锁的时间单位,默认毫秒
*
* @return 单位
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}秉持着 Spring 约定大于配置的思想,一些参数我们设置默认值。
并且大多数时候,锁是针对于某个特定的方法的,那么锁键就可以由两部分组成:
prefixKey:前缀,通常为方法全限定名,用于表示该锁属于哪个方法全限定名:类名#方法名
key:锁的主要键切面类用于拦截打上了 @RedissonLock 注解的方法,通过动态代理执行加锁。
/**
* Redisson分布式锁切面类
*
* @author Ershi
* @date 2024/12/08
*/
@Aspect
@Component
@Order(0) // 分布式锁要在事务注解前执行
public class RedissonLockAspect {
@Autowired
private LockService lockService;
/**
* 为打上@RedissonLock注解的方法启用Redisson分布式锁,并设置key
*
* @param joinPoint
* @return {@link Object}
*/
@Around("@annotation(com.ershi.hichat.common.common.annotation.RedissonLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取被拦截方法的方法对象
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取方法上的RedissonLock注解
RedissonLock redissonLock = method.getAnnotation(RedissonLock.class);
// 确定锁的前缀key:如果注解的prefixKey属性为空,则使用SpEl表达式获取“类名#方法名”;否则使用prefixKey属性值
String prefix = StrUtil.isBlank(redissonLock.prefixKey()) ? SpElUtils.getMethodKey(method) : redissonLock.prefixKey();//默认方法限定名+注解排名(可能多个)
// 解析SpEl表达式,获取锁的键值
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 不抛出异常,那传参就传不进去。

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

再把工具类中的参数替换:
public <T> T executeWithLockThrows(String key, int waitTime, TimeUnit timeUnit, SupplierThrow<T> supplier) throws Throwable/**
* Spring EL表达式解析工具类
* 提供方法参数解析和方法键获取功能
* @author Ershi
* @date 2024/12/08
*/
public class SpElUtils {
// 使用SpelExpressionParser作为表达式解析器
private static final ExpressionParser parser = new SpelExpressionParser();
// 使用DefaultParameterNameDiscoverer来发现参数名
private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 解析SpringEL表达式,动态获取方法指定参数的值
*
* @param method 要解析的方法
* @param args 方法的参数值数组
* @param spEl SpEL表达式字符串 -> 要获取值的参数名
* @return 解析后的字符串结果,返回目标参数的值
*/
public static String parseMethodArgsSpEl(Method method, Object[] args, String spEl) {
// 解析方法参数名,如果无法解析则使用空数组
String[] params = Optional.ofNullable(parameterNameDiscoverer.getParameterNames(method)).orElse(new String[]{});
// 创建标准的EL上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 将方法参数名-参数值绑定到EL上下文中
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);
}
// 解析SpEL表达式
Expression expression = parser.parseExpression(spEl);
// 返回表达式解析结果
return expression.getValue(context, String.class);
}
/**
* 生成方法的唯一键
*
* @param method 方法对象
* @return 方法的唯一键,格式为:类名#方法名
*/
public static String getMethodKey(Method method) {
// 拼接方法所属类和方法名作为方法键
return method.getDeclaringClass() + "#" + method.getName();
}
}关于 SpringEL 表达式不懂得可以自己找下教程,这里就不赘述了。
现在使用就非常方便了,只需要在需要加锁的方法上打上注解@RedissonLock,切面类就会自动拦截方法开启锁。
@RedissonLock(key = "#idempotentId", waitTime = 5000)
通常我们会通过切分代码,来达到锁操作去锁最精准位置,这就避免不了类内调用方法,比如:

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

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

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

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

通过抽象组件可以极大化的增加开发效率。那有没有现成的分布式锁注解框架呢?有,baomidou 的 lock4j,非常灵活。但也因为太过灵活,很多扩展有时候用不到,还要花时间去学习,不如自己写一个。
而且我们这个还支持函数式调用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。