Redis入墳(二)高級特性,發布訂閱、事務、Lua腳本

目標
1、學習 Redis 的一些高級特性,包括發布訂閱、事務、Lua 腳本

1、發布訂閱模式

1.1列表的局限

前面我們說通過隊列的 rpush 和 lpop 可以實現消息隊列(隊尾進隊頭出),但是消費者需要不停地調用 lpop 查看 List 中是否有等待處理的消息(比如寫一個 while 循環)。

為了減少通信的消耗,可以 sleep()一段時間再消費,但是會有兩個問題:

  1. 如果生產者生產消息的速度遠大于消費者消費消息的速度,List 會占用大量的內存。
  2. 消息的實時性降低
    list 還提供了一個阻塞的命令:blpop,沒有任何元素可以彈出的時候,連接會被阻塞。blpop queue 5,阻塞5秒。
    基于 list 實現的消息隊列,不支持一對多的消息分發。

1.2發布訂閱模式

除了通過 list 實現消息隊列之外,Redis 還提供了一組命令實現發布/訂閱模式。

這種方式,發送者和接收者沒有直接關聯(實現了解耦),接收者也不需要持續嘗試獲取消息。

1.2.1 訂閱頻道

可以訂閱一個或者多個頻道。消息的發布者(生產者)可以給指定的頻道發布消息。只要有消息到達了頻道,所有訂閱了這個頻道的訂閱者都會收到這條消息。

需要注意的注意是,發出去的消息不會被持久化,因為它已經從隊列里面移除了,所以消費者只能收到它開始訂閱這個頻道之后發布的消息。

下面我們來看一下發布訂閱命令的使用方法。
訂閱者訂閱頻道:可以一次訂閱多個,比如這個客戶端訂閱了 3 個頻道。

subscribe channel-1 channel-2 channel-3

發布者可以向指定頻道發布消息(并不支持一次向多個頻道發送消息):

publish channel-1 2673

取消訂閱(不能在訂閱狀態下使用):

unsubscribe channel-1

1.2.2 按規則(Pattern) 訂閱頻道

支持?和占位符。?代表一個字符,代表 0 個或者多個字符。
消費端 1,關注運動信息:

psubscribe *sport

消費端 2,關注所有新聞:

psubscribe news*

消費端 3,關注天氣新聞:

psubscribe news-weather

生產者,發布 3 條信息

publish news-sport yaoming
publish news-music jaychou
publish news-weather rain

java 代碼

package pubsub;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

public class PublishSubscribe {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.0.224", 6379);
        jedis.subscribe(new Subscriber(),"channel");

    }
}
class Subscriber extends JedisPubSub{

    @Override
    public void onMessage(String channel, String message) {
        System.out.println("接收到的消息:"+message);
    }

    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        System.out.println("onSubscribe---channel:"+channel+",subscribedChannels:"+subscribedChannels);
    }

    @Override
    public void onPUnsubscribe(String pattern, int subscribedChannels) {
        System.out.println("onPUnsubscribe---pattern:"+pattern+",subscribedChannels:"+subscribedChannels);
    }

    @Override
    public void onPSubscribe(String pattern, int subscribedChannels) {
        System.out.println("onPSubscribe---pattern:"+pattern+",subscribedChannels:"+subscribedChannels);
    }

    @Override
    public void unsubscribe(String... channels) {
        super.unsubscribe(channels);
    }
}


package pubsub;

import redis.clients.jedis.Jedis;

public class PublishTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.0.224", 6379);
        jedis.publish("channel","你好呀");
        jedis.close();

    }
}

2 、Redis 事務

2.1 為什么要用事務

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

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

Redis 的事務有兩個特點:

  1. 按進入隊列的順序執行。
  2. 不會受到其他客戶端的請求的影響。
    Redis 的事務涉及到四個命令:multi(開啟事務),exec(執行事務),discard(取消事務),watch(監視)

2.2 事務的用法

案例場景:tom 和 mic 各有 1000 元,tom 需要向 mic 轉賬 100 元。
tom 的賬戶余額減少 100 元,mic 的賬戶余額增加 100 元。

127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set mic 1000
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby tom 100
QUEUED
127.0.0.1:6379> incrby mic 100
QUEUED
127.0.0.1:6379> exec
1) (integer) 900
2) (integer) 1100
127.0.0.1:6379> get tom
"900"
127.0.0.1:6379> get mic
"1100"

通過 multi 的命令開啟事務。事務不能嵌套,多個 multi 命令效果一樣。
multi 執行后,客戶端可以繼續向服務器發送任意多條命令, 這些命令不會立即被執行, 而是被放到一個隊列中, 當 exec 命令被調用時, 所有隊列中的命令才會被執行。

通過 exec 的命令執行事務。如果沒有執行 exec,所有的命令都不會被執行。如果中途不想執行事務了,怎么辦?可以調用 discard 可以清空事務隊列,放棄執行。

multi
set k1 1
set k2 2
set k3 3
discard

2.3 watch 命令

在 Redis 中還提供了一個 watch 命令。
它可以為 Redis 事務提供 CAS 樂觀鎖行為(Check and Set / Compare and Swap),也就是多個線程更新變量的時候,會跟原值做比較,只有它沒有被其他線程修改的情況下,才更新成新的值。

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

2.4 事務可能遇到的問題

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

2.4.1 在執行 exec 之前發生錯誤

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

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set gupao 666
QUEUED
127.0.0.1:6379> hset qingshan 2673
(error) ERR wrong number of arguments for 'hset' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

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

2.4.2 在執行 exec 之后發生錯誤

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

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> hset k1 a b
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 k1
"1"

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

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

思考(作業):
為什么在一個事務中存在錯誤,Redis 不回滾?

3 Lua 腳本

Lua是一種輕量級腳本語言,它是用 C 語言編寫的,跟數據的存儲過程有點類似。 使用 Lua 腳本來執行 Redis 命令的好處:

  1. 一次發送多個命令,減少網絡開銷。
  2. Redis 會將整個腳本作為一個整體執行,不會被其他請求打斷,保持原子性。
  3. 對于復雜的組合命令,我們可以放在文件中,可以實現程序之間的命令集復用。

3.1 在 Redis 中調用 Lua 腳本

使用 eval 方法,語法格式:

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

3.2 在 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 表示參數值(實參)。

3.2.1 設置鍵值對

在 Redis 中調用 Lua 腳本執行 Redis 命令
以上命令等價于 set gupao 2673
在 redis-cli 中直接寫 Lua 腳本不夠方便,也不能實現編輯和復用,通常我們會把腳本放在文件里面,然后執行這個文件。

3.2.2 在 Redis 中調用 Lua 腳本文件中的命令, 操作 Redis

創建 Lua 腳本文件:

cd /usr/local/soft/redis5.0.5/src
vim gupao.lua

Lua 腳本內容,先設置,再取值:

redis.call('set','gupao','lua666')
return redis.call('get','gupao')

在 Redis 客戶端中調用 Lua 腳本

cd /usr/local/soft/redis5.0.5/src
redis-cli --eval gupao.lua 0

得到返回值:

[root@localhost src]# redis-cli --eval gupao.lua 0
"lua666"

3.2.3 案例: 對 IP 進行限流

需求:某個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 1
elseif tonumber(num)>tonumber(ARGV[2]) then
  return 0
else
  return 1
end

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…]
? 多個參數之間用一個 空格 分割

3.2.4 緩存 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 = 0
else
  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 2
OK
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6
(integer) 12

3.2.5 腳本超時

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','gupao','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 來實現,但是要注意那些耗時的操作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372