redis拾遺(三)——發佈訂閱,事務和lua腳本

# 前言

本篇博客總結一些redis在實際中的應用實例

發佈訂閱模式

如果提到發佈訂閱模式,我們首先想到的就是消息中間件,消息中間件中有很多比較冗雜的概念。但是redis其實也可以爲我們實現一個簡易版本的發佈訂閱模式

基於list實現的簡易隊列

redis中可以通過隊列的rpush和lpop可以實現消息隊列,但是消費者如果採用的普通的pop彈出消息的命令,則需要不斷的去輪詢消息隊列,看是否有消息。爲此,redis提供了一個阻塞消費消息的命令,blpop/brpop,如果消費端沒有從隊列中取出消息則會一直阻塞,如下動圖所示,通過rpush+blpop組成的動圖實例,右邊的爲消費者。

在這裏插入圖片描述
上述消息發送模式並不針對一對多,同時blpop/brpop需要指定超時時間

訂閱頻道

這種方式和消息中間件相差不大,消費者通過訂閱指定頻道的數據,生產者會往指定頻道中發送數據。只有訂閱了對應頻道的消費者會受到消息,訂閱支持正則表達式的通配符訂閱。命令:publish/subscribe/psubscribe(正則表達式的方式訂閱)

具體實例如下動圖所示,右邊兩個爲消費者,左邊一個爲生產者,在往不同隊列中發送消息。

在這裏插入圖片描述

jedis對應的操作

生產者

/**
 * 生產者
 */
@Slf4j
public class PublishTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.72.128", 6379);
        jedis.publish("news-music", "JayZhou");
        jedis.publish("news-games", "JayZhouPlayGames");
        log.info("消息發送完畢");
    }
}

消費者

/**
 * 消費者
 */
@Slf4j
public class ConsumerTest {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.72.128", 6379);
        final MyListener listener = new MyListener();
        // 使用模式匹配的方式設置頻道
        // 會阻塞
        jedis.psubscribe(listener, new String[]{"news-*"});//這裏會阻塞,下一行日誌永遠不會被打印
        log.info("一輪消息接受完畢");
    }
}

事件監聽處理器

/**
 * 監聽的處理方式,需要繼承JedisPubSub
 */
public class MyListener extends JedisPubSub {
    // 取得訂閱的消息後的處理
    public void onMessage(String channel, String message) {
        System.out.println(channel + "=" + message);
    }

    // 初始化訂閱時候的處理
    public void onSubscribe(String channel, int subscribedChannels) {
        // System.out.println(channel + "=" + subscribedChannels);
    }

    // 取消訂閱時候的處理
    public void onUnsubscribe(String channel, int subscribedChannels) {
        // System.out.println(channel + "=" + subscribedChannels);
    }

    // 初始化按表達式的方式訂閱時候的處理
    public void onPSubscribe(String pattern, int subscribedChannels) {
        // System.out.println(pattern + "=" + subscribedChannels);
    }

    // 取消按表達式的方式訂閱時候的處理
    public void onPUnsubscribe(String pattern, int subscribedChannels) {
        // System.out.println(pattern + "=" + subscribedChannels);
    }

    // 取得按表達式的方式訂閱的消息後的處理
    public void onPMessage(String pattern, String channel, String message) {
        System.out.println(pattern + "=" + channel + "=" + message);
    }
}

運行的時候,先運行消費者,再運行生產者。

redis事務

基本的三個命令

什麼是事務,這裏不再贅述,redis事務涉及四個命令:multi(開啓事務),exec(執行事務),discard(取消事務),watch(監視),前三個命令是事務的基本命令,幾乎只要提到事務這個概念,就會有前三個,只是最後一個可能對我們有些陌生

簡單的命令實例

tom給jack轉賬200(凡是提到事務,轉賬似乎是永遠繞不開的一個實例)

正常的事務執行命令

127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set jack 1000
OK
127.0.0.1:6379> multi ## 開啓redis事務
OK
127.0.0.1:6379> decrby tom 200	
QUEUED ## redis 命令被緩存
127.0.0.1:6379> incrby jack 200
QUEUED
127.0.0.1:6379> exec ## 批量執行
1) (integer) 800
2) (integer) 1200
127.0.0.1:6379> mget tom jack
1) "800"
2) "1200" ## 金額正常減少

discard丟棄事務

127.0.0.1:6379> set tom 1000
OK
127.0.0.1:6379> set jack 1000
OK
127.0.0.1:6379> multi ## 開啓事務
OK
127.0.0.1:6379> decrby tom 200
QUEUED
127.0.0.1:6379> incrby jack 200
QUEUED
127.0.0.1:6379> discard ## 丟棄事務
OK
127.0.0.1:6379> mget tom jack ## 金額未變化
1) "1000"
2) "1000"

需要注意的是redis的事務命令是不能嵌套的,在一個multi中不能開啓另一個multi

watch

