上篇文章可能舉得例子有點(diǎn)不恰當(dāng),導(dǎo)致有些小伙伴沒看懂為什么余額會變負(fù),。 這次我們舉得實(shí)際一點(diǎn),,還是上篇文章 account 表,假設(shè) id=1,,balance=1000,,不過這次我們扣款 1000,兩個(gè)事務(wù)的時(shí)序圖如下: 這次使用兩個(gè)命令窗口真實(shí)執(zhí)行一把: 注意事務(wù) 2,,③處查詢到 id=1,,balance=1000,但是實(shí)際上由于此時(shí)事務(wù) 1 已經(jīng)提交,,最新結(jié)果如②處所示 id=1,,balance=900。 本來 Java 代碼層會做一層余額判斷: if (balance - amount < 0) { 但是此時(shí)由于 ③ 處使用快照讀,,讀到是個(gè)舊值,未讀到最新值,,導(dǎo)致這層校驗(yàn)失效,,從而代碼繼續(xù)往下運(yùn)行,執(zhí)行了數(shù)據(jù)更新,。 更新語句又采用如下寫法:
這條更新語句又必須是在這條記錄的最新值的基礎(chǔ)做更新,,更新語句執(zhí)行結(jié)束,這條記錄就變成了 id=1,,balance=-1000,。 之前有朋友疑惑 t12 更新之后,再次進(jìn)行快照讀,結(jié)果會是多少,。 上圖執(zhí)行結(jié)果 ④ 可以看到結(jié)果為 id=1,balance=-1000,可以看到已經(jīng)查詢最新的結(jié)果記錄,。 這行數(shù)據(jù)最新版本由于是事務(wù) 2 自己更新的,自身事務(wù)更新永遠(yuǎn)對自己可見,。 另外這次問題上本質(zhì)上因?yàn)?Java 層與數(shù)據(jù)庫層數(shù)據(jù)不一致導(dǎo)致,,有的朋友留言提出,可以在更新余額時(shí)加一層判斷: UPDATE account set balance=balance-1000 WHERE id =1 and balance>0; 然后更新完成,,Java 層判斷更新有效行數(shù)是否大于 0,。這種做法確實(shí)能規(guī)避這個(gè)問題。 最后這位朋友留言總結(jié)的挺好,,粘貼一下: 手?jǐn)]分布式鎖現(xiàn)在切回正文,,這篇文章本來是準(zhǔn)備寫下 Mysql 查詢左匹配的問題,但是還沒研究出來,。那就先寫下最近在鼓搗一個(gè)東西,使用 Redis 實(shí)現(xiàn)可重入分布鎖,。 看到這里,,有的朋友可能會提出來使用 redisson 不香嗎,為什么還要自己實(shí)現(xiàn),? 哎,,redisson 真的很香,但是現(xiàn)有項(xiàng)目中沒辦法使用,,只好自己手?jǐn)]一個(gè)可重入的分布式鎖了,。 雖然用不了 redisson,但是我可以研究其源碼,,最后實(shí)現(xiàn)的可重入分布鎖參考了 redisson 實(shí)現(xiàn)方式,。 分布式鎖分布式鎖特性就要在于排他性,同一時(shí)間內(nèi)多個(gè)調(diào)用方加鎖競爭,,只能有一個(gè)調(diào)用方加鎖成功,。 Redis 由于內(nèi)部單線程的執(zhí)行,內(nèi)部按照請求先后順序執(zhí)行,,沒有并發(fā)沖突,,所以只會有一個(gè)調(diào)用方才會成功獲取鎖。 而且 Redis 基于內(nèi)存操作,,加解鎖速度性能高,,另外我們還可以使用集群部署增強(qiáng) Redis 可用性。 加鎖使用 Redis 實(shí)現(xiàn)一個(gè)簡單的分布式鎖,,非常簡單,,可以直接使用 SETNX 命令。 SETNX 是『SET if Not eXists』,如果不存在,,才會設(shè)置,,使用方法如下: 不過直接使用 SETNX 有一個(gè)缺陷,我們沒辦法對其設(shè)置過期時(shí)間,,如果加鎖客戶端宕機(jī)了,,這就導(dǎo)致這把鎖獲取不了了。 有的同學(xué)可能會提出,,執(zhí)行 SETNX 之后,,再執(zhí)行 EXPIRE 命令,主動設(shè)置過期時(shí)間,,偽碼如下:
不過這樣還是存在缺陷,,加鎖代碼并不能原子執(zhí)行,如果調(diào)用加鎖語句,,還沒來得及設(shè)置過期時(shí)間,,應(yīng)用就宕機(jī)了,還是會存在鎖過期不了的問題,。 不過這個(gè)問題在 Redis 2.6.12 版本 就可以被完美解決,。這個(gè)版本增強(qiáng)了 SET 命令,可以通過帶上 NX,EX 命令原子執(zhí)行加鎖操作,,解決上述問題,。參數(shù)含義如下:
使用 SET 命令實(shí)現(xiàn)分布式鎖只需要一行代碼: SET lock_name anystring NX EX lock_time 解鎖解鎖相比加鎖過程,就顯得非常簡單,,只要調(diào)用
不過這種方式卻存在一個(gè)缺陷,,可能會發(fā)生錯解鎖問題。 假設(shè)應(yīng)用 1 加鎖成功,,鎖超時(shí)時(shí)間為 30s,。由于應(yīng)用 1 業(yè)務(wù)邏輯執(zhí)行時(shí)間過長,30 s 之后,,鎖過期自動釋放,。 這時(shí)應(yīng)用 2 接著加鎖,加鎖成功,,執(zhí)行業(yè)務(wù)邏輯,。這個(gè)期間,應(yīng)用 1 終于執(zhí)行結(jié)束,,使用 這樣就導(dǎo)致了應(yīng)用 1 錯誤釋放應(yīng)用 2 的鎖,,另外鎖被釋放之后,其他應(yīng)用可能再次加鎖成功,,這就可能導(dǎo)致業(yè)務(wù)重復(fù)執(zhí)行,。 為了使鎖不被錯誤釋放,我們需要在加鎖時(shí)設(shè)置隨機(jī)字符串,,比如 UUID,。 SET lock_name uuid NX EX lock_time 釋放鎖時(shí),需要提前獲取當(dāng)前鎖存儲的值,,然后與加鎖時(shí)的 uuid 做比較,,偽代碼如下:
上述代碼我們不能通過 Java 代碼運(yùn)行,因?yàn)闊o法保證上述代碼原子化執(zhí)行。 幸好 Redis 2.6.0 增加執(zhí)行 Lua 腳本的功能,,lua 代碼可以運(yùn)行在 Redis 服務(wù)器的上下文中,,并且整個(gè)操作將會被當(dāng)成一個(gè)整體執(zhí)行,中間不會被其他命令插入,。 這就保證了腳本將會以原子性的方式執(zhí)行,,當(dāng)某個(gè)腳本正在運(yùn)行的時(shí)候,不會有其他腳本或 Redis 命令被執(zhí)行,。在其他的別的客戶端看來,,執(zhí)行腳本的效果,要么是不可見的,,要么就是已完成的。 EVAL 與 EVALSHAEVALRedis 可以使用 EVAL 執(zhí)行 LUA 腳本,,而我們可以在 LUA 腳本中執(zhí)行判斷求值邏輯,。EVAL 執(zhí)行方式如下: EVAL script numkeys key [key ...] arg [arg ...]
命令最后,,是一些附加參數(shù),可以用來當(dāng)做 Redis Key 值存儲的 Value 值,,使用方式如 用一個(gè)簡單例子運(yùn)行一下 EVAL 命令:
運(yùn)行效果如下: 可以看到 在 Lua 腳本可以使用下面兩個(gè)函數(shù)執(zhí)行 Redis 命令:
兩個(gè)函數(shù)作用法與作用完全一致,,只不過對于錯誤的處理方式不一致,感興趣的小伙伴可以具體點(diǎn)擊以下鏈接,,查看錯誤處理一章,。 http://doc./script/eval.html 下面我們統(tǒng)一在 Lua 腳本中使用 eval 'return redis.call('set',KEYS[1],ARGV[1])' 1 foo 樓下小黑哥 運(yùn)行效果如下: EVALSHA
當(dāng) Redis 第一次收到 Lua 腳本時(shí),首先將會對 Lua 腳本進(jìn)行 sha1 獲取簽名值,,然后內(nèi)部將會對其緩存起來,。后續(xù)執(zhí)行時(shí),直接通過 sha1 計(jì)算過后簽名值查找已經(jīng)編譯過的腳本,,加快執(zhí)行速度,。 雖然 Redis 內(nèi)部已經(jīng)優(yōu)化執(zhí)行的速度,但是每次都需要發(fā)送腳本,,還是有網(wǎng)絡(luò)傳輸?shù)某杀?,如果腳本很大,這其中花在網(wǎng)絡(luò)傳輸?shù)臅r(shí)間就會相應(yīng)的增加,。 所以 Redis 又實(shí)現(xiàn)了
運(yùn)行效果如下: “ 可以看到,,如果之前未執(zhí)行過 優(yōu)化執(zhí)行 EVAL我們可以結(jié)合使用 //連接本地的 Redis 服務(wù) 上面的代碼看起來還是很復(fù)雜吧,,不過這是使用原生 jedis 的情況下。如果我們使用 Spring Boot 的話,,那就沒這么麻煩了,。Spring 組件執(zhí)行的 不過需要注意的是,,如果 Spring-Boot 使用 Jedis 作為連接客戶端,并且使用Redis Cluster 集群模式,,需要使用 2.1.9 以上版本的spring-boot-starter-data-redis,不然執(zhí)行過程中將會拋出:
詳細(xì)情況可以參考這個(gè)修復(fù)的 IssueAdd support for scripting commands with Jedis Cluster 優(yōu)化分布式鎖講完 Redis 執(zhí)行 LUA 腳本的相關(guān)命令,,我們來看下如何優(yōu)化上面的分布式鎖,使其無法釋放其他應(yīng)用加的鎖,。 “ 加鎖的 Redis 命令如下: SET lock_name uuid NX EX lock_time 加鎖代碼如下:
由于 /** 解鎖需要使用 Lua 腳本:
這段腳本將會判斷傳入的唯一標(biāo)識是否與 Redis 存儲的標(biāo)示一致,,如果一直,釋放該鎖,,否則立刻返回,。 釋放鎖的方法如下: /** “ Redis 分布式鎖的缺陷無法重入由于上述加鎖命令使用了 如果想將 Redis 分布式鎖改造成可重入的分布式鎖,有兩種方案:
第一種方案可以參考這篇文章分布式鎖的實(shí)現(xiàn)之 redis 篇。第二個(gè)解決方案,,下一篇文章就會具體來聊聊,,敬請期待。 鎖超時(shí)釋放假設(shè)線程 A 加鎖成功,,鎖超時(shí)時(shí)間為 30s,。由于線程 A 內(nèi)部業(yè)務(wù)邏輯執(zhí)行時(shí)間過長,,30s 之后鎖過期自動釋放。 此時(shí)線程 B 成功獲取到鎖,,進(jìn)入執(zhí)行內(nèi)部業(yè)務(wù)邏輯,。此時(shí)線程 A 還在執(zhí)行執(zhí)行業(yè)務(wù),而線程 B 又進(jìn)入執(zhí)行這段業(yè)務(wù)邏輯,,這就導(dǎo)致業(yè)務(wù)邏輯重復(fù)被執(zhí)行,。 這個(gè)問題我覺得,一般由于鎖的超時(shí)時(shí)間設(shè)置不當(dāng)引起,,可以評估下業(yè)務(wù)邏輯執(zhí)行時(shí)間,,在這基礎(chǔ)上再延長一下超時(shí)時(shí)間。 如果超時(shí)時(shí)間設(shè)置合理,,但是業(yè)務(wù)邏輯還有偶發(fā)的超時(shí),,個(gè)人覺得需要排查下業(yè)務(wù)執(zhí)行過長的問題。 如果說一定要做到業(yè)務(wù)執(zhí)行期間,,鎖只能被一個(gè)線程占有的,,那就需要增加一個(gè)守護(hù)線程,定時(shí)為即將的過期的但未釋放的鎖增加有效時(shí)間,。 加鎖成功后,,同時(shí)創(chuàng)建一個(gè)守護(hù)線程。守護(hù)線程將會定時(shí)查看鎖是否即將到期,,如果鎖即將過期,,那就執(zhí)行 EXPIRE 等命令重新設(shè)置過期時(shí)間。 說實(shí)話,,如果要這么做,,真的挺復(fù)雜的,感興趣的話可以參考下 redisson watchdog 實(shí)現(xiàn)方式,。 Redis 分布式鎖集群問題為了保證生產(chǎn)高可用,,一般我們會采用主從部署方式。采用這種方式,,我們可以將讀寫分離,,主節(jié)點(diǎn)提供寫服務(wù),從節(jié)點(diǎn)提供讀服務(wù),。 Redis 主從之間數(shù)據(jù)同步采用異步復(fù)制方式,,主節(jié)點(diǎn)寫入成功后,立刻返回給客戶端,,然后異步復(fù)制給從節(jié)點(diǎn),。 如果數(shù)據(jù)寫入主節(jié)點(diǎn)成功,但是還未復(fù)制給從節(jié)點(diǎn),。此時(shí)主節(jié)點(diǎn)掛了,,從節(jié)點(diǎn)立刻被提升為主節(jié)點(diǎn),。 這種情況下,還未同步的數(shù)據(jù)就丟失了,,其他線程又可以被加鎖了,。 針對這種情況, Redis 官方提出一種 RedLock 的算法,,需要有 N 個(gè)Redis 主從節(jié)點(diǎn),,解決該問題,詳情參考: https:///topics/distlock,。 這個(gè)算法自己實(shí)現(xiàn)還是很復(fù)雜的,,幸好 redisson 已經(jīng)實(shí)現(xiàn)的 RedLock,詳情參考:redisson redlock 總結(jié)本來這篇文章是想寫 Redis 可重入分布式鎖的,,可是沒想到寫分布式鎖的實(shí)現(xiàn)方案就已經(jīng)寫了這么多,,再寫下去,文章可能就很長,,所以拆分成兩篇來寫,。 嘿嘿,這不下星期不用想些什么了,,真是個(gè)小機(jī)靈鬼~ 好了,,幫大家再次總結(jié)一下本文內(nèi)容。 簡單的 Redis 分布式鎖的實(shí)現(xiàn)方式還是很簡單的,,我們可以直接用 SETNX/DEL 命令實(shí)現(xiàn)加解鎖,。 不過這種實(shí)現(xiàn)方式不夠健壯,可能存在應(yīng)用宕機(jī),,鎖就無法被釋放的問題,。 所以我們接著引入以下命令以及 Lua 腳本增強(qiáng) Redis 分布式鎖。
最后 Redis 分布鎖還是存在一些缺陷,,在這里提出一些解決方案,,感興趣同學(xué)可以自己實(shí)現(xiàn)一下。 |
|