Redis 從 2.6 開始內嵌了 Lua 環境來支援使用者擴充套件功能. 通過 Lua 指令碼, 我們可以原子化
地執行多條 Redis 命令.
Redis 中的 Lua 指令碼
在 Redis 中執行 Lua 指令碼需要用到 EVAL
和 EVALSHA
和 SCRIPT ***
這幾個命令, 下面分別來介紹一下:
-
EVAL
: 執行 Lua 指令碼EVAL script numkeys key[key ...] arg [arg ...] 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second" 複製程式碼
- script 就是 Lua 指令碼本身
- numkeys 表示指令碼中涉及到的 Redis Key 的數量
- key[key ...] 表示指令碼中涉及到的所有 Redis Key
- arg [arg ...] 表示指令碼中涉及到的所有引數(變數), 不限制個數
在 Lua 指令碼中可以通過
KEYS[]
陣列加上腳標訪問具體的 Redis Key, 通過ARGV[]
資料加腳標訪問傳入的引數(變數). 注意, 腳標都是從 1 開始的. -
EVALSHA
: 從快取中執行 Lua 指令碼EVAL sha1 numkeys key[key ...] arg [arg ...] 127.0.0.1:6379> SCRIPT LOAD "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" "a42059b356c875f0717db19a51f6aaca9ae659ea" 127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second" 複製程式碼
EVALSHA
和EVAL
的引數差不多. 只是把指令碼改成了快取中指令碼的 sha1 值, 其餘沒有區別. -
SCRIPT LOAD
: 將指令碼快取到伺服器中. -
SCRIPT FLUSH
: 清空伺服器中的所有指令碼 -
SCRIPT EXISTS
: 判斷指令碼是否存在於伺服器中 -
SCRIPT KILL
: 停止當前正在執行的指令碼
在 Redis 中執行的 Lua 指令碼必須是純函式形式. 也就是說, 給定一段指令碼並且傳入相同的引數, 寫入 Redis 中的資料也必須是一致的. Redis 會拒絕隨機性的寫入, 因為這會造成資料的不一致性.
Lua 指令碼的持久化和主從複製(Redis 5.0 以下)
Redis 允許在 Lua 指令碼中通過 redis.call()
和 redis.pcall()
來執行 Redis 命令. 如果 Lua 指令碼對 Redis 中的資料進行了更改, 那麼除了更新資料庫中的資料之外, 還會執行另外兩個操作:
- 把這段 Lua 指令碼寫入到 AOF 檔案中, 保證 Redis 在重啟時候可以執行該指令碼
- 把這段 Lua 指令碼複製給從庫執行, 保證主從資料一致性
127.0.0.1:6379> eval "redis.call('set', KEYS[1], ARGV[1]); return redis.call('get', KEYS[1])" 1 mykey myvalue
"myvalue"
# 檢視 AOF 檔案
➜ cat appendonly.aof
*5
$4
eval
$70
redis.call('set', KEYS[1], ARGV[1]); return redis.call('get', KEYS[1])
$1
1
$5
mykey
$7
myvalue
複製程式碼
所以, 如果 Redis 接受隨機性寫入的話, Redis 在重啟前後或者在主從庫之間就會存在資料不一致的現象, 當然, 這是不被允許的.
Redis 防止隨機寫入(Redis 5.0 以下)
比如, 我在 Lua 指令碼中獲取當前時間並將當前時間 SET 到一個 KEY 中, Redis 就會拒絕操作並丟擲一個異常; 也就是說, Redis 會拒絕存在隨機寫入的 Lua 指令碼執行.
127.0.0.1:6379> eval "local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.
複製程式碼
Redis 中一共有 10 個隨機類命令: SPOP
, SRANDMEMBER
, SSCAN
, ZSCAN
, HSCAN
, SCAN
, RANDOMKEY
, LASTSAVE
, PUBSUB
, TIME
.
當一些返回資料是無序的命令, 比如 SMEMBERS
在 Lua 中被呼叫時, 返回的資料都是進行過排序後返回的, 所以得到的資料順序都是一致的.
並且 Redis 修改了 Lua 指令碼中的隨機數生成函式(math.random
, math.randomseed
)使得新指令碼執行的時候, 返回的種子都是一樣的. 所以在 Lua 指令碼中, 如果未使用 math.randomseed
,僅僅使用 math.random
, 生成的隨機數序列都是一樣的.
Redis 允許隨機寫入的情況
等下, 不是說為了保證 Redis 重啟前後和主從之間的資料一致性, Redis 會拒絕執行執行存在隨機寫入的 Lua 指令碼執行嗎? 怎麼又可以了呢?
-
5.0 版本之前 Redis 3.2 提供了
redis.replicate_commands()
, 但是需要在執行 Lua 指令碼的時候的手動開啟.127.0.0.1:6379> eval "redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0 "1552060128" 複製程式碼
在 Lua 指令碼中, 從呼叫
redis.replicate_commands()
開始到指令碼結束, 這一部分指令碼所產生的 Redis 命令會被包在一個MULTI / EXEC
事務中, 併發給 AOF 或者從庫. 當然, 只有對資料庫中的資料產生變化的 Redis 命令才會被生成幷包裝進MULTI / EXEC
事務.# AOF 檔案 ➜ cat appendonly.aof *1 $5 MULTI *3 $3 set $3 now $10 1552114016 *1 $4 EXEC 複製程式碼
注意: Redis 只是會將呼叫
redis.replicate_commands()
後面的部分放進事務中. 在其前面的部分如果呼叫了寫操作是會破壞資料的一致性的, 此時,redis.replicate_commands()
並不會生效. 見?:127.0.0.1:6379> eval "redis.call('set', 'key', 'value') redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0 (error) ERR Error running script (call to f_a8c3ce5ccbfc3074b49ea277b7370ded0c2d354b): @user_script:1: @user_script: 1: Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode. 127.0.0.1:6379> keys * 1) "now" 2) "key" # AOF 檔案 ➜ cat appendonly.aof *3 $4 eval $156 redis.call('set', 'key', 'value') redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now') $1 0 複製程式碼
所以, 如果在 Lua 指令碼中需要進行隨機寫入的話, 建議在指令碼的開頭就呼叫
redis.replicate_commands()
-
5.0 版本及以後版本預設開啟
Redis 對於隨機寫入的持久化和主從複製的控制
Redis 3.2 還引入了另一個機制: 可以自行決定是否持久化或者進行主從複製, 可以通過 redis.set_repl(***)
設定, 引數可以為:
redis.REPL_ALL
: 開啟 AOF 持久化和主從複製(預設)redis.REPL_AOF
: 僅開啟 AOF 持久化redis.REPL_REPLICA
: 僅開啟主從複製redis.REPL_SLAVE
: 同redis.REPL_REPLICA
redis.REPL_NONE
: 都不開啟 一般redis.set_repl(***)
很少用到, 因為這會造成重啟前後和主從庫之間資料不一致. 保留預設的redis.REPL_ALL
就可以了.