在 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 + "提交成功");
}
}
四、关键注意事项
- 锁粒度要合理 :
- 避免使用全局锁(如固定 key),否则会导致所有请求串行,降低并发效率;
- 建议按业务维度加锁(如商品 ID、订单 ID),仅让同一业务维度的请求互斥。
- 防止死锁 :
- 必须在
finally块中释放锁,确保无论方法是否异常,锁都会释放; - 分布式锁必须设置
leaseTime(自动过期时间),防止服务宕机导致锁无法释放。
- 必须在
- SpEL 表达式规范 :
- 字符串常量需要用单引号包裹(如
'stock:' + #productId),否则会解析失败。
- 字符串常量需要用单引号包裹(如
总结
@Lock注解是通过 AOP 切面 + 锁机制 实现的 "语法糖",核心是在方法执行前后加锁 / 释放锁,保证原子性;- 单实例场景下,
@Lock适配ReentrantLock(本地锁),多实例场景下适配 Redisson(分布式锁),业务代码无需修改; - 使用
@Lock时需注意锁粒度 (按业务参数加锁)和防死锁(finally 释放、设置过期时间),这是解决并发问题的关键。