天天看點

redis 分布式鎖_細品redis分布式鎖

背景

已經寫了兩節的redis的高性能資料結構了點選檢視,今天換個口味,今天我們看一下redis在分布式系統中的應用,使用redis做分布式鎖,這可以說是老生常談的問題了。

redis分布式鎖

分布式鎖解決的問題

說到鎖,第一反應就是線程阻塞,在這裡需要注意的是這裡的次元會上升一個層次,不單單是一個服務(程序)的線程之間,是多個服務之間的并發安全問題,也可以這麼說吧多個程序(這兩個程序之間是分别在兩個服務上的)之間的并發問題。是以說這裡使用線程之間的鎖是不能解決問題的如(reentrantLock,Sychronized,CyclicBarrier,CountDownlotch,Semaphore,volatile)這些JUC包和JDK提供的鎖機制。他們隻能處理同一個程序不同線程之間的并發問題。是以為了解決不同程序,不同server時間的并發安全問題就創造出了redis分布式鎖。這裡說這麼多主要還是區分一下分布式鎖和線程鎖(這樣稱呼不知道是否合适,或者說是對象鎖)的差別。

根據圖了解一下。兩個client操作資料庫同一條資料進行修改。

redis 分布式鎖_細品redis分布式鎖

實作

  1. 分布式鎖的本質就是,不同服務間或同一個服務間的線程在redis裡面争搶坑位,當一個線程占用了這個坑位,門一鎖,那麼其他線程就得放棄或者重試等待了。當一個線程占坑結束使用del指令釋放坑位,也就是廁所的門是打開狀态。實際使用方法是,給這個坑位設定一個name,這個那麼有值說明就被占用

    占坑一般是使用 setnx(set if not exists) 指令,隻允許被一個用戶端占坑。先來先占, 用

    完了,再調用 del 指令釋放茅坑。

// 這裡的冒号:就是一個普通的字元,沒特别含義,它可以是任意其它字元,不要誤解
> setnx lock:codehole true
OK
... do something critical ...
> del lock:codehole
(integer) 1
           
  1. 但是上面的方案是有問題的,就是當這個線程執行異常了,導緻實行del釋放坑位指令沒有生效那就有問題了。但是有人會說我在java中使用finally塊裡進行釋放坑位。這确實也是一種辦法,但是如果這個使用的服務直接挂掉了怎麼辦呢?
//僞代碼
try{
            
        }catch (Exception e){
            e.printStackTrace();
        }finally {
        //就算異常了也會執行,删除操作
            delKey;
        }
    }
           
  1. 那就是給這個鎖(坑位)來個時間限定,你到時間就得自動進行釋放。那麼實作方式是先擷取鎖,然後再設定定時時間:
> setnx lock:codehole true
OK
> expire lock:codehole 5
... do something critical ...
> del lock:codehole
(integer) 1
           
  1. 由于上面所說的這個方式不是操作過程不是原子性(要全部執行,要麼全部不執行。不會出現執行一半)的是以也會出現拿到鎖後。然後出現這隻鎖逾時時間異常或者服務挂掉的問題。這樣也就回到了最初的問題了。
  2. 那我們想想這樣有什麼好的辦法處理呢? 說到原子性,這不就是事物的四大特性之一嗎。那我們就使用redis的事物來處理這個問題呗。将指令 setnx 和 expire放在同一個事物裡面進行執行。 就在redis 2.8的時候處理了這個問題,那就是這兩個指令是可以一塊執行。這也是我們使用分布式鎖的奧義所在。
> set lock:codehole true ex 5 nx OK ... do something critical ... 
> del lock:codehole
           
  1. 那由于線程執行時間過長鎖逾時了怎麼辦呢?

