Spring中使用自定義@Lock 注解解決線程并發(fā)的核心思路
在 Spring 生態(tài)中,并沒(méi)有原生的 @Lock 注解,但業(yè)界常用的做法是自定義 @Lock 注解 + AOP 切面 來(lái)封裝鎖邏輯(支持本地鎖 / 分布式鎖),讓代碼更簡(jiǎn)潔、可復(fù)用。下面我會(huì)完整講解如何定義和使用 @Lock 注解解決線程并發(fā)問(wèn)題,包括本地鎖版(單實(shí)例)和分布式鎖版(多實(shí)例)兩種核心場(chǎng)景。
一、核心思路
@Lock 注解的本質(zhì)是通過(guò) AOP 攔截標(biāo)注了該注解的方法,在方法執(zhí)行前加鎖、執(zhí)行后釋放鎖,從而保證方法執(zhí)行的原子性,解決多線程競(jìng)爭(zhēng)共享資源的問(wèn)題。
二、場(chǎng)景 1:?jiǎn)螌?shí)例下的 @Lock(本地鎖)
適用于單服務(wù)器部署的應(yīng)用,使用 ReentrantLock(可重入鎖)實(shí)現(xiàn)。
步驟 1:定義 @Lock 注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 自定義鎖注解(本地鎖)
* 用于標(biāo)記需要加鎖的方法,解決單實(shí)例并發(fā)問(wèn)題
*/
@Target(ElementType.METHOD) // 僅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 運(yùn)行時(shí)生效
@Documented
public @interface Lock {
/**
* 鎖的唯一標(biāo)識(shí)(支持SpEL表達(dá)式,如 "#userId")
* 為空時(shí)默認(rèn)使用「類名.方法名」作為鎖標(biāo)識(shí)
*/
String key() default "";
/**
* 加鎖超時(shí)時(shí)間(默認(rèn)5秒)
* 超時(shí)未獲取鎖則拋出異常,避免線程阻塞
*/
long waitTime() default 5;
/**
* 鎖的自動(dòng)釋放時(shí)間(默認(rèn)30秒)
* 防止死鎖(ReentrantLock本身不會(huì)死鎖,但超時(shí)釋放更安全)
*/
long leaseTime() default 30;
/**
* 時(shí)間單位(默認(rèn)秒)
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}步驟 2:實(shí)現(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注解的切面實(shí)現(xiàn)(本地鎖)
*/
@Aspect // 標(biāo)記為AOP切面
@Component // 交給Spring容器管理
public class LocalLockAspect {
// 緩存鎖實(shí)例:key=鎖標(biāo)識(shí),value=ReentrantLock對(duì)象(ConcurrentHashMap保證線程安全)
private final Map<String, Lock> lockCache = new ConcurrentHashMap<>();
// SpEL表達(dá)式解析器(解析注解中的key參數(shù))
private final ExpressionParser spelParser = new SpelExpressionParser();
// 參數(shù)名解析器(獲取方法參數(shù)名,用于SpEL解析)
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 環(huán)繞通知:攔截所有標(biāo)注了@Lock的方法
*/
@Around("@annotation(lock)")
public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
// 1. 解析鎖的唯一標(biāo)識(shí)(key)
String lockKey = resolveLockKey(joinPoint, lock);
// 2. 獲取或創(chuàng)建鎖(不存在則新建,存在則復(fù)用)
Lock reentrantLock = lockCache.computeIfAbsent(lockKey, k -> new ReentrantLock());
// 3. 加鎖(支持超時(shí)等待,避免線程無(wú)限阻塞)
boolean locked = false;
try {
locked = ((java.util.concurrent.locks.Lock) reentrantLock).tryLock(
lock.waitTime(),
lock.leaseTime(),
lock.timeUnit()
);
if (!locked) {
throw new RuntimeException("獲取鎖失敗,請(qǐng)稍后重試");
}
// 4. 執(zhí)行原方法(核心業(yè)務(wù)邏輯)
return joinPoint.proceed();
} finally {
// 5. 釋放鎖(僅當(dāng)前線程持有鎖時(shí)釋放,避免誤釋放)
if (locked && reentrantLock.isHeldByCurrentThread()) {
reentrantLock.unlock();
}
}
}
/**
* 解析@Lock注解中的key(支持SpEL表達(dá)式)
*/
private String resolveLockKey(ProceedingJoinPoint joinPoint, Lock lock) {
// 如果注解key為空,默認(rèn)使用「類名.方法名」作為鎖標(biāo)識(shí)
if (lock.key().isEmpty()) {
return joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName();
}
// 解析SpEL表達(dá)式(如 "#userId" 解析為實(shí)際的用戶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ā)問(wèn)題
以 “庫(kù)存扣減” 這個(gè)典型并發(fā)場(chǎng)景為例:
import org.springframework.stereotype.Service;
@Service
public class StockService {
// 共享資源:模擬庫(kù)存(多線程競(jìng)爭(zhēng)的核心)
private int stock = 100;
/**
* 扣減庫(kù)存(并發(fā)場(chǎng)景)
* @param productId 商品ID(按商品ID加鎖,不同商品不互斥,提高并發(fā)效率)
* @param num 扣減數(shù)量
*/
@Lock(key = "'stock:' + #productId") // SpEL表達(dá)式:鎖標(biāo)識(shí)為 "stock:商品ID"
public void deductStock(Long productId, int num) {
if (stock >= num) {
// 模擬耗時(shí)操作(如數(shù)據(jù)庫(kù)查詢/更新)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
stock -= num;
System.out.printf("商品%s扣減庫(kù)存%d,剩余庫(kù)存:%d%n", productId, num, stock);
} else {
throw new RuntimeException("商品" + productId + "庫(kù)存不足");
}
}
// 獲取庫(kù)存(測(cè)試用)
public int getStock() {
return stock;
}
}步驟 4:測(cè)試并發(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個(gè)線程并發(fā)扣減同一商品(productId=1)的庫(kù)存(每個(gè)扣10)
for (int i = 0; i < 10; i++) {
new Thread(() -> stockService.deductStock(1L, 10)).start();
}
}
}測(cè)試結(jié)果(無(wú)并發(fā)問(wèn)題,庫(kù)存最終為 0):
商品1扣減庫(kù)存10,剩余庫(kù)存:90
商品1扣減庫(kù)存10,剩余庫(kù)存:80
商品1扣減庫(kù)存10,剩余庫(kù)存:70
...
商品1扣減庫(kù)存10,剩余庫(kù)存:0
三、場(chǎng)景 2:分布式下的 @Lock(分布式鎖)
如果應(yīng)用部署在多臺(tái)服務(wù)器(多實(shí)例),本地鎖會(huì)失效,需要將 @Lock 注解適配為分布式鎖(基于 Redis/Redisson)。
步驟 1:引入 Redisson 依賴(Maven)
<!-- Redisson:Redis分布式鎖的最佳實(shí)踐 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>步驟 2:修改 @Lock 切面為分布式鎖實(shí)現(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注解的切面實(shí)現(xiàn)(分布式鎖)
*/
@Aspect
@Component
public class DistributedLockAspect {
@Autowired
private RedissonClient redissonClient; // 注入Redisson客戶端(SpringBoot自動(dòng)配置)
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. 解析鎖標(biāo)識(shí)
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("獲取分布式鎖失敗,請(qǐng)稍后重試");
}
// 4. 執(zhí)行業(yè)務(wù)方法
return joinPoint.proceed();
} finally {
// 5. 釋放鎖(僅當(dāng)前線程持有鎖時(shí)釋放)
if (locked && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
// 復(fù)用之前的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:分布式場(chǎng)景下使用 @Lock
用法和本地鎖完全一致,無(wú)需修改業(yè)務(wù)代碼:
@Service
public class OrderService {
/**
* 提交訂單(分布式并發(fā)場(chǎng)景)
* @param orderId 訂單ID(按訂單ID加鎖,不同訂單不互斥)
*/
@Lock(key = "'order:' + #orderId", leaseTime = 10) // 鎖過(guò)期時(shí)間10秒
public void submitOrder(String orderId) {
// 分布式并發(fā)操作:扣減庫(kù)存、生成訂單記錄
System.out.println("訂單" + orderId + "提交成功");
}
}四、關(guān)鍵注意事項(xiàng)
- 鎖粒度要合理:
- 避免使用全局鎖(如固定 key),否則會(huì)導(dǎo)致所有請(qǐng)求串行,降低并發(fā)效率;
- 建議按業(yè)務(wù)維度加鎖(如商品 ID、訂單 ID),僅讓同一業(yè)務(wù)維度的請(qǐng)求互斥。
- 防止死鎖:
- 必須在
finally塊中釋放鎖,確保無(wú)論方法是否異常,鎖都會(huì)釋放; - 分布式鎖必須設(shè)置
leaseTime(自動(dòng)過(guò)期時(shí)間),防止服務(wù)宕機(jī)導(dǎo)致鎖無(wú)法釋放。
- 必須在
- SpEL 表達(dá)式規(guī)范:
- 字符串常量需要用單引號(hào)包裹(如
'stock:' + #productId),否則會(huì)解析失敗。
- 字符串常量需要用單引號(hào)包裹(如
總結(jié)
@Lock注解是通過(guò) AOP 切面 + 鎖機(jī)制 實(shí)現(xiàn)的 “語(yǔ)法糖”,核心是在方法執(zhí)行前后加鎖 / 釋放鎖,保證原子性;- 單實(shí)例場(chǎng)景下,
@Lock適配ReentrantLock(本地鎖),多實(shí)例場(chǎng)景下適配 Redisson(分布式鎖),業(yè)務(wù)代碼無(wú)需修改; - 使用
@Lock時(shí)需注意鎖粒度(按業(yè)務(wù)參數(shù)加鎖)和防死鎖(finally 釋放、設(shè)置過(guò)期時(shí)間),這是解決并發(fā)問(wèn)題的關(guān)鍵。
到此這篇關(guān)于Spring中使用自定義@Lock 注解解決線程并發(fā)的核心思路的文章就介紹到這了,更多相關(guān)Spring使用自定義@Lock 注解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)商城訂單超時(shí)取消功能
大多數(shù)的B2C商城項(xiàng)目都會(huì)有限時(shí)活動(dòng),當(dāng)用戶下單后都會(huì)有支付超時(shí)時(shí)間,當(dāng)訂單超時(shí)后訂單的狀態(tài)就會(huì)自動(dòng)變成已取消 ,這個(gè)功能的實(shí)現(xiàn)有很多種方法,本文的實(shí)現(xiàn)方法適合大多數(shù)比較小的商城使用。具體實(shí)現(xiàn)方式可以跟隨小編一起看看吧2019-12-12
Spring的@Autowired加到接口上但獲取的是實(shí)現(xiàn)類的問(wèn)題
這篇文章主要介紹了Spring的@Autowired加到接口上但獲取的是實(shí)現(xiàn)類的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
springboot訪問(wèn)template下的html頁(yè)面的實(shí)現(xiàn)配置
這篇文章主要介紹了springboot訪問(wèn)template下的html頁(yè)面的實(shí)現(xiàn)配置,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
SpringBoot集成elasticsearch使用圖文詳解
Spring Boot集成Elasticsearch其實(shí)非常簡(jiǎn)單,這篇文章主要給大家介紹了關(guān)于SpringBoot集成elasticsearch使用的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04
java 實(shí)現(xiàn)文件夾的拷貝實(shí)例代碼
這篇文章主要介紹了java 實(shí)現(xiàn)文件夾的拷貝實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04

