lua腳本在redis集群中執行報錯--Lua script attempted to access a non local key in a cluster node


EVAL、EVALSHA命令

Redis從2.6.0版本開始提供了eval命令,通過內置的Lua解釋器,可以讓用戶執行一段Lua腳本並返回數據。因為Redis單線程模型的特點,可以保證多個命令的原子性(因為最近的項目需要用到簡單的分布式鎖,所以會用到lua來釋放鎖)

腳本性能

  1. Redis保證了腳本執行的原子性,所以在當前腳本沒執行完之前,別的命令和腳本都是等待狀態,所以一定要控制好腳本中的內容,防止出現需要消耗大量時間的內容(邏輯相對簡單)。

帶寬優化

  1. 為了避免每次執行都重復的將Lua腳本內容發送,Redis提供了evalsha命令,只需要將Lua腳本內容的SHA1校驗和發送即可(evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0)。
  2. Lua腳本中的變量(動態數據)請使用KEYSARGV獲取,如果把變量放在腳本中,必然會導致每次的腳本內容都不同(SHA1),Redis緩存大量無用或者一次性的腳本內容。

Redis Cluster 或 阿里雲Redis集群版使用注意事項

Redis從3.0開始支持了Cluster功能,之前使用eval的時候可能沒什么問題,但當切換成Cluster模式的時候,可能會出現一些問題:

  1. ERR Error running script (call to f_4a610f5543b3c3450220da7bd47825d3b6bffae8): @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node
  2. ERR eval/evalsha command keys must be in same slot(阿里雲Redis集群版)

上面的錯誤是因為Redis要求單個Lua腳本操作的key必須在同一個節點上,但是Cluster會將數據自動分布到不同的節點(虛擬的16384個slot,具體看官方文檔),阿里雲集群版的官網其實也有對應說明:在Redis集群版實例中,事務、腳本等命令要求所有的key必須在同一個slot中,如果不在同一個slot中將返回以下錯誤信息(:command keys must in same slot)

如何解決?

CLUSTER KEYSLOT key的文檔中提供了解決方法,你需要將把key中的一部分使用{}包起來,redis將通過{}中間的內容作為計算slot的key,類似key1{mykey}key2{mykey}(如果你的key是“REDIS_LOCK_FORPR”,可以講該key的一部分用{}括起來,例如“REDIS_LOCK_{FORPR}”)這樣的都會存放到同一個slot中(缺點是不能平滑的過度老業務,需要修改原來使用的key,如果之前的key是統一管理的,也沒那么麻煩)

官方地址:https://redis.io/commands/cluster-keyslot

// 部分代碼

private static final String DISTRIBUTE_LOCK_SCRIPT_UNLOCK_VAL = "if" +
            " redis.call('get', KEYS[1]) == ARGV[1]" +
            " then" +
            " return redis.call('del', KEYS[1])" +
            " else" +
            " return 0" +
            " end";

Object eval = 0;
List<String> keys = new ArrayList<>();
keys.add(REDIS_LOCK_PREFIX + lockKey);
List<String> argv = new ArrayList<>();
argv.add(lockValue);
try {
  // 這里不用指名有幾個key,jedis內部會根據keys集合大小來獲取
  eval = jedis.eval(DISTRIBUTE_LOCK_SCRIPT_UNLOCK_VAL, keys, argv);
} catch (Exception e) {
  logger.error("解鎖失敗:" + e.getMessage());
} finally {
  if (jedis != null) {
    jedis.close();
  }
}

集群環境中 lua 處理

redis 集群中,會將鍵分配的不同的槽位上,然后分配到對應的機器上,當操作的鍵為一個的時候,自然沒問題,但如果操作的鍵為多個的時候,集群如何知道這個操作落到那個機器呢?比如簡單的mget命令,mget test1 test2 test3,還有我們上面執行腳本時候傳入多個參數,帶着這個問題我們繼續。

首先用 docker 啟動一個 redis 集群,docker pull grokzen/redis-cluster,拉取這個鏡像,然后執行docker run -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 --name redis-cluster-script -e "IP=0.0.0.0" grokzen/redis-cluster啟動這個容器,這個容器啟動了一個 redis 集群,3 主 3 從。

我們從任意一個節點進入集群,比如redis-cli -c -p 7003,進入后執行cluster nodes可以看到集群的信息,我們鏈接的是從庫,執行set lua fun,有同學可能會問了,從庫也可以執行寫嗎,沒問題的,集群會計算出 lua 這個鍵屬於哪個槽位,然后定向到對應的主庫。

執行mset lua fascinating redis powerful,可以看到集群反回了錯誤信息,告訴我們本次請求的鍵沒有落到同一個槽位上

(error) CROSSSLOT Keys in request don't hash to the same slot

同樣,還是上面的 lua 腳本,我們加上集群端口號,執行redis-cli -p 7000 --eval /tmp/limit_fun.lua limit_vgroup 192.168.1.19 , 10 3 1548660999,一樣返回上面的錯誤。

針對這個問題,redis官方為我們提供了hash tag這個方法來解決,什么意思呢,我們取鍵中的一段來計算 hash,計算落入那個槽中,這樣同一個功能不同的 key 就可以落入同一個槽位了,hash tag 是通過{}這對括號括起來的字符串,比如上面的,我們改為mset lua{yes} fascinating redis{yes} powerful,就可以執行成功了,我這里 mset 這個操作落到了 7002 端口的機器。

同理,我們對傳入腳本的鍵名做 hash tag 處理就可以了,這里要注意不僅傳入鍵名要有相同的 hash tag,里面實際操作的 key 也要有相同的 hash tag,不然會報錯Lua script attempted to access a non local key in a cluster node,什么意思呢,就拿我們上面的例子來說,執行的時候如下所示,可以看到,前面的兩個鍵都加了 hash tag —— yes,這樣沒問題,因為腳本里面只是用了一個拼接的 key —— limit_vgroup{yes}_192.168.1.19{yes}

redis-cli -c -p 7000 --eval /tmp/limit_fun.lua limit_vgroup{yes} 192.168.1.19{yes} , 10 3 1548660999

如果我們在腳本里面加上redis.call("GET", "yesyes")(別讓這個鍵跟我們拼接的鍵落在一個solt),可以看到就報了上面的錯誤,所以在執行腳本的時候,只要傳入參數鍵、腳本里面執行 redis 命令時候的鍵有相同的 hash tag 即可。

另外,這里有個 hash tag 規則:

鍵中包含{字符;建中包含{字符,並在{字符右邊;並且{,}之間有至少一個字符,之間的字符就用來做鍵的 hash tag。

所以,鍵limit_vgroup{yes}_192.168.1.19{yes}的 hash tag 是 yesfoo{}{bar}鍵的 hash tag就是它本身。foo{{bar}}鍵的 hash tag 是 {bar

總結

  • redis集群版的lua腳本,可以通過key的部分字符串hash來解決
  • redis集群版的分布式是會根據KEY進行hash取模然后打到不同的slot,這種思想是典型的分而治之。分治,分流,降級。

思考

如果某個業務都通過key{mykey}去儲存獲取內容,所有的操作都會hash到同一個slot,這個slot所在的節點壓力就會變大(不均衡),如果解決?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM