「承」Redis 原理篇——關於 Redis 中的事務

前言

關於 Redis 的“起承轉合”,我前面已經用五個篇章的長度作了一個 Redis 基礎篇——“起”篇的詳細闡述,相信大家無論之前有沒有接觸過 Redis,都能從中學到不少東西。基礎篇的內容顧名思義,只是個基礎,主要說了 Redis 的發展以及 Redis 的基本資料型別,內容跟平時使用關聯會比較大,難度不算大,希望大家能好好消化。 這裡送上基礎篇的飛機票:

【起】Redis 概述篇——帶你走過 Redis 的前世今生

【起】Redis 基礎篇——基本資料結構之String,Hash

【起】Redis 基礎篇——基本資料結構之 List,Set

【起】Redis 基礎篇——基本資料結構之 ZSet,Bitmap…

【起】Redis 基礎篇——基本資料結構之總結篇

在“承”篇中,我會圍繞 Redis 的原理來闡述,講一些相對比較高階的特性,比如本篇章要講到的 pub/sub(釋出/訂閱)模式,持久化機制,高效能特性,食物,記憶體回收機制等等,在接下來的篇章中,我會為大家穿針引線,把每個篇章的內容都串起來,這裡就先不佔用大家的前言篇章。

那話歸正題,我們今天來看一下關於 Redis 的釋出/訂閱模式。

正文

Redis 事務

先看官網 redis。io/topics/tran… redisdoc。com/topic/trans…

為什麼要用事務

我們知道 Redis 的單個命令是原子性的(比如 get set mget mset),如果涉及到多個命令的時候,需要把多個命令作為一個不可分割的處理序列,就需要用到事務。

例如我們之前說的用 setnx 實現分散式鎖,我們先 set,然後設定對 key 設定 expire,防止 del 發生異常的時候鎖不會被釋放,業務處理完了以後再 del,這三個動作我們就希望它們作為一組命令執行。

Redis 的事務有兩個特點:

按進入佇列的順序執行;

不會受到其他客戶端的請求的影響;

Redis 的事務涉及到四個命令:multi(開啟事務),exec(執行事務),discard(取消事務),watch(監視)

事務的用法

案例場景:tom 和 mic 各有 1000 元,tom 需要向 mic 轉賬 100 元。

tom 的賬戶餘額減少 100 元,mic 的賬戶餘額增加 100 元。

127。0。0。1:6379> set tom 1000OK127。0。0。1:6379> set mic 1000OK127。0。0。1:6379> multiOK127。0。0。1:6379> decrby tom 100QUEUED127。0。0。1:6379> incrby mic 100QUEUED127。0。0。1:6379> exec1) (integer) 9002) (integer) 1100127。0。0。1:6379> get tom“900”127。0。0。1:6379> get mic“1100”複製程式碼

透過 multi 的命令開啟事務。事物不能巢狀,多個 multi 命令效果一樣。

multi 執行後,客戶端可以繼續向伺服器傳送任意多條命令, 這些命令不會立即被執行, 而是被放到一個佇列中, 當 exec 命令被呼叫時, 所有佇列中的命令才會被執行。

透過 exec 的命令執行事務。如果沒有執行 exec,所有的命令都不會被執行。如果中途不想執行事務了,怎麼辦?

可以呼叫 discard 可以清空事務佇列,放棄執行。

multiset k1 1set k2 2set k3 3discard複製程式碼

watch 命令

在 Redis 中還提供了一個 watch 命令。

它可以為 Redis 事務提供 CAS 樂觀鎖行為( Check and Set / Compare and Swap),也就是多個執行緒更新變數的時候,會跟原值做比較,只有它沒有被其他執行緒修改的情況下,才更新成新的值。

我們可以用 watch 監視一個或者多個 key,如果開啟事務之後,至少有一個被監視 key 鍵在 exec 執行之前被修改了, 那麼整個事務都會被取消(key 提前過期除外)。可以用 unwatch 取消。

client 1client 2127。0。0。1:6379> set balance 1000

OK

