在單機的Redis的使用下,Redis的分布式鎖可以通過Lua進行實現,通過setnx和expire命令連用的方式,但是假如在以下情況下,就會造成無鎖的現象。
注:分布式鎖能不用就不用,尤其是在高並發的情況下。最近也在學Lua,就是為了和Redis和Nginx做整合,簡單的學習一下。
不該釋放的鎖
但是,直接執行del mylock
是有問題的,我們不能直接執行 del mylock
為什么?—— 會導致 “信號錯誤”,釋放了不該釋放的鎖 。假設如下場景:
時間線 | 線程1 | 線程2 | 線程3 |
---|---|---|---|
時刻1 | 執行 setnx mylock val1 加鎖 | 執行 setnx mylock val2 加鎖 | 執行 setnx mylock val2 加鎖 |
時刻2 | 加鎖成功 | 加鎖失敗 | 加鎖失敗 |
時刻3 | 執行任務... | 嘗試加鎖... | 嘗試加鎖... |
時刻4 | 任務繼續(鎖超時,自動釋放了) | setnx 獲得了鎖(因為線程1的鎖超時釋放了) | 仍然嘗試加鎖... |
時刻5 | 任務完畢,del mylock 釋放鎖 | 執行任務中... | 獲得了鎖(因為線程1釋放了線程2的) |
... |
上面的表格中,有兩個維度,一個是縱向的時間線,一個是橫線的線程並發競爭。我們可以發現線程 1 在開始的時候比較幸運,獲得了鎖,最先開始執行任務,但是,由於他比較耗時,最后鎖超時自動釋放了他都還沒執行完。 因此,線程 2 和線程3 的機會來了。而這一輪,線程2 比較幸運,得到了鎖。可是,當線程2正在執行任務期間,線程1 執行完了,還把線程2的鎖給釋放了。這就相當於,本來你鎖着門在廁所里邊尿尿,進行到一半的時候,別人進來了,因為他配了一把和你一模一樣的鑰匙!這就亂套了啊
因此,我們需要安全的釋放鎖——“不是我的鎖,我不能瞎釋放”。所以,我們在加鎖的時候,就需要標記“這是我的鎖”,在釋放的時候在判斷 “ 這是不是我的鎖?”。這里就需要在釋放鎖的時候加上邏輯判斷,合理的邏輯應該是這樣的:
1. 線程1 准備釋放鎖 , 鎖的key 為 mylock 鎖的 value 為 thread1_magic_num 2. 查詢當前鎖 current_value = get mylock 3. 判斷 if current_value == thread1_magic_num -- > 是 我(線程1)的鎖 else -- >不是 我(線程1)的鎖 4. 是我的鎖就釋放,否則不能釋放(而是執行自己的其他邏輯)。
為了實現上面這個邏輯,我們是無法通過 redis 自帶的命令直接完成的。如果,再寫復雜的代碼去控制釋放鎖,則會讓整體代碼太過於復雜了。所以,我們引入了lua腳本。結合Lua 腳本實現釋放鎖的功能,更簡單,redis 執行lua腳本也是原子的,所以更合適,讓合適的人干合適的事,豈不更好。
Lua實現分布式鎖
加鎖:
--[[ 思路: 1.用2個局部變量接受參數 2.由於redis內置lua解析器,執行加鎖命令 3.如果加鎖成功,則設置超時時間 4.返回加鎖命令的執行結果 ]] local key = KEYS[1] local value = KEYS[2] local rs1 = redis.call('SETNX',key,value) if rs1 == true then redis.call('SETEX', key,3600, value) end return rs1
解鎖:
--[[ 思路: 1.接受redis傳來的參數 2.判斷是否是自己的鎖,是則刪掉 3.返回結果值 ]] local key = KEYS[1] local value = KEYS[2] if redis.call('get',key) == value then return redis.call('del',key) else return false end
如此,我們便實現了鎖的安全釋放。同時,我們還需要結合業務邏輯,進行具體健壯性的保證,比如如果結束了一定不能忘記釋放鎖,異常了也要釋放鎖,某種情況下是否需要回滾事務等。總結這個分布式鎖使用的過程便是:
- 加鎖時 key 同,value 不同。
- 釋放鎖時,根據value判斷,是不是我的鎖,不能釋放別人的鎖。
- 及時釋放鎖,而不是利用自動超時。
- 鎖超時時間一定要結合業務情況權衡,過長,過短都不行。
- 程序異常之處,要捕獲,並釋放鎖。如果需要回滾的,主動做回滾、補償。保證整體的健壯性,一致性。
用redis做分布式鎖真的靠譜嗎
上面的文字中,我們討論如何使用redis作為分布式鎖,並討論了一些細節問題,如鎖超時的問題、安全釋放鎖的問題。目前為止,似乎很完美的解決的我們想要的分布式鎖功能。然而事情並沒有這么簡單,用redis做分布式鎖並不“靠譜”。
不靠譜的情況
比如在 Sentinel 集群中,主節點掛掉時,從節點會取而代之,客戶端上卻並沒有明顯感知。原先第一個客戶端在主節點中申請成功了一把鎖,但是這把鎖還沒有來得及同步到從節點,主節點突然掛掉了。然后從節點變成了主節點,這個新的節點內部沒有這個鎖,所以當
另一個客戶端過來請求加鎖時,立即就批准了。這樣就會導致系統中同樣一把鎖被兩個客戶端同時持有,不安全性由此產生。不過這種不安全也僅僅是在主從發生 failover 的情況下才會產生,而且持續時間極短,業務系統多數情況下可以容忍。
redlock
為了解決故障轉移情況下的缺陷,Antirez 發明了 Redlock 算法,使用redlock算法,需要多個redis實例,加鎖的時候,它會想多半節點發送 setex mykey myvalue
命令,只要過半節點成功了,那么就算加鎖成功了。釋放鎖的時候需要想所有節點發送del命令。這是一種基於【大多數都同意】的一種機制。感興趣的可以查詢相關資料。在實際工作中使用的時候,我們可以選擇已有的開源實現,python有redlock-py,java 中有Redisson redlock。
為了使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨立沒有主從關系。同很多分布式算法一樣,redlock 也使用「大多數機制」。
加鎖時,它會向過半節點發送 set(key, value, nx=True, ex=xxx) 指令,只要過半節點 set成功,那就認為加鎖成功。釋放鎖時,需要向所有節點發送 del 指令。不過 Redlock 算法還
需要考慮出錯重試、時鍾漂移等很多細節問題,同時因為 Redlock 需要向多個節點進行讀寫,意味着相比單實例 Redis 性能會下降一些。
Redlock 使用場景
如果你很在乎高可用性,希望掛了一台 redis 完全不受影響,那就應該考慮 redlock。不過代價也是有的,需要更多的 redis 實例,性能也下降了,代碼上還需要引入額外的library,運維上也需要特殊對待,這些都是需要考慮的成本,使用前請再三斟酌