当前位置: 首页 > news >正文

深度剖析:基于AOP、自定义注解与设计模式构建高度可定制的分布式锁解决方案

文章目录

  • 一、前言
  • 二、项目背景
    • 为什么需要自定义分布式锁注解?
  • 三、核心技术概览
    • 1. 自定义注解 `@MyLock` 设计与实现
    • 2. 锁类型枚举 `MyLockType`
    • 3. 锁获取策略枚举 `MyLockStrategy` 与策略模式
    • 4. 锁工厂 `MyLockFactory` 与工厂模式
      • 优化方案
      • 图示理解
    • 5. AOP 切面 `MyLockAspect` 实现
  • 四、最佳实践与使用示例
    • 技术深度追问模拟
  • 五、方案优势总结

一、前言

在分布式系统中,并发控制是一个绕不开的难题。为了保证数据的一致性和系统的稳定性,我们常常需要引入分布式锁。传统的分布式锁实现方式可能存在代码侵入性强、不够灵活等问题。

本文将深入剖析一种基于Spring AOP自定义注解以及多种设计模式构建的高度可定制分布式锁解决方案。通过这种方式,我们可以优雅地在业务代码中声明式地使用分布式锁,并且能够灵活配置锁的类型、获取策略等,极大地提升了开发效率和代码的可维护性。

二、项目背景

本文的实现来源于 [仿MOOC在线学习平台] 项目中的实践。在处理高并发场景下的资源竞争时,设计并实现了这套自定义分布式锁注解,以解决项目中分布式锁的统一管理和灵活应用问题。

为什么需要自定义分布式锁注解?

在没有自定义注解之前,我们可能会直接在业务代码中使用分布式锁客户端(例如 Redisson)的API来获取和释放锁:

