应用 1:千帆竞发 ——分布式锁
1.分布式應用進行邏輯處理時經常會遇到并發問題
比如一個操作要修改用戶的狀態,修改狀態需要先讀出用戶的狀態,在內存里進行修
改,改完了再存回去。如果這樣的操作同時進行了,就會出現并發問題,因為讀取和保存狀態這兩個操作不是原子的。(Wiki 解釋:所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch 線程切換。)
這個時候就要使用到分布式鎖來限制程序的并發執行。
2.分布式鎖
(1)分布式鎖本質上要實現的目標就是在 Redis 里面占一個“茅坑”,當別的進程也要來占時,發現已經有人蹲在那里了,就只好放棄或者稍后再試。
(2)占坑一般是使用 setnx(set if not exists) 指令,只允許被一個客戶端占坑。先來先占, 用完了,再調用 del 指令釋放茅坑。
> setnx lock:codehole true OK ... do something critical ... > del lock:codehole (integer) 1這樣的代碼可能出現的第一個問題就是邏輯執行到中間出現問題而沒有執行del,這樣就會造成死鎖,鎖得不到釋放
(3)通過添加過期時間自動將鎖釋放
> setnx lock:codehole true OK > expire lock:codehole 5 ... do something critical ... > del lock:codehole (integer) 1這樣的代碼還是可能出現問題:如果在 setnx 和 expire 之間服務器進程突然掛掉了,可能是因為機器掉電或者是被人為殺掉的,就會導致 expire 得不到執行,也會造成死鎖。
(4)將setnx和expire兩條指令變成一個原子指令
如果這兩條指令可以一起執行就不會出現問題。也許你會想到用 Redis 事務來解決。但是這里不行,因為 expire是依賴于 setnx 的執行結果的,如果 setnx 沒搶到鎖,expire 是不應該執行的。事務里沒有 if-else 分支邏輯,事務的特點是一口氣執行,要么全部執行要么一個都不執行。
Redis 2.8 版本中作者加入了 set 指令的擴展參數,使得 setnx 和expire 指令可以一起執行,解決了將兩條指令變成一個原子操作。 > set lock:codehole true ex 5 nx OK ... do something critical ... > dellock:codehole 上面這個指令就是 setnx 和 expire 組合在一起的原子指令,它就是分布式鎖的奧義所在。
(5)Redis分布式鎖不能解決超時問題
如果在加鎖和釋放鎖之間的邏輯執行的太長,以至于超出了鎖的超時限制,就會出現問題。因為這時候鎖過期了,第二個線程重新持有了這把鎖,但是緊接著第一個線程執行完了業務邏輯,就把鎖給釋放了,第三個線程就會在第二個線程邏輯執行完之間拿到了鎖。
為了避免這個問題,Redis 分布式鎖不要用于較長時間的任務。如果真的偶爾出現了,數據出現的小波錯亂可能需要人工介入解決。
當然也有更安全的方案:
為 set 指令的 value 參數設置為一個隨機數,釋放鎖時先匹配隨機數是否一致,然后再刪除 key。但是匹配 value 和刪除 key 不是一個原子操作,Redis 也沒有提供類似于delifequals這樣的指令,這就需要使用 Lua 腳本來處理了,因為 Lua 腳本可以保證連續多個指令的原子性執行。
tag = random.nextint() # 隨機數 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)可重入性是指線程在持有鎖的情況下再次請求加鎖,如果一個鎖支持同一個線程的多次加鎖,那么這個鎖就是可重入的。
(2)Redis 分布式鎖如果要支持可重入,需要對客戶端的 set 方法進行包裝,使用線程的 Threadlocal 變量存儲當前持有鎖的計數。(Threadlocal 變量可以自行搜索了解,是一個線程局部變量)
(3)可重入鎖的部分實現(以下還不是可重入鎖的全部,精確一點還需要考慮內存鎖計數的過期時間,代碼復雜度將會繼續升高)
# -*- 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")以上為學習《Redis深度歷險核心原理和應用實踐》筆記
總結
以上是生活随笔為你收集整理的应用 1:千帆竞发 ——分布式锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何找技术方向
- 下一篇: 项目管理之-WBS(Work Break