Spring中使用自定义@Lock 注解解决线程并发问题

在 Spring 生态中,并没有原生的 @Lock 注解,但业界常用的做法是自定义 @Lock 注解 + AOP 切面 来封装锁逻辑(支持本地锁 / 分布式锁),让代码更简洁、可复用。下面我会完整讲解如何定义和使用 @Lock 注解解决线程并发问题,包括本地锁版 (单实例)和分布式锁版(多实例)两种核心场景。

一、核心思路

@Lock 注解的本质是通过 AOP 拦截标注了该注解的方法,在方法执行前加锁、执行后释放锁,从而保证方法执行的原子性,解决多线程竞争共享资源的问题。

二、场景 1:单实例下的 @Lock(本地锁)

适用于单服务器部署的应用,使用 ReentrantLock(可重入锁)实现。

步骤 1:定义 @Lock 注解
复制代码
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 自定义锁注解(本地锁)
 * 用于标记需要加锁的方法,解决单实例并发问题
 */
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface Lock {
    /**
     * 锁的唯一标识(支持SpEL表达式,如 "#userId")
     * 为空时默认使用「类名.方法名」作为锁标识
     */
    String key() default "";

    /**
     * 加锁超时时间(默认5秒)
     * 超时未获取锁则抛出异常,避免线程阻塞
     */
    long waitTime() default 5;

    /**
     * 锁的自动释放时间(默认30秒)
     * 防止死锁(ReentrantLock本身不会死锁,但超时释放更安全)
     */
    long leaseTime() default 30;

    /**
     * 时间单位(默认秒)
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}
步骤 2:实现 @Lock 注解的 AOP 切面(核心)
复制代码
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.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Lock注解的切面实现(本地锁)
 */
@Aspect // 标记为AOP切面
@Component // 交给Spring容器管理
public class LocalLockAspect {

    // 缓存锁实例:key=锁标识,value=ReentrantLock对象(ConcurrentHashMap保证线程安全)
    private final Map<String, Lock> lockCache = new ConcurrentHashMap<>();

    // SpEL表达式解析器(解析注解中的key参数)
    private final ExpressionParser spelParser = new SpelExpressionParser();
    // 参数名解析器(获取方法参数名,用于SpEL解析)
    private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    /**
     * 环绕通知:拦截所有标注了@Lock的方法
     */
    @Around("@annotation(lock)")
    public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
        // 1. 解析锁的唯一标识(key)
        String lockKey = resolveLockKey(joinPoint, lock);

        // 2. 获取或创建锁(不存在则新建,存在则复用)
        Lock reentrantLock = lockCache.computeIfAbsent(lockKey, k -> new ReentrantLock());

        // 3. 加锁(支持超时等待,避免线程无限阻塞)
        boolean locked = false;
        try {
            locked = ((java.util.concurrent.locks.Lock) reentrantLock).tryLock(
                    lock.waitTime(),
                    lock.leaseTime(),
                    lock.timeUnit()
            );
            if (!locked) {
                throw new RuntimeException("获取锁失败,请稍后重试");
            }

            // 4. 执行原方法(核心业务逻辑)
            return joinPoint.proceed();
        } finally {
            // 5. 释放锁(仅当前线程持有锁时释放,避免误释放)
            if (locked && reentrantLock.isHeldByCurrentThread()) {
                reentrantLock.unlock();
            }
        }
    }

    /**
     * 解析@Lock注解中的key(支持SpEL表达式)
     */
    private String resolveLockKey(ProceedingJoinPoint joinPoint, Lock lock) {
        // 如果注解key为空,默认使用「类名.方法名」作为锁标识
        if (lock.key().isEmpty()) {
            return joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName();
        }

        // 解析SpEL表达式(如 "#userId" 解析为实际的用户ID)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();

        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }

        return spelParser.parseExpression(lock.key()).getValue(context, String.class);
    }
}
步骤 3:使用 @Lock 注解解决并发问题

以 "库存扣减" 这个典型并发场景为例:

复制代码
import org.springframework.stereotype.Service;

@Service
public class StockService {
    // 共享资源:模拟库存(多线程竞争的核心)
    private int stock = 100;

    /**
     * 扣减库存(并发场景)
     * @param productId 商品ID(按商品ID加锁,不同商品不互斥,提高并发效率)
     * @param num 扣减数量
     */
    @Lock(key = "'stock:' + #productId") // SpEL表达式:锁标识为 "stock:商品ID"
    public void deductStock(Long productId, int num) {
        if (stock >= num) {
            // 模拟耗时操作(如数据库查询/更新)
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            stock -= num;
            System.out.printf("商品%s扣减库存%d,剩余库存:%d%n", productId, num, stock);
        } else {
            throw new RuntimeException("商品" + productId + "库存不足");
        }
    }

    // 获取库存(测试用)
    public int getStock() {
        return stock;
    }
}
步骤 4:测试并发效果
复制代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class LockTest {
    public static void main(String[] args) {
        // 初始化Spring容器(扫描注解所在包)
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext("com.example");
        StockService stockService = context.getBean(StockService.class);

        // 模拟10个线程并发扣减同一商品(productId=1)的库存(每个扣10)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> stockService.deductStock(1L, 10)).start();
        }
    }
}