127。0。0。1:6379> watch balance

OK

127。0。0。1:6379> multi

OK

127。0。0。1:6379> incrby balance 100

QUEUED——127。0。0。1:6379> decrby balance 100

(integer) 900127。0。0。1:6379> exec

(nil)

127。0。0。1:6379> get balance

“900”-

事務可能遇到的問題

我們把事務執行遇到的問題分成兩種,一種是在執行 exec 之前發生錯誤,一種是在執行 exec 之後發生錯誤。

在執行 exec 之前發生錯誤

比如:入隊的命令存在語法錯誤,包括引數數量,引數名等等(編譯器錯誤)。

127。0。0。1:6379> multiOK127。0。0。1:6379> set ck 666QUEUED127。0。0。1:6379> hset ck 2673(error) ERR wrong number of arguments for ‘hset’ command127。0。0。1:6379> exec(error) EXECABORT Transaction discarded because of previous errors。複製程式碼

在這種情況下事務會被拒絕執行,也就是佇列中所有的命令都不會得到執行。

在執行 exec 之後發生錯誤

比如,型別錯誤,比如對 String 使用了 Hash 的命令,這是一種執行時錯誤。

127。0。0。1:6379> flushallOK127。0。0。1:6379> multiOK127。0。0。1:6379> set k1 1QUEUED127。0。0。1:6379> hset k1 a bQUEUED127。0。0。1:6379> exec1) OK2) (error) WRONGTYPE Operation against a key holding the wrong kind of value127。0。0。1:6379> get k1“1”複製程式碼

最後我們發現 set k1 1的命令是成功的,也就是在這種發生了執行時異常的情況下,只有錯誤的命令沒有被執行,但是其他命令沒有受到影響。

這個顯然不符合我們對原子性的定義,也就是我們沒辦法用 Redis 的這種事務機制來實現原子性,保證資料的一致。

思考:

為什麼在一個事務中存在錯誤,Redis 不回滾? 這個問題我們必須從 Redis 的設計開始說起,用過關係型資料庫的朋友們應該會感到奇怪,為啥 Redis 不支援事務回滾?我們先看看,Redis 有沒有必要支援回滾。 我們知道,Redis 的命令執行失敗只有在語法錯誤的情況下(在將這個命令放入事務佇列期間,Redis能夠發現此類問題),這個問題一般不會發生在生產環境中。 而且事務回滾並不能解決任何程式錯誤。例如,如果某個查詢會將一個鍵的值遞增2,而不是1,或者遞增錯誤的鍵,那麼事務回滾機制是沒有辦法解決這些程式問題的。 另外就是,Redis已經在系統內部進行功能簡化,這樣可以確保更快的執行速度,因為Redis不需要事務回滾的能力。

Lua 指令碼

Lua/ˈluə/是一種輕量級指令碼語言,它是用 C 語言編寫的,跟資料的儲存過程有點類似。

使用 Lua 指令碼來執行 Redis 命令的好處:

一次傳送多個命令,減少網路開銷。

Redis 會將整個指令碼作為一個整體執行,不會被其他請求打斷,保持原子性。

對於複雜的組合命令,我們可以放在檔案中,可以實現程式之間的命令集複用。

在 Redis 中呼叫 Lua 指令碼

使用 eval /ɪ‘væl/ 方法,語法格式:

redis> eval lua-script key-num [key1 key2 key3 。。。。] [value1 value2 value3 。。。。]複製程式碼

eval 代表執行 Lua 語言的命令。

lua-script 代表 Lua 語言指令碼內容。

key-num 表示引數中有多少個 key,需要注意的是 Redis 中 key 是從 1 開始的,如果沒有 key 的引數,那麼寫 0。

[key1 key2 key3…]是 key 作為引數傳遞給 Lua 語言,也可以不填,但是需要和 key-num 的個數對應起來。

[value1 value2 value3 …。]這些引數傳遞給 Lua 語言,它們是可填可不填的。

示例,返回一個字串,0 個引數:

redis> eval “return ’Hello World‘” 0複製程式碼

