1. 程式人生 > 其它 >redis怎麼修改_面試官問我Redis事務,還問我有哪些實現方式

redis怎麼修改_面試官問我Redis事務,還問我有哪些實現方式

技術標籤:redis怎麼修改

「第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事務可以一次執行多個命令, 並且帶有以下兩個重要的保證:

  • 事務是一個單獨的隔離操作:事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端傳送來的命令請求所打斷。
  • 事務是一個原子操作:事務中的命令要麼全部被執行,要麼全部都不執行。

官方腔換成方言就是:

Redis事務提供了一種 “將多個命令打包, 然後一次性、按順序地執行” 的機制, 並且事務在執行的期間不會主動中斷 —— 伺服器在執行完事務中的所有命令之後, 才會繼續處理其他客戶端的其他命令。

或者你也可以把Redis事務理解為一個佇列,開啟事務後,往後的提交的Redis命令都會依次入隊,遇到觸發當前事務指令時,佇列中的指令會依次被取出並執行。

「值得注意的是」

“事務中的命令要麼全部被執行,要麼全部都不執行” 這句話單純想表達的是:“事務執行需要對應的觸發條件(命令)”

下面看個例子先整體瞭解一下Redis事務:

127.0.0.1:6379>getname
"zhangsan"
127.0.0.1:6379>getsex
"female"
127.0.0.1:6379>MULTI#開啟事務
OK
127.0.0.1:6379>setnamedashu
QUEUED#命令入隊
127.0.0.1:6379>setsexmale
QUEUED#命令入隊
127.0.0.1:6379>EXEC#觸發當前事務
1)OK
2)OK
127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>getsex
"male"
127.0.0.1:6379>

實現Redis事務有哪些方式

瞭解完Redis事務是什麼回事後,接下來我們繼續看看實現Redis事務有哪些方式。

命令模式

命令模式是實現redis事務比較常見的方式,該方式的主要命令有:MULTI、EXEC、DISCARD、WATCH。

MULTI

MULTI 命令用於開啟一個事務,它總是返回 OK 。

MULTI 執行之後, 客戶端可以繼續向伺服器傳送任意多條命令,這些命令不會立即被執行, 而是被放到一個佇列中,等待事務被觸發。

EXEC

EXEC 命令負責觸發並執行事務中的所有命令

  • 如果客戶端在使用 MULTI 開啟了一個事務之後,卻因為斷線而沒有成功執行 EXEC ,那麼事務中的所有命令都不會被執行。
  • 如果客戶端成功在開啟事務之後執行 EXEC ,那麼事務中的所有命令都會被執行。

EXEC 命令返回的是一個數組, 陣列中的每個元素都是執行事務中的命令所產生的回覆。 回覆元素的先後順序和命令傳送的先後順序一致。

DISCARD

DISCARD 命令可以理解為是搞破壞的。當 DISCARD 命令被執行時, 事務會被丟棄, 事務佇列會被清空, 並且客戶端會從事務狀態中退出。

我們看個例子:

127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>MULTI
OK
127.0.0.1:6379>setnamesaycode
QUEUED
127.0.0.1:6379>DISCARD
OK
127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>

我們可以看到雖然開啟事務後我們重新設定了name的值,但是當我們執行DISCARD命令後,該事務被成功丟棄了,所以當我們再次獲取name的值的時候,我們可以看到它的值並沒有發生改變。

WATCH

WATCH 命令用於在事務開始之前監視任意數量的鍵,當呼叫 EXEC 命令執行事務時, 如果任意一個被監視的鍵已經被其他客戶端修改了, 那麼整個事務不再執行, 直接返回失敗。

看例子:

  • 首先我們在一個Redis客戶端一上使用 WATCH 命令監控兩個key,分別為name和sex,然後開啟事務,在事務中修改name的值,
  • 在客戶端一執行 EXEC 命令之前,我們另外開一個客戶端二,在客戶端二中我們修改sex的值為man
  • 接著我們回到客戶端一執行 EXEC 命令
