Redis - Lua 腳本

Redis 從 2.6 開始內嵌了 Lua 環境來支持用戶擴展功能. 經過 Lua 腳本, 咱們能夠原子化地執行多條 Redis 命令.redis

Redis 中的 Lua 腳本


在 Redis 中執行 Lua 腳本須要用到 EVALEVALSHASCRIPT *** 這幾個命令, 下面分別來介紹一下:shell

  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 值, 其他沒有區別.bash

  3. SCRIPT LOAD: 將腳本緩存到服務器中.服務器

  4. SCRIPT FLUSH: 清空服務器中的全部腳本併發

  5. SCRIPT EXISTS: 判斷腳本是否存在於服務器中app

  6. SCRIPT KILL: 中止當前正在執行的腳本dom

在 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 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 就能夠了.

參考

相關文章
相關標籤/搜索