1. 程式人生 > 其它 >Java使用Redis實現分散式鎖

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();
}

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