詳解redis中的鎖以及使用場景
分布式鎖
什么是分布式鎖?
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。
為什么要使用分布式鎖?
為了保證共享資源的數(shù)據(jù)一致性。
什么場景下使用分布式鎖?
數(shù)據(jù)重要且要保證一致性
如何實現(xiàn)分布式鎖?
主要介紹使用redis來實現(xiàn)分布式鎖
redis事務(wù)
redis事務(wù)介紹:
1.redis事務(wù)可以一次執(zhí)行多個命令,本質(zhì)是一組命令的集合。
2.一個事務(wù)中的所有命令都會序列化,按順序串行化的執(zhí)行而不會被其他命令插入
**作用:**一個隊列中,一次性、順序性、排他性的執(zhí)行一系列命令
multi指令的使用
1. 下面指令演示了一個完整的事物過程,所有指令在exec前不執(zhí)行,而是緩存在服務(wù)器的一個事物隊列中
2. 服務(wù)器一旦收到exec指令才開始執(zhí)行事物隊列,執(zhí)行完畢后一次性返回所有結(jié)果
3. 因為redis是單線程的,所以不必擔(dān)心自己在執(zhí)行隊列是被打斷,可以保證這樣的“原子性”
注:redis事物在遇到指令失敗后,后面的指令會繼續(xù)執(zhí)行
# Multi 命令用于標記一個事務(wù)塊的開始事務(wù)塊內(nèi)的多條命令會按照先后順序被放進一個隊列當(dāng)中,最后由 EXEC 命令原子性( atomic )地執(zhí)行 > multi(開始一個redis事物) incr books incr books > exec (執(zhí)行事物) > discard (丟棄事物)
注:mysql的rollback與redis的discard的區(qū)別
mysql回滾為sql全部成功才執(zhí)行,一條sql失敗則全部失敗,執(zhí)行rollback后所有語句造成的影響消失
redis的discard只是結(jié)束本次事務(wù),正確命令造成的影響仍然還在.
1)redis如果在一個事務(wù)中的命令出現(xiàn)錯誤,那么所有的命令都不會執(zhí)行;
2)redis如果在一個事務(wù)中出現(xiàn)運行錯誤,那么正確的命令會被執(zhí)行。
watch 指令作用
實質(zhì):WATCH 只會在數(shù)據(jù)被其他客戶端搶先修改了的情況下通知執(zhí)行命令的這個客戶端(通過 WatchError 異常)但不會阻止其他客戶端對數(shù)據(jù)的修改
1. watch其實就是redis提供的一種樂觀鎖,可以解決并發(fā)修改問題
2. watch會在事物開始前盯住一個或多個關(guān)鍵變量,當(dāng)服務(wù)器收到exec指令要順序執(zhí)行緩存中的事物隊列時,redis會檢查關(guān)鍵變量自watch后是否被修改
3. WATCH 只會在數(shù)據(jù)被其他客戶端搶先修改了的情況下通知執(zhí)行命令的這個客戶端(通過 WatchError 異常)但不會阻止其他客戶端對數(shù)據(jù)的修改
watch+multi實現(xiàn)樂觀鎖
setnx指令(redis的分布式鎖)
1、分布式鎖
分布式鎖本質(zhì)是占一個坑,當(dāng)別的進程也要來占坑時發(fā)現(xiàn)已經(jīng)被占,就會放棄或者稍后重試
占坑一般使用 setnx(set if not exists)指令,只允許一個客戶端占坑
先來先占,用完了在調(diào)用del指令釋放坑
> setnx lock:codehole true .... do something critical .... > del lock:codehole
但是這樣有一個問題,如果邏輯執(zhí)行到中間出現(xiàn)異常,可能導(dǎo)致del指令沒有被調(diào)用,這樣就會陷入死鎖,鎖永遠無法釋放
為了解決死鎖問題,我們拿到鎖時可以加上一個expire過期時間,這樣即使出現(xiàn)異常,當(dāng)?shù)竭_過期時間也會自動釋放鎖
> setnx lock:codehole true > expire lock:codehole 5 .... do something critical .... > del lock:codehole
這樣又有一個問題,setnx和expire是兩條指令而不是原子指令,如果兩條指令之間進程掛掉依然會出現(xiàn)死鎖
為了治理上面亂象,在redis 2.8中加入了set指令的擴展參數(shù),使setnx和expire指令可以一起執(zhí)行
> set lock:codehole true ex 5 nx ''' do something ''' > del lock:codehole
redis解決超賣問題
1、使用reids的 watch + multi 指令實現(xiàn)
#! /usr/bin/env python # -*- coding: utf-8 -*- import redis def sale(rs): while True: with rs.pipeline() as p: try: p.watch('apple') # 監(jiān)聽key值為apple的數(shù)據(jù)數(shù)量改變 count = int(rs.get('apple')) print('拿取到了蘋果的數(shù)量: %d' % count) p.multi() # 事務(wù)開始 if count> 0 : # 如果此時還有庫存 p.set('apple', count - 1) p.execute() # 執(zhí)行事務(wù) p.unwatch() break # 當(dāng)庫存成功減一或沒有庫存時跳出執(zhí)行循環(huán) except Exception as e: # 當(dāng)出現(xiàn)watch監(jiān)聽值出現(xiàn)修改時,WatchError異常拋出 print('[Error]: %s' % e) continue # 繼續(xù)嘗試執(zhí)行 rs = redis.Redis(host='127.0.0.1', port=6379) # 連接redis rs.set('apple',1000) # # 首先在redis中設(shè)置某商品apple 對應(yīng)數(shù)量value值為1000 sale(rs)
1)原理
1. 當(dāng)用戶購買時,通過 WATCH 監(jiān)聽用戶庫存,如果庫存在watch監(jiān)聽后發(fā)生改變,就會捕獲異常而放棄對庫存減一操作
2. 如果庫存沒有監(jiān)聽到變化并且數(shù)量大于1,則庫存數(shù)量減一,并執(zhí)行任務(wù)
2)弊端
1. Redis 在嘗試完成一個事務(wù)的時候,可能會因為事務(wù)的失敗而重復(fù)嘗試重新執(zhí)行
2. 保證商品的庫存量正確是一件很重要的事情,但是單純的使用 WATCH 這樣的機制對服務(wù)器壓力過大
2、使用reids的 watch + multi + setnx 指令實現(xiàn)
1)為什么要自己構(gòu)建鎖
然有類似的 SETNX 命令可以實現(xiàn) Redis 中的鎖的功能,但他鎖提供的機制并不完整
. 并且setnx也不具備分布式鎖的一些高級特性,還是得通過我們手動構(gòu)建
2)創(chuàng)建一個redis鎖
在 Redis 中,可以通過使用 SETNX 命令來構(gòu)建鎖:rs.setnx(lock_name, uuid值)
. 而鎖要做的事情就是將一個隨機生成的 128 位 UUID 設(shè)置位鍵的值,防止該鎖被其他進程獲取
3)釋放鎖
鎖的刪除操作很簡單,只需要將對應(yīng)鎖的 key 值獲取到的 uuid 結(jié)果進行判斷驗證
. 符合條件(判斷uuid值)通過 delete 在 redis 中刪除即可,pipe.delete(lockname)
3. 此外當(dāng)其他用戶持有同名鎖時,由于 uuid 的不同,經(jīng)過驗證后不會錯誤釋放掉別人的鎖
4)解決鎖無法釋放問題
1. 在之前的鎖中,還出現(xiàn)這樣的問題,比如某個進程持有鎖之后突然程序崩潰,那么會導(dǎo)致鎖無法釋放
2. 而其他進程無法持有鎖繼續(xù)工作,為了解決這樣的問題,可以在獲取鎖的時候加上鎖的超時功能
import redis import uuid import time # 1.初始化連接函數(shù) def get_conn(host="127.0.0.1",port=6379): rs = redis.Redis(host=host, port=port) return rs # 2. 構(gòu)建redis鎖 def acquire_lock(rs, lock_name, expire_time=10): ''' rs: 連接對象 lock_name: 鎖標識 acquire_time: 過期超時時間 return -> False 獲鎖失敗 or True 獲鎖成功 ''' identifier = str(uuid.uuid4()) end = time.time() + expire_time while time.time() < end: # 當(dāng)獲取鎖的行為超過有效時間,則退出循環(huán),本次取鎖失敗,返回False if rs.setnx(lock_name, identifier): # 嘗試取得鎖 return identifier # time.sleep(.001) return False # 3. 釋放鎖 def release_lock(rs, lockname, identifier): ''' rs: 連接對象 lockname: 鎖標識 identifier: 鎖的value值,用來校驗 ''' if rs.get(lockname).decode() == identifier: # 防止其他進程同名鎖被誤刪 rs.delete(lockname) return True # 刪除鎖 else: return False # 刪除失敗 #有過期時間的鎖 def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10): ''' rs: 連接對象 lock_name: 鎖標識 acquire_time: 過期超時時間 locked_time: 鎖的有效時間 return -> False 獲鎖失敗 or True 獲鎖成功 ''' identifier = str(uuid.uuid4()) end = time.time() + expire_time while time.time() < end: # 當(dāng)獲取鎖的行為超過有效時間,則退出循環(huán),本次取鎖失敗,返回False if rs.setnx(lock_name, identifier): # 嘗試取得鎖 # print('鎖已設(shè)置: %s' % identifier) rs.expire(lock_name, locked_time) return identifier time.sleep(.001) return False '''在業(yè)務(wù)函數(shù)中使用上面的鎖''' def sale(rs): start = time.time() # 程序啟動時間 with rs.pipeline() as p: ''' 通過管道方式進行連接 多條命令執(zhí)行結(jié)束,一次性獲取結(jié)果 ''' while 1: lock = acquire_lock(rs, 'lock') if not lock: # 持鎖失敗 continue #開始監(jiān)測"lock" p.watch("lock") try: #開啟事務(wù) p.multi() count = int(rs.get('apple')) # 取量 p.set('apple', count-1) # 減量 # time.sleep(5) #提交事務(wù) p.execute() print('當(dāng)前庫存量: %s' % count) #成功則跳出循環(huán) break except: #事務(wù)失敗對應(yīng)處理 print("修改數(shù)據(jù)失敗") #無論成功與否最終都需要釋放鎖 finally: res = release_lock(rs, 'lock', lock) #釋放鎖成功, if res: print("刪除鎖成功") #釋放鎖失敗,強制刪除 else: print("刪除鎖失敗,強制刪除鎖") res = rs.delete('lock') print(res) print('[time]: %.2f' % (time.time() - start)) rs = redis.Redis(host='127.0.0.1', port=6379) # 連接redis # rs.set('apple',1000) # # 首先在redis中設(shè)置某商品apple 對應(yīng)數(shù)量value值為1000 sale(rs)
優(yōu)化鎖無法釋放的問題,為鎖添加過期時間
def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10): ''' rs: 連接對象 lock_name: 鎖標識 acquire_time: 過期超時時間 locked_time: 鎖的有效時間 return -> False 獲鎖失敗 or True 獲鎖成功 ''' identifier = str(uuid.uuid4()) end = time.time() + expire_time while time.time() < end: # 當(dāng)獲取鎖的行為超過有效時間,則退出循環(huán),本次取鎖失敗,返回False if rs.setnx(lock_name, identifier): # 嘗試取得鎖 # print('鎖已設(shè)置: %s' % identifier) rs.expire(lock_name, locked_time) return identifier time.sleep(.001) return False
關(guān)于redis中的鎖
Watch:監(jiān)測一個key。如果這個key的value改變,那個接下來的事務(wù)操作全部失效
multi: 開啟一個事務(wù)。
Setnx: 跟set一樣都往redis添加一個key。不一定的地方在于:set的時候如果這個值存在,就是修改操作。不存在就是添加操作。setnx:存在的時候不能再次添加,不存在的時候才能添加。
到此這篇關(guān)于詳解redis中的鎖以及使用場景的文章就介紹到這了,更多相關(guān)redis 鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis?bitmap數(shù)據(jù)結(jié)構(gòu)之java對等操作詳解
bitmap是以其高性能出名。其基本原理是一位存儲一個標識,其他衍生知道咱就不說了,而redis就是以這種原生格式存儲的,這篇文章主要介紹了redis?bitmap數(shù)據(jù)結(jié)構(gòu)之java對等操作,需要的朋友可以參考下2022-10-10windows環(huán)境下Redis+Spring緩存實例講解
這篇文章主要為大家詳細介紹了windows環(huán)境下Redis+Spring緩存實例教程,感興趣的小伙伴們可以參考一下2016-04-04