當前位置: 妍妍網 > 碼農

手動擼一個 Redis 分布式鎖

2024-03-10碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

對於使用 Java 的小夥伴,其實我們完全不用手動擼一個分布式鎖,直接使用 Redisson 就行。

但是因為這些封裝好的組建,讓我們越來越懶。

我們使用一些封裝好的開源組建時,可以了解其中的原理,或者自己動手寫一個,可以更好提升你的技術水平。

今天我就教大家用原生的 Redis,手動擼一個 Redis 分布式鎖,很有意思。


01 問題引入

其實透過 Redis 實作分布式鎖,經常會有面試官會問,很多同學都知道用 SetNx() 去獲取鎖,解決並行問題。

SetNx() 是什麽?我簡單解答一下。

Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在時,為 key 設定指定的值。

對於下面 2 種問題 ,你知道如何解決麽?

  • 如果獲取鎖的機器掛掉,如何處理?

  • 當鎖超時時,A、B 兩個執行緒同時獲取鎖,可能導致鎖被同時獲取,如何解決?

  • 這個就是我們實作 Redis 分布式鎖時,需要重點解決的 2 個問題。


    02 理論知識

    剛才說過,透過 SetNx() 去獲取鎖,可以解決並行問題。

    當獲取到鎖,處理完業務邏輯後,會將鎖釋放。

    但當機器宕機,或者重新開機時,沒有執行 Del() 刪除鎖操作,會導致鎖一直沒有釋放。

    所以,我們還需要記錄鎖的超時時間,判斷鎖是否超時。

    這裏我們透過 GetKey() 獲取鎖的超時時間 A,透過和當前時間比較,判斷鎖是否超時。

    如果鎖未超時,直接返回,如果鎖超時,重新設定鎖的超時時間,成功獲取鎖。

    還有其它問題麽?當然!

    因為在並行場景下,會存在 A、B 兩個執行緒同時執行 SetNx(),導致兩個執行緒同時獲取到鎖。

    那如何解決呢?將 SetNx() 用 GetSet() 替換。

    GetSet() 是什麽?我簡單解答一下。

    Redis Getset 命令用於設定指定 key 的值,並返回 key 的舊值。

    這裏不太好理解,我舉個例子。

    假如 A、B 兩個執行緒,A 先執行,B 後執行:

  • 對於執行緒 A 和 B,透過 GetKey 獲取的超時時間都是 T1 = 100;

  • 對於執行緒 A,將超時時間 Ta = 200 透過 GetSet() 設定,返回 T2 = 100,此時滿足條件 「T1 == T2」,獲取鎖成功;

  • 對於執行緒 B,將超時時間 Tb = 201 透過 GetSet() 設定, 由於鎖超時時間已經被 A 重新設定,所以返回 T2 = 200 ,此時不滿足條件 「T1 == T2」,獲取鎖失敗。

  • 可能有同學會繼續問,之前設定的超時是 Ta = 200,現在變成了 Tb = 201,延長或縮短了鎖的超時時間,不會有問題麽?

    其實在現實並行場景中,能走到這一步,基本是「同時」進來的,兩者的時間差非常小,可以忽略此影響。


    03 程式碼實戰

    這裏給出 Go 程式碼,註釋都寫得非常詳細,即使你不會 Go,讀註釋也能讀懂。

    // 獲取分布式鎖,需要考慮以下情況:
    // 1. 機器A獲取到鎖,但是在未釋放鎖之前,機器掛掉或者重新開機,會導致其它機器全部hang住,這時需要根據鎖的超時時間,判斷該鎖是否需要重設;
    // 2. 當鎖超時時,需要考慮兩台機器同時去獲取該鎖,需要透過GETSET方法,讓先執行該方法的機器獲取鎖,另外一台繼續等待。
    funcGetDistributeLock(key string, expireTime int64)bool {
     currentTime := time.Now().Unix()
     expires := currentTime + expireTime
     redisAlias := "jointly"
    // 1.獲取鎖,並將value值設定為鎖的超時時間
     redisRet, err := redis.SetNx(redisAlias, key, expires)
    ifnil == err && utils.MustInt64(1) == redisRet {
    // 成功獲取到鎖
    returntrue
     }
    // 2.當獲取到鎖的機器突然重新開機&掛掉時,就需要判斷鎖的超時時間,如果鎖超時,新的機器可以重新獲取鎖
    // 2.1 獲取鎖的超時時間
     currentLockTime, err := redis.GetKey(redisAlias, key)
    if err != nil {
    returnfalse
     }
    // 2.2 當"鎖的超時時間"大於等於"當前時間",證明鎖未超時,直接返回
    if utils.MustInt64(currentLockTime) >= currentTime {
    returnfalse
     }
    // 2.3 將最新的超時時間,更新到鎖的value值,並返回舊的鎖的超時時間
     oldLockTime, err := redis.GetSet(redisAlias, key, expires)
    if err != nil {
    returnfalse
     }
    // 2.4 當鎖的兩個"舊的超時時間"相等時,證明之前沒有其它機器進行GetSet操作,成功獲取鎖
    // 說明:這裏存在並行情況,如果有A和B同時競爭,A會先GetSet,當B再去GetSet時,oldLockTime就等於A設定的超時時間
    if utils.MustString(oldLockTime) == currentLockTime {
    returntrue
     }
    returnfalse
    }




    刪除鎖邏輯:

    // 刪除分布式鎖
    // @return bool true-刪除成功;false-刪除失敗
    funcDelDistributeLock(key string)bool {
     redisAlias := "jointly"
     redisRet := redis.Del(redisAlias, key)
    if redisRet != nil {
    returnfalse
     }
    returntrue
    }

    業務邏輯:

    funcDoProcess(processId int) {
     fmt.Printf("啟動第%d個執行緒\n", processId)
     redisKey := "redis_lock_key"
    for {
    // 獲取分布式鎖
    isGetLock := GetDistributeLock(redisKey, 10)
    if isGetLock {
    fmt.Printf("Get Redis Key Success, id:%d\n", processId)
    time.Sleep(time.Second * 3)
    // 刪除分布式鎖
    DelDistributeLock(redisKey)
    else {
    // 如果未獲取到該鎖,為了避免redis負載過高,先睡一會
    time.Sleep(time.Second * 1)
    }
     }
    }

    最後起個 10 個多執行緒,去執行這個 DoProcess():

    funcmain() {
    // 初始化資源
    var group string = "group"
    var name string = "name"
    var host string
    // 初始化資源
     host = "http://ip:port"
     _, err := xrpc.NewXRpcDefault(group, name, host)
    if err != nil {
    panic(fmt.Sprintf("initRpc when init rpc failed, err:%v", err))
     }
     redis.SetRedis("louzai""redis_louzai")
    // 開啟10個執行緒,去搶Redis分布式鎖
    for i := 0; i <= 9; i ++ {
    go DoProcess(i)
     }
    // 避免子執行緒結束,主執行緒睡一會
     time.Sleep(time.Second * 100)
    return
    }

    程式跑了100 s,我們可以看到,每次都只有 1 個執行緒獲取到鎖,分別是 2、1、5、9、3,執行結果如下:

    啟動第0個執行緒
    啟動第6個執行緒
    啟動第9個執行緒
    啟動第4個執行緒
    啟動第5個執行緒
    啟動第2個執行緒
    啟動第1個執行緒
    啟動第8個執行緒
    啟動第7個執行緒
    啟動第3個執行緒
    Get Redis Key Success, id:2
    Get Redis Key Success, id:2
    Get Redis Key Success, id:1
    Get Redis Key Success, id:5
    Get Redis Key Success, id:5
    Get Redis Key Success, id:5
    Get Redis Key Success, id:5
    Get Redis Key Success, id:5
    Get Redis Key Success, id:5
    Get Redis Key Success, id:5
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:9
    Get Redis Key Success, id:3
    Get Redis Key Success, id:3
    Get Redis Key Success, id:3
    Get Redis Key Success, id:3
    Get Redis Key Success, id:3


    04 後記

    這個程式碼,其實是我很久之前寫的,因為當時 Go 沒有開源的分布式鎖,但是我又需要透過單機去執行某個任務,所以就自己手動擼了一個,後來線上上跑了 2 年,一直都沒有問題。

    不過期間也遇到過一個坑,就是我們服務遷移時,忘了將舊機器的分布式鎖停掉,導致鎖經常被舊機器搶占,當時覺得很奇怪,我的鎖呢?

    寫這篇文章時,又讓我想到當時工作的場景。

    最後再切回正題,本文由淺入深,詳細講解了 Redis 實作的詳細過程, 以及鎖超時、並行場景下,如何保證鎖能正常釋放,且只有一個執行緒去獲取鎖。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:樓仔

    來源:樓仔

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]