redis mysql 解决超卖_Redis 分布式锁解决超卖问题
Redis 分布式鎖解決超賣問題
1,Redis 事物介紹
1. Redis 事物是可以一次執(zhí)行多個(gè)命令, 本質(zhì)是一組命令的集合.
2. 一個(gè)事務(wù)中的所有命令都會(huì)序列化, 按順序串行化的執(zhí)行而不會(huì)被其他命令插入
作用: 一個(gè)隊(duì)列中, 一次性, 順序性, 排他性的執(zhí)行一系列命令
2,multi 指令基本使用
1. 下面指令演示了一個(gè)完整的事物過程, 所有指令在 exec 前不執(zhí)行, 而是緩存在服務(wù)器的一個(gè)事物隊(duì)列中
2. 服務(wù)器一旦收到 exec 指令才開始執(zhí)行事物隊(duì)列, 執(zhí)行完畢后一次性返回所有結(jié)果
3. 因?yàn)?Redis 是單線程的, 所以不必?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(丟棄事物)
[[emailprotected]~]#Redis-cli
127.0.0.1:6379>multi
OK
127.0.0.1:6379>settest123
QUEUED
127.0.0.1:6379>exec
1)OK
127.0.0.1:6379>gettest
"123"
127.0.0.1:6379>multi
OK
127.0.0.1:6379>settest456
QUEUED
127.0.0.1:6379>discard
OK
127.0.0.1:6379>gettest
"123"
127.0.0.1:6379>
在命令行測(cè)試 Redis 事物
#! /usr/bin/env python
# -*- coding: utf-8 -*-
importRedis
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'))
使用 python 測(cè)試 Redis 事物
注: MySQL 的 rollback 與 Redis 的 discard 的區(qū)別
1. MySQL 回滾為 sql 全部成功才執(zhí)行, 一條 sql 失敗則全部失敗, 執(zhí)行 rollback 后所有語句造成的影響消失
2. Redis 的 discard 只是結(jié)束本次事務(wù), 正確命令造成的影響仍然還在.
1)Redis 如果在一個(gè)事務(wù)中的命令出現(xiàn)錯(cuò)誤, 那么所有的命令都不會(huì)執(zhí)行;
2)Redis 如果在一個(gè)事務(wù)中出現(xiàn)運(yùn)行錯(cuò)誤, 那么正確的命令會(huì)被執(zhí)行.
3,watch 指令作用
實(shí)質(zhì): WATCH 只會(huì)在數(shù)據(jù)被其他客戶端搶先修改了的情況下通知執(zhí)行命令的這個(gè)客戶端 (通過 WatchError 異常) 但不會(huì)阻止其他客戶端對(duì)數(shù)據(jù)的修改
1.watch 其實(shí)就是 Redis 提供的一種樂觀鎖, 可以解決并發(fā)修改問題
2. watch 會(huì)在事物開始前盯住一個(gè)或多個(gè)關(guān)鍵變量, 當(dāng)服務(wù)器收到 exec 指令要順序執(zhí)行緩存中的事物隊(duì)列時(shí), Redis 會(huì)檢查關(guān)鍵變量自 watch 后是否被修改
3. WATCH 只會(huì)在數(shù)據(jù)被其他客戶端搶先修改了的情況下通知執(zhí)行命令的這個(gè)客戶端 (通過 WatchError 異常) 但不會(huì)阻止其他客戶端對(duì)數(shù)據(jù)的修改
1.2 setnx(Redis 分布式鎖)
1, 分布式鎖
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 指令釋放坑>setnxlock:codeholetrue
....dosomething critical....
>dellock:codehole
4. 但是這樣有一個(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)釋放鎖>setnxlock:codeholetrue
>expirelock:codehole5
....dosomething critical....
>dellock:codehole
6. 這樣又有一個(gè)問題, setnx 和 expire 是兩條指令而不是原子指令, 如果兩條指令之間進(jìn)程掛掉依然會(huì)出現(xiàn)死鎖
7. 為了治理上面亂象, 在 Redis 2.8 中加入了 set 指令的擴(kuò)展參數(shù), 使 setnx 和 expire 指令可以一起執(zhí)行>setlock:codeholetrueex5nx
'''do something'''
>dellock:codehole
1.3 Redis 解決超賣問題
1, 使用 reids 的 watch + multi 指令實(shí)現(xiàn)
#! /usr/bin/env python
# -*- coding: utf-8 -*-
importRedis
defsale(rs):
whileTrue:
withrs.pipeline()asp:
try:
p.watch('apple')# 監(jiān)聽 key 值為 apple 的數(shù)據(jù)數(shù)量改變
count=int(rs.get('apple'))
print('拿取到了蘋果的數(shù)量: %d'%count)
p.multi()# 事務(wù)開始
ifcount>0:# 如果此時(shí)還有庫存
p.set('apple',count-1)
p.execute()# 執(zhí)行事務(wù)
p.unwatch()
break# 當(dāng)庫存成功減一或沒有庫存時(shí)跳出執(zhí)行循環(huán)
exceptExceptionase:# 當(dāng)出現(xiàn) watch 監(jiān)聽值出現(xiàn)修改時(shí), 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 對(duì)應(yīng)數(shù)量 value 值為 1000
sale(rs)
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. 鎖的刪除操作很簡單, 只需要將對(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í)功能
#! /usr/bin/env python
# -*- coding: utf-8 -*-
importRedis
importuuid
importtime
# 1. 初始化連接函數(shù)
defget_conn(host,port=6379):
rs=Redis.Redis(host=host,port=port)
returnrs
# 2. 構(gòu)建 Redis 鎖
defacquire_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_time
whiletime.time()
# 當(dāng)獲取鎖的行為超過有效時(shí)間, 則退出循環(huán), 本次取鎖失敗, 返回 False
ifrs.setnx(lock_name,identifier):# 嘗試取得鎖
returnidentifier
time.sleep(.001)
returnFalse
# 3. 釋放鎖
defrelease_lock(rs,lockname,identifier):
'''
rs: 連接對(duì)象
lockname: 鎖標(biāo)識(shí)
identifier: 鎖的 value 值, 用來校驗(yàn)
'''
pipe=rs.pipeline(True)
try:
pipe.watch(lockname)
ifrs.get(lockname).decode()==identifier:# 防止其他進(jìn)程同名鎖被誤刪
pipe.multi()# 開啟事務(wù)
pipe.delete(lockname)
pipe.execute()
returnTrue# 刪除鎖
pipe.unwatch()# 取消事務(wù)
exceptExceptionase:
pass
returnFalse# 刪除失敗
'''在業(yè)務(wù)函數(shù)中使用上面的鎖'''
defsale(rs):
start=time.time()# 程序啟動(dòng)時(shí)間
withrs.pipeline()asp:
'''
通過管道方式進(jìn)行連接
多條命令執(zhí)行結(jié)束, 一次性獲取結(jié)果
'''
whileTrue:
lock=acquire_lock(rs,'lock')
ifnotlock:# 持鎖失敗
continue
try:
count=int(rs.get('apple'))# 取量
p.set('apple',count-1)# 減量
p.execute()
print('當(dāng)前庫存量: %s'%count)
break
finally:
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)
setnx+watch+multi 解決超賣問題
defacquire_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_time
whiletime.time()
# 當(dāng)獲取鎖的行為超過有效時(shí)間, 則退出循環(huán), 本次取鎖失敗, 返回 False
ifrs.setnx(lock_name,identifier):# 嘗試取得鎖
# print('鎖已設(shè)置: %s' % identifier)
rs.expire(lock_name,locked_time)
returnidentifier
time.sleep(.001)
returnFalse
優(yōu)化: 給分布式鎖加超時(shí)時(shí)間防止死鎖
來源: http://www.bubuko.com/infodetail-3475503.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的redis mysql 解决超卖_Redis 分布式锁解决超卖问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python语言结构_Python语言表
- 下一篇: helm滚动更新mysql_kubern