?「第12期」 距離大叔的80期小目標還有68期,今天大叔要跟大家分享的內容是 —— Reids中的事務。同樣,這也是redis中重要指數為四顆星的必備基礎知識點。下面一起來了解一下吧。
?
相信大家對Redis并不陌生了吧,對 Redis五種數據類型(String,Hash,List,Set, SortedSet) 的使用也應該是得心應手了。今天為什么要跟大家聊聊Redis的事務呢?
首先Redis事務在實際的場景應用上也占著比較重要的地位,例如在秒殺場景中,我們就可以利用Redis事務中的watch命令監聽key,實現樂觀鎖,保證不會出現沖突,也防止商品超賣。
另外就是Redis事務也是面試過程中面試官著重照顧的基礎知識對象,假設面試官問你實現Redis事務有哪些方式?事務發生錯誤時Redis是怎么處理的?Redis事務支持回滾嗎等等這些問題,你是否能脫口而出回答上來呢?如果你對這方便的基礎知識有所欠缺,那是不是就栽跟頭了呢?
所以,這就是大叔想聊聊Redis事務的必要性所在。下面大叔將圍繞以下幾點與大家分享:
redis數據類型 面試。官方給出的定義是這樣子的:
Redis事務可以一次執行多個命令, 并且帶有以下兩個重要的保證:
官方腔換成方言就是:
Redis事務提供了一種 “將多個命令打包, 然后一次性、按順序地執行” 的機制, 并且事務在執行的期間不會主動中斷 —— 服務器在執行完事務中的所有命令之后, 才會繼續處理其他客戶端的其他命令。
或者你也可以把Redis事務理解為一個隊列,開啟事務后,往后的提交的Redis命令都會依次入隊,遇到觸發當前事務指令時,隊列中的指令會依次被取出并執行。
「值得注意的是」:
redis支持事務嗎。“事務中的命令要么全部被執行,要么全部都不執行” 這句話單純想表達的是:“事務執行需要對應的觸發條件(命令)”
下面看個例子先整體了解一下Redis事務:
127.0.0.1:6379>?get?name
"zhangsan"
127.0.0.1:6379>?get?sex
"female"
127.0.0.1:6379>?MULTI??#?開啟事務
OK
127.0.0.1:6379>?set?name?dashu
QUEUED????????????????#?命令入隊
127.0.0.1:6379>?set?sex?male
QUEUED????????????????#?命令入隊
127.0.0.1:6379>?EXEC??#?觸發當前事務
1)?OK
2)?OK
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>?get?sex
"male"
127.0.0.1:6379>
了解完Redis事務是什么回事后,接下來我們繼續看看實現Redis事務有哪些方式。
命令模式是實現redis事務比較常見的方式,該方式的主要命令有:MULTI、EXEC、DISCARD、WATCH。
MULTI 命令用于開啟一個事務,它總是返回 OK 。
MULTI 執行之后, 客戶端可以繼續向服務器發送任意多條命令,這些命令不會立即被執行, 而是被放到一個隊列中,等待事務被觸發。
redis事務回滾,EXEC 命令負責觸發并執行事務中的所有命令
EXEC 命令返回的是一個數組, 數組中的每個元素都是執行事務中的命令所產生的回復。 回復元素的先后順序和命令發送的先后順序一致。
DISCARD 命令可以理解為是搞破壞的。當 DISCARD 命令被執行時, 事務會被丟棄, 事務隊列會被清空, 并且客戶端會從事務狀態中退出。
我們看個例子:
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>?MULTI
OK
127.0.0.1:6379>?set?name?saycode
QUEUED
127.0.0.1:6379>?DISCARD
OK
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>
我們可以看到雖然開啟事務后我們重新設置了name的值,但是當我們執行DISCARD命令后,該事務被成功丟棄了,所以當我們再次獲取name的值的時候,我們可以看到它的值并沒有發生改變。
WATCH 命令用于在事務開始之前監視任意數量的鍵,當調用 EXEC 命令執行事務時, 如果任意一個被監視的鍵已經被其他客戶端修改了, 那么整個事務不再執行, 直接返回失敗。
redis怎么用、看例子:
#?客戶端一
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>?get?sex
"male"
127.0.0.1:6379>?WATCH?name?sex
OK
127.0.0.1:6379>?MULTI
OK
127.0.0.1:6379>?set?name?saycode
QUEUED
127.0.0.1:6379>?EXEC
(nil)??????????????????#?事務失敗?
127.0.0.1:6379>?get?sex
"man"
127.0.0.1:6379>?get?name
"dashu"
#---------?這是一條分割線?---------#
#?客戶端二
127.0.0.1:6379>?get?sex
"male"
127.0.0.1:6379>?set?sex?man
OK
從上面執行的結果可以看到,客戶端一中的事務失敗了,事務中所修改的name的值也不成功。主要原因是:調用 EXEC 命令執行事務時,被監控的sex 被客戶端二修改了,所以客戶端一的事務不再執行
在每個代表數據庫的 redis.h/redisDb 結構類型中, 都保存了一個 watched_keys 字典, 字典的鍵是這個數據庫被監視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了所有監視這個鍵的客戶端。
比如說,以下字典就展示了一個 watched_keys 字典的例子:
其中, 鍵 key1 正在被 client2 、 client5 和 client1 三個客戶端監視, 其他一些鍵也分別被其他別的客戶端監視著。
WATCH 命令的作用, 就是將當前客戶端和要監視的鍵在 watched_keys 中進行關聯。
redis有什么用?舉個例子, 如果當前客戶端為 client10086 , 那么當客戶端執行 WATCH key1 key2 時, 前面展示的 watched_keys 將被修改成這個樣子:
通過watched_keys字典, 如果程序想檢查某個鍵是否被監視, 那么它只要檢查字典中是否存在這個鍵即可; 如果程序要獲取監視某個鍵的所有客戶端, 那么只要取出鍵的值(一個鏈表), 然后對鏈表進行遍歷即可。
在任何對數據庫鍵空間(key space)進行修改的命令成功執行之后 (比如FLUSHDB、SET、DEL、LPUSH、SADD、ZREM,諸如此類),multi.c/touchWatchedKey函數都會被調用 —— 它檢查數據庫的watched_keys字典, 看是否有客戶端在監視已經被命令修改的鍵, 如果有的話, 程序將所有監視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS選項打開:
當客戶端發送 EXEC 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:
了解完其工作原理后,我們發現該 WATCH 命令可以為 Redis 事務提供 check-and-set (CAS)行為。
上面講到的是如何給我們需要的key加監控,那我們應該如何取消監控呢?
redis hget。除了上面介紹的命令模式可以實現Redis事務外,其實還有一種非常重要的方式:Lua腳本。
為什么要夸Lua腳本呢?我們來看看Lua腳本有什么優勢:
香嗎?真香!反正用過的都說好。可以看到相比命令模式還是優勢還蠻大的。
那么Lua腳本要怎么用呢?下面跟大家介紹幾個常見的常用的命令:
EVAL 可以理解為是lua腳本的解釋器,它的語法格式如下:
EVAL?script?numkeys?key?[key?...]?arg?[arg?...]
官方腔有點重對吧,沒事,咱們來看個例子:
eval?"return?{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"?2?key1?key2?first?second
redis持久化。eval的第一個參數是腳本的內容,第二個參數是腳本里面KEYS數組的長度(不包括ARGV參數的個數),這里是兩個;緊接著就會有兩個參數,用于傳遞個KEYS數組;后面剩下的參數全部傳遞給ARGV數組,相當于命令行參數。
127.0.0.1:6379>?eval?"return?{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"?2?username?age?jack?20
1)?"username"
2)?"age"
3)?"jack"
4)?"20"
如果我們想在lua腳本中調用redis的命令該如何操作?其實我們可以在腳本中使用 redis.call() 或 redis.pcall() 直接調用。兩者用法類似,只是在遇到錯誤時,返回錯誤的提示方式不同。
舉個例子:
127.0.0.1:6379>?get?name
"saycode"
127.0.0.1:6379>?eval?"return?redis.call('set',KEYS[1],'dashu')"?1?name
OK
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>?eval?"return?redis.call('get','name')"?0
"dashu"
127.0.0.1:6379>
SCRIPT LOAD 和 EVALSHA 經常配合使用。我們看個例子:
127.0.0.1:6379>?SCRIPT?LOAD?"return?redis.call('set',KEYS[1],'30')"
"6445747e70ce11ad0b9717d78e8ff16fb0faed46"
127.0.0.1:6379>?evalsha?6445747e70ce11ad0b9717d78e8ff16fb0faed46?1?age
OK
127.0.0.1:6379>?get?age
"30"
127.0.0.1:6379>
更多命令可以參看Redis Script 官方文檔
有了上面的知識,我們就可以使用lua腳本來靈活的使用redis的事務,這里舉幾個簡單的例子:
redis面試題?場景1:使用redis限制30分鐘內一個IP只允許訪問5次
思路:每次想把當前的時間插入到redis的list中,然后判斷list長度是否達到5次,如果大于5次,那么取出隊首的元素,和當前時間進行判斷,如果在30分鐘之內,則返回-1,其它情況返回1。我們來看一下具體實現:
eval?"redis.call('rpush',?KEYS[1],ARGV[1]);if?(redis.call('llen',KEYS[1])?>tonumber(ARGV[2]))?then?if?tonumber(ARGV[1])-redis.call('lpop',?KEYS[1])?1?'test_127.0.0.1'?1451460590?5?1800
Lua腳本 對于實現Redis事務確實是一種不錯的選擇,相信未來會有越來越多的開發者傾向于使用腳本來實現事務。不過我們在使用的時候也要注意以下兩點:
好了,以上就是實現Redis事務方式的有關內容,如果你之前還沒有了解到第二種腳本方式,趕緊給大叔點贊打call吧哈哈~
我們接著往下看。
Redis的事務和傳統的關系型數據庫事務的最大區別在于,Redis不支持事務回滾機制(rollback)。
redis,也就是說:當在事務過程中發生錯誤時,Redis事務失敗時并不進行回滾(roll back),而是繼續執行余下的命令。官方給出的理由是這樣子的:
看個例子:
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>?MULTI
OK
127.0.0.1:6379>?set?name?saycode
QUEUED
127.0.0.1:6379>?lpop?name
QUEUED
127.0.0.1:6379>?EXEC
1)?OK
2)?(error)?WRONGTYPE?Operation?against?a?key?holding?the?wrong?kind?of?value
127.0.0.1:6379>?get?name
"saycode"
127.0.0.1:6379>
上面例子中,我們在事務中重新設置name的值,并且使用一個命令去操作一個錯誤的數據類型,可以看到最終事務還是成功執行了,同時也會返回事務中發生錯誤的指令的出錯原因
實際上,事務的錯誤我們可以總結兩種情況:
對于發生在 EXEC 執行之前的錯誤,客戶端的做法是檢查命令入隊所得的返回值:如果命令入隊時返回 QUEUED ,那么入隊成功;否則,就是入隊失敗。如果有命令在入隊時失敗,那么大部分客戶端都會停止并取消這個事務。看例子:
127.0.0.1:6379>?get?name
"saycode"
127.0.0.1:6379>?get?sex
"man"
127.0.0.1:6379>?MULTI
OK
127.0.0.1:6379>?set?name?dashu
QUEUED
127.0.0.1:6379>?sett?sex?woman
(error)?ERR?unknown?command?`sett`,?with?args?beginning?with:?`sex`,?`woman`,
127.0.0.1:6379>?EXEC
(error)?EXECABORT?Transaction?discarded?because?of?previous?errors.
127.0.0.1:6379>?get?name
"saycode"
127.0.0.1:6379>?get?sex
"man"
至于那些在 EXEC 命令執行之后所產生的錯誤, 并沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行。
127.0.0.1:6379>?get?name
"dashu"
127.0.0.1:6379>?MULTI
OK
127.0.0.1:6379>?set?name?saycode
QUEUED
127.0.0.1:6379>?lpop?name
QUEUED
127.0.0.1:6379>?EXEC
1)?OK
2)?(error)?WRONGTYPE?Operation?against?a?key?holding?the?wrong?kind?of?value
127.0.0.1:6379>?get?name
"saycode"
127.0.0.1:6379>
redis高級面試題、我們可以看到:即使事務中有某條/某些命令執行失敗了, 事務隊列中的其他命令仍然會繼續執行 —— Redis 不會停止執行事務中的命令。
了解完Redis事務的基礎,最后我們來寫個Demo來實現樂觀鎖,業務場景是商品搶購,偽代碼如下:
#?樂觀鎖
public?function?actionBuy(){
????$userId?=?mt_rand(1,99999999);
????$goods?=?$this->goods;
????$redis?=?Yii::$app->redis;
????$lock?=?"Huawei?p40";
????try?{
????????$inventory['num']?=?$redis->get('goodNums');
????????if($inventory['num']<=0){
????????????throw?new?\Exception('活動結束');
????????}
????????$redis->watch($lock);
????????$redis->multi();
????????//todo:這里還需要重新判斷下庫存,否則會出現超發,高并發情況下$inventory['num']肯定會出現同時讀取一個值;為了方便測試,沒寫db操作
????????//redis事務是將命令放入隊列中,無法取goodNums來判斷庫存是否結束,此處使用數據庫來判斷庫存合理
????????//業務處理??減庫存,創建訂單
????????$redis->decr('goodNums');
????????$redis->sadd('order',$userId);
????????$redis->exec();
????????Common::addLog('shop.log',$userId.'?搶購成功');
????}catch?(\Exception?$e){
????????$redis->discard();
????????Common::addLog('shop.log',$e->getMessage());
????????throw?new?\Exception('搶購失敗');
????}
????die('success');
}
好了,今天的分享就到這里了,關注公眾號「大叔說碼」 獲取更多干貨,我們下期見~
參考:
1、 https://redis.io/topics/transactions
2、https://zhuanlan.zhihu.com/p/146865185
redis面試中常被問到的?3、https://walkingsun.github.io/WindBlog/2019/03/14/redis/
4、https://blog.csdn.net/fangjian1204/article/details/5058508
5、https://redis.io/commands/eval
6、https://techlog.cn/article/list/10183180
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态