分布式鎖過期

  1. Redis 的分布式鎖不能解決逾時問題,如果在加鎖和釋放鎖之間的邏輯執行的太長,以至

    于超出了鎖的逾時限制,就會出現問題。因為這時候鎖過期了,第二個線程重新持有了這把鎖,

    但是緊接着第一個線程執行完了業務邏輯,就把鎖給釋放了,第三個線程就會在第二個線程邏

    輯執行完之間拿到了鎖。

  2. 首先這種問題我們得規避,産生的原因就是業務邏輯執行時間太長,那麼就要在使用的時候盡量用于時間短的,盡量避業務存在誇服務調用,還有就是設定盡量合理的過期時間盡量大與業務執行時間。
  3. 還有一個更加安全的方案就是給每一個鎖都放一個值,在進行del的時候我們可以先對比一下是不是之前放的value。這帶點樂觀鎖的意思了。compare and swap 對比後替換。但是比對和key删除不是一個原子性操作,操心。還好redis提供一個腳本執行方式,且腳本是具有原子性的,使用lua腳本達到原子性的目标。(在使用阿裡雲的redis sharding模式得注意一下。他們sharding模式對lua腳本的支援有問題。我踩的坑。使用redission的RmapCache時其源碼有lua腳本導緻最後運作失敗)
# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
           

可重入性

  1. 可重入性指的是在持有鎖的情況下再次請求加鎖,仍然可以 再次請求加鎖,那麼這個鎖就是可重入的。例如java中的reentrantLock 和 sychronized都是可重入鎖。
  2. 他們實作的大概原理是通過樂觀鎖去修改他們的鎖狀态,如果是同一個線程id那就會更改成功,可以擷取到鎖,如果不是同一個線程ID那就回鎖更新。那我們使用redis如何實作呢?同一個道理呗。我們可以把這個線程ID 或者是這個任務ID,可以是線程次元,也可以是任務次元。進行可重入。

中間件的使用

  1. jedis(在使用的時,在連接配接池回收有問題,應該是我自己的代碼寫的有問題,最後沒有解決)
  2. redission (更換了redssion後回收連接配接池的問題解決了,但是将原來的redis單機版升sharding模式時出現了lua腳本的問題)

上面是兩個redis的中間件用起來很舒服,括号裡面是我踩的坑。

真實案例分享

  • 需求

    微服務間調用,對外暴露接口,為防止接口短時間内被同一個任務ID重複調用(保證前一個任務執行完成)。防止同時操作一條資料。

  • 解決方案:使用redis分布式鎖,使用taskId作為鎖。設定過期時間為2s,等待時間為2秒。
  • 2s的意思是,業務執行最長時間為2s。執行不完就釋放。
  • 3s的意思是鎖逾時我等待的這個任務一定能執行。

    使用的是:redssion封裝好的:

@Override
    public Response discernCommon(DiscernCallBackResultTo discernCallBackResultTo) {
        //防止并發,通過同一個TSKID做鎖 重複新增發票,鎖過期時間3秒,等待時間為2秒
        boolean lock = RedissonLockUtils.tryLock(discernCallBackResultTo.getTaskId(), 2, 3);
        if (lock) {
            try {
                dealDiscernResult(discernCallBackResultTo);
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), Coder.of(ErrorCode.DISCERN_DEAL_EXCEPTION), e);
            } finally {
             RedissonLockUtils.unlock(discernCallBackResultTo.getTaskId());
            }
        } else {
            EventMessage confilct = new EventMessage(EventEnum.EVENT_CONFILC_TASKID.getEventId())
                    .addExtValue("taskId", discernCallBackResultTo.getTaskId());
            Tracker.getInstance().record(eventMessage);
        }
        return Response.ok("回調成功!");
    }
           

總結

  • redis分布式鎖的實作和演進
  • redis鎖過期導緻鎖釋放錯誤,通過加任務好的方式,先對比在删除
  • redis分布式鎖的可重入鎖的實作,通過樂觀鎖的方式,對其線程iD 或者任務ID進行可重入設計。

參考

  • 《redis 極度深寒》
  • http://ifeve.com/?x=34&y=9&s=%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81

繼續閱讀