在redis-cluster下使用lua脚本的限制

redis-cluster由于不同的key可能会分配到不同的槽位,甚至不同的节点,所以在执行lua脚本时,需要有一定的限制。
因此在编写脚本时要特别注意键的选择,确保操作的键在同一分片甚至同一slot上。

一、集群中Lua脚本的限制

Redis Cluster对使用Lua脚本增加了一些限制,在此基础上,Redis集群版对使用Lua脚本存在如下额外限制:

  • 小版本限制,若无法执行EVAL的相关命令,并报错ERR command eval not support for normal user时,请升级小版本后重试,具体操作请参见升级小版本。
  • 所有Key必须在一个slot上,否则报错-ERR eval/evalsha command keys must be in same slot\r\n。
    您可以通过CLUSTER KEYSLOT命令获取目标Key的哈希槽(Hash Slot)进行确认。(在redis7.0可以使用其他方法支持同一节点不同slot,后面会说
  • 对单个节点执行SCRIPT LOAD命令时,不保证将该Lua脚本存入至其他节点中!!!
  • 不支持发布订阅命令,包括PSUBSCRIBE、PUBSUB、PUBLISH、PUNSUBSCRIBE、SUBSCRIBE和UNSUBSCRIBE。
  • 不支持UNPACK函数。

CASE:

假如有两个keytest01test02test01存在于第一组节点的槽位,test02存在于第三组节点槽位。
此时执行简单的lua脚本:

EVAL "return redis.call('get', KEYS[1]) + redis.call('get', KEYS[2])" test01 test02

根据不同的redis版本和实现,可能会返回1, null,也可能报错:CROSSSLOT Keys in request don't hash to the same slot
提示很清晰,是因为test01test02不在同一个slot上。但是根源其实是不在同一个机器上。我们再插入一条新的key:

SET test1 1

修改一下lua脚本并且执行:(同一个节点的同一个槽位上时是OK的)

EVAL "return redis.call('get', KEYS[1]) + redis.call('get', KEYS[2])" test01 test1

可以看到成功返回了2。这是因为查看test1slot,处于第一个节点同一个槽位上。因此我们可以得出结论,无法在lua脚本内操作两个不同的节点上的key。又由于我们无法预先规划hash算法的值,也无法得知slot究竟会怎么在节点上分布,因此可以大致认为在lua中操作多个key是不允许的。

二、解决方法

1.redis hashtag

为了应对这种问题,我们引入了hashtag(手动设置槽位)。查看以下样例:

set test{01} 1
set test2{01} 1
CLUSTER KEYSLOT test{01}
CLUSTER KEYSLOT test2{01}

可以发现test{01}test2{01}都落在9191这个slot上。这是因为在指定了hashtag之后,redis就只会用hashtag中的内容作为hash。例如test{01}test2{01}就都是使用01作为hash内容,使得他们可以进入到同一个槽位中。
这种做法特别适合不同业务中同一个id对应不同key的情况。例如在redis中我们分别存储了玩家的等级和经验:level:{userid}exp:{userid},使用这种做法就可以让属于一个玩家的不同key都进入到同一个slot,方便在脚本中原子地操作。
当然,使用这种做法可能会导致key的存储不平衡,最极端的例子就是hashtag内的key是同一个,那就会导致所有的数据都进入同一个slot

2.redis 7.0可在同一个节点的不同slot访问

如果你用的redis版本足够新(>7.0),那么可以尝试如下的lua脚本:

#!lua flags=allow-cross-slot-keys
return redis.call('GET', ARGV[1]) + redis.call('GET', ARGV[2])

请注意,这里的lua flags只是使得我们可以跨slot访问key。如果没有这么赋值,一般会出现报错Lua script attempted to access a non local key in a cluster node。但是依旧不允许传入不在同一个节点的keys。
因此我们这里使用的是ARGV而非KEYS

PS:
(1)此种方法需要redis7.0+allow-cross-slot-keys
(2)只能跨slot,但不能跨节点

3.redis proxy

Redis 6.0官方支持redis proxy
因为 Redis Cluster 内部使用的是P2P中的Gossip协议,每个节点既可以从其他节点得到服务,也可以向其他节点提供服务,没有中心的概念,通过一个节点可以获取到整个集群的所有信息。所以如果应用连接Redis Cluster可以配置一个节点地址,也可以配置多个节点地址。但需要注意如果集群进行了上下节点的的操作,其应用也需要进行修改,这样会导致需要重启应用,非常的不友好。从Redis 6.0开始支持了Prxoy,可以直接用Proxy来管理各个集群节点。
本文来介绍下如何使用官方自带的proxy:redis-cluster-proxy,通过使用 redis-cluster-proxy 可以与组成Redis集群的一组实例进行通讯,就像是单个实例一样。Redis群集代理是多线程的,使用多路复用通信模型,因此每个线程都有自己的与群集的连接,该连接由属于该线程本身的所有客户端共享。在某些特殊情况下(例如MULTI事务或阻塞命令),多路复用将被禁用;并且客户端将拥有自己的集群连接。这样客户端仅发送诸如GET和SET之类的简单命令就不需要Redis集群的专有连接。

代理模式(Proxy)执行Lua的额外限制
(也可以通过script_check_enable参数关闭以下检查项(不推荐)。)

  • 所有key都应该由KEYS数组来传递,redis.call/pcall中调用的Redis命令,key的位置必须是KEYS array,且不能使用Lua变量替换KEYS,否则直接返回错误信息:
  • 调用必须要带有key,否则直接返回错误信息:
  • 不支持在MULTI、EXEC事务中使用EVAL、EVALSHA、SCRIPT系列命令。
  • 不支持在Lua中执行跨Redis节点的命令,例如KEYS、SCAN等。为了保证Lua执行的原子性,Proxy会根据KEYS参数将Lua发送到一个Redis节点执行并获取结果,从而导致该结果与全局结果不一致。

参考:
https://help.aliyun.com/zh/redis/support/usage-of-lua-scripts?spm=a2c4g.92942.0.0.4b7e34c4V35sy0

还有一些公司自己实现的redis proxy待探索。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Contents
滚动至顶部