基于lua-redis实现榜单类服务的数据一致性
程序员文章站
2022-05-02 17:46:15
...
最近遇到一个bug,在涨幅榜(涨幅大于0)中出现了涨幅为负数的情况。通过解决这个bug学习到了如何在redis中使用lua来保持数据的一致性。
有兴趣的可以下载app(币世界),其中有很多榜单,这里简化处理流程,以便突出重点。
需求描述(简化)
实现币种的涨跌幅榜,并且涨幅榜中涨幅全是正的,跌幅榜中涨幅全是负数。
榜单存储设计
- 把币种的涨幅(从+inf到-inf)和币种id放到zset中,并把币种的涨幅作为score。
- 把币种的基本信息放到redis的hash中。
榜单流程设计
- 从zset中读取币种id
- 读取涨幅榜数据
ZREVRANGEBYSCORE top +inf (0 LIMIT 0 10
- 读取跌幅榜数据
ZRANGEBYSCORE top -inf (0 LIMIT 0 10
- 读取涨幅榜数据
- 从hash中读取所有的币种详情
问题所在
- 问题发生在当读取zset的时候,如果hash中币种的信息(涨幅)发生改变,那么极有可能出现榜单和涨幅不对应的情况,进一步抽象是需要在多次读取中保持事务。
- 有兴趣同学可以思考下,为什么没把币种信息直接放到到zset中?
如何解决?
- 可以使用 Lua 脚本实现原子性操作,因此读取zset和读取hash放到同一个lua中这个问题就解决了。
- 简化代码
local res = {}
local data = nil
if ARGV[6] == "2"
then
data = redis.call("ZREVRANGEBYSCORE", KEYS[1], ARGV[3], ARGV[2], "LIMIT", ARGV[4], ARGV[1])
else
data = redis.call("ZRANGEBYSCORE", KEYS[1], ARGV[2], ARGV[3], "LIMIT", ARGV[4], ARGV[1])
end
if table.getn(data) > 0
then
res[2] = redis.call("HMGET", "coin_top_coin", unpack(data))
end
return res
最终部分代码
d := redisFetcher.ExecuteScript(`
local res = {}
if ARGV[5] == "0"
then
res[1] = redis.call("ZCARD", KEYS[1])
end
local data=nil
if ARGV[6] == "2"
then
data = redis.call("ZREVRANGEBYSCORE", KEYS[1], ARGV[3], ARGV[2],"LIMIT", ARGV[4], ARGV[1])
else
data = redis.call("ZRANGEBYSCORE", KEYS[1], ARGV[2], ARGV[3],"LIMIT",ARGV[4], ARGV[1])
end
if (ARGV[5] > "0")
then
local temp={}
local rankDatas = redis.call("ZRANGEBYSCORE", "top_rank", "-inf", "+inf","LIMIT",0,ARGV[5])
for k,v in pairs(data) do
for k1,v1 in pairs(rankDatas) do
if v == v1 then
table.insert(temp,v)
end
end
end
data = temp
res[1] = table.getn(data)
end
if table.getn(data)>0
then
res[2] = redis.call("HMGET", "coin_top_coin", unpack(data))
end
return res
`,
[]string{topForm.TopName}, int64(size), topForm.Min, topForm.Max, int(offset), topForm.Scope, topForm.SortType).([]interface{})
总结
- 可以使用 Lua 脚本实现原子性操作
- 如果多次请求有结果依赖的时候,可以把请求操作改成lua,放到redis端执行。