一个工作中遇到的一连串Redis key同步问题

一个工作中遇到的一连串Redis key同步问题

Jul 11, 2023 ·
6 分钟阅读

问题起源

有一个开发任务是实现一个去重算法,需求是要求可以对一定时间或一定范围的数据做排重,要支持分布式并发(因为是在Flink中实现)。从其他代码中抄了一份过来,这个算法是这样子实现的:

local union = ARGV[1]
local maxKey = tonumber(ARGV[2])
local addNum = redis.call(\"sadd\", \"distinct:set\", union)
if
addNum == 1
then
local count = redis.call(\"lpush\", \"distinct:queue\", union)
print(count)
if
count > maxKey
then
local remove = redis.call(\"rpop\", \"distinct:queue\", 1)
redis.call(\"srem\", \"distinct:set\", remove[1])
end
return 1
else
return 0
end

问题描述&解决

问题1

这段lua脚本执行在redis集群的时候遇到了第一个异常:

READONLY You can't write against a read only replicas.

从异常信息来看,是lua脚本在尝试写的时候写在了一个只读的从节点上,看起来需要一个参数指定写redis的时候指定写到主节点的配置。

问题1解决

在一阵Google后,锁定了redis客户端的ReadFrom参数。将此参数设置为REPLICA,问题解决。

问题2

解决第一个问题后,出现了第二个异常:

lua script attempted to access a non local key in a cluster node.

从异常信息来看,lua脚本访问到了不属于当前执行脚本的节点上的key。这里想到,redis集群模式会根据key的值做hash,将key hash到集群的很多个slot上,可能是因为set和queue的key被hash到了不同的slot导致的。

问题2解决?

在又一阵Google后,找到了一个解决方式:hash tag,即在两个key的相同部分使用{}包裹,含有被{}包裹的key,在计算hash的时候只使用{}中间的部分参与计算,而不会使用整个key来计算。即以上lua脚本的key改为{distinct}:set{distinct}:queue。(这里可以使用一个redis命令CLUSTER KEYSLOT key来查看这个key经过redis的hash之后是否一样。经过验证后,使用了hash tag的两个key分配的slot是同一个。)

问题3

可惜事不遂人愿,使用hash tag后,上面的异常依然会抛出。 这里注意到,异常透露出的信息并不是两个key没有分配在同一个节点上,而是分配的节点没有在lua脚本当前执行所在的节点上。这里经过再一次疯狂google之后发现了端倪。 首先是根据异常搜索到一篇博客指出,可以使用redis的KEYS命令,将要操作的key作为参数传递给lua脚本。之后在redis官方文档中找到了关于KEYS的描述:

Important: to ensure the correct execution of scripts, both in standalone and clustered deployments, all names of keys that a function accesses must be explicitly provided as input key arguments. The script should only access keys whose names are given as input arguments. Scripts should never access keys with programmatically-generated names or based on the contents of data structures stored in the database.

大概翻译是:不论是独立部署还是集群部署,为了确保脚本的正确执行,函数访问所有的key都需要用KEYS命令传递,脚本不应该使用程序生成的名称或者数据库中存储的结构中的名称来访问key。 OK,那么上面的lua脚本改为这样:

local union = ARGV[1]
local maxKey = tonumber(ARGV[2])
local queueKey = KEYS[1]
local setKey = KEYS[2]
local addNum = redis.call("sadd", setKey, union)
if
addNum == 1
then
local count = redis.call("lpush", queueKey, union)
print(count)
if
count > maxKey
then
local remove = redis.call("rpop", queueKey, 1)
redis.call("srem", setKey, remove[1])
end
return 1
else
return 0
end

将key改为参数传递之后,ReadFrom不设置参数也不会出现问题1的异常了,问题顺利解决。