getset原子性 redis_一文看透 Redis 分布式锁进化史(解读 + 缺陷分析)
- 各個版本的Redis分布式鎖
- V1.0
- V1.1 基于[GETSET]
- V2.0 基于[SETNX]
- V3.0
- V3.1
- 分布式Redis鎖:Redlock
- 總結
- 《Netty 實現原理與源碼解析 —— 精品合集》
- 《Spring 實現原理與源碼解析 —— 精品合集》
- 《MyBatis 實現原理與源碼解析 —— 精品合集》
- 《Spring MVC 實現原理與源碼解析 —— 精品合集》
- 《Spring Boot 實現原理與源碼解析 —— 精品合集》
- 《數據庫實體設計合集》
- 《Java 面試題 —— 精品合集》
- 《Java 學習指南 —— 精品合集》
近兩年來微服務變得越來越熱門,越來越多的應用部署在分布式環境中,在分布式環境中,數據一致性是一直以來需要關注并且去解決的問題,分布式鎖也就成為了一種廣泛使用的技術,常用的分布式實現方式為Redis,Zookeeper,其中基于Redis的分布式鎖的使用更加廣泛。
但是在工作和網絡上看到過各個版本的Redis分布式鎖實現,每種實現都有一些不嚴謹的地方,甚至有可能是錯誤的實現,包括在代碼中,如果不能正確的使用分布式鎖,可能造成嚴重的生產環境故障,本文主要對目前遇到的各種分布式鎖以及其缺陷做了一個整理,并對如何選擇合適的Redis分布式鎖給出建議。
各個版本的Redis分布式鎖
V1.0
tryLock(){ SETNX Key 1EXPIRE Key Seconds } release(){ DELETE Key }這個版本應該是最簡單的版本,也是出現頻率很高的一個版本,首先給鎖加一個過期時間操作是為了避免應用在服務重啟或者異常導致鎖無法釋放后,不會出現鎖一直無法被釋放的情況。
這個方案的一個問題在于每次提交一個Redis請求,如果執行完第一條命令后應用異?;蛘咧貑?#xff0c;鎖將無法過期,一種改善方案就是使用Lua腳本(包含SETNX和EXPIRE兩條命令),但是如果Redis僅執行了一條命令后crash或者發生主從切換,依然會出現鎖沒有過期時間,最終導致無法釋放。
另外一個問題在于,很多同學在釋放分布式鎖的過程中,無論鎖是否獲取成功,都在finally中釋放鎖,這樣是一個鎖的錯誤使用,這個問題將在后續的V3.0版本中解決。
針對鎖無法釋放問題的一個解決方案基于GETSET命令來實現
V1.1 基于GETSET
tryLock(){ NewExpireTime=CurrentTimestamp+ExpireSecondsif(SETNX Key NewExpireTime Seconds){oldExpireTime = GET(Key)if( oldExpireTime < CurrentTimestamp){NewExpireTime=CurrentTimestamp+ExpireSecondsCurrentExpireTime=GETSET(Key,NewExpireTime)if(CurrentExpireTime == oldExpireTime){return 1;}else{return 0;}}} } release(){ DELETE key}思路:
問題:
V2.0 基于SETNX
tryLock(){ SETNX Key 1 Seconds } release(){ DELETE Key }Redis 2.6.12版本后SETNX增加過期時間參數,這樣就解決了兩條命令無法保證原子性的問題。但是設想下面一個場景:
大致的流程圖
存在問題:
V3.0
tryLock(){ SETNX Key UnixTimestamp Seconds } release(){ EVAL(//LuaScriptif redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])elsereturn 0end) }這個方案通過指定Value為時間戳,并在釋放鎖的時候檢查鎖的Value是否為獲取鎖的Value,避免了V2.0版本中提到的C1釋放了C2持有的鎖的問題;另外在釋放鎖的時候因為涉及到多個Redis操作,并且考慮到Check And Set 模型的并發問題,所以使用Lua腳本來避免并發問題。
存在問題:
如果在并發極高的場景下,比如搶紅包場景,可能存在UnixTimestamp重復問題,另外由于不能保證分布式環境下的物理時鐘一致性,也可能存在UnixTimestamp重復問題,只不過極少情況下會遇到。
V3.1
tryLock(){ SET Key UniqId Seconds } release(){ EVAL(//LuaScriptif redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])elsereturn 0end) }Redis 2.6.12后SET同樣提供了一個NX參數,等同于SETNX命令,官方文檔上提醒后面的版本有可能去掉SETNX, SETEX, PSETEX,并用SET命令代替,另外一個優化是使用一個自增的唯一UniqId代替時間戳來規避V3.0提到的時鐘問題。
這個方案是目前最優的分布式鎖方案,但是如果在Redis集群環境下依然存在問題:
由于Redis集群數據同步為異步,假設在Master節點獲取到鎖后未完成數據同步情況下Master節點crash,此時在新的Master節點依然可以獲取鎖,所以多個Client同時獲取到了鎖
分布式Redis鎖:Redlock
V3.1的版本僅在單實例的場景下是安全的,針對如何實現分布式Redis的鎖,國外的分布式專家有過激烈的討論, antirez提出了分布式鎖算法Redlock,在distlock話題下可以看到對Redlock的詳細說明,下面是Redlock算法的一個中文說明(引用)
假設有N個獨立的Redis節點
然而Martin Kleppmann針對這個算法提出了質疑,提出應該基于fencing token機制(每次對資源進行操作都需要進行token驗證)
接著antirez又回復了Martin Kleppmann的質疑,給出了過期機制的合理性,以及實際場景中如果出現停頓問題導致多個Client同時訪問資源的情況下如何處理。
針對Redlock的問題,基于Redis的分布式鎖到底安全嗎給出了詳細的中文說明,并對Redlock算法存在的問題提出了分析。
總結
不論是基于SETNX版本的Redis單實例分布式鎖,還是Redlock分布式鎖,都是為了保證下特性
死鎖:鎖最終應該能夠被釋放,即使Client端crash或者出現網絡分區(通常基于超時機制)
容錯性:只要超過半數Redis節點可用,鎖都能被正確獲取和釋放
所以在開發或者使用分布式鎖的過程中要保證安全性和活性,避免出現不可預測的結果。
另外每個版本的分布式鎖都存在一些問題,在鎖的使用上要針對鎖的實用場景選擇合適的鎖,通常情況下鎖的使用場景包括:
Efficiency(效率):只需要一個Client來完成操作,不需要重復執行,這是一個對寬松的分布式鎖,只需要保證鎖的活性即可;
Correctness(正確性):多個Client保證嚴格的互斥性,不允許出現同時持有鎖或者對同時操作同一資源,這種場景下需要在鎖的選擇和使用上更加嚴格,同時在業務代碼上盡量做到冪等
在Redis分布式鎖的實現上還有很多問題等待解決,我們需要認識到這些問題并清楚如何正確實現一個Redis 分布式鎖,然后在工作中合理的選擇和正確的使用分布式鎖。
來源:http://t.cn/Rmayeve
總結
以上是生活随笔為你收集整理的getset原子性 redis_一文看透 Redis 分布式锁进化史(解读 + 缺陷分析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MYSQL中有时候不得不使用replac
- 下一篇: linux cmake编译源码,linu