Redis事物分布式锁
redis事物介紹
1. redis事物是可以一次執(zhí)行多個(gè)命令,本質(zhì)是一組命令的集合。
2. 一個(gè)事務(wù)中的所有命令都會(huì)序列化,按順序串行化的執(zhí)行而不會(huì)被其他命令插入
作用:一個(gè)隊(duì)列中,一次性、順序性、排他性的執(zhí)行一系列命令
exec指令的作用:
如果關(guān)鍵變量被人改動(dòng)過,exec指令就會(huì)返回null回復(fù)告知客戶端事物執(zhí)行失敗,這個(gè)時(shí)候客戶端會(huì)選擇重試
multi 指令基本使用
1. 下面指令演示了一個(gè)完整的事物過程,所有指令在exec前不執(zhí)行,而是緩存在服務(wù)器的一個(gè)事物隊(duì)列中
2. 服務(wù)器一旦收到exec指令才開始執(zhí)行事物隊(duì)列,執(zhí)行完畢后一次性返回所有結(jié)果
3. 因?yàn)閞edis是單線程的,所以不必?fù)?dān)心自己在執(zhí)行隊(duì)列是被打斷,可以保證這樣的“原子性”
注:redis事物在遇到指令失敗后,后面的指令會(huì)繼續(xù)執(zhí)行
# Multi 命令用于標(biāo)記一個(gè)事務(wù)塊的開始事務(wù)塊內(nèi)的多條命令會(huì)按照先后順序被放進(jìn)一個(gè)隊(duì)列當(dāng)中,最后由 EXEC 命令原子性( atomic )地執(zhí)行 > multi(開始一個(gè)redis事物) incr books incr books > exec (執(zhí)行事物) > discard (丟棄事物)在命令行 測(cè)試redis事物
[root@redis ~]# redis-cli 127.0.0.1:6379> multi OK 127.0.0.1:6379> set test 123 QUEUED 127.0.0.1:6379> exec 1) OK 127.0.0.1:6379> get test "123" 127.0.0.1:6379> multi OK 127.0.0.1:6379> set test 456 QUEUED 127.0.0.1:6379> discard OK 127.0.0.1:6379> get test "123" 127.0.0.1:6379>使用Python測(cè)試redis事物
#! /usr/bin/env python # -*- coding: utf-8 -*- import redis r = redis.Redis(host='127.0.0.1') pipe = r.pipeline() pipe.multi() #開啟事務(wù) pipe.set('key2', 4) #存儲(chǔ)子命令 pipe.execute() #執(zhí)行事務(wù) print(r.get('key2'))注:mysql的rollback與redis的discard的區(qū)別
1)redis如果在一個(gè)事務(wù)中的命令出現(xiàn)錯(cuò)誤,那么所有的命令都不會(huì)執(zhí)行;
2)redis如果在一個(gè)事務(wù)中出現(xiàn)運(yùn)行錯(cuò)誤,那么正確的命令會(huì)被執(zhí)行。
watch 指令作用
實(shí)質(zhì):WATCH 只會(huì)在數(shù)據(jù)被其他客戶端搶先修改了的情況下通知執(zhí)行命令的這個(gè)客戶端(通過 WatchError 異常)但不會(huì)阻止其他客戶端對(duì)數(shù)據(jù)的修改
分布式鎖
1. 分布式鎖本質(zhì)是占一個(gè)坑,當(dāng)別的進(jìn)程也要來占坑時(shí)發(fā)現(xiàn)已經(jīng)被占,就會(huì)放棄或者稍后重試
2. 占坑一般使用 setnx(set if not exists)指令,只允許一個(gè)客戶端占坑
3. 先來先占,用完了在調(diào)用del指令釋放坑
> setnx lock:codehole true .... do something critical .... > del lock:codehole4. 但是這樣有一個(gè)問題,如果邏輯執(zhí)行到中間出現(xiàn)異常,可能導(dǎo)致del指令沒有被調(diào)用,這樣就會(huì)陷入死鎖,鎖永遠(yuǎn)無法釋放
5. 為了解決死鎖問題,我們拿到鎖時(shí)可以加上一個(gè)expire過期時(shí)間,這樣即使出現(xiàn)異常,當(dāng)?shù)竭_(dá)過期時(shí)間也會(huì)自動(dòng)釋放鎖
> setnx lock:codehole true > expire lock:codehole 5 .... do something critical .... > del lock:codehole6. 這樣又有一個(gè)問題,setnx和expire是兩條指令而不是原子指令,如果兩條指令之間進(jìn)程掛掉依然會(huì)出現(xiàn)死鎖
7. 為了治理上面亂象,在redis 2.8中加入了set指令的擴(kuò)展參數(shù),使setnx和expire指令可以一起執(zhí)行
> set lock:codehole true ex 5 nx ''' do something ''' > del lock:codeholeredis解決超賣問題
1、使用reids的 watch + multi 指令實(shí)現(xiàn)
watch+multi解決超賣問題
1)原理
1. 當(dāng)用戶購買時(shí),通過 WATCH 監(jiān)聽用戶庫存,如果庫存在watch監(jiān)聽后發(fā)生改變,就會(huì)捕獲異常而放棄對(duì)庫存減一操作
2. 如果庫存沒有監(jiān)聽到變化并且數(shù)量大于1,則庫存數(shù)量減一,并執(zhí)行任務(wù)
2)弊端
1. Redis 在嘗試完成一個(gè)事務(wù)的時(shí)候,可能會(huì)因?yàn)槭聞?wù)的失敗而重復(fù)嘗試重新執(zhí)行
2. 保證商品的庫存量正確是一件很重要的事情,但是單純的使用 WATCH 這樣的機(jī)制對(duì)服務(wù)器壓力過大
2、使用reids的 watch + multi + setnx 指令實(shí)現(xiàn)
1)為什么要自己構(gòu)建鎖
1. 雖然有類似的 SETNX 命令可以實(shí)現(xiàn) Redis 中的鎖的功能,但他鎖提供的機(jī)制并不完整
2. 并且setnx也不具備分布式鎖的一些高級(jí)特性,還是得通過我們手動(dòng)構(gòu)建
2)創(chuàng)建一個(gè)redis鎖
1. 在 Redis 中,可以通過使用 SETNX 命令來構(gòu)建鎖:rs.setnx(lock_name, uuid值)
2. 而鎖要做的事情就是將一個(gè)隨機(jī)生成的 128 位 UUID 設(shè)置位鍵的值,防止該鎖被其他進(jìn)程獲取
3)釋放鎖
1. 鎖的刪除操作很簡(jiǎn)單,只需要將對(duì)應(yīng)鎖的 key 值獲取到的 uuid 結(jié)果進(jìn)行判斷驗(yàn)證
2. 符合條件(判斷uuid值)通過 delete 在 redis 中刪除即可,pipe.delete(lockname)
3. 此外當(dāng)其他用戶持有同名鎖時(shí),由于 uuid 的不同,經(jīng)過驗(yàn)證后不會(huì)錯(cuò)誤釋放掉別人的鎖
4)解決鎖無法釋放問題
1. 在之前的鎖中,還出現(xiàn)這樣的問題,比如某個(gè)進(jìn)程持有鎖之后突然程序崩潰,那么會(huì)導(dǎo)致鎖無法釋放
2. 而其他進(jìn)程無法持有鎖繼續(xù)工作,為了解決這樣的問題,可以在獲取鎖的時(shí)候加上鎖的超時(shí)功能
watch + multi + setnx解決超賣問題
#! /usr/bin/env python # -*- coding: utf-8 -*- import redis import uuid import time# 1.初始化連接函數(shù) def get_conn(host,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: 連接對(duì)象lock_name: 鎖標(biāo)識(shí)acquire_time: 過期超時(shí)時(shí)間return -> False 獲鎖失敗 or True 獲鎖成功'''identifier = str(uuid.uuid4())end = time.time() + expire_timewhile time.time() < end:# 當(dāng)獲取鎖的行為超過有效時(shí)間,則退出循環(huán),本次取鎖失敗,返回Falseif rs.setnx(lock_name, identifier): # 嘗試取得鎖return identifiertime.sleep(.001)return False# 3. 釋放鎖 def release_lock(rs, lockname, identifier):'''rs: 連接對(duì)象lockname: 鎖標(biāo)識(shí)identifier: 鎖的value值,用來校驗(yàn)'''pipe = rs.pipeline(True)try:pipe.watch(lockname)if rs.get(lockname).decode() == identifier: # 防止其他進(jìn)程同名鎖被誤刪pipe.multi() # 開啟事務(wù)pipe.delete(lockname)pipe.execute()return True # 刪除鎖pipe.unwatch() # 取消事務(wù)except Exception as e:passreturn False # 刪除失敗'''在業(yè)務(wù)函數(shù)中使用上面的鎖''' def sale(rs):start = time.time() # 程序啟動(dòng)時(shí)間with rs.pipeline() as p:'''通過管道方式進(jìn)行連接多條命令執(zhí)行結(jié)束,一次性獲取結(jié)果'''while True:lock = acquire_lock(rs, 'lock')if not lock: # 持鎖失敗continuetry:count = int(rs.get('apple')) # 取量p.set('apple', count-1) # 減量p.execute()print('當(dāng)前庫存量: %s' % count)breakfinally:release_lock(rs, 'lock', lock)print('[time]: %.2f' % (time.time() - start))rs = redis.Redis(host='127.0.0.1', port=6379) # 連接redis rs.set('apple',1000) # # 首先在redis中設(shè)置某商品apple 對(duì)應(yīng)數(shù)量value值為1000 sale(rs)優(yōu)化:給分布式鎖加超時(shí)時(shí)間防止死鎖
def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10):'''rs: 連接對(duì)象lock_name: 鎖標(biāo)識(shí)acquire_time: 過期超時(shí)時(shí)間locked_time: 鎖的有效時(shí)間return -> False 獲鎖失敗 or True 獲鎖成功'''identifier = str(uuid.uuid4())end = time.time() + expire_timewhile time.time() < end:# 當(dāng)獲取鎖的行為超過有效時(shí)間,則退出循環(huán),本次取鎖失敗,返回Falseif rs.setnx(lock_name, identifier): # 嘗試取得鎖# print('鎖已設(shè)置: %s' % identifier)rs.expire(lock_name, locked_time)return identifiertime.sleep(.001)return False來自于原址參考
總結(jié)
以上是生活随笔為你收集整理的Redis事物分布式锁的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于Redis的单点登录
- 下一篇: 布隆过滤器Redis缓存穿透雪崩击穿热点