#客戶端一
127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>getsex
"male"
127.0.0.1:6379>WATCHnamesex
OK
127.0.0.1:6379>MULTI
OK
127.0.0.1:6379>setnamesaycode
QUEUED
127.0.0.1:6379>EXEC
(nil)#事務失敗
127.0.0.1:6379>getsex
"man"
127.0.0.1:6379>getname
"dashu"

#---------這是一條分割線---------#

#客戶端二
127.0.0.1:6379>getsex
"male"
127.0.0.1:6379>setsexman
OK

從上面執行的結果可以看到,客戶端一中的事務失敗了,事務中所修改的name的值也不成功。主要原因是:呼叫 EXEC 命令執行事務時,被監控的sex 被客戶端二修改了,所以客戶端一的事務不再執行

WATCH命令的實現

在每個代表資料庫的 redis.h/redisDb 結構型別中, 都儲存了一個 watched_keys 字典, 字典的鍵是這個資料庫被監視的鍵, 而字典的值則是一個連結串列, 連結串列中儲存了所有監視這個鍵的客戶端。

比如說,以下字典就展示了一個 watched_keys 字典的例子:

595998e3892a4c1372409c7760d4cc70.png

其中, 鍵 key1 正在被 client2 、 client5 和 client1 三個客戶端監視, 其他一些鍵也分別被其他別的客戶端監視著。

WATCH 命令的作用, 就是將當前客戶端和要監視的鍵在 watched_keys 中進行關聯。

舉個例子, 如果當前客戶端為 client10086 , 那麼當客戶端執行 WATCH key1 key2 時, 前面展示的 watched_keys 將被修改成這個樣子:

27ce34ae14878ed11e449f5000eb938c.png

通過watched_keys字典, 如果程式想檢查某個鍵是否被監視, 那麼它只要檢查字典中是否存在這個鍵即可; 如果程式要獲取監視某個鍵的所有客戶端, 那麼只要取出鍵的值(一個連結串列), 然後對連結串列進行遍歷即可。

WATCH的觸發原理

在任何對資料庫鍵空間(key space)進行修改的命令成功執行之後 (比如FLUSHDB、SET、DEL、LPUSH、SADD、ZREM,諸如此類),multi.c/touchWatchedKey函式都會被呼叫 —— 它檢查資料庫的watched_keys字典, 看是否有客戶端在監視已經被命令修改的鍵, 如果有的話, 程式將所有監視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS選項開啟:

ad98162c30f422732bd2eb01485fc4fe.png

當客戶端傳送 EXEC 命令、觸發事務執行時, 伺服器會對客戶端的狀態進行檢查:

  • 如果客戶端的 REDIS_DIRTY_CAS 選項已經被開啟,那麼說明被客戶端監視的鍵至少有一個已經被修改了,事務的安全性已經被破壞。伺服器會放棄執行這個事務,直接向客戶端返回空回覆,表示事務執行失敗。
  • 如果 REDIS_DIRTY_CAS 選項沒有被開啟,那麼說明所有監視鍵都安全,伺服器正式執行事務。

瞭解完其工作原理後,我們發現該 WATCH 命令可以為 Redis 事務提供 check-and-set (CAS)行為。

上面講到的是如何給我們需要的key加監控,那我們應該如何取消監控呢?

  • 實際上,當 EXEC 被呼叫時, 不管事務是否成功執行, 對所有鍵的監視都會被取消。
  • 另外, 當客戶端斷開連線時, 該客戶端對鍵的監視也會被取消。
  • 使用無引數的 UNWATCH 命令可以手動取消對所有鍵的監視

2、Lua指令碼

除了上面介紹的命令模式可以實現Redis事務外,其實還有一種非常重要的方式:Lua指令碼。

為什麼要誇Lua指令碼呢?我們來看看Lua指令碼有什麼優勢:

  • 原子操作:Redis確保指令碼執行期間,其它任何指令碼或者命令都無法執行。也就是說,在編寫指令碼的過程中無需擔心會出現競態條件,無需使用事務。
  • 減少網路開銷:可以將多個請求通過指令碼的形式一次傳送,減少網路時延。因此使用指令碼要更簡單,速度更快
  • 複用。客戶端傳送的指令碼會永久存在redis中,這樣,其他客戶端可以複用這一指令碼而不需要使用程式碼完成相同的邏輯。