watch 爲redis提供了一種CAS樂觀鎖行爲(何爲CAS就不解釋了)。可以通過watch監視一個或者多個key ,如果開啓事務之後,至少有一個被監視。被監視的key鍵在exec執行之前被修改了,那麼整個事務都會被取消( key提前過期除外)。可以用unwatch取消對指定key值的監控。

如下實例

客戶端1 客戶端2
127.0.0.1:6379> set account 10000
OK
127.0.0.1:6379> watch account
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby account 1000
QUEUED
127.0.0.1:6379>
127.0.0.1:6379> incrby account 5000
(integer) 15000
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get account
"15000"

上述實例可以看到,客戶端1的事務最終並未執行成功。

需要說明的是,redis的事務不支持回滾,如果在事務的幾個命令中有錯誤的,在錯誤命令之前的命令均會正常執行。redis官網針對這個問題是如下解釋的——鑑於沒有任何機制能避免程序員自己造成的錯誤, 並且這類錯誤通常不會在生產環境中出現, 所以 Redis 選擇了更簡單、更快速的無回滾方式來處理事務。因此我們沒有辦法來利用redis的事務機制保證原子性和數據一致性

lua腳本

lua是什麼——lua 百度百科

linux下四行命令安裝lua

curl -R -O http://www.lua.org/ftp/lua-5.4.0.tar.gz
tar zxf lua-5.4.0.tar.gz
cd lua-5.4.0
make all test

安裝完成之後,直接敲lua,會出現如下提示信息表示安裝成功

在這裏插入圖片描述

爲什麼redis中要執行lua腳本,前面我們說了,redis的事務機制無法滿足原子性,那麼批量命令的原子性需要通過lua來完成,lua與redis的關係,某種程度上來說像一個存儲過程和數據庫的關係。lua可以一次發送多個命令,同時能保證這些命令的原子性。

redis中執行lua腳本

命令:

eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
  • eval redis執行lua腳本的命令
  • lua-script 表示lua腳本的內容
  • key-num 表示參數的個數
  • [key1 key2 key3 …] 表示參數列表
  • [value1 value2 value3 …] 表示對應的值的列表

實例

127.0.0.1:6379> eval "return 'hello this is lua script'" 0
"hello this is lua script" ###直接輸出lua腳本的結果

lua中執行redis腳本

在lua中可以直接調用redis的命令,通過redis.call即可實現

命令:

redis.call(command, [KEYS1 KEYS2 KEYS3 ....] [ARGV1 ARGV2 ARGV3 ....])
  • command redis的命令
  • [KEYS1 KEYS2 KEYS3 …] 形參名列表
  • [ARGV1 ARGV2 ARGV3 …] 實參列表

實例:

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lua luatest
OK
127.0.0.1:6379> get lua
"luatest"

redis調用lua文件

命令:

redis-cli --eval [lua文件路徑] [KEYS列表] , [ARGV列表]

通過啓動時執行lua腳本文件

編輯一個lua文件,內容如下

redis.call('set','luakey','luaRedisScript')
return redis.call('get','luakey')

在啓動redis客戶端的時候,指定腳本路徑即可

[root@localhost bin]# redis-cli --eval /usr/local/self_lua_script/luaone.lua
"luaRedisScript"

實例

這裏還是用比較常見的實例,ip地址訪問次數限流

指定的ip地址,在5秒內只能訪問10次。準備的lua腳本如下:

-- KEYS[1]ip地址,ARGV[1] 過期時間 ,ARGV[2]訪問次數限制
local visitNum=redis.call('incr',KEYS[1])
if tonumber(visitNum) == 1 then
        redis.call('expire',KEYS[1],ARGV[1])
        return 1
elseif tonumber(visitNum)>tonumber(KEYS[2]) then
        return 0
else
        return 1
end

執行指定的lua腳本

redis-cli --eval /usr/local/self_lua_script/ip_limit.lua app:ip:limit:192.168.72.128 , 5 10 ## KEYS列表和ARGV列表逗號兩頭都要有空格

執行之後,可以看到相關效果,如果返回0,表示不允許訪問。

最後說一點

如果lua腳本不安全,怎麼搞?

eval 'while(true) do end' 0

如果某個客戶端執行了上述的腳本,則redis服務端無法給其他客戶端提供服務了,那如何避免出現這種問題?爲了防止某個腳本執行時間過長導致Redis無法提供服務,Redis提供了lua-time-limit參數限制腳本的最長運行時間,默認爲5秒鐘。

在客戶端中有一個script kill 命令,可以暴力中斷上述腳本的執行

但如果執行如下腳本

eval "redis.call('set','testKey','testValue') while(true) do end"

則 script kill命令也不管用了。遇到這種情況,只能通過 shutdown nosave 命令來強行終止 redis。 shutdown nosave 和 shutdown 的區別在於 shutdown nosave 不會進行持久化操作,意味着發生在上一次快照後的數據庫修改都會丟失。

總結

本篇博客總結了redis中一些複製的使用操作,基於list的簡易生產消費,基於publish/subscribe和psubscribe的訂閱消息操作。redis的事務。以及最後的lua腳本,同時提供了一個ip地址限流的實例。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章