Java使用Redis實現分布式鎖


1、概述

此處使用Redis的setNx命令和expire命令和del命令來實現分布式鎖。

首先我們要知道, 我們的redis執行命令是隊列方式的,並不存在多個命令同時運行,所有命令都是串行的訪問。那么這就說明我們多個客戶端連接Redis的時候不存在其並發的問題。

其實實現分布式鎖並不僅僅可以使用Redis完成,也可以使用其他的方式來完成,最主要的目的就是有一個地方能作為鎖狀態,然后通過這個鎖的狀態來實現代碼中的功能。只要我們這個鎖操作的時候是是串行的,那么就能實現分布式鎖。

其實有一個問題,為什么我們不使用Java中的synchronized而要去搞一個分布式鎖呢?其實就是因為現在都是分布式環境,而Java內置的synchronized是針對單個Java進程的鎖,而分布式環境下有n個Java進程,而分布式鎖實現的多個Java進程之間的鎖。

那么為什么我們要使用setNx命令,而不使用其他命令呢?例如get命令,這種當我們獲取到key以后,可能已經是臟數據了,而我們的setNx的意思是,我們設置一個key,如果此key已經存在,那么則返回0,不存在則返回1並設置成功,我們就可以利用這個方式來實現所謂的分布式鎖。

注意,分布式鎖實現最重要的地方就是有一個步驟能做到串行且不會臟數據。

廢話不多說直接上現成的方法。

2、代碼

/**
 * Redis 鎖工具類
 *
 * @author dh
 * @date 20211028103258
 **/
@Component
public class RedisLockHelper {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 獲取鎖
     * @param key       鎖key
     * @param seconds   最大鎖時間
     * @return true:成功,false:失敗
     */
    public boolean lock(String key,Long seconds){
        return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
            /** 如果不存在,那么則true,則允許執行, */
            Boolean acquire = connection.setNX(key.getBytes(), String.valueOf(key).getBytes());
            /** 防止死鎖,將其key設置過期時間 */
            connection.expire(key.getBytes(), seconds);
            if (acquire) {
                return true;
            }
            return false;
        });
    }

    /**
     * 刪除鎖
     * @param key
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

}

3、案例

如果理解力強的朋友拿到這個方法就很快的能實現業務中的功能,我們這里給一個防止重復提交的實現案例。

防重復提交注解RepeatSubmitIntercept


/**
 * 重復提交攔截注解
 * @author dh
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmitIntercept {

    /**
     * 最大阻擋時間,默認5s
     */
    long maxTime() default 5L;

    /**
     * 重復提交時返回msg
     */
    String errorTitle() default "當前操作重復!";

    /**
     * 攔截方式:
     *  1、如果為0:那么則根據當前用戶攔截,那么當前方法該用戶在上次請求完成前內只能訪問一次.
     *  2、如果為1:那么則根據當前指定參數名進行攔截,那么當前方法該用戶同一參數在上次請求完成前只能訪問一次.
     */
    int type() default 0;

    /**
     * 攔截方式:
     *  如果攔截方式為0,那么根據請求頭來判斷用戶
     */
    String userHead() default CacheConstants.AUTHORIZATION_HEADER;

    /**
     * 如果攔截方式為1時,指定的參數名稱集合
     * @return
     */
    String []parameters() default {};

    /**
     * redis中key前綴,一般不需要修改此
     */
    String redis_lock_prefix() default "super_bridal_repeat_submit_lock_prefix_";

    /**
     * 當該方法處於被攔截狀態時,重復嘗試次數,0則不嘗試
     * @return
     */
    int rewaitCount() default 0;
}

aop

/**
     * 防重復提交的注解
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("@annotation(包名.........RepeatSubmitIntercept)")
    public Object noRepeatSubmitAround(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = ServletUtils.getRequest();
        String uriStringBase64 = Base64.getEncoder().encodeToString(request.getRequestURI().getBytes());
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        RepeatSubmitIntercept repeatSubmitIntercept = method.getAnnotation(RepeatSubmitIntercept.class);
        if (repeatSubmitIntercept.maxTime() < 1L) {
            throw new RepeatSubmitInterceptException("重復提交攔截器報錯--設置最大阻擋時間錯誤,至少大於1s", 500);
        }
        if (StringUtils.isBlank(repeatSubmitIntercept.errorTitle())) {
            throw new RepeatSubmitInterceptException("重復提交攔截器報錯--錯誤信息提醒請勿設置為空/空串", 500);
        }
        if (StringUtils.isBlank(repeatSubmitIntercept.redis_lock_prefix())) {
            throw new RepeatSubmitInterceptException("重復提交攔截器報錯--前綴Key不能為空/空串", 500);
        }
        String token = Convert.toStr(ServletUtils.getRequest().getHeader(repeatSubmitIntercept.userHead()));
        StringBuilder key = new StringBuilder()
                .append(repeatSubmitIntercept.redis_lock_prefix())
                .append(token)
                .append("/")
                .append(uriStringBase64);
        if (StringUtils.isEmpty(token)) {
            throw new RepeatSubmitInterceptException("重復提交攔截器報錯--當前攔截方式為[用戶攔截],但其請求頭中token為空!", 500);
        }
        /** 用戶攔截的方式 */
        if (repeatSubmitIntercept.type() == 0) {
            /** 此處應該使用請求頭中token作為key,那么此處不做其他操作. */
        } else if (repeatSubmitIntercept.type() == 1) {
            /** 從請求參數中獲取key */
            // ...................省略
        } else {
            throw new RepeatSubmitInterceptException("重復提交攔截器報錯--當前攔截方式為未設置!", 500);
        }
        if (redisLockHelper.lock(key.toString(), repeatSubmitIntercept.maxTime())) {
            return execute(key.toString(), point);
        } else {
            /**
             * 1、判斷允許重復等待
             * 2、重復等待操作
             * */
            if (repeatSubmitIntercept.rewaitCount() > 0) {
                int i = 0;
                while (i < repeatSubmitIntercept.rewaitCount()) {
                    /** 暫停100ms再去拿 */
                    Thread.sleep(100);
                    i++;
                    if (redisLockHelper.lock(key.toString(), repeatSubmitIntercept.maxTime())) {
                        return execute(key.toString(), point);
                    }
                }
            }
        }
        throw new RepeatSubmitInterceptException(repeatSubmitIntercept.errorTitle(), 500);
    }

注意這里的RepeatSubmitInterceptException是自定義的異常。

使用的地方

@GetMapping("/test1")
@RepeatSubmitIntercept()
public AjaxResult test1(){
    System.out.println("進入了請求:" + System.currentTimeMillis());
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return AjaxResult.success();
}

該實現中如有問題歡迎留言。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM