切換語言為:簡體

SpringBoot專案中一個註解實現分散式鎖

  • 爱糖宝
  • 2024-10-30
  • 2042
  • 0
  • 0

在我們開發過程中,很多業務場景下都需要新增鎖,尤其是在分散式系統中,確保資料一致性和防止併發問題至關重要。其中,Redis 作為一個高效能的鍵值儲存,常常被用來實現分散式鎖。使用 Redisson 這個開源庫,可以非常方便地在我們的 Java 應用中實現分散式鎖。

接下來,我將展示如何透過自定義註解來實現這一功能,讓我們可以在業務邏輯中以更簡潔的方式使用分散式鎖

使用的技術選型:

  • SpringBoot、EL表示式、Redisson。

  • 可能有的小夥伴對EL表示式不太熟悉,我簡單介紹下,具體的大家可以到網上查閱

EL 表示式在 Spring 中用於動態獲取和操作物件的屬性,通常用於 JSP 和 Thymeleaf 等檢視模板引擎中。它的特點包括:

  • 簡潔性:使用簡潔的語法來訪問物件屬性和方法。

  • 動態性:允許在執行時動態評估表示式。

  • 安全性:透過表示式語言提供的安全特性,可以避免直接暴露 Java 物件。

簡單來說,就是我可以透過符合某種規則的表示式,輕鬆的實現在對Java物件屬性的訪問

設計思路及依賴

我們採用SpingBoot的AOP技術對指定的註解進行攔截,使用EL表示式實現動態獲取方法引數,基於Redisson 鎖的相關操作

  • 流程圖如下

SpringBoot專案中一個註解實現分散式鎖

  • 依賴如下:

 <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) {}


0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.