緩存redis分佈式鎖實現

高併發時,同步調用應該去考量鎖的性能損耗。能用無鎖數據結構,就不要用鎖;能鎖區塊,就不要鎖整個方法體;能用對象鎖,就不要用類鎖。redis

對多個資源、數據庫表、對象同時加鎖時,須要保持一致的加鎖順序,不然可能會形成死鎖。 說明:線程一須要對錶 A、B、C 依次所有加鎖後才能夠進行更新操做,那麼線程二的加鎖順序也必須是 A、B、C,不然可能出現死鎖。數據庫

分佈式鎖特色:安全

  • 安全性。在任意時刻,只有一個客戶端能夠得到鎖(排他性)
  • 避免死鎖。client最終必定能夠得到鎖,即便鎖住資源的客戶端在release鎖以前崩潰或不可達
  • 容錯性。只要鎖服務集羣中的大部分節點存活,Client 就能夠進行加鎖解鎖操做。

分佈式鎖服務,你須要考慮下面幾個設計session

  • 須要給一個鎖被釋放的方式,以免請求者不把鎖還回來,致使死鎖的問題。Redis 使用超時時間,ZooKeeper 能夠依靠自身的 sessionTimeout 來刪除節點。
  • 分佈式鎖服務應該是高可用的,並且須要持久化。能夠參考 Redis 的文檔 RedLock
  • 非阻塞方式的鎖服務。
  • 支持鎖的可重入性。

1.使用redis的setnx()、expire()方法

這個方案相對於memcached()的add()方案,redis佔優點的是,其支持的數據類型更多,而memcached只支持String一種數據類型。除此以外,不管是從性能上來講,仍是操做方便性來講,其實都沒有太多的差別,徹底看你的選擇,好比公司中用哪一個比較多,你就能夠用哪一個。數據結構

首先說明一下setnx()命令,setnx的含義就是SET if Not Exists,其主要有兩個參數 setnx(key, value)。該方法是原子的,若是key不存在,則設置當前key成功,返回1;若是當前key已經存在,則設置當前key失敗,返回0。可是要注意的是setnx命令不能設置key的超時時間,只能經過expire()來對key設置。併發

具體的使用步驟以下:dom

  1. setnx(lockkey, 1) 若是返回0,則說明佔位失敗;若是返回1,則說明佔位成功分佈式

  2. expire()命令對lockkey設置超時時間,爲的是避免死鎖問題。memcached

  3. 執行完業務代碼後,能夠經過delete命令刪除key。高併發

這個方案實際上是能夠解決平常工做中的需求的,但從技術方案的探討上來講,可能還有一些能夠完善的地方。好比,若是在第一步setnx執行成功後,在expire()命令執行成功前,發生了宕機的現象,那麼就依然會出現死鎖的問題,因此若是要對其進行完善的話,可使用redis的setnx()、get()和getset()方法來實現分佈式鎖。

2.使用redis的setnx()、get()、getset()方法

這個方案的背景主要是在setnx()和expire()的方案上針對可能存在的死鎖問題,作了一版優化。

那麼先說明一下這三個命令,對於setnx()和get()這兩個命令,相信不用再多說什麼。那麼getset()命令?這個命令主要有兩個參數 getset(key,newValue)。該方法是原子的,對key設置newValue這個值,而且返回key原來的舊值。假設key原來是不存在的,那麼屢次執行這個命令,會出現下邊的效果:

  1. getset(key, 「value1″) 返回nil 此時key的值會被設置爲value1

  2. getset(key, 「value2″) 返回value1 此時key的值會被設置爲value2

  3. 依次類推!

介紹完要使用的命令後,具體的使用步驟以下:

  1. setnx(lockkey, 當前時間+過時超時時間) ,若是返回1,則獲取鎖成功;若是返回0則沒有獲取到鎖,轉向2。

  2. get(lockkey)獲取值oldExpireTime ,並將這個value值與當前的系統時間進行比較,若是小於當前系統時間,則認爲這個鎖已經超時,能夠容許別的請求從新獲取,轉向3。

  3. 計算newExpireTime=當前時間+過時超時時間,而後getset(lockkey, newExpireTime) 會返回當前lockkey的值currentExpireTime。

  4. 判斷currentExpireTime與oldExpireTime 是否相等,若是相等,說明當前getset設置成功,獲取到了鎖。若是不相等,說明這個鎖又被別的請求獲取走了,那麼當前請求能夠直接返回失敗,或者繼續重試。

  5. 在獲取到鎖以後,當前線程能夠開始本身的業務處理,當處理完畢後,比較本身的處理時間和對於鎖設置的超時時間,若是小於鎖設置的超時時間,則直接執行delete釋放鎖;若是大於鎖設置的超時時間,則不須要再鎖進行處理。

3.SET NX(推薦)

SET resource_name my_random_value NX PX 30000

四、SET EX

SETEX cd 3000 "goodbye my love"

Redis 的官方文檔

  • my_random_value是由客戶端生成的一個隨機字符串,至關因而客戶端持有鎖的標誌

  • NX表示只有當resource_name對應的key值不存在的時候才能SET成功,至關於只有第一個請求的客戶端才能得到鎖

  • PX 30000表示這個鎖有一個30秒的自動過時時間。

至於解鎖,爲了防止客戶端1得到的鎖,被客戶端2給釋放,採用下面的Lua腳原本釋放鎖

if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else 
    return 0 
end

在執行這段LUA腳本的時候,KEYS[1]的值爲resource_name,ARGV[1]的值爲my_random_value。原理就是先獲取鎖對應的value值,保證和客戶端傳進去的my_random_value值相等,這樣就能避免本身的鎖被其餘人釋放。另外,採起Lua腳本操做保證了原子性。

例如,下面的例子演示了不區分 Client 會致使錯誤

  • Client A 得到了一個鎖。
  • 當嘗試釋放鎖的請求發送給 Redis 時被阻塞,沒有及時到達Redis
  • 鎖定時間超時,Redis 認爲鎖的租約到期,釋放了這個鎖。
  • Client B 從新申請到了這個鎖。
  • Client A 的解鎖請求到達,將 Client B 鎖定的key解鎖
  • Client C 也得到了鎖。
  • Client B 和 Client C 同時持有鎖。

經過執行上面腳本的方式釋放鎖,Client 的解鎖操做只會解鎖本身曾經加鎖的資源,因此是安全的。

經常使用的四種方案

  1. 基於數據庫表作樂觀鎖,用於分佈式鎖。

  2. 使用memcached的add()方法,用於分佈式鎖。

  3. 使用redis的setnx()、expire()方法,用於分佈式鎖。

  4. 使用redis的setnx()、get()、getset()方法,用於分佈式鎖。

  5. 使用redis的setex(),用於分佈式鎖

相關文章
相關標籤/搜索