应用 1:千帆竞发 ——分布式锁
1.分布式應(yīng)用進(jìn)行邏輯處理時(shí)經(jīng)常會(huì)遇到并發(fā)問題
比如一個(gè)操作要修改用戶的狀態(tài),修改狀態(tài)需要先讀出用戶的狀態(tài),在內(nèi)存里進(jìn)行修
改,改完了再存回去。如果這樣的操作同時(shí)進(jìn)行了,就會(huì)出現(xiàn)并發(fā)問題,因?yàn)樽x取和保存狀態(tài)這兩個(gè)操作不是原子的。(Wiki 解釋:所謂原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作;這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何 context switch 線程切換。)
這個(gè)時(shí)候就要使用到分布式鎖來限制程序的并發(fā)執(zhí)行。
2.分布式鎖
(1)分布式鎖本質(zhì)上要實(shí)現(xiàn)的目標(biāo)就是在 Redis 里面占一個(gè)“茅坑”,當(dāng)別的進(jìn)程也要來占時(shí),發(fā)現(xiàn)已經(jīng)有人蹲在那里了,就只好放棄或者稍后再試。
(2)占坑一般是使用 setnx(set if not exists) 指令,只允許被一個(gè)客戶端占坑。先來先占, 用完了,再調(diào)用 del 指令釋放茅坑。
> setnx lock:codehole true OK ... do something critical ... > del lock:codehole (integer) 1這樣的代碼可能出現(xiàn)的第一個(gè)問題就是邏輯執(zhí)行到中間出現(xiàn)問題而沒有執(zhí)行del,這樣就會(huì)造成死鎖,鎖得不到釋放
(3)通過添加過期時(shí)間自動(dòng)將鎖釋放
> setnx lock:codehole true OK > expire lock:codehole 5 ... do something critical ... > del lock:codehole (integer) 1這樣的代碼還是可能出現(xiàn)問題:如果在 setnx 和 expire 之間服務(wù)器進(jìn)程突然掛掉了,可能是因?yàn)闄C(jī)器掉電或者是被人為殺掉的,就會(huì)導(dǎo)致 expire 得不到執(zhí)行,也會(huì)造成死鎖。
(4)將setnx和expire兩條指令變成一個(gè)原子指令
如果這兩條指令可以一起執(zhí)行就不會(huì)出現(xiàn)問題。也許你會(huì)想到用 Redis 事務(wù)來解決。但是這里不行,因?yàn)?expire是依賴于 setnx 的執(zhí)行結(jié)果的,如果 setnx 沒搶到鎖,expire 是不應(yīng)該執(zhí)行的。事務(wù)里沒有 if-else 分支邏輯,事務(wù)的特點(diǎn)是一口氣執(zhí)行,要么全部執(zhí)行要么一個(gè)都不執(zhí)行。
Redis 2.8 版本中作者加入了 set 指令的擴(kuò)展參數(shù),使得 setnx 和expire 指令可以一起執(zhí)行,解決了將兩條指令變成一個(gè)原子操作。 > set lock:codehole true ex 5 nx OK ... do something critical ... > dellock:codehole 上面這個(gè)指令就是 setnx 和 expire 組合在一起的原子指令,它就是分布式鎖的奧義所在。
(5)Redis分布式鎖不能解決超時(shí)問題
如果在加鎖和釋放鎖之間的邏輯執(zhí)行的太長,以至于超出了鎖的超時(shí)限制,就會(huì)出現(xiàn)問題。因?yàn)檫@時(shí)候鎖過期了,第二個(gè)線程重新持有了這把鎖,但是緊接著第一個(gè)線程執(zhí)行完了業(yè)務(wù)邏輯,就把鎖給釋放了,第三個(gè)線程就會(huì)在第二個(gè)線程邏輯執(zhí)行完之間拿到了鎖。
為了避免這個(gè)問題,Redis 分布式鎖不要用于較長時(shí)間的任務(wù)。如果真的偶爾出現(xiàn)了,數(shù)據(jù)出現(xiàn)的小波錯(cuò)亂可能需要人工介入解決。
當(dāng)然也有更安全的方案:
為 set 指令的 value 參數(shù)設(shè)置為一個(gè)隨機(jī)數(shù),釋放鎖時(shí)先匹配隨機(jī)數(shù)是否一致,然后再刪除 key。但是匹配 value 和刪除 key 不是一個(gè)原子操作,Redis 也沒有提供類似于delifequals這樣的指令,這就需要使用 Lua 腳本來處理了,因?yàn)?Lua 腳本可以保證連續(xù)多個(gè)指令的原子性執(zhí)行。
tag = random.nextint() # 隨機(jī)數(shù) if redis.set(key, tag, nx=True, ex=5): do_something() redis.delifequals(key, tag) # 假象的 delifequals 指令(匹配刪除工作)lua腳本 # delifequals if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) elsereturn 0 end3.可重入性
(1)可重入性是指線程在持有鎖的情況下再次請(qǐng)求加鎖,如果一個(gè)鎖支持同一個(gè)線程的多次加鎖,那么這個(gè)鎖就是可重入的。
(2)Redis 分布式鎖如果要支持可重入,需要對(duì)客戶端的 set 方法進(jìn)行包裝,使用線程的 Threadlocal 變量存儲(chǔ)當(dāng)前持有鎖的計(jì)數(shù)。(Threadlocal 變量可以自行搜索了解,是一個(gè)線程局部變量)
(3)可重入鎖的部分實(shí)現(xiàn)(以下還不是可重入鎖的全部,精確一點(diǎn)還需要考慮內(nèi)存鎖計(jì)數(shù)的過期時(shí)間,代碼復(fù)雜度將會(huì)繼續(xù)升高)
# -*- coding: utf-8 import redis import threadinglocks = threading.local() locks.redis = {}def key_for(user_id):return "account_{}".format(user_id)def _lock(client, key):return bool(client.set(key, True, nx=True, ex=5))def _unlock(client, key):client.delete(key)def lock(client, user_id):key = key_for(user_id)if key in locks.redis:locks.redis[key] += 1return Trueok = _lock(client, key)if not ok:return Falselocks.redis[key] = 1return Truedef unlock(client, user_id):key = key_for(user_id)if key in locks.redis:locks.redis[key] -= 1if locks.redis[key] <= 0:del locks.redis[key]return Truereturn Falseclient = redis.StrictRedis() print "lock", lock(client, "codehole") print "lock", lock(client, "codehole") print "unlock", unlock(client, "codehole") print "unlock", unlock(client, "codehole")以上為學(xué)習(xí)《Redis深度歷險(xiǎn)核心原理和應(yīng)用實(shí)踐》筆記
總結(jié)
以上是生活随笔為你收集整理的应用 1:千帆竞发 ——分布式锁的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何找技术方向
- 下一篇: 项目管理之-WBS(Work Break