如何使用註解方式實現 Redis 分佈式鎖

引入 Redisson

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.14.1</version>
</dependency>

初始化 Redisson

@Configuration
public class RedissonConfiguration {
  
    // 此處更換自己的 Redis 地址即可
    @Value("${redis.addr}")
    private String addr;

    @Bean
    public RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress(String.format("%s%s", "redis://", addr))
                .setConnectionPoolSize(64)              // 連接池大小
                .setConnectionMinimumIdleSize(8)        // 保持最小連接數
                .setConnectTimeout(1500)                // 建立連接超時時間
                .setTimeout(2000)                       // 執行命令的超時時間, 從命令發送成功時開始計時
                .setRetryAttempts(2)                    // 命令執行失敗重試次數
                .setRetryInterval(1000);                // 命令重試發送時間間隔

        return Redisson.create(config);
    }
}

這樣我們就可以在項目裡面使用 Redisson 瞭。

編寫 Redisson 分佈式鎖工具類

Redis 分佈式鎖的工具類,主要是調用 Redisson 客戶端實現,做瞭輕微的封裝。

@Service
@Slf4j
public class LockManager {
    /**
     * 最小鎖等待時間
     */
    private static final int MIN_WAIT_TIME = 10;

    @Resource
    private RedissonClient redisson;

    /**
     * 加鎖,加鎖失敗拋默認異常 - 操作頻繁, 請稍後再試
     *
     * @param key        加鎖唯一key
     * @param expireTime 鎖超時時間 毫秒
     * @param waitTime   加鎖最長等待時間 毫秒
     * @return LockResult  加鎖結果
     */
    public LockResult lock(String key, long expireTime, long waitTime) {
        return lock(key, expireTime, waitTime, () -> new BizException(ResponseEnum.COMMON_FREQUENT_OPERATION_ERROR));
    }
    /**
     * 加鎖,加鎖失敗拋異常 - 自定義異常
     *
     * @param key               加鎖唯一key
     * @param expireTime        鎖超時時間 毫秒
     * @param waitTime          加鎖最長等待時間 毫秒
     * @param exceptionSupplier 加鎖失敗時拋該異常,傳null時加鎖失敗不拋異常
     * @return LockResult  加鎖結果
     */
    private LockResult lock(String key, long expireTime, long waitTime, Supplier<BizException> exceptionSupplier) {
        if (waitTime < MIN_WAIT_TIME) {
            waitTime = MIN_WAIT_TIME;
        }
        LockResult result = new LockResult();
        try {
            RLock rLock = redisson.getLock(key);
            try {
                if (rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS)) {
                    result.setLockResultStatus(LockResultStatus.SUCCESS);
                    result.setRLock(rLock);
                } else {
                    result.setLockResultStatus(LockResultStatus.FAILURE);
                }
            } catch (InterruptedException e) {
                log.error("Redis 獲取分佈式鎖失敗, key: {}, e: {}", key, e.getMessage());
                result.setLockResultStatus(LockResultStatus.EXCEPTION);
                rLock.unlock();
            }
        } catch (Exception e) {
            log.error("Redis 獲取分佈式鎖失敗, key: {}, e: {}", key, e.getMessage());
            result.setLockResultStatus(LockResultStatus.EXCEPTION);
        }

        if (exceptionSupplier != null && LockResultStatus.FAILURE.equals(result.getLockResultStatus())) {
            log.warn("Redis 加鎖失敗, key: {}", key);
            throw exceptionSupplier.get();
        }

        log.info("Redis 加鎖結果:{}, key: {}", result.getLockResultStatus(), key);

        return result;
    }
    /**
     * 解鎖
     */
    public void unlock(RLock rLock) {
        try {
            rLock.unlock();
        } catch (Exception e) {
            log.warn("Redis 解鎖失敗", e);
        }
    }
}

加鎖結果狀態枚舉類。

public enum LockResultStatus {
    /**
     * 通信正常,並且加鎖成功
     */
    SUCCESS,
    /**
     * 通信正常,但獲取鎖失敗
     */
    FAILURE,
    /**
     * 通信異常和內部異常,鎖狀態未知
     */
    EXCEPTION;
}

加鎖結果類封裝瞭加鎖狀態和RLock。

@Setter
@Getter
public class LockResult {

    private LockResultStatus lockResultStatus;

    private RLock rLock;
}

自此我們就可以使用分佈式鎖瞭,使用方式:

@Service
@Slf4j
public class TestService {

    @Resource
    private LockManager lockManager;

    public String test(String userId) {
        // 鎖:userId, 鎖超時時間:5s, 鎖等待時間:50ms
        LockResult lockResult = lockManager.lock(userId, 5000, 50);

        try {
            //  業務代碼
        } finally {
            lockManager.unlock(lockResult.getRLock());
        }

        return "";
    }
}

為瞭防止程序發生異常,所以每次我們都需要在finally代碼塊裡手動釋放鎖。為瞭更方便優雅的使用 Redis 分佈式鎖,我們使用註解方式實現下。

聲明註解 @Lock

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {
    /**
     * lock key
     */
    String value();

    /**
     * 鎖超時時間,默認5000ms
     */
    long expireTime() default 5000L;

    /**
     * 鎖等待時間,默認50ms
     */
    long waitTime() default 50L;

}

註解解析類

@Aspect
@Component
@Slf4j
public class LockAnnotationParser {

    @Resource
    private LockManager lockManager;

    /**
     * 定義切點
     */
    @Pointcut(value = "@annotation(Lock)")
    private void cutMethod() {
    }
		
    /**
     * 切點邏輯具體實現
     */
    @Around(value = "cutMethod() && @annotation(lock)")
    public Object parser(ProceedingJoinPoint point, Lock lock) throws Throwable {
        String value = lock.value();
        if (isEl(value)) {
            value = getByEl(value, point);
        }
        LockResult lockResult = lockManager.lock(getRealLockKey(value), lock.expireTime(), lock.waitTime());
        try {
            return point.proceed();
        } finally {
            lockManager.unlock(lockResult.getRLock());
        }
    }

    /**
     * 解析 SpEL 表達式並返回其值
     */
    private String getByEl(String el, ProceedingJoinPoint point) {
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        String[] paramNames = getParameterNames(method);
        Object[] arguments = point.getArgs();
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(el);
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < arguments.length; i++) {
            context.setVariable(paramNames[i], arguments[i]);
        }
        return expression.getValue(context, String.class);
    }
    /**
     * 獲取方法參數名列表
     */
    private String[] getParameterNames(Method method) {
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        return u.getParameterNames(method);
    }
    private boolean isEl(String str) {
        return str.contains("#");
    }
    /**
     * 鎖鍵值
     */
    private String getRealLockKey(String value) {
        return String.format("lock:%s", value);
    }
}

下面使用註解方式使用分佈式鎖:

@Service
@Slf4j
public class TestService {
    @Lock("'test_'+#user.userId")
    public String test(User user) {
        // 業務代碼
        return "";
    }
}

當然也可以自定義鎖的超時時間和等待時間

@Service
@Slf4j
public class TestService {
    @Lock(value = "'test_'+#user.userId", expireTime = 3000, waitTime = 30)
    public String test(User user) {
        // 業務代碼
        return "";
    }
}

到此這篇關於如何使用註解方式實現 Redis 分佈式鎖的文章就介紹到這瞭,更多相關Redis 分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: