1. Go:分布式鎖實(shí)現(xiàn)原理與最佳實(shí)踐

        共 4201字,需瀏覽 9分鐘

         ·

        2021-02-05 09:32

        分布式鎖應(yīng)用場(chǎng)景

        很多應(yīng)用場(chǎng)景是需要系統(tǒng)保證冪等性的(如api服務(wù)或消息消費(fèi)者),并發(fā)情況下或消息重復(fù)很容易造成系統(tǒng)重入,那么分布式鎖是保障冪等的一個(gè)重要手段。

        另一方面,很多搶單場(chǎng)景或者叫交易撮合場(chǎng)景,如dd司機(jī)搶單或唯一商品搶拍等都需要用一把“全局鎖”來(lái)解決并發(fā)造成的問(wèn)題。在防止并發(fā)情況下造成庫(kù)存超賣(mài)的場(chǎng)景,也常用分布式鎖來(lái)解決。


        實(shí)現(xiàn)分布式鎖方案

        這里介紹常見(jiàn)兩種:redis鎖、zookeeper鎖



        1.Redis實(shí)現(xiàn)方案

        1.1實(shí)現(xiàn)原理

        redis分布式鎖基本都知道setnx命令(if not exists),其實(shí)現(xiàn)原理即:如果進(jìn)入redis添加某個(gè)鍵不存在可以設(shè)置成功,如果已存在則會(huì)設(shè)置失敗。

        說(shuō)明:setnx命令已過(guò)時(shí),這里推薦使用set +nx參數(shù)來(lái)實(shí)現(xiàn)。

        set命令:set key value ex seconds nx

        • ex 表示過(guò)期時(shí)間,精確到秒 (對(duì)應(yīng)另一個(gè)參數(shù)px過(guò)期時(shí)間精確到毫秒)

        • nx 表示if not exists,只有鍵不存在才能設(shè)置成功(對(duì)應(yīng)另一個(gè)參數(shù)xx只有鍵存在才能設(shè)置成功)

        設(shè)置過(guò)期時(shí)間的作用,如果某個(gè)并行任務(wù)(進(jìn)程/線程/協(xié)程)持有鎖,但不能正常釋放,將導(dǎo)致所有任務(wù)都無(wú)法獲取鎖,獲取執(zhí)行權(quán)限。而引入了過(guò)期時(shí)間解決此問(wèn)題的同時(shí),也會(huì)引入新的問(wèn)題,具體后面分析。


        1.2代碼實(shí)現(xiàn)

        import?"github.com/go-redis/redis"??//redis package//connect?redisvar client = redis.NewClient(&redis.Options{    Addr:     "localhost:6379",    Password: "",    DB:       0,})//lockfunc?lock(myfunc?func())?{????var?lockKey?=?"mylockr"    //lock????lockSuccess,?err?:=?client.SetNX(lockKey,?1,?time.Second*5).Result()    if err != nil || !lockSuccess {        fmt.Println("get lock fail")        return    } else {        fmt.Println("get lock")????}    //run func????myfunc()    //unlock????_,?err?:=?client.Del(lockKey).Result()    if err != nil {????????fmt.Println("unlock?fail")    } else {        fmt.Println("unlock")    }}//do actionvar counter int64func incr() {    counter++    fmt.Printf("after incr is %d\n", counter)}//5?goroutine?compete?lockvar wg sync.WaitGroupfunc?main()?{    for i := 0; i < 5; i++ {        wg.Add(1)        go func() {            lock(incr)        }()    }    wg.Wait()    fmt.Printf("final counter is %d \n", counter)}

        以上代碼截取關(guān)鍵部分,完整代碼參見(jiàn):

        https://github.com/skyhackvip/lock/blob/master/redislock.go

        代碼執(zhí)行結(jié)果:

        根據(jù)執(zhí)行結(jié)果可以看到,每次執(zhí)行最后的計(jì)數(shù)不一樣,多個(gè)協(xié)程間互相搶鎖,只有拿到鎖才會(huì)計(jì)數(shù)加1,搶鎖失敗則不執(zhí)行。

        這里說(shuō)明下:由于routine執(zhí)行時(shí)間太短,執(zhí)行完把鎖釋放了所以才有其他routine可以拿到鎖。如果incr代碼中增加sleep時(shí)間,那么結(jié)果都是1了。

        用一張圖來(lái)更直觀解釋具體執(zhí)行情況:


        1.3方案缺陷

        剛才提到使用了過(guò)期時(shí)間,雖然解決了“死鎖”問(wèn)題,但會(huì)引來(lái)新的問(wèn)題,具體問(wèn)題分析如下:

        可以看到routine1拿到鎖,但由于執(zhí)行時(shí)間過(guò)長(zhǎng)(比鎖失效時(shí)間長(zhǎng)),導(dǎo)致鎖提前失效釋放,routine3可以正常拿到鎖,而之后routine1進(jìn)行鎖釋放,當(dāng)routine3進(jìn)行鎖釋放時(shí)就會(huì)失敗,如果此時(shí)有其他并發(fā)來(lái)的時(shí)候鎖也會(huì)有問(wèn)題。


        1.4方案優(yōu)化

        那么有什么有效解決方案呢?

        簡(jiǎn)單來(lái)說(shuō)就是利用lock的value,還記得之前代碼設(shè)置lock的時(shí)候隨便使用了一個(gè)值1就打發(fā)了。

        resp := client.SetNX(lockKey, 1, time.Second*5)

        這里的1可以改為能識(shí)別該routine的唯一值(如uid,orderid等),也可以使用uuid隨機(jī)生成一個(gè)。(關(guān)于如何生成uuid方案參見(jiàn)公眾號(hào)上一篇文章)

        func?lock(myfunc func())?{????//lock    uuid := getUuid()    lockSuccess,?err?:=?client.SetNX(lockKey,?uuid,?time.Second*5).Result()    if err != nil || !lockSuccess {        fmt.Println("get lock fail")        return    } else {        fmt.Println("get lock")    }       //run?func    myfunc()    //unlock    value, _ := client.Get(lockKey).Result()    if value == uuid { //compare value,if equal then del    ????_,?err?:=?client.Del(lockKey).Result()        if err != nil {            fmt.Println("unlock fail")    ????}? else {    ????    fmt.Println("unlock")    ????}    }}

        這里增加了value的比較,確認(rèn)了是當(dāng)前routine,才會(huì)進(jìn)行刪除。至此問(wèn)題解決了嗎?

        value, _ := client.Get(lockKey).Result() 和 value==uuid

        這個(gè)操作本身不具有“原子性”,可能當(dāng)獲取到value并且對(duì)比一致了,但此時(shí)lock過(guò)期失效了,而同時(shí)另一個(gè)routine拿到了結(jié)果,那么這里又會(huì)把別人的鎖誤刪除了。


        1.5方案再優(yōu)化

        那么有沒(méi)有辦法保障操作的原子性呢,這里可以使用lua徹底解決,lua是嵌入式語(yǔ)言,redis本身支持。使用golang操作redis運(yùn)行l(wèi)ua命令,保障問(wèn)題解決。上代碼如下:

        func lock(myfunc func()) {    //...code    //unlock    var luaScript = redis.NewScript(`        if redis.call("get", KEYS[1]) == ARGV[1]            then                return redis.call("del", KEYS[1])            else                return 0        end    `)    rs, _ := luaScript.Run(client, []string{lockKey}, uuid).Result()    if rs == 0 {        fmt.Println("unlock fail")    } else {        fmt.Println("unlock")    }}

        lua腳本中KEYS[1]代表lock的key,ARGV[1]代表lock的value,也就是生成的uuid。通過(guò)執(zhí)行l(wèi)ua來(lái)保障這里刪除鎖的操作是原子的。

        完整代碼參見(jiàn):https://github.com/skyhackvip/lock/blob/master/redislualock.go


        1.6redis鎖適用場(chǎng)景

        由redis設(shè)置的鎖,多個(gè)并發(fā)任務(wù)進(jìn)行爭(zhēng)搶占用,因此非常適合高并發(fā)情況下,用來(lái)進(jìn)行搶鎖。


        2.zookeeper鎖

        2.1實(shí)現(xiàn)原理

        使用zk的臨時(shí)節(jié)點(diǎn)插入值,如果插入成功后watch會(huì)通知所有監(jiān)聽(tīng)節(jié)點(diǎn),此時(shí)其他并行任務(wù)不可再進(jìn)行插入。具體圖示如下:

        2.2代碼實(shí)現(xiàn)

        import "github.com/samuel/go-zookeeper/zk" //package//connect?zkconn,?_,?err?:=?zk.Connect([]string{"localhost:2181"},?time.Second)//zklockfunc zklock(conn *zk.Conn, myfunc func()) {????lock?:=?zk.NewLock(conn,?"/mylock",?zk.WorldACL(zk.PermAll))    ????err := lock.Lock()    if err != nil {        panic(err)    }   ????fmt.Println("get?lock")????myfunc()    lock.Unlock()    fmt.Println("unlock")}//goroutine?runfor?i?:=?0;?i??????go?zklock(conn,?incr)}

        完整代碼參見(jiàn):https://github.com/skyhackvip/lock/blob/master/zklock.go

        執(zhí)行結(jié)果如下:


        每次執(zhí)行,執(zhí)行結(jié)果都是5。

        2.3zookeeper鎖適用場(chǎng)景

        相比于redis搶鎖導(dǎo)致其他routine搶鎖失敗退出,使用zk實(shí)現(xiàn)的鎖會(huì)讓其他routine處于“等鎖”狀態(tài)。


        ?3.方案對(duì)比選擇


        redis鎖

        zookeeper鎖

        描述

        使用set nx實(shí)現(xiàn)

        使用臨時(shí)節(jié)點(diǎn)+watch實(shí)現(xiàn)

        依賴(lài)

        redis

        zookeeper

        適用場(chǎng)景

        并發(fā)搶鎖

        鎖占用時(shí)間長(zhǎng)其他任務(wù)可等待。如消息冪等消費(fèi)。

        高可用性

        redis發(fā)生故障主從切換等可能導(dǎo)致鎖失效

        利用paxos協(xié)議能保證分布式一致性,數(shù)據(jù)更可靠

        如果不是對(duì)鎖有特別高的要求,一般情況下使用redis鎖就夠了。除提到的這兩種外使用etcd也可以完成鎖需求,具體可以參考下方資料。


        更多參考資料

        etcd實(shí)現(xiàn)鎖:

        https://github.com/zieckey/etcdsync


        文章相關(guān)實(shí)現(xiàn)代碼:

        https://github.com/skyhackvip/lock



        推薦閱讀


        福利

        我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù)?ebook?獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

        瀏覽 65
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 美女日逼日逼 | 白丝校花被狂揉大胸 | 51嘿嘿嘿国产精品伦理 | 国产一级特黄大片做受 | 午夜成人精品视频 |