香嗎?真香!反正用過的都說好。可以看到相比命令模式還是優勢還蠻大的。

那麼Lua指令碼要怎麼用呢?下面跟大家介紹幾個常見的常用的命令:

EVAL

EVAL 可以理解為是lua指令碼的直譯器,它的語法格式如下:

EVALscriptnumkeyskey[key...]arg[arg...]
  • script:一段 Lua 指令碼或 Lua 指令碼檔案所在路徑及檔名。
  • numkeys:Lua 指令碼對應引數數量
  • key [key ...]:Lua 中通過全域性變數 KEYS 陣列儲存的傳入引數
  • arg [arg ...]:Lua 中通過全域性變數 ARGV 陣列儲存的傳入附加引數

官方腔有點重對吧,沒事,咱們來看個例子:

eval"return{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"2key1key2firstsecond

eval的第一個引數是指令碼的內容,第二個引數是腳本里面KEYS陣列的長度(不包括ARGV引數的個數),這裡是兩個;緊接著就會有兩個引數,用於傳遞個KEYS陣列;後面剩下的引數全部傳遞給ARGV陣列,相當於命令列引數。

127.0.0.1:6379>eval"return{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"2usernameagejack20
1)"username"
2)"age"
3)"jack"
4)"20"

redis.call() / redis.call()

如果我們想在lua指令碼中呼叫redis的命令該如何操作?其實我們可以在指令碼中使用 redis.call() 或 redis.pcall() 直接呼叫。兩者用法類似,只是在遇到錯誤時,返回錯誤的提示方式不同。

舉個例子:

127.0.0.1:6379>getname
"saycode"
127.0.0.1:6379>eval"returnredis.call('set',KEYS[1],'dashu')"1name
OK
127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>eval"returnredis.call('get','name')"0
"dashu"
127.0.0.1:6379>

SCRIPT LOAD 和 EVALSHA

  • SCRIPT LOAD:提前載入 Lua 指令碼,返回對應指令碼的 SHA1 摘要
  • EVALSHA:執行指令碼,與EVAL相似,只不過它的引數為指令碼的 SHA1 摘要

SCRIPT LOAD 和 EVALSHA 經常配合使用。我們看個例子:

127.0.0.1:6379>SCRIPTLOAD"returnredis.call('set',KEYS[1],'30')"
"6445747e70ce11ad0b9717d78e8ff16fb0faed46"
127.0.0.1:6379>evalsha6445747e70ce11ad0b9717d78e8ff16fb0faed461age
OK
127.0.0.1:6379>getage
"30"
127.0.0.1:6379>

更多命令可以參看Redis Script 官方文件

有了上面的知識,我們就可以使用lua指令碼來靈活的使用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]))theniftonumber(ARGV[1])-redis.call('lpop',KEYS[1])1'test_127.0.0.1'145146059051800

Lua指令碼 對於實現Redis事務確實是一種不錯的選擇,相信未來會有越來越多的開發者傾向於使用指令碼來實現事務。不過我們在使用的時候也要注意以下兩點:

  • 注意Redis版本。指令碼功能是 Redis 2.6 才引入的。
  • 由於指令碼執行的原子性,所以我們不要在指令碼中執行過長開銷的程式,否則會驗證影響其它請求的執行。

好了,以上就是實現Redis事務方式的有關內容,如果你之前還沒有了解到第二種指令碼方式,趕緊給大叔點贊打call吧哈哈~

我們接著往下看。

Redis事務是否支援回滾

Redis的事務和傳統的關係型資料庫事務的最大區別在於,Redis不支援事務回滾機制(rollback)。

