Redis如何實(shí)現(xiàn)分布式鎖
今天我們來聊一聊分布式鎖的那些事。
相信大家對(duì)鎖已經(jīng)不陌生了,我們?cè)诙嗑€程環(huán)境中,如果需要對(duì)同一個(gè)資源進(jìn)行操作,為了避免數(shù)據(jù)不一致,我們需要在操作共享資源之前進(jìn)行加鎖操作。在計(jì)算機(jī)科學(xué)中,鎖(lock)或互斥(mutex)是一種同步機(jī)制,用于在有許多執(zhí)行線程的環(huán)境中強(qiáng)制對(duì)資源的訪問限制。
比如你去相親,發(fā)現(xiàn)你和一大哥同時(shí)和一個(gè)女的相親,那怎么行呢...,搞不好還要被揍一頓。
那什么是分布式鎖呢。當(dāng)多個(gè)客戶端需要爭(zhēng)搶鎖時(shí),我們就需要分布式鎖。這把鎖不能是某個(gè)客戶端本地的鎖,否則的話,其它客戶端是無法訪問的。所以分布式鎖是需要存儲(chǔ)在共享存儲(chǔ)系統(tǒng)中的,比如Redis、Zookeeper等,可以被多個(gè)客戶端共享訪問和獲取。今天我們就來看一下如何使用Redis來實(shí)現(xiàn)分布式鎖。
一、前言
在正式開始之前,我們先來了解兩個(gè)Redis的命令:
SETNX key value
這個(gè)命名的含義是,當(dāng)key存在時(shí),不做任何賦值操作;當(dāng)key不存在時(shí),就創(chuàng)建key,并賦值成value,即(不存在即設(shè)置)。
SET key value [EX seconds | PX milliseconds] NX
SET后加NX選項(xiàng),就和SETNX命令類似了,也實(shí)現(xiàn)不存在即設(shè)置的功能。此外,這個(gè)命令在執(zhí)行時(shí),可以通過EX或者PX設(shè)置鍵值對(duì)的過期時(shí)間。
二、正文
開始之前,我們先引入一個(gè)場(chǎng)景:
假設(shè)要給某個(gè)商品舉行秒殺活動(dòng),我們事先把庫存數(shù)據(jù)100已經(jīng)存入到了redis中,我們現(xiàn)在需要來進(jìn)行庫存扣減。
如圖所示,我們假設(shè)有1000個(gè)客戶端來進(jìn)行庫存扣減操作,那我們?cè)撊绾巫?,才能保證庫存扣減順序一致且不會(huì)超扣呢。
我們首先想到的就是加鎖,在進(jìn)行庫存扣減之前,我們先拿到鎖,然后進(jìn)行扣減,最后再釋放鎖。在redis中我們創(chuàng)建一個(gè)key來代表一個(gè)鎖變量,然后對(duì)應(yīng)的值來表示鎖變量的值。我們來看一下如何進(jìn)行加鎖。
假設(shè)1000個(gè)客戶端同時(shí)進(jìn)行加鎖請(qǐng)求。因?yàn)閞edis使用單線程來處理請(qǐng)求,所以redis會(huì)串行執(zhí)行他們的請(qǐng)求操作。假設(shè)redis先處理客戶端2的請(qǐng)求,讀取lock_key的值,發(fā)現(xiàn)lock_key為0,所以客戶端2就把lock_key的value設(shè)置成1,表示已經(jīng)進(jìn)行了加鎖操作。如果此時(shí)客戶端3被處理,發(fā)現(xiàn)lock_key的值已經(jīng)為1了,所以就返回加鎖失敗的信息。
當(dāng)拿到鎖的客戶端2處理完共享資源后,就要進(jìn)行釋放鎖的操作,釋放鎖很簡(jiǎn)單,就是將lock_key重新設(shè)置為0。
由于加鎖操作包含了三個(gè)操作(讀取鎖變量、判斷鎖變量的值以及把鎖變量的值設(shè)置成1),而這三個(gè)操作在執(zhí)行的過程中需要保證原子性。那怎么保證原子性呢?
我們可以使用SETNX命令來實(shí)現(xiàn)加鎖操作,SETNX命令表示key不存在時(shí)就創(chuàng)建,key存在時(shí)就不做任何賦值操作,當(dāng)加鎖時(shí)候,我們執(zhí)行
SETNX lock_key 1
對(duì)于釋放鎖操作來說,我們可以使用DEL命令來刪除鎖變量。比如客戶端2進(jìn)行加鎖,執(zhí)行SETNX lock_key 1,如果lock_key不存在,則會(huì)創(chuàng)建lock_key,返回加鎖成功,此時(shí)客戶端2可以進(jìn)行共享資源的訪問。如果這時(shí)客戶端1來發(fā)起請(qǐng)求加鎖操作,而此時(shí)lock_key已經(jīng)存在,SETNX lock_key 1不做任何賦值操作操作,返回加鎖失敗,所以客戶端1加鎖失敗。當(dāng)客戶端2執(zhí)行完共享資源訪問后,執(zhí)行DEL命令來釋放鎖。此時(shí)當(dāng)有其它客戶端再來訪問時(shí),lock_key已經(jīng)不存在了,就可以進(jìn)行正常的加鎖操作了。所以,我們可以使用SETNX和DEL命令組合來進(jìn)行加鎖和釋放鎖的操作。
不過這里有兩個(gè)問題:
1.當(dāng)某個(gè)客戶端執(zhí)行完SETNX命令、加鎖后,此時(shí)發(fā)生了異常,結(jié)果一直沒有執(zhí)行DEL操作命令來釋放鎖。因此,這個(gè)客戶端一直占用著這個(gè)鎖,其它客戶端無法拿到鎖。
解決這個(gè)問題,一個(gè)有效的方法就是,給鎖變量設(shè)置一個(gè)過期時(shí)間。這樣一來,即使持有鎖的客戶端發(fā)生了異常,無法主動(dòng)的釋放鎖,Redis也會(huì)根據(jù)鎖變量的過期時(shí)間把它刪除。其它客戶端在鎖變量過期后,就可以重新進(jìn)行加鎖操作了。
2.如果客戶端1執(zhí)行了SETNX命令加鎖后。如果此時(shí)客戶端2執(zhí)行DEL命令刪除鎖,這時(shí),客戶端A的鎖就被誤釋放了。這是我們不能接受的。
為了解決這個(gè)問題,我們需要能區(qū)分來自不同客戶端的鎖操作。我們?cè)撊绾巫瞿??我們可以給每個(gè)客戶端生成一個(gè)唯一值,在進(jìn)行加鎖時(shí),我們把鎖變量賦值成這個(gè)唯一值。這樣在釋放鎖的時(shí)候,客戶端需要判斷,當(dāng)前鎖變量的值是否和自己的唯一標(biāo)識(shí)相等,在相等的情況下,才能釋放鎖。
下面來看一下如何在Redis中進(jìn)行實(shí)現(xiàn)。我們可以使用SET加EX/PX和NX選項(xiàng),來進(jìn)行加鎖操作。
SET lock_key uuid NX PX 100
其中l(wèi)ock_key是鎖變量,uuid表示客戶端的唯一標(biāo)識(shí),PX 100表示100ms過期。由于我們?cè)卺尫沛i時(shí)需要對(duì)比客戶端的標(biāo)識(shí)和鎖變量的值是否一致,這包含了多個(gè)操作,為了保證原子性,我們需要使用lua腳本,下面是lua腳本的實(shí)現(xiàn)。
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
其中KEY[1]表示lock_key,ARGV[1]表示當(dāng)前客戶端的唯一標(biāo)識(shí),這兩個(gè)值是我們?cè)趫?zhí)行l(wèi)ua腳本時(shí)作為參數(shù)傳入的。下面我們來看一下完整的代碼實(shí)現(xiàn)。
import redis import traceback import uuid import time class Inventory(object): def __init__(self): pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) client = redis.StrictRedis(connection_pool=pool, max_connections=20) self.client=client self.uuid=str(uuid.uuid1()) print(self.uuid) self.key="lock_key" self.inventory_key="inventory" def unlock(self): unlock_script="" \ "if redis.call(\"get\",KEYS[1]) == ARGV[1] then" \ " return redis.call(\"del\",KEYS[1])" \ "else" \ " return 0 " \ "end" try: unlock_cmd=self.client.register_script(unlock_script) result=unlock_cmd(keys=[self.key],args=[self.uuid]) if result==1: print("釋放成功") else: print("釋放出錯(cuò)") except: print(traceback.format_exc()) def lock(self): try: while True: result=self.client.set(self.key,self.uuid,px=100,nx=True) print(result) if result==1: break print("sleep 1s") time.sleep(1) print("加鎖成功") return True except: print(traceback.format_exc()) def inventory(self): if self.lock(): print("庫存扣減") self.client.decr(self.inventory_key) print("扣減完成") self.unlock() inv=Inventory() inv.inventory()
到此,我們就把Redis實(shí)現(xiàn)分布式鎖就聊完了。既然都讀到了這里,不妨給個(gè)「三連」吧,你的三連就是我最大的動(dòng)力。
到此這篇關(guān)于Redis如何實(shí)現(xiàn)分布式鎖的文章就介紹到這了,更多相關(guān)Redis 分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Redis實(shí)現(xiàn)微信步數(shù)排行榜功能
這篇文章主要介紹了使用Redis實(shí)現(xiàn)微信步數(shù)排行榜功能,本文通過圖文實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06關(guān)于在Redis中使用Pipelining加速查詢的問題
這篇文章主要介紹了在Redis中使用Pipelining加速查詢,Redis是一個(gè)client-server模式的TCP服務(wù),也被稱為Request/Response協(xié)議的實(shí)現(xiàn),本文通過一個(gè)例子給大家詳細(xì)介紹,感興趣的朋友一起看看吧2022-05-05解決Redis的緩存與數(shù)據(jù)庫雙寫不一致問題
在使用緩存和數(shù)據(jù)庫配合時(shí),常見的CacheAsidePattern模式要求讀操作先訪問緩存,若缺失再讀數(shù)據(jù)庫并更新緩存;寫操作則是先寫數(shù)據(jù)庫后刪除緩存,但這種模式可能導(dǎo)致緩存與數(shù)據(jù)庫間的雙寫不一致問題2024-10-10Redis實(shí)現(xiàn)消息的發(fā)布訂閱原理分析
本文主要介紹了Redis實(shí)現(xiàn)消息的發(fā)布訂閱原理分析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07解密Redis助力雙11背后電商秒殺系統(tǒng)(推薦)
這篇文章主要介紹了解密Redis助力雙11背后電商秒殺系統(tǒng),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10Redis?sentinel哨兵集群的實(shí)現(xiàn)步驟
本文主要介紹了Redis?sentinel哨兵集群的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07