Redis - Lua 指令碼

Dxtr發表於2019-03-09

Redis 從 2.6 開始內嵌了 Lua 環境來支援使用者擴充套件功能. 通過 Lua 指令碼, 我們可以原子化地執行多條 Redis 命令.

Redis 中的 Lua 指令碼


在 Redis 中執行 Lua 指令碼需要用到 EVALEVALSHASCRIPT *** 這幾個命令, 下面分別來介紹一下:

  1. 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 開始的.

  2. 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"
    複製程式碼

    EVALSHAEVAL 的引數差不多. 只是把指令碼改成了快取中指令碼的 sha1 值, 其餘沒有區別.

  3. SCRIPT LOAD: 將指令碼快取到伺服器中.

  4. SCRIPT FLUSH: 清空伺服器中的所有指令碼

  5. SCRIPT EXISTS: 判斷指令碼是否存在於伺服器中

  6. 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 指令碼執行嗎? 怎麼又可以了呢?

Redis - Lua 指令碼
從 Redis 3.2 開始(5.0 以後是預設開啟), 提供了另外一種持久化和主從複製的方案可以允許隨機寫入. 相較於前一種直接複製 Lua 指令碼並重新執行指令碼這一方案, 第二種方案不復制 Lua 指令碼, 並且指令碼只會執行一次, 執行完後對資料庫產生的資料變化會生成 Redis 命令用於持久化和主從同步. 由於 Lua 指令碼只會執行一次, 所以就不存在之前執行多次造成的隨機性不一致現象, 自然允許隨機行操作了.

  1. 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()

  2. 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 就可以了.

參考

相關文章