也就是說:當在事務過程中發生錯誤時,Redis事務失敗時並不進行回滾(roll back),而是繼續執行餘下的命令。官方給出的理由是這樣子的:

  • 從實用性的角度來說,Redis失敗的命令是由程式設計錯誤造成的(例如錯誤的語法,命令用在了錯誤型別的命令),而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
  • 保證Redis效能。因為不需要對回滾進行支援,所以 Redis 的內部可以保持簡單且快速

看個例子:

127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>MULTI
OK
127.0.0.1:6379>setnamesaycode
QUEUED
127.0.0.1:6379>lpopname
QUEUED
127.0.0.1:6379>EXEC
1)OK
2)(error)WRONGTYPEOperationagainstakeyholdingthewrongkindofvalue
127.0.0.1:6379>getname
"saycode"
127.0.0.1:6379>

上面例子中,我們在事務中重新設定name的值,並且使用一個命令去操作一個錯誤的資料型別,可以看到最終事務還是成功執行了,同時也會返回事務中發生錯誤的指令的出錯原因

事務中發生錯誤Redis如何表現

實際上,事務的錯誤我們可以總結兩種情況:

  • 一種是:事務在執行 EXEC 之前,入隊的命令可能會出錯。比如命令可能會產生語法錯誤(引數數量錯誤,引數名錯誤,等等),或者其他更嚴重的錯誤,比如記憶體不足(如果伺服器使用 maxmemory 設定了最大記憶體限制的話)。

對於發生在 EXEC 執行之前的錯誤,客戶端的做法是檢查命令入隊所得的返回值:如果命令入隊時返回 QUEUED ,那麼入隊成功;否則,就是入隊失敗。如果有命令在入隊時失敗,那麼大部分客戶端都會停止並取消這個事務。看例子:

127.0.0.1:6379>getname
"saycode"
127.0.0.1:6379>getsex
"man"
127.0.0.1:6379>MULTI
OK
127.0.0.1:6379>setnamedashu
QUEUED
127.0.0.1:6379>settsexwoman
(error)ERRunknowncommand`sett`,withargsbeginningwith:`sex`,`woman`,
127.0.0.1:6379>EXEC
(error)EXECABORTTransactiondiscardedbecauseofpreviouserrors.
127.0.0.1:6379>getname
"saycode"
127.0.0.1:6379>getsex
"man"
  • 還有一種是:命令可能在 EXEC 呼叫之後失敗。比如事務中的命令可能處理了錯誤型別的鍵,例如將列表命令用在了字串鍵上面

至於那些在 EXEC 命令執行之後所產生的錯誤, 並沒有對它們進行特別處理: 即使事務中有某個/某些命令在執行時產生了錯誤, 事務中的其他命令仍然會繼續執行。

127.0.0.1:6379>getname
"dashu"
127.0.0.1:6379>MULTI
OK
127.0.0.1:6379>setnamesaycode
QUEUED
127.0.0.1:6379>lpopname
QUEUED
127.0.0.1:6379>EXEC
1)OK
2)(error)WRONGTYPEOperationagainstakeyholdingthewrongkindofvalue
127.0.0.1:6379>getname
"saycode"
127.0.0.1:6379>

我們可以看到:即使事務中有某條/某些命令執行失敗了, 事務佇列中的其他命令仍然會繼續執行 —— Redis 不會停止執行事務中的命令。

Redis事務的實戰應用

瞭解完Redis事務的基礎,最後我們來寫個Demo來實現樂觀鎖,業務場景是商品搶購,虛擬碼如下:

#樂觀鎖
publicfunctionactionBuy(){
$userId=mt_rand(1,99999999);
$goods=$this->goods;
$redis=Yii::$app->redis;
$lock="Huaweip40";

try{
$inventory['num']=$redis->get('goodNums');
if($inventory['num']<=0){
thrownew\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());
thrownew\Exception('搶購失敗');
}

die('success');
}

好了,今天的分享就到這裡了,關注公眾號「大叔說碼」 獲取更多幹貨,我們下期見~

87930a81371b00f804ba441489d54d12.png

參考:

1、 https://redis.io/topics/transactions

2、https://zhuanlan.zhihu.com/p/146865185

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