测试结果(无并发问题,库存最终为 0):

复制代码
商品1扣减库存10,剩余库存:90
商品1扣减库存10,剩余库存:80
商品1扣减库存10,剩余库存:70
...
商品1扣减库存10,剩余库存:0

三、场景 2:分布式下的 @Lock(分布式锁)

如果应用部署在多台服务器(多实例),本地锁会失效,需要将 @Lock 注解适配为分布式锁(基于 Redis/Redisson)。

步骤 1:引入 Redisson 依赖(Maven)

xml

复制代码
<!-- Redisson:Redis分布式锁的最佳实践 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>
步骤 2:修改 @Lock 切面为分布式锁实现
复制代码
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.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Lock注解的切面实现(分布式锁)
 */
@Aspect
@Component
public class DistributedLockAspect {

    @Autowired
    private RedissonClient redissonClient; // 注入Redisson客户端(SpringBoot自动配置)

    private final ExpressionParser spelParser = new SpelExpressionParser();
    private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    @Around("@annotation(lock)")
    public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
        // 1. 解析锁标识
        String lockKey = resolveLockKey(joinPoint, lock);

        // 2. 获取分布式锁
        RLock redissonLock = redissonClient.getLock(lockKey);

        // 3. 加锁
        boolean locked = false;
        try {
            locked = redissonLock.tryLock(
                    lock.waitTime(),
                    lock.leaseTime(),
                    lock.timeUnit()
            );
            if (!locked) {
                throw new RuntimeException("获取分布式锁失败,请稍后重试");
            }

            // 4. 执行业务方法
            return joinPoint.proceed();
        } finally {
            // 5. 释放锁(仅当前线程持有锁时释放)
            if (locked && redissonLock.isHeldByCurrentThread()) {
                redissonLock.unlock();
            }
        }
    }

    // 复用之前的resolveLockKey方法(解析SpEL)
    private String resolveLockKey(ProceedingJoinPoint joinPoint, Lock lock) {
        if (lock.key().isEmpty()) {
            return joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName();
        }
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], args[i]);
        }
        return spelParser.parseExpression(lock.key()).getValue(context, String.class);
    }
}
步骤 3:分布式场景下使用 @Lock

用法和本地锁完全一致,无需修改业务代码:

复制代码
@Service
public class OrderService {
    /**
     * 提交订单(分布式并发场景)
     * @param orderId 订单ID(按订单ID加锁,不同订单不互斥)
     */
    @Lock(key = "'order:' + #orderId", leaseTime = 10) // 锁过期时间10秒
    public void submitOrder(String orderId) {
        // 分布式并发操作:扣减库存、生成订单记录
        System.out.println("订单" + orderId + "提交成功");
    }
}

四、关键注意事项

  1. 锁粒度要合理
    • 避免使用全局锁(如固定 key),否则会导致所有请求串行,降低并发效率;
    • 建议按业务维度加锁(如商品 ID、订单 ID),仅让同一业务维度的请求互斥。
  2. 防止死锁
    • 必须在 finally 块中释放锁,确保无论方法是否异常,锁都会释放;
    • 分布式锁必须设置 leaseTime(自动过期时间),防止服务宕机导致锁无法释放。
  3. SpEL 表达式规范
    • 字符串常量需要用单引号包裹(如 'stock:' + #productId),否则会解析失败。

总结

  1. @Lock 注解是通过 AOP 切面 + 锁机制 实现的 "语法糖",核心是在方法执行前后加锁 / 释放锁,保证原子性;
  2. 单实例场景下,@Lock 适配 ReentrantLock(本地锁),多实例场景下适配 Redisson(分布式锁),业务代码无需修改;
  3. 使用 @Lock 时需注意锁粒度 (按业务参数加锁)和防死锁(finally 释放、设置过期时间),这是解决并发问题的关键。
相关推荐
XiaoLeisj2 小时前
Android 权限管理实战:运行时申请、ActivityResultLauncher 与设置页授权
android·java·权限
FreeFly辉2 小时前
VScode搭建javaDemo
java·vscode
@小匠2 小时前
Spring-Gateway-理论知识总结/常问面试题
数据库·spring·gateway
知我Deja_Vu2 小时前
【避坑指南】ConcurrentHashMap 并发操作的致命陷阱
java·开发语言
未来之窗软件服务2 小时前
自己写算法(十)js加密UUID保护解密——东方仙盟化神期
java·javascript·算法·代码加密·东方仙盟算法
lang201509282 小时前
08 ByteBuddy 加载策略全解析:从“隔离”到“注入”,如何避开循环依赖的深坑?
java·byte buddy
沙漏无语2 小时前
(一)TiDB简介
java·开发语言·tidb
Chan162 小时前
LeetCode 热题 100 | 链表
java·数据结构·spring boot·算法·leetcode·链表·java-ee
weixin_704266052 小时前
[特殊字符] Spring IOC/DI 核心知识点 CSDN 风格总结
java·后端·spring