// 示例:传统方式使用Redisson锁
RLock lock = redissonClient.getLock("myLockName");
try {boolean isLocked = lock.tryLock(1, 10, TimeUnit.SECONDS); // 尝试获取锁if (isLocked) {// 执行业务逻辑// ...} else {// 获取锁失败处理// ...}
} finally {if (lock.isLocked() && lock.isHeldByCurrentThread()) { // 释放锁前检查lock.unlock();}
}

这种方式存在以下问题:

  • 代码侵入性强: 业务逻辑中充斥着锁相关的代码,使得业务代码不够纯粹,难以阅读和维护。
  • 重复代码多: 每个需要加锁的地方都需要重复编写获取锁、释放锁的逻辑,容易出错。
  • 不够灵活: 如果需要修改锁的类型、等待时间、释放时间等,需要在每个使用锁的地方进行修改。
  • 可读性差: 业务开发者需要关注锁的细节,降低了代码的可读性。

自定义分布式锁注解可以很好地解决这些问题,通过将锁的逻辑与业务逻辑分离,实现声明式编程。


三、核心技术概览

本方案的核心技术包括:

  • Spring AOP (面向切面编程): 用于拦截带有特定注解的方法,并在方法执行前后织入锁的逻辑。
  • 自定义注解 (@MyLock): 用于标记需要加分布式锁的方法,并提供丰富的配置选项。
  • Redisson: 一个基于Redis的分布式Java对象和服务框架,提供了强大的分布式锁实现。
  • 设计模式:
  • 策略模式: 用于封装不同的锁获取策略(例如,快速失败、有限重试等)。
  • 工厂模式: 用于根据注解配置创建不同类型的锁实例。
  • SPEL (Spring Expression Language): 用于动态解析锁的名称,支持从方法参数中获取值作为锁名称的一部分。

1. 自定义注解 @MyLock 设计与实现

@MyLock 注解是整个方案的入口点,它定义了分布式锁的各种配置项。

package com.mooc.promotion.anotation;import com.mooc.promotion.enums.MyLockType;
import com.mooc.promotion.util.MyLockStrategy;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;/*** 自定义Redisson锁的注解*/
@Retention(RetentionPolicy.RUNTIME) // 运行时保留注解信息,AOP才能获取到
@Target(ElementType.METHOD) // 只能应用于方法上
public @interface MyLock {// 锁名称,支持SPEL表达式String name();// tryLock等方法的参数,三个同理long waitTime() default 1; // 等待获取锁的时间// 默认为-1,表示开启看门狗机制long leaseTime() default -1; // 锁的持有时间,-1表示开启看门狗TimeUnit unit() default TimeUnit.SECONDS; // 时间单位// 默认锁类型是:可重入锁MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK; // 锁的类型// 默认获取锁失败策略是:有限重试后抛出异常MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT; // 获取锁失败的策略
}

设计考量:

  • name(): 锁的名称是分布式锁的关键标识。为了支持更灵活的命名,例如基于方法参数动态生成锁名称,我们支持在 name() 中使用 SPEL 表达式。
  • waitTime(), leaseTime(), unit(): 这些参数直接映射到 Redisson tryLock 方法的参数,用于控制锁的获取行为和持有时间。leaseTime 默认为 -1,开启 Redisson 的看门狗机制,避免死锁。
  • lockType(): 定义了锁的类型,通过枚举 MyLockType 提供多种 Redisson 锁的支持(可重入锁、公平锁、读写锁)。
  • lockStrategy(): 定义了获取锁失败后的处理策略,通过枚举 MyLockStrategy 提供多种策略选项。这是策略模式的应用,极大地提高了锁获取行为的可定制性。

2. 锁类型枚举 MyLockType

MyLockType 枚举定义了我们支持的 Redisson 锁类型。

package com.mooc.promotion.enums;/*** Redisson的锁类型的枚举*/
public enum MyLockType {RE_ENTRANT_LOCK, // 可重入锁,默认锁FAIR_LOCK, // 公平锁READ_LOCK, // 读锁WRITE_LOCK, // 写锁;
}

设计考量:

  • 不同的业务场景需要不同类型的锁。例如,在读多写少的场景下,使用读写锁可以提高并发性能。通过枚举,我们可以清晰地表达支持的锁类型,并在后续的工厂模式中根据枚举值创建相应的锁实例。

3. 锁获取策略枚举 MyLockStrategy 与策略模式

MyLockStrategy 枚举是策略模式的应用,它定义了获取锁失败后的不同处理方式,并封装了相应的 tryLock 逻辑。

package com.mooc.promotion.util;import com.mooc.common.exceptions.BizIllegalException;
import com.mooc.promotion.anotation.MyLock;
import org.redisson.api.RLock;import java.util.concurrent.TimeUnit;/*** 行为枚举类,做策略工厂模式* 每个枚举项都需要实现本类的抽象方法* 对应执行tryLock或者lock的五种策略*/
public enum MyLockStrategy {// 不重试、直接结束SKIP_FAST() {@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {// 尝试获取锁,等待时间为0,即不等待return lock.tryLock(0, prop.leaseTime(), prop.unit());}},// 不重试、抛出异常FAIL_FAST() {@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {// 尝试获取锁,等待时间为0,即不等待boolean isLock = lock.tryLock(0, prop.leaseTime(), prop.unit());if (!isLock) {// 获取锁失败,抛出业务异常throw new BizIllegalException("请求太频繁");}return true; // 获取锁成功}},// 无限重试KEEP_TRYING() {@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {// 使用 lock() 方法,会一直阻塞直到获取到锁lock.lock(prop.leaseTime(), prop.unit());return true; // 获取锁成功}},// 有限重试后直接结束SKIP_AFTER_RETRY_TIMEOUT() {@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {// 尝试获取锁,等待指定时间return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());}},// 有限重试后抛出异常FAIL_AFTER_RETRY_TIMEOUT() {@Overridepublic boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {// 尝试获取锁,等待指定时间boolean isLock = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());if (!isLock) {// 获取锁失败,抛出业务异常throw new BizIllegalException("请求太频繁");}return true; // 获取锁成功}},; // 枚举项结束/*** 抽象方法,定义了获取锁的行为* @param lock Redisson锁对象* @param prop MyLock注解属性* @return 是否成功获取到锁* @throws InterruptedException 线程中断异常*/public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
}

设计考量:

  • 策略模式的应用: MyLockStrategy 枚举的每个枚举项都代表一种获取锁的策略,并实现了抽象方法 tryLock()。在 AOP 中,我们只需要根据 @MyLock 注解中指定的策略来调用相应的 tryLock() 方法,实现了行为的解耦和可替换。
  • 丰富的策略选项: 提供了多种常用的锁获取策略,涵盖了快速失败、无限重试、有限重试等场景,满足了不同业务需求。
  • 与注解属性关联: tryLock() 方法接收 MyLock 注解作为参数,使得策略的执行可以根据注解的配置进行调整。

4. 锁工厂 MyLockFactory 与工厂模式

MyLockFactory 负责根据 @MyLock 注解中指定的锁类型创建相应的 Redisson 锁实例。

package com.mooc.promotion.util;import com.mooc.promotion.enums.MyLockType;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;/*** Redisson锁的工厂类*/
@Component
@RequiredArgsConstructor // Lombok注解,生成包含final字段的构造函数
public class MyLockFactory {private final RedissonClient redissonClient;/*** 根据锁类型创建锁对象* @param lockType 锁类型* @param lockName 锁名称* @return RLock锁对象*/public RLock getLock(MyLockType lockType, String lockName) {switch (lockType) {case FAIR_LOCK:return redissonClient.getFairLock(lockName);case READ_LOCK:return redissonClient.getReadWriteLock(lockName).readLock();case WRITE_LOCK:return redissonClient.getReadWriteLock(lockName).writeLock();case RE_ENTRANT_LOCK:default:return redissonClient.getLock(lockName);}}
}

设计考量:

  • 工厂模式的应用: MyLockFactory 隐藏了 Redisson 锁创建的复杂性,客户端(AOP 切面)只需要通过 getLock() 方法传入锁类型和名称即可获取到相应的锁实例。
  • MyLockType 枚举关联: 工厂方法根据 MyLockType 枚举值来判断需要创建哪种类型的锁。
  • 解耦: 将锁的创建逻辑从 AOP 切面中分离出来,使得切面代码更加简洁。

优化方案

可进一步优化方案:注意到,该工厂中存在大量的 if-else(或者switch)判断,所以我们可以使用 Map 来进一步优化

/**
* 锁类型工厂,根据锁类型封装对应的「获取对应锁」的「函数式接口」
*/
@Component
public class MyLockFactory {// 注意:这里的value可以认为就是一个「根据锁名获取锁的方法」private final Map<MyLockType, Function<String, RLock>> lockHandlers;public MyLockFactory(RedissonClient redissonClient) { // 通过方法注入this.lockHandlers = new EnumMap<>(MyLockType.class); // 使用EnumMap,性能更好且线程安全// 初始化Map,将不同的锁类型映射到对应的获取锁的函数式接口this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);// 对于读写锁,需要先获取读写锁对象,再获取读锁或写锁this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock());this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());}/*** 根据锁类型和锁名称获取锁对象* @param lockType 锁类型* @param name 锁名称* @return RLock锁对象*/public RLock getLock(MyLockType lockType, String name) {// 从Map中根据锁类型获取对应的函数式接口,然后调用apply方法执行获取锁的逻辑Function<String, RLock> handler = lockHandlers.get(lockType);if (handler == null) {// 处理未知锁类型的情况,可以抛出异常或返回默认锁throw new IllegalArgumentException("Unsupported lock type: " + lockType);}return handler.apply(name);}
}

这样,省去了大量对于锁的类型if-else 判断,先使用 Map 存储起来,要获取什么类型的锁,直接传入类型,自动取出匹配的类型的 「根据锁名获取锁的方法」,通过将不同的锁类型映射到不同的函数式接口,我们实现了获取锁逻辑的解耦。

图示理解

在这里插入图片描述

getLock 执行流程:
在这里插入图片描述


5. AOP 切面 MyLockAspect 实现

MyLockAspect 是整个方案的核心,它使用 Spring AOP 的环绕通知来拦截带有 @MyLock 注解的方法,并在方法执行前后织入锁的逻辑。

package com.mooc.promotion.aspect;import com.mooc.common.utils.StringUtils;
import com.mooc.promotion.anotation.MyLock;
import com.mooc.promotion.util.MyLockFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;@Component // 标记为Spring组件
@Aspect // 标记为AOP切面
@Slf4j // Lombok注解,用于日志记录
@RequiredArgsConstructor // Lombok注解,生成包含final字段的构造函数
public class MyLockAspect implements Ordered {// 从最开始的直接抽取切面方法+自定义注解,到最后压根用不上redissonClient// 其中最最核心的就是两次工厂模式:一个锁的类型工厂模式,一个行为策略的工厂模式// private final RedissonClient redissonClient; // RedissonClient 不再直接在切面中使用,而是通过工厂类private final MyLockFactory lockFactory; // 注入锁工厂/*** SPEL的正则规则*/private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");/*** 方法参数解析器,用于解析SPEL表达式*/private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();/*** 结合切点表达式方法和注解的简化写法的环绕通知* @param pjp 切入点对象,用于获取方法信息和执行方法* @param myLock MyLock注解实例,由Spring AOP自动注入* @return 业务方法的返回值* @throws Throwable 业务方法可能抛出的异常*/@Around("@annotation(myLock)") // 切点表达式,拦截带有@MyLock注解的方法public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {// 1. 解析锁名称,支持SPEL表达式String lockName = getLockName(myLock.name(), pjp);// 2. 通过锁工厂创建锁对象,根据注解指定的锁类型RLock lock = lockFactory.getLock(myLock.lockType(), lockName);// 3. 尝试获取锁,根据注解指定的获取锁策略// 这里是策略模式的应用,调用MyLockStrategy枚举项的tryLock方法boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);// 4. 判断是否成功获取锁if (!isLock) {log.debug("线程:[{}]获取锁失败,锁尚未被其他线程释放!锁名称:{}", Thread.currentThread().getName(), lockName);// 如果获取锁失败,并且策略不是抛出异常(抛异常的策略会在tryLock方法内部抛出),则直接返回null// 注意:这里假设业务方法返回null表示处理失败,或者根据实际业务需求进行调整return null;}// 5. 成功获取锁,执行业务方法try {log.debug("线程:[{}]获取到锁:{},即将执行业务方法!", Thread.currentThread().getName(), lockName);return pjp.proceed(); // 执行被拦截的业务方法} finally {// 6. 释放锁// 在finally块中释放锁,确保锁一定会被释放,避免死锁// 检查锁是否被当前线程持有,避免释放其他线程的锁if (lock.isLocked() && lock.isHeldByCurrentThread()) {log.debug("线程:[{}]已释放锁:{}!", Thread.currentThread().getName(), lockName);lock.unlock();} else {log.warn("线程:[{}]尝试释放未持有的锁:{}!", Thread.currentThread().getName(), lockName);}}}/*** 解析锁名称(spEL表达式)* 支持从方法参数中获取值作为锁名称的一部分* @param name 原始锁名称,可能包含SPEL表达式* @param pjp 切入点对象* @return 解析后的锁名称*/private String getLockName(String name, ProceedingJoinPoint pjp) {// 1. 判断是否存在spel表达式if (StringUtils.isBlank(name) || !name.contains("#")) {// 不存在,直接返回原始名称return name;}// 2. 构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表// MethodBasedEvaluationContext 提供了基于方法的上下文,方便通过参数名访问参数值EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);// 3. 构建SPEL解析器ExpressionParser parser = new SpelExpressionParser();// 4. 循环处理,因为表达式中可以包含多个表达式Matcher matcher = pattern.matcher(name);StringBuffer sb = new StringBuffer(); // 使用StringBuffer来构建解析后的字符串while (matcher.find()) {// 4.1. 获取表达式,例如 "#{paramName}"String fullExpression = matcher.group(); // 完整的表达式,例如 "#{paramName}"String expressionContent = matcher.group(1); // 表达式内容,例如 "paramName"// 4.2. 这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文// 例如:#{T(java.lang.System).currentTimeMillis()}Expression expression = parser.parseExpression(expressionContent.charAt(0) == 'T' ? expressionContent : "#" + expressionContent);// 4.3. 解析出表达式对应的值Object value = expression.getValue(context);// 4.4. 用值替换锁名称中的SPEL表达式// 使用Matcher的appendReplacement方法进行替换,更安全高效matcher.appendReplacement(sb, ObjectUtils.nullSafeToString(value));}matcher.appendTail(sb); // 将剩余的字符串追加到StringBufferreturn sb.toString(); // 返回解析后的锁名称}/*** 解析切入点对应的方法对象* @param pjp 切入点对象* @return 方法对象*/private Method resolveMethod(ProceedingJoinPoint pjp) {// 1. 获取方法签名MethodSignature signature = (MethodSignature) pjp.getSignature();// 2. 获取目标对象的类Class<?> clazz = pjp.getTarget().getClass();// 3. 方法名称String name = signature.getName();// 4. 方法参数列表Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();// 5. 尝试获取方法对象,考虑父类方法return tryGetDeclaredMethod(clazz, name, parameterTypes);}/*** 尝试获取声明的方法对象,包括父类方法* @param clazz 类对象* @param name 方法名称* @param parameterTypes 方法参数类型列表* @return 方法对象*/private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {try {// 尝试获取当前类声明的方法return clazz.getDeclaredMethod(name, parameterTypes);} catch (NoSuchMethodException e) {// 如果当前类没有找到,则尝试从父类寻找Class<?> superClass = clazz.getSuperclass();if (superClass != null) {return tryGetDeclaredMethod(superClass, name, parameterTypes);}}return null; // 未找到方法}@Overridepublic int getOrder() {// 实现Ordered接口,设置切面的优先级// 数字越小优先级越高,确保锁的逻辑在其他可能的切面(如事务)之前或之后执行,具体取决于业务需求// 0 表示较高的优先级return 0;}
}

设计考量:

  • @Around("@annotation(myLock)"): 使用注解作为切点表达式,简洁明了地指定了需要拦截的目标方法。myLock 参数会自动绑定到被拦截方法上的 @MyLock 注解实例。
  • ProceedingJoinPoint: 在环绕通知中,通过 ProceedingJoinPoint 对象可以访问被拦截方法的签名、参数等信息,并控制方法的执行 (pjp.proceed())。
  • SPEL 解析 getLockName(): 这个方法是亮点之一。它利用 Spring 的 SPEL 表达式解析能力,结合 MethodBasedEvaluationContext,能够从方法参数中动态获取值,构建更具业务意义的锁名称。例如,@MyLock(name = "order:#{orderId}") 可以根据订单ID生成不同的锁名称。
  • 锁的获取与释放:
    • try 块中执行业务方法 pjp.proceed()
    • finally 块中释放锁,确保即使业务方法抛出异常,锁也能被释放,避免死锁。
    • 在释放锁之前,通过 lock.isLocked() && lock.isHeldByCurrentThread() 进行判断,避免释放未持有的锁或非当前线程持有的锁,提高了代码的健壮性。
  • 策略模式的应用: myLock.lockStrategy().tryLock(lock, myLock) 直接调用了 @MyLock 注解中指定的策略的 tryLock 方法,实现了策略的灵活切换。
  • 工厂模式的应用: lockFactory.getLock(myLock.lockType(), lockName) 通过锁工厂创建锁实例,将锁的创建逻辑与切面逻辑解耦。
  • Ordered 接口: 实现了 Ordered 接口,可以设置切面的执行顺序。在复杂的应用中,可能有多个切面作用于同一个方法,通过设置优先级可以控制它们的执行顺序。

四、最佳实践与使用示例

使用示例:

在需要加分布式锁的方法上,直接添加 @MyLock 注解即可:

@Service
public class OrderService {@MyLock(name = "createOrder:#{orderId}", // 锁名称,根据orderId动态生成lockType = MyLockType.RE_ENTRANT_LOCK, // 使用可重入锁waitTime = 5, // 等待5秒获取锁leaseTime = 10, // 锁持有10秒(如果看门狗未开启)unit = TimeUnit.SECONDS,lockStrategy = MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT) // 有限重试后抛出异常public void createOrder(Long orderId, OrderDTO orderDTO) {// 业务逻辑:创建订单// ...System.out.println("订单 " + orderId + " 创建成功,线程:" + Thread.currentThread().getName());}@MyLock(name = "updateStock:#{productId}", // 锁名称,根据productId动态生成lockType = MyLockType.WRITE_LOCK, // 使用写锁lockStrategy = MyLockStrategy.SKIP_FAST) // 不重试,快速跳过public void updateStock(Long productId, int quantity) {// 业务逻辑:更新库存// ...System.out.println("商品 " + productId + " 库存更新成功,线程:" + Thread.currentThread().getName());}
}

最佳实践:

  • 锁名称的规范化: 建议采用统一的命名规范来定义锁名称,例如 业务模块:业务标识,方便管理和排查问题。
  • 合理选择锁类型: 根据业务场景选择合适的锁类型(可重入锁、公平锁、读写锁),以提高并发性能。
  • 谨慎设置等待时间和持有时间: 等待时间过长可能导致线程阻塞,持有时间过短可能导致锁提前释放,持有时间过长可能增加死锁风险。结合 Redisson 的看门狗机制,通常可以将 leaseTime 设置为 -1,依赖看门狗自动续期。
  • 选择合适的获取锁策略: 根据业务对并发的要求选择合适的策略。例如,对实时性要求高的场景可以使用 FAIL_FASTSKIP_FAST;对数据一致性要求极高的场景可以使用 KEEP_TRYINGFAIL_AFTER_RETRY_TIMEOUT
  • SPEL 表达式的安全性: 在使用 SPEL 表达式时,要注意避免注入攻击。确保从可信的来源获取用于表达式解析的数据。
  • 异常处理: 在业务方法中捕获和处理可能发生的异常,确保锁在异常发生时也能正确释放。虽然 AOP 的 finally 块可以保证锁释放,但业务异常的处理仍然重要。
  • 监控与报警: 对分布式锁的使用情况进行监控,例如锁的竞争情况、平均等待时间等,及时发现和解决潜在问题。

技术深度追问模拟

面试官: 你刚才提到了自定义分布式锁注解,能详细讲讲你是如何实现的吗?特别是如何实现锁名称的动态解析和锁获取策略的灵活配置?

候选人: 好的,我的实现主要基于 Spring AOP、自定义注解和设计模式。

首先,我定义了一个 @MyLock 注解,用于标记需要加锁的方法。这个注解包含了锁的名称、类型、等待时间、持有时间以及获取锁失败时的策略等属性。

为了实现锁名称的动态解析,我在 @MyLock 注解的 name() 属性中支持 SPEL 表达式。在 AOP 切面中,我通过 Spring 提供的 SPEL 解析工具,结合 MethodBasedEvaluationContext,能够获取到被拦截方法的参数,并根据 SPEL 表达式从参数中提取值,动态地构建出最终的锁名称。例如,如果方法是 updateOrder(Long orderId, ...),我在注解中可以写 @MyLock(name = "order:#{orderId}"),这样锁的名称就会包含具体的订单ID。

为了实现锁获取策略的灵活配置,我使用了策略模式。我定义了一个 MyLockStrategy 枚举,每个枚举项代表一种获取锁的策略,并实现了统一的 tryLock 方法。在 @MyLock 注解中,我通过 lockStrategy() 属性指定使用哪种策略。在 AOP 切面中,我直接调用注解中指定的策略的 tryLock 方法来尝试获取锁,这样就将锁获取的具体行为与切面逻辑解耦了。

此外,我还使用了工厂模式来创建不同类型的 Redisson 锁实例。我定义了一个 MyLockFactory 类,根据 @MyLock 注解中指定的锁类型 (lockType),通过 RedissonClient 创建对应的可重入锁、公平锁或读写锁。这使得我在 AOP 切面中不需要关心锁创建的具体细节,只需要通过工厂获取即可。

最后,我使用 Spring AOP 的环绕通知来拦截带有 @MyLock 注解的方法。在环绕通知中,我首先解析出动态的锁名称,然后通过工厂创建锁对象,接着根据注解指定的策略尝试获取锁。如果获取成功,就执行业务方法;无论业务方法是否抛出异常,都在 finally 块中释放锁,确保锁的正确释放。

面试官: 你提到了 Redisson 的看门狗机制,你在实现中是如何处理的?为什么将 leaseTime 默认为 -1?

候选人: Redisson 的看门狗机制是 Redisson 提供的一种自动续期机制,可以避免因为业务方法执行时间过长导致锁过期而被其他线程获取,从而引发数据不一致问题。当 leaseTime 设置为 -1 时,Redisson 会开启看门狗,每隔一段时间(默认是锁过期时间的 1/3)就会自动给锁续期,直到业务方法执行完毕并主动释放锁。

在我的实现中,我将 @MyLock 注解的 leaseTime 默认值设置为 -1,就是为了默认开启 Redisson 的看门狗机制。这样,大多数情况下开发者不需要关心锁的持有时间,Redisson 会自动处理续期,降低了死锁的风险。当然,如果用户有特殊需求,也可以显式地设置 leaseTime 为一个正值,关闭看门狗,此时需要自己评估业务方法的执行时间,确保锁不会提前过期。

面试官: 你在 MyLockStrategy 枚举中实现了不同的获取锁策略,如果用户需要一种新的策略,例如在获取锁失败后执行特定的补偿逻辑,你的设计是否容易扩展?

候选人: 是的,我的设计具有很好的扩展性。由于我使用了策略模式,MyLockStrategy 枚举是开放给用户扩展的。如果用户需要一种新的获取锁策略,他们只需要:

  1. MyLockStrategy 枚举中添加一个新的枚举项
  2. 实现该枚举项的抽象方法 tryLock(RLock lock, MyLock prop),在其中定义新的获取锁逻辑,包括失败后的补偿逻辑。
  3. @MyLock 注解中使用新添加的策略枚举项即可。

这种基于枚举的策略模式实现,使得新增策略非常方便,只需要修改枚举类本身,而不需要修改 AOP 切面或其他核心逻辑,符合开闭原则


例如,假如要增加一个「重试3次后降级」的策略

MyLockStrategy 中添加新枚举项:

RETRY_THEN_FALLBACK {public boolean tryLock(RLock lock, MyLock prop) {for(int i=0; i<3; i++){if(lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit())) {return true;}}// 执行降级逻辑return false; }
}

使用时指定策略:

@MyLock(lockStrategy = MyLockStrategy.RETRY_THEN_FALLBACK)

面试官: 你觉得这套自定义分布式锁方案还有哪些可以改进的地方?

候选人: 尽管这套方案已经比较完善,但仍有一些可以改进的地方:

  1. 锁粒度的控制: 目前锁的粒度是方法级别的。对于一些需要更细粒度控制的场景,例如只锁定方法中的某个代码块,可能需要进一步的扩展,例如结合注解和方法参数来定义更细粒度的锁。
  2. 异常处理的细化: 目前获取锁失败后的处理策略相对固定(抛异常或跳过)。可以考虑更细化的异常处理,例如允许用户自定义获取锁失败后的回调函数或处理器。
  3. 与业务上下文的集成: 在某些场景下,锁的获取和释放可能需要与业务上下文(例如事务)更紧密地集成。可以考虑与 Spring 的事务管理机制进行更深入的整合。
  4. 更丰富的锁类型支持: 除了 Redisson 提供的锁类型,如果需要支持其他类型的分布式锁(例如 ZooKeeper 分布式锁),可能需要修改锁工厂,甚至引入适配器模式来统一不同分布式锁客户端的接口。
  5. 性能优化: 对于非常高并发的场景,可以进一步优化 SPEL 解析的性能,或者考虑缓存解析结果。

总的来说,这套方案提供了一个良好的基础框架,可以根据具体的业务需求进行进一步的扩展和优化。


五、方案优势总结

设计点传统实现本方案
扩展性修改代码新增枚举/策略
可读性业务耦合声明式注解
灵活性硬编码SpEL动态锁名
健壮性手动处理策略模式封装
维护成本多处修改集中管理

架构启示:这种设计是Spring生态的经典实践,类似@Transactional注解实现机制,体现了「约定优于配置」的设计哲学。

http://www.lqws.cn/news/595117.html

相关文章:

  • 亚马逊云科技中国峰会:数新智能CTO原攀峰详解一站式AI原生数智平台DataCyber在Amazon EKS的实践
  • 基于SSM万华城市货运服务系统的设计与实现
  • eNSP实验一:IPv4编址及IPv4路由基础
  • 新手向:从零开始Node.js超详细安装、配置与使用指南
  • 业务系统-AI 智能导航设计(系统设计篇 下)
  • 制作一款打飞机游戏74:游戏原型
  • 【仿muduo库实现并发服务器】LoopThreadPool模块
  • 第八十六篇 大数据排序算法:从厨房整理到分布式排序的智慧
  • 复合型浪涌保护器五大核心技术重构电气防护体系
  • 智慧医疗的定义与作用
  • 【QT】TXT电子书语音朗读器开发(2)
  • A模块 系统与网络安全 第三门课 网络通信原理-3
  • STM32F103_Bootloader程序开发10 - 实现IAP通讯看门狗与提升“跳转状态机”的健壮性
  • 达梦数据库配置SYSDBA本地免密登录
  • langchain从入门到精通(三十三)——RAG优化策略(九) MultiVector实现多向量检索文档
  • 在识IO函数
  • Day 3:Python模块化、异常处理与包管理实战案例
  • 比Axure更简单?墨刀高保真原型交互“监听变量”使用教程
  • 【Axure视频教程】大小图轮播
  • 应用场景全解析:飞算 JavaAI 的实战舞台
  • 星璇抽奖测试报告
  • 开源模型应用落地-OpenAI Agents SDK-集成Qwen3-8B-探索input_guardrail 的创意应用(五)
  • Hibernate对象生命周期全解析
  • SQLite与MySQL:嵌入式与客户端-服务器数据库的权衡
  • 复现一个nanoGPT——model.py
  • 【PDF-XSS攻击】springboot项目-上传文件-解决PDF文件XSS攻击
  • [密码学实战]深入解析ASN.1和DER编码:以数字签名值为例
  • 用openCV实现基础的人脸检测与情绪识别
  • 华为交换机堆叠与集群技术深度解析附带脚本
  • Sketch v2025「Athens」全新发布,3大更新重塑UI/UX设计的关键逻辑