在我們開發過程中,很多業務場景下都需要新增鎖,尤其是在分散式系統中,確保資料一致性和防止併發問題至關重要。其中,Redis 作為一個高效能的鍵值儲存,常常被用來實現分散式鎖。使用 Redisson 這個開源庫,可以非常方便地在我們的 Java 應用中實現分散式鎖。
接下來,我將展示如何透過自定義註解來實現這一功能,讓我們可以在業務邏輯中以更簡潔的方式使用分散式鎖
使用的技術選型:
SpringBoot、EL表示式、Redisson。
可能有的小夥伴對EL表示式不太熟悉,我簡單介紹下,具體的大家可以到網上查閱
EL 表示式在 Spring 中用於動態獲取和操作物件的屬性,通常用於 JSP 和 Thymeleaf 等檢視模板引擎中。它的特點包括:
簡潔性:使用簡潔的語法來訪問物件屬性和方法。
動態性:允許在執行時動態評估表示式。
安全性:透過表示式語言提供的安全特性,可以避免直接暴露 Java 物件。
簡單來說,就是我可以透過符合某種規則的表示式,輕鬆的實現在對Java物件屬性的訪問
設計思路及依賴
我們採用SpingBoot的AOP技術對指定的註解進行攔截,使用EL表示式實現動態獲取方法引數,基於Redisson 鎖的相關操作
流程圖如下
依賴如下:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.26.1</version> </dependency>
註解定義
之後我們可以直接在方法上新增這個註解,就能實現分散式鎖的功能了
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { String prefix() default ""; // 使用 SpEL 表示式動態生成字首 String key(); // 使用 SpEL 表示式從方法引數中獲取 long leaseTime() default 10; // 鎖的持有時間 TimeUnit timeUnit() default TimeUnit.SECONDS; // 持有時間單位 long waitTime();// 鎖的等待時間 }
在我們加鎖的過程中,經常使用以下幾個引數
leaseTime 鎖的持有時間
waitTime 鎖的等待時間
timeUnit 時間單位
key 鎖的key是什麼
prefix key的字首
切面
基於Redisson
的分散式鎖實現
@Aspect @Component @Slf4j @ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = true) public class RedisLockAspect { @Autowired private RedissonClient redissonClient; @Around("@annotation(distributedLock)") public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable { // 獲取 SpEL 表示式的 key 值 // 解析 prefix 和 key String prefix = ELUtils.parseExpression(distributedLock.prefix(), joinPoint); String key = ELUtils.parseExpression(distributedLock.key(), joinPoint); String lockKey = prefix + key; // 組合成完整的鎖 key long leaseTime = distributedLock.leaseTime(); TimeUnit timeUnit = distributedLock.timeUnit(); RLock lock = redissonClient.getLock(lockKey); log.info("start lock {}", lockKey); try { // 嘗試獲取鎖 if (lock.tryLock(distributedLock.waitTime(), leaseTime, timeUnit)) { // 獲取到鎖,執行方法 return joinPoint.proceed(); } else { // 未獲取到鎖,丟擲異常或自定義處理邏輯 throw new RuntimeException("Unable to acquire lock for key: " + key); } } finally { if (lock.isHeldByCurrentThread()) { log.info("end lock {}", lockKey); // 只有是當前執行緒纔去釋放鎖 lock.unlock(); } } } }
這裏,我們透過EL表示式實現了在註解中動態獲取請求引數的功能。
基於ReentrantLock
單機版本
有的同學們的專案可能是單機部署,在這我也給出了,基於
ReentrantLock
的單機版本鎖實現
@Aspect @Component @Slf4j @ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = true) public class RedisLockAspect { @Autowired private RedissonClient redissonClient; @Around("@annotation(distributedLock)") public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable { // 獲取 SpEL 表示式的 key 值 // 解析 prefix 和 key String prefix = ELUtils.parseExpression(distributedLock.prefix(), joinPoint); String key = ELUtils.parseExpression(distributedLock.key(), joinPoint); String lockKey = prefix + key; // 組合成完整的鎖 key long leaseTime = distributedLock.leaseTime(); TimeUnit timeUnit = distributedLock.timeUnit(); RLock lock = redissonClient.getLock(lockKey); log.info("start lock {}", lockKey); try { // 嘗試獲取鎖 if (lock.tryLock(distributedLock.waitTime(), leaseTime, timeUnit)) { // 獲取到鎖,執行方法 return joinPoint.proceed(); } else { // 未獲取到鎖,丟擲異常或自定義處理邏輯 throw new RuntimeException("Unable to acquire lock for key: " + key); } } finally { if (lock.isHeldByCurrentThread()) { log.info("end lock {}", lockKey); // 只有是當前執行緒纔去釋放鎖 lock.unlock(); } } } }
工具類
public class ELUtils { private static final ExpressionParser parser = new SpelExpressionParser(); public static String parseExpression(String expression, ProceedingJoinPoint joinPoint) { if (expression.contains("#") || expression.contains("T(")) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 建立解析上下文並設定方法引數 StandardEvaluationContext context = new StandardEvaluationContext(); Object[] args = joinPoint.getArgs(); String[] paramNames = signature.getParameterNames(); for (int i = 0; i < paramNames.length; i++) { context.setVariable(paramNames[i], args[i]); } // 解析 SpEL 表示式 return parser.parseExpression(expression).getValue(context, String.class); } else { // 如果是普通字串,直接返回 return expression; } } }
使用
我們可以設定鎖的key字首是 login_lock:,key 是 請求引數中的 phone 的值,等待時間是0s,鎖的持續時間是10s。
@DistributedLock(prefix = "login_lock:",key = "#loginRequest.phone",waitTime = 0,leaseTime = 10) public BaseResponse userLogin(@RequestBody LoginRequest loginRequest) {}