<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    posts - 495,comments - 227,trackbacks - 0
    http://chaopeng.me/blog/2014/01/26/redis-lock.html

    http://blog.csdn.net/ugg/article/details/41894947

    http://www.jeffkit.info/2011/07/1000/

    Redis有一系列的命令,特點是以NX結尾,NX是Not eXists的縮寫,如SETNX命令就應該理解為:SET if Not eXists。這系列的命令非常有用,這里講使用SETNX來實現分布式鎖。

    用SETNX實現分布式鎖

    利用SETNX非常簡單地實現分布式鎖。例如:某客戶端要獲得一個名字foo的鎖,客戶端使用下面的命令進行獲取:

    SETNX lock.foo <current Unix time + lock timeout + 1>

    •  如返回1,則該客戶端獲得鎖,把lock.foo的鍵值設置為時間值表示該鍵已被鎖定,該客戶端最后可以通過DEL lock.foo來釋放該鎖。
    •  如返回0,表明該鎖已被其他客戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖超時。

    解決死鎖

    上面的鎖定邏輯有一個問題:如果一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎么解決?我們可以通過鎖的鍵對應的時間戳來判斷這種情況是否發生了,如果當前的時間已經大于lock.foo的值,說明該鎖已失效,可以被重新使用。

    發生這種情況時,可不能簡單的通過DEL來刪除鎖,然后再SETNX一次,當多個客戶端檢測到鎖超時后都會嘗試去釋放它,這里就可能出現一個競態條件,讓我們模擬一下這個場景:

    1.  C0操作超時了,但它還持有著鎖,C1和C2讀取lock.foo檢查時間戳,先后發現超時了。
    2.  C1 發送DEL lock.foo
    3.  C1 發送SETNX lock.foo 并且成功了。
    4.  C2 發送DEL lock.foo
    5.  C2 發送SETNX lock.foo 并且成功了。

    這樣一來,C1,C2都拿到了鎖!問題大了!

    幸好這種問題是可以避免D,讓我們來看看C3這個客戶端是怎樣做的:

    1. C3發送SETNX lock.foo 想要獲得鎖,由于C0還持有鎖,所以Redis返回給C3一個0
    2. C3發送GET lock.foo 以檢查鎖是否超時了,如果沒超時,則等待或重試。
    3. 反之,如果已超時,C3通過下面的操作來嘗試獲得鎖:
      GETSET lock.foo <current Unix time + lock timeout + 1>
    4. 通過GETSET,C3拿到的時間戳如果仍然是超時的,那就說明,C3如愿以償拿到鎖了。
    5. 如果在C3之前,有個叫C4的客戶端比C3快一步執行了上面的操作,那么C3拿到的時間戳是個未超時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒拿到鎖,但它改寫了C4設置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。

    注意:為了讓分布式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作,因為可能客戶端因為某個耗時的操作而掛起,操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了。

    示例偽代碼

    根據上面的代碼,我寫了一小段Fake代碼來描述使用分布式鎖的全過程:

    1. # get lock
    2. lock = 0
    3. while lock != 1:
    4.     timestamp = current Unix time + lock timeout + 1
    5.     lock = SETNX lock.foo timestamp
    6.     if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
    7.         break;
    8.     else:
    9.         sleep(10ms)
    10.  
    11. # do your job
    12. do_job()
    13.  
    14. # release
    15. if now() < GET lock.foo:
    16.     DEL lock.foo

    是的,要想這段邏輯可以重用,使用python的你馬上就想到了Decorator,而用Java的你是不是也想到了那誰?AOP + annotation?行,怎樣舒服怎樣用吧,別重復代碼就行。



    背景
    在 很多互聯網產品應用中,有些場景需要加鎖處理,比如:秒殺,全局遞增ID,樓層生成等等。大部分的解決方案是基于DB實現的,Redis為單進程單線程模 式,采用隊列模式將并發訪問變成串行訪問,且多客戶端對Redis的連接并不存在競爭關系。其次Redis提供一些命令SETNX,GETSET,可以方 便實現分布式鎖機制。

    Redis命令介紹
    使用Redis實現分布式鎖,有兩個重要函數需要介紹

    SETNX命令(SET if Not eXists)
    語法:
    SETNX key value
    功能:
    當且僅當 key 不存在,將 key 的值設為 value ,并返回1;若給定的 key 已經存在,則 SETNX 不做任何動作,并返回0。

    GETSET命令
    語法:
    GETSET key value
    功能:
    將給定 key 的值設為 value ,并返回 key 的舊值 (old value),當 key 存在但不是字符串類型時,返回一個錯誤,當key不存在時,返回nil。

    GET命令
    語法:
    GET key
    功能:
    返回 key 所關聯的字符串值,如果 key 不存在那么返回特殊值 nil 。

    DEL命令
    語法:
    DEL key [KEY …]
    功能:
    刪除給定的一個或多個 key ,不存在的 key 會被忽略。

    兵貴精,不在多。分布式鎖,我們就依靠這四個命令。但在具體實現,還有很多細節,需要仔細斟酌,因為在分布式并發多進程中,任何一點出現差錯,都會導致死鎖,hold住所有進程。

    加鎖實現

    SETNX 可以直接加鎖操作,比如說對某個關鍵詞foo加鎖,客戶端可以嘗試
    SETNX foo.lock <current unix time>

    如果返回1,表示客戶端已經獲取鎖,可以往下操作,操作完成后,通過
    DEL foo.lock

    命令來釋放鎖。
    如果返回0,說明foo已經被其他客戶端上鎖,如果鎖是非堵塞的,可以選擇返回調用。如果是堵塞調用調用,就需要進入以下個重試循環,直至成功獲得鎖或者重試超時。理想是美好的,現實是殘酷的。僅僅使用SETNX加鎖帶有競爭條件的,在某些特定的情況會造成死鎖錯誤。

    處理死鎖

    在 上面的處理方式中,如果獲取鎖的客戶端端執行時間過長,進程被kill掉,或者因為其他異常崩潰,導致無法釋放鎖,就會造成死鎖。所以,需要對加鎖要做時 效性檢測。因此,我們在加鎖時,把當前時間戳作為value存入此鎖中,通過當前時間戳和Redis中的時間戳進行對比,如果超過一定差值,認為鎖已經時 效,防止鎖無限期的鎖下去,但是,在大并發情況,如果同時檢測鎖失效,并簡單粗暴的刪除死鎖,再通過SETNX上鎖,可能會導致競爭條件的產生,即多個客 戶端同時獲取鎖。

    C1獲取鎖,并崩潰。C2和C3調用SETNX上鎖返回0后,獲得foo.lock的時間戳,通過比對時間戳,發現鎖超時。
    C2 向foo.lock發送DEL命令。
    C2 向foo.lock發送SETNX獲取鎖。
    C3 向foo.lock發送DEL命令,此時C3發送DEL時,其實DEL掉的是C2的鎖。
    C3 向foo.lock發送SETNX獲取鎖。

    此時C2和C3都獲取了鎖,產生競爭條件,如果在更高并發的情況,可能會有更多客戶端獲取鎖。所以,DEL鎖的操作,不能直接使用在鎖超時的情況下,幸好我們有GETSET方法,假設我們現在有另外一個客戶端C4,看看如何使用GETSET方式,避免這種情況產生。

    C1獲取鎖,并崩潰。C2和C3調用SETNX上鎖返回0后,調用GET命令獲得foo.lock的時間戳T1,通過比對時間戳,發現鎖超時。
    C4 向foo.lock發送GESET命令,
    GETSET foo.lock <current unix time>
    并得到foo.lock中老的時間戳T2

    如果T1=T2,說明C4獲得時間戳。
    如果T1!=T2,說明C4之前有另外一個客戶端C5通過調用GETSET方式獲取了時間戳,C4未獲得鎖。只能sleep下,進入下次循環中。

    現在唯一的問題是,C4設置foo.lock的新時間戳,是否會對鎖產生影響。其實我們可以看到C4和C5執行的時間差值極小,并且寫入foo.lock中的都是有效時間錯,所以對鎖并沒有影響。
    為 了讓這個鎖更加強壯,獲取鎖的客戶端,應該在調用關鍵業務時,再次調用GET方法獲取T1,和寫入的T0時間戳進行對比,以免鎖因其他情況被執行DEL意 外解開而不知。以上步驟和情況,很容易從其他參考資料中看到。客戶端處理和失敗的情況非常復雜,不僅僅是崩潰這么簡單,還可能是客戶端因為某些操作被阻塞 了相當長時間,緊接著 DEL 命令被嘗試執行(但這時鎖卻在另外的客戶端手上)。也可能因為處理不當,導致死鎖。還有可能因為sleep設置不合理,導致Redis在大并發下被壓垮。 最為常見的問題還有

    GET返回nil時應該走那種邏輯?

    第一種走超時邏輯
    C1客戶端獲取鎖,并且處理完后,DEL掉鎖,在DEL鎖之前。C2通過SETNX向foo.lock設置時間戳T0 發現有客戶端獲取鎖,進入GET操作。
    C2 向foo.lock發送GET命令,獲取返回值T1(nil)。
    C2 通過T0>T1+expire對比,進入GETSET流程。
    C2 調用GETSET向foo.lock發送T0時間戳,返回foo.lock的原值T2
    C2 如果T2=T1相等,獲得鎖,如果T2!=T1,未獲得鎖。

    第二種情況走循環走setnx邏輯
    C1客戶端獲取鎖,并且處理完后,DEL掉鎖,在DEL鎖之前。C2通過SETNX向foo.lock設置時間戳T0 發現有客戶端獲取鎖,進入GET操作。
    C2 向foo.lock發送GET命令,獲取返回值T1(nil)。
    C2 循環,進入下一次SETNX邏輯

    兩 種邏輯貌似都是OK,但是從邏輯處理上來說,第一種情況存在問題。當GET返回nil表示,鎖是被刪除的,而不是超時,應該走SETNX邏輯加鎖。走第一 種情況的問題是,正常的加鎖邏輯應該走SETNX,而現在當鎖被解除后,走的是GETST,如果判斷條件不當,就會引起死鎖,很悲催,我在做的時候就碰到 了,具體怎么碰到的看下面的問題

    GETSET返回nil時應該怎么處理?

    C1和C2客戶端調用GET接口,C1返回T1,此時C3網絡情況更好,快速進入獲取鎖,并執行DEL刪除鎖,C2返回T2(nil),C1和C2都進入超時處理邏輯。
    C1 向foo.lock發送GETSET命令,獲取返回值T11(nil)。
    C1 比對C1和C11發現兩者不同,處理邏輯認為未獲取鎖。
    C2 向foo.lock發送GETSET命令,獲取返回值T22(C1寫入的時間戳)。
    C2 比對C2和C22發現兩者不同,處理邏輯認為未獲取鎖。

    此 時C1和C2都認為未獲取鎖,其實C1是已經獲取鎖了,但是他的處理邏輯沒有考慮GETSET返回nil的情況,只是單純的用GET和GETSET值就行 對比,至于為什么會出現這種情況?一種是多客戶端時,每個客戶端連接Redis的后,發出的命令并不是連續的,導致從單客戶端看到的好像連續的命令,到 Redis server后,這兩條命令之間可能已經插入大量的其他客戶端發出的命令,比如DEL,SETNX等。第二種情況,多客戶端之間時間不同步,或者不是嚴格 意義的同步。

    時間戳的問題

    我們看到foo.lock的value值為時間戳,所以要在多客戶端情況下,保證鎖有效,一定要同步各服務器的時間,如果各服務器間,時間有差異。時間不一致的客戶端,在判斷鎖超時,就會出現偏差,從而產生競爭條件。
    鎖的超時與否,嚴格依賴時間戳,時間戳本身也是有精度限制,假如我們的時間精度為秒,從加鎖到執行操作再到解鎖,一般操作肯定都能在一秒內完成。這樣的話,我們上面的CASE,就很容易出現。所以,最好把時間精度提升到毫秒級。這樣的話,可以保證毫秒級別的鎖是安全的。

    分布式鎖的問題

    1:必要的超時機制:獲取鎖的客戶端一旦崩潰,一定要有過期機制,否則其他客戶端都降無法獲取鎖,造成死鎖問題。
    2:分布式鎖,多客戶端的時間戳不能保證嚴格意義的一致性,所以在某些特定因素下,有可能存在鎖串的情況。要適度的機制,可以承受小概率的事件產生。
    3:只對關鍵處理節點加鎖,良好的習慣是,把相關的資源準備好,比如連接數據庫后,調用加鎖機制獲取鎖,直接進行操作,然后釋放,盡量減少持有鎖的時間。
    4:在持有鎖期間要不要CHECK鎖,如果需要嚴格依賴鎖的狀態,最好在關鍵步驟中做鎖的CHECK檢查機制,但是根據我們的測試發現,在大并發時,每一次CHECK鎖操作,都要消耗掉幾個毫秒,而我們的整個持鎖處理邏輯才不到10毫秒,玩客沒有選擇做鎖的檢查。
    5:sleep學問,為了減少對Redis的壓力,獲取鎖嘗試時,循環之間一定要做sleep操作。但是sleep時間是多少是門學問。需要根據自己的Redis的QPS,加上持鎖處理時間等進行合理計算。
    6:至于為什么不使用Redis的muti,expire,watch等機制,可以查一參考資料,找下原因。

    鎖測試數據

    未使用sleep
    第一種,鎖重試時未做sleep。單次請求,加鎖,執行,解鎖時間 


    可以看到加鎖和解鎖時間都很快,當我們使用

    ab -n1000 -c100 'http://sandbox6.wanke.etao.com/test/test_sequence.php?tbpm=t'
    AB 并發100累計1000次請求,對這個方法進行壓測時。


    我們會發現,獲取鎖的時間變成,同時持有鎖后,執行時間也變成,而delete鎖的時間,將近10ms時間,為什么會這樣?
    1:持有鎖后,我們的執行邏輯中包含了再次調用Redis操作,在大并發情況下,Redis執行明顯變慢。
    2:鎖的刪除時間變長,從之前的0.2ms,變成9.8ms,性能下降近50倍。
    在這種情況下,我們壓測的QPS為49,最終發現QPS和壓測總量有關,當我們并發100總共100次請求時,QPS得到110多。當我們使用sleep時

    使用Sleep時

    單次執行請求時

    我們看到,和不使用sleep機制時,性能相當。當時用相同的壓測條件進行壓縮時 

    獲取鎖的時間明顯變長,而鎖的釋放時間明顯變短,僅是不采用sleep機制的一半。當然執行時間變成就是因為,我們在執行過程中,重新創建數據庫連接,導致時間變長的。同時我們可以對比下Redis的命令執行壓力情況 

    上 圖中細高部分是為未采用sleep機制的時的壓測圖,矮胖部分為采用sleep機制的壓測圖,通上圖看到壓力減少50%左右,當然,sleep這種方式還 有個缺點QPS下降明顯,在我們的壓測條件下,僅為35,并且有部分請求出現超時情況。不過綜合各種情況后,我們還是決定采用sleep機制,主要是為了 防止在大并發情況下把Redis壓垮,很不行,我們之前碰到過,所以肯定會采用sleep機制。

    參考資料

    http://www.worlduc.com/FileSystem/18/2518/590664/9f63555e6079482f831c8ab1dcb8c19c.pdf
    http://redis.io/commands/setnx
    http://m.tkk7.com/caojianhua/archive/2013/01/28/394847.html


    引子

    redis是一個很強大的數據結構存儲的nosql數據庫,很方便針對業務模型進行效率的優化。最近我的工作是負責對現有Java服務器框架進行整理,并將網絡層與邏輯層脫離,以便于邏輯層和網絡層的橫向擴展。 盡管我在邏輯層上使用了AKKA作為核心框架,盡可能lockfree,但是還是免不了需要跨jvm的鎖。所以我需要實現一個分布式鎖。

    官方的實現

    官方在SETNX 這一頁給了一個實現。

    • C4 sends SETNX lock.foo in order to acquire the lock
    • The crashed client C3 still holds it, so Redis will reply with 0 to C4.
    • C4 sends GET lock.foo to check if the lock expired. If it is not, it will sleep for some time and retry from the start.
    • Instead, if the lock is expired because the Unix time at lock.foo is older than the current Unix time, C4 tries to perform: GETSET lock.foo (current Unix timestamp + lock timeout + 1)
    • Because of the GETSET semantic, C4 can check if the old value stored at key is still an expired timestamp. If it is, the lock was acquired.
    • If another client, for instance C5, was faster than C4 and acquired the lock with the GETSET operation, the C4 GETSET operation will return a non expired timestamp. C4 will simply restart from the first step. Note that even if C4 set the key a bit a few seconds in the future this is not a problem.

    但是使用官方推薦的getset實現的話,未競爭到鎖的一方確實可以判斷到自己未能競爭到鎖,但卻將持有鎖一方的時間修改了,這樣的直接后果就是,持有鎖的一方無法解鎖!!!

    基于lua的實現

    其實官方實現出現的問題,是因為使用redis獨立的命令不能將get-check-set這個過程進行原子化,所以我決定引入redis-lua,將get-check-set這個過程使用lua腳本來實現。

    加鎖:

    • script params: lock_key, current_timestamp, lock_timeout
    • setnx lock_key (current_timestamp + lock_timeout). if not success, set lock_key (current_timestamp + lock_timeout) if current_timestamp > value
    • client save current_timestamp(lock_create_timestamp)

    解鎖:

    • script params: lock_key, lock_create_timestamp, lock_timeout
    • delete if lock_create_timestamp + lock_timeout == value

    具體的實現:

    LUA
    1. ---lock

    2. local now = tonumber(ARGV[1])
    3. local timeout = tonumber(ARGV[2])
    4. local to = now + timeout
    5. local locked = redis.call('SETNX', KEYS[1], to)
    6. if (locked == 1) then
    7. return 0
    8. end
    9. local kt = redis.call('type', KEYS[1]);
    10. if (kt['ok'] ~= 'string') then
    11. return 2
    12. end
    13. local keyValue = tonumber(redis.call('get', KEYS[1]))
    14. if (now > keyValue) then
    15. redis.call('set', KEYS[1], to)
    16. return 0
    17. end
    18. return 1

    19. ---unlock

    20. local begin = tonumber(ARGV[1])
    21. local timeout = tonumber(ARGV[2])
    22. local kt = redis.call('type', KEYS[1]);
    23. if (kt['ok'] == 'string') then
    24. local keyValue = tonumber(redis.call('get', KEYS[1]))
    25. if ((keyValue - begin) == timeout) then
    26. redis.call('del', KEYS[1])
    27. return 0
    28. end
    29. end
    30. return 1

    已知問題

    redis的分布式鎖會有單點的問題。當然我們的業務量也沒有達到掛掉專門做鎖的redis單點的水平。

    posted on 2016-05-12 17:52 SIMONE 閱讀(853) 評論(0)  編輯  收藏 所屬分類: JAVA
    主站蜘蛛池模板: 100部毛片免费全部播放完整| 最新亚洲成av人免费看| 综合在线免费视频| 亚洲视频欧洲视频| AV无码免费永久在线观看| 亚洲日韩乱码中文无码蜜桃| 1000部拍拍拍18勿入免费视频软件 | 日韩免费在线中文字幕| 波多野结衣一区二区免费视频| 自拍偷自拍亚洲精品播放| 四虎永久免费地址在线网站| 深夜特黄a级毛片免费播放| 亚洲性在线看高清h片| 久久久精品午夜免费不卡| 亚洲欧洲日产国产综合网| 国产成人精品免费视频大全麻豆| 亚洲久悠悠色悠在线播放| 免费毛片在线播放| 一区二区三区在线观看免费| 亚洲色婷婷一区二区三区| 无码免费一区二区三区免费播放 | 亚洲国产综合人成综合网站00| 国产成人免费爽爽爽视频 | 亚洲精品无码av天堂| a色毛片免费视频| 亚洲国产综合在线| 国产国产成年年人免费看片| a毛片成人免费全部播放| 亚洲日本va午夜中文字幕一区| 噼里啪啦电影在线观看免费高清 | 黄床大片30分钟免费看| 久久亚洲高清观看| 免费中文熟妇在线影片| 免费人成大片在线观看播放电影 | 亚洲狠狠综合久久| 成年女人免费视频播放77777| 一边摸一边爽一边叫床免费视频| 亚洲av成人无码久久精品| 在线日韩av永久免费观看| 日本免费在线中文字幕| 亚洲AV色欲色欲WWW|