由于redis cluster
不支持事务,在一些原子性的操作时无法使用Multi
。因此lua
脚本成了最合适的选择,但是由于redis
在cluster
下多个slot
的问题,使用lua
稍不注意会导致很多问题。
跨slot
使用lua
脚本
Redis Cluster
将整个数据集分为16384个槽位,每个槽位可以分配给集群中的一个节点或多个节点,每个节点可以负责多个槽位。Redis Cluster
使用哈希槽(hash slot)来实现数据的分布和负载均衡。具体来说,当一个客户端向Redis Cluster发送一个命令时,Redis Cluster首先对命令中的键(key)进行哈希计算,得到一个哈希值,然后将该哈希值对16384取模,得到一个槽位号,最后将命令转发给负责该槽位的节点处理。
通过使用哈希槽,Redis Cluster
可以实现数据的分布和负载均衡,同时也可以实现节点的动态扩容和缩容。当一个节点加入或离开集群时,Redis Cluster
会自动将该节点负责的槽位重新分配给其他节点,从而保证整个集群的数据分布均衡。
如下图所示,是我的一个测试集群的槽位分布:
我的集群中有6个节点,分成了3组节点,每组都互为主备。此时执行以下两个set
语句,并且获取它们所在的槽位:
1 | SET test01 1 |
可以发现它们的槽位分别在1840和14163上:
此时根据之前的槽位分布我们可以知道,test01
存在于第一组节点,test02
存在于第三组节点。
此时执行简单的lua
脚本:
1 | 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
。
提示很清晰,是因为test01
和test02
不在同一个slot
上。但是根源其实是不在同一个机器上。我们再插入一条新的key:
1 | SET test1 1 |
修改一下lua
脚本并且执行:
1 | EVAL "return redis.call('get', KEYS[1]) + redis.call('get', KEYS[2])" test01 test1 |
可以看到成功返回了2。这是因为查看test1
的slot
,发现是1840,处于第一个节点上。因此我们可以得出结论,无法在lua
脚本内操作两个不同的节点上的key。又由于我们无法预先规划hash算法的值,也无法得知slot
究竟会怎么在节点上分布,因此可以大致认为在lua
中操作多个key是不允许的。
redis
hashtag
为了应对这种问题,我们引入了hashtag。查看以下样例:
1 | set test{01} 1 |
可以发现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
。
另一种办法
如果你用的redis
版本足够新(>7.0),那么可以尝试如下的lua
脚本:
1 | #!lua flags=allow-cross-slot-keys |
请注意,这里的lua flags
只是使得我们可以跨slot
访问key。如果没有这么赋值,一般会出现报错Lua script attempted to access a non local key in a cluster node
。但是依旧不允许传入不在同一个节点的keys。
因此我们这里使用的是ARGV
而非KEYS
。
总结
综合来看,在redis cluster
下使用lua
脚本是唯一可以原子化操作序列的方式。为了避免跨slot
操作key,可以使用hashtag,也可以用新特性直接操作。但是hashtag无疑是更好的方式,毕竟跨了slot
对性能也略有影响,官方也不建议这么操作。