在 Lua 指令碼中呼叫 Redis 命令

使用 redis。call(command, key [param1, param2…])進行操作。語法格式:

redis> eval “redis。call(’set‘,KEYS[1],ARGV[1])” 1 lua-key lua-value複製程式碼

command 是命令,包括 set、get、del 等。

key 是被操作的鍵。

param1,param2…代表給 key 的引數。

注意跟 Java 不一樣,定義只有形參,呼叫只有實參。

Lua 是在呼叫時用 key 表示形參,argv 表示引數值(實參)。

設定鍵值對

在Redis 中呼叫 Lua 指令碼執行 Redis 命令

redis> eval “return redis。call(’set‘,KEYS[1],ARGV[1])” 1 ck 2673 redis> get ck複製程式碼

以上命令等價於 set ck 2673。

在 redis-cli 中直接寫 Lua 指令碼不夠方便,也不能實現編輯和複用,通常我們會把指令碼放在檔案裡面,然後執行這個檔案。

在 Redis 中呼叫 Lua 指令碼檔案中的命令,操作 Redis

建立 Lua 指令碼檔案:

cd /usr/local/soft/redis5。0。5/srcvim ck。lua複製程式碼

Lua 指令碼內容,先設定,再取值:

redis。call(’set‘,’ck‘,’lua666‘)return redis。call(’get‘,’ck‘)複製程式碼

在 Redis 客戶端中呼叫 Lua 指令碼

cd /usr/local/soft/redis5。0。5/srcredis-cli ——eval ck。lua 0複製程式碼

得到返回值:

[root@localhost src]# redis-cli ——eval ck。lua 0“lua666”複製程式碼

案例:對 IP 進行限流

需求:在 X 秒內只能訪問 Y 次。

設計思路:用 key 記錄 IP,用 value 記錄訪問次數。

拿到 IP 以後,對 IP+1。 如果是第一次訪問,對 key 設定過期時間(引數 1)。 否則判斷次數,超過限定的次數(引數 2),返回 0。 如果沒有超過次數則返回 1。 超過時間, key 過期之後,可以再次訪問。

KEY[1]是 IP, ARGV[1]是過期時間 X,ARGV[2]是限制訪問的次數 Y。

——ip_limit。lua

——IP 限流,對某個 IP 頻率進行限制 ,6 秒鐘訪問 10 次

local num=redis。call(’incr‘,KEYS[1])if tonumber(num)==1 then redis。call(’expire‘,KEYS[1],ARGV[1]) return 1elseif tonumber(num)>tonumber(ARGV[2]) then return 0else return 1end複製程式碼

6 秒鐘內限制訪問 10 次,呼叫測試(連續呼叫 10 次):

。/redis-cli ——eval “ip_limit。lua” app:ip:limit:192。168。8。111 , 6 10複製程式碼

app:ip:limit:192。168。8。111 是 key 值 ,後面是引數值,中間要加上一個空格 和一個逗號,再加上一個空格。

即:。/redis-cli –eval [lua 指令碼][key…]空格,空格[args…]

多個引數之間用一個 空格 分割 。

程式碼:LuaTest。java

快取 Lua 指令碼

為什麼要快取 在指令碼比較長的情況下,如果每次呼叫指令碼都需要把整個指令碼傳給 Redis 服務端,會產生比較大的網路開銷。為了解決這個問題,Redis 提供了 EVALSHA 命令,允許開發者透過指令碼內容的 SHA1 摘要來執行指令碼。

如何快取 Redis 在執行 script load 命令時會計算指令碼的 SHA1 摘要並記錄在指令碼快取中,執行 EVALSHA 命令時 Redis 會根據提供的摘要從指令碼快取中查詢對應的指令碼內容,如果找到了則執行指令碼,否則會返回錯誤:“NOSCRIPT No matching script。 Please use EVAL。”

127。0。0。1:6379> script load “return ’Hello World‘”“470877a599ac74fbfda41caa908de682c5fc7d4b”127。0。0。1:6379> evalsha “470877a599ac74fbfda41caa908de682c5fc7d4b” 0 “Hello World”複製程式碼

