Spring中使用自定義@Lock 注解解決線程并發(fā)的核心思路
在 Spring 生態(tài)中,并沒有原生的 @Lock 注解,但業(yè)界常用的做法是自定義 @Lock 注解 + AOP 切面 來封裝鎖邏輯(支持本地鎖 / 分布式鎖),讓代碼更簡潔、可復用。下面我會完整講解如何定義和使用 @Lock 注解解決線程并發(fā)問題,包括本地鎖版(單實例)和分布式鎖版(多實例)兩種核心場景。
一、核心思路
@Lock 注解的本質(zhì)是通過 AOP 攔截標注了該注解的方法,在方法執(zhí)行前加鎖、執(zhí)行后釋放鎖,從而保證方法執(zhí)行的原子性,解決多線程競爭共享資源的問題。
二、場景 1:單實例下的 @Lock(本地鎖)
適用于單服務器部署的應用,使用 ReentrantLock(可重入鎖)實現(xiàn)。
步驟 1:定義 @Lock 注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 自定義鎖注解(本地鎖)
* 用于標記需要加鎖的方法,解決單實例并發(fā)問題
*/
@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:實現(xiàn) @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注解的切面實現(xiàn)(本地鎖)
*/
@Aspect // 標記為AOP切面
@Component // 交給Spring容器管理
public class LocalLockAspect {
// 緩存鎖實例:key=鎖標識,value=ReentrantLock對象(ConcurrentHashMap保證線程安全)
private final Map<String, Lock> lockCache = new ConcurrentHashMap<>();
// SpEL表達式解析器(解析注解中的key參數(shù))
private final ExpressionParser spelParser = new SpelExpressionParser();
// 參數(shù)名解析器(獲取方法參數(shù)名,用于SpEL解析)
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 環(huán)繞通知:攔截所有標注了@Lock的方法
*/
@Around("@annotation(lock)")
public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
// 1. 解析鎖的唯一標識(key)
String lockKey = resolveLockKey(joinPoint, lock);
// 2. 獲取或創(chuàng)建鎖(不存在則新建,存在則復用)
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. 執(zhí)行原方法(核心業(yè)務邏輯)
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 注解解決并發(fā)問題
以 “庫存扣減” 這個典型并發(fā)場景為例:
import org.springframework.stereotype.Service;
@Service
public class StockService {
// 共享資源:模擬庫存(多線程競爭的核心)
private int stock = 100;
/**
* 扣減庫存(并發(fā)場景)
* @param productId 商品ID(按商品ID加鎖,不同商品不互斥,提高并發(fā)效率)
* @param num 扣減數(shù)量
*/
@Lock(key = "'stock:' + #productId") // SpEL表達式:鎖標識為 "stock:商品ID"
public void deductStock(Long productId, int num) {
if (stock >= num) {
// 模擬耗時操作(如數(shù)據(jù)庫查詢/更新)
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:測試并發(fā)效果
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個線程并發(fā)扣減同一商品(productId=1)的庫存(每個扣10)
for (int i = 0; i < 10; i++) {
new Thread(() -> stockService.deductStock(1L, 10)).start();
}
}
}測試結(jié)果(無并發(fā)問題,庫存最終為 0):
商品1扣減庫存10,剩余庫存:90
商品1扣減庫存10,剩余庫存:80
商品1扣減庫存10,剩余庫存:70
...
商品1扣減庫存10,剩余庫存:0
三、場景 2:分布式下的 @Lock(分布式鎖)
如果應用部署在多臺服務器(多實例),本地鎖會失效,需要將 @Lock 注解適配為分布式鎖(基于 Redis/Redisson)。
步驟 1:引入 Redisson 依賴(Maven)
<!-- Redisson:Redis分布式鎖的最佳實踐 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>步驟 2:修改 @Lock 切面為分布式鎖實現(xiàn)
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注解的切面實現(xiàn)(分布式鎖)
*/
@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. 執(zhí)行業(yè)務方法
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
用法和本地鎖完全一致,無需修改業(yè)務代碼:
@Service
public class OrderService {
/**
* 提交訂單(分布式并發(fā)場景)
* @param orderId 訂單ID(按訂單ID加鎖,不同訂單不互斥)
*/
@Lock(key = "'order:' + #orderId", leaseTime = 10) // 鎖過期時間10秒
public void submitOrder(String orderId) {
// 分布式并發(fā)操作:扣減庫存、生成訂單記錄
System.out.println("訂單" + orderId + "提交成功");
}
}四、關鍵注意事項
- 鎖粒度要合理:
- 避免使用全局鎖(如固定 key),否則會導致所有請求串行,降低并發(fā)效率;
- 建議按業(yè)務維度加鎖(如商品 ID、訂單 ID),僅讓同一業(yè)務維度的請求互斥。
- 防止死鎖:
- 必須在
finally塊中釋放鎖,確保無論方法是否異常,鎖都會釋放; - 分布式鎖必須設置
leaseTime(自動過期時間),防止服務宕機導致鎖無法釋放。
- 必須在
- SpEL 表達式規(guī)范:
- 字符串常量需要用單引號包裹(如
'stock:' + #productId),否則會解析失敗。
- 字符串常量需要用單引號包裹(如
總結(jié)
@Lock注解是通過 AOP 切面 + 鎖機制 實現(xiàn)的 “語法糖”,核心是在方法執(zhí)行前后加鎖 / 釋放鎖,保證原子性;- 單實例場景下,
@Lock適配ReentrantLock(本地鎖),多實例場景下適配 Redisson(分布式鎖),業(yè)務代碼無需修改; - 使用
@Lock時需注意鎖粒度(按業(yè)務參數(shù)加鎖)和防死鎖(finally 釋放、設置過期時間),這是解決并發(fā)問題的關鍵。
到此這篇關于Spring中使用自定義@Lock 注解解決線程并發(fā)的核心思路的文章就介紹到這了,更多相關Spring使用自定義@Lock 注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Spring的@Autowired加到接口上但獲取的是實現(xiàn)類的問題
這篇文章主要介紹了Spring的@Autowired加到接口上但獲取的是實現(xiàn)類的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
springboot訪問template下的html頁面的實現(xiàn)配置
這篇文章主要介紹了springboot訪問template下的html頁面的實現(xiàn)配置,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12
SpringBoot集成elasticsearch使用圖文詳解
Spring Boot集成Elasticsearch其實非常簡單,這篇文章主要給大家介紹了關于SpringBoot集成elasticsearch使用的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-04-04

