在redis-cluster模式下使用lua脚本
由于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 | |
可以发现它们的槽位分别在1840和14163上:
此时根据之前的槽位分布我们可以知道,test01存在于第一组节点,test02存在于第三组节点。
此时执行简单的lua脚本:
1 | |
根据不同的redis版本和实现,可能会返回1, null,也可能报错:CROSSSLOT Keys in request don't hash to the same slot。
提示很清晰,是因为test01和test02不在同一个slot上。但是根源其实是不在同一个机器上。我们再插入一条新的key:
1 | |
修改一下lua脚本并且执行:
1 | |
可以看到成功返回了2。这是因为查看test1的slot,发现是1840,处于第一个节点上。因此我们可以得出结论,无法在lua脚本内操作两个不同的节点上的key。又由于我们无法预先规划hash算法的值,也无法得知slot究竟会怎么在节点上分布,因此可以大致认为在lua中操作多个key是不允许的。
redis hashtag
为了应对这种问题,我们引入了hashtag。查看以下样例:
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只是使得我们可以跨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对性能也略有影响,官方也不建议这么操作。