自稱案例

Redis 有 incrby 這樣的自增命令,但是沒有自乘,比如乘以 3,乘以 5。

我們可以寫一個自乘的運算,讓它乘以後面的引數:

local curVal = redis。call(“get”, KEYS[1])if curVal == false then curVal = 0else curVal = tonumber(curVal)end curVal = curVal * tonumber(ARGV[1]) redis。call(“set”, KEYS[1], curVal) return curVal複製程式碼

把這個指令碼變成單行,語句之間使用分號隔開

local curVal = redis。call(“get”, KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal=curVal *tonumber(ARGV[1]); redis。call(“set”, KEYS[1], curVal); return curVal複製程式碼

script load ’命令‘

127。0。0。1:6379> script load ’local curVal = redis。call(“get”, KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis。call(“set”, KEYS[1], curVal); return curVal‘“be4f93d8a5379e5e5b768a74e77c8a4eb0434441”複製程式碼

呼叫:

127。0。0。1:6379> set num 2OK127。0。0。1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6 (integer) 12複製程式碼

指令碼超時

Redis 的指令執行本身是單執行緒的,這個執行緒還要執行客戶端的 Lua 指令碼,如果 Lua 指令碼執行超時或者陷入了死迴圈,是不是沒有辦法為客戶端提供服務了呢?

eval ’while(true) do end‘ 0複製程式碼

為了防止某個指令碼執行時間過長導致 Redis 無法提供服務, Redis 提供了 lua-time-limit 引數限制指令碼的最長執行時間,預設為 5 秒鐘。

lua-time-limit 5000(redis。conf 配置檔案中)複製程式碼

當指令碼執行時間超過這一限制後,Redis 將開始接受其他命令但不會執行(以確保指令碼的原子性,因為此時指令碼並沒有被終止),而是會返回“BUSY”錯誤。

Redis 提供了一個 script kill 的命令來終止指令碼的執行。新開一個客戶端:

script kill複製程式碼

如果當前執行的 Lua 指令碼對 Redis 的資料進行了修改(SET、DEL 等),那麼透過 script kill 命令是不能終止指令碼執行的。

127。0。0。1:6379> eval “redis。call(’set‘,’ck‘,’666‘) while true do end” 0複製程式碼

因為要保證指令碼執行的原子性,如果指令碼執行了一部分終止,那就違背了指令碼原子性的要求。最終要保證指令碼要麼都執行,要麼都不執行。

127。0。0。1:6379> script kill(error) UNKILLABLE Sorry the script already executed write commands against the dataset。 You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command。複製程式碼

遇到這種情況,只能透過 shutdown nosave 命令來強行終止 redis。

shutdown nosave 和 shutdown 的區別在於 shutdown nosave 不會進行持久化操作,意味著發生在上一次快照後的資料庫修改都會丟失。

總結:如果我們有一些特殊的需求,可以用 Lua 來實現,但是要注意那些耗時的操作。

By the way

有問題?可以給我留言或私聊 有收穫?那就順手點個讚唄~

當然,也可以到我的公眾號下「6曦軒」,

回覆“學習”,即可領取一份

【Java工程師進階架構師的影片教程】~

回覆“面試”,可以獲得:

【本人嘔心瀝血整理的 Java 面試題】

回覆“MySQL腦圖”,可以獲得

【MySQL 知識點梳理高畫質腦圖】

還有【阿里雲】【騰訊雲】的購買優惠噢~具體請聯絡我

曦軒我是科班出身的程式設計師,php,Android以及硬體方面都做過,不過最後還是選擇專注於做 Java,所以有啥問題可以到公眾號提問討論(技術情感傾訴都可以哈哈哈),看到的話會盡快回復,希望可以跟大家共同學習進步,關於服務端架構,Java 核心知識解析,職業生涯,面試總結等文章會不定期堅持推送輸出,歡迎大家關注~~~

「承」Redis 原理篇——關於 Redis 中的事務