Lua script attempted to access a non local key in a cluster node 问题解决

一、问题描述

最近优化公司需要对不同的业务系统的缓存工具提供一个标准化的解决方案。各个业务系统将缓存数据通过map结构进行存储,然后在缓存系统中将这些map获取出来,然后保存在redis数据库中。技术经理想到的最好解决方案是将map集合直接存储在redis的hash表中。但是要求对hash表中的每个元素设置缓存时间。

存在问题:
1)如果要往hash表添加数据,可以通过hmset命令,但是该命令无法设置过期时间;
2)hash中的每个key-value键值对也无法设置过期时间;

二、解决思路

我第一时间想到的是,在程序中遍历map集合获取每一个key-value,然后在把每一个key-value添加到redis中,然后设置过期时间。但是技术经理说,如果map集合数据量很大时候,这样的性能肯定不行。所以只能够想其他方法。

经过讨论之后,暂时想到两种解决方案:

  • 方案一:定义两个hash(hash_a和hash_b),hash_a用于保存缓存数据,hash_b用于保存每个key的过期时间。当执行查询时候,先查询该hash_b中每个key的过期时间,如果该过期时间大于当前时间,代表该key过期,那么就直接返回;如果没有过期,则通过key查询hash_a,得到缓存数据后返回给用户;

  • 方案二:编写一个lua脚本,该脚本从hash表中读取缓存数据,然后在调用setex方法重新将每一个key-value添加到redis中,并指定缓存时间。由于lua脚本在redis中本地运行,因此执行脚本的性能会很高。
    在这里插入图片描述
    方案三:使用Pipeline实现。首先将后台的map数据读取出来,然后使用setex命令将每个map元素添加到pipeline中。添加完成后再一次性将所有命令发送到redis中批量执行。

综合考虑,方案一操作比较繁琐,需要同时多两个hash进行操作。方案三redis集群不支持使用pipeline,所以最终选择方案二来实现。

三、代码实现

假设redis中有以下数据:

# 添加数据
hmset human name zhongliwen age 30

按照方案二,lua脚本要实现的功能是将每一个属性读取出来,这里有name和age。然后执行setex命令将它们重新添加到redis中。

这个脚本比较简单。但是如果是刚学lua的同学,可能在编写时候遇到各种问题,需要有耐心慢慢调试。这里简单说明一下上面的lua代码:
```lua
# 定义变量,用于存储hash对象的所有属性名称。
local result = redis.call('hkeys', KEYS[1])

# 循环遍历result,k代表table的下标,v代表hash对象每个属性名称。
for k, v in ipairs(result) 
do 
  # 获取hash对象指定属性的值
  local val = redis.call('hget', KEYS[1], (v))
   
  # 将属性添加到redis中,并指定ARGV[1],ARGV[1]代表过期时间。
  redis.call('setex', v, ARGV[1], val) 
end

上面就是lua脚本的全部代码。编写完成后,在redis集群的任意节点上编译该脚本。

script load "local result = redis.call('hkeys', KEYS[1]) for k, v in ipairs(result) do local val = redis.call('hget', KEYS[1], (v)) redis.call('setex', v, ARGV[1], val) end"

script load命令用于将脚本代码添加到缓存,执行该命令后会得到一串数字。通过这串数字可以在redis本地或客户端执行脚本的代码。
在这里插入图片描述
调用脚本的代码:

evalsha cd1e27f9d2b4844f55a1a94586cc46ee351f36f1 1 human 30

cd1e27f9d2b4844f55a1a94586cc46ee351f36f1 就是要执行的lua脚本hash。数字1代表后面只有1个参数,human就是要传递给lua脚本的参数,前面指定数字1,这里就只能有一个参数。数字30代表额外附加的参数。

但是,当执行上面脚本时候遇到了问题:

ERR Error running script (call to cd1e27f9d2b4844f55a1a94586cc46ee351f36f1): @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node

这个问题的原因是在redis集群环境下,如果通过lua脚本对redis的key进行操作时候,必须要保证这些key是在同一个slot中。如果是单机环境是不会出现这个问题。

解决办法是在存储数据的时候,需要对key进行处理。redis规定key中{...}部分用于计算slot槽的位置。所以,可以在每个key中添加相同的{...},这样它们就会存储到同一个slot槽中。如果使用使用hash结构,那么要保证key和field都是保存在同一个slot槽中。

因此,执行hmset命令如下:

hmset human{human} name{human} zhongliwen age{human} 30

上面的key和field后面都添加了{human},redis会通过{human}来计算存储到哪个slot中。这样可以保证lua脚本操作的key都是存储在同一个slot里面。

猜你喜欢

转载自blog.csdn.net/zhongliwen1981/article/details/108737045