欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基于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端执行。