创建时间: May 25, 2022 8:00 PM 标签: Redis
这是我参与「第三届青训营 -后端场」笔记创作活动的的第N篇笔记。
在使用 Go-Redis 完成小组项目开发的过程中,我遇到一些需要合理使用 Redis 才能很好实现的任务。
Feed 视频流中,利用 Redis ZSet 的有序特性,存储 video_id 和时间戳映射,从而实现以时间戳作为游标的有序遍历视频。
在经过几小时的 RTFM ( Read the friendly manual ),我找到可以实现这一需求的指令——zrangebyscore,如果它正常工作,它将如官方文档所说可以提供一个时间复杂度为 的查找算法( limit 不可以太大。
时间复杂度: O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)). www.redis.cn/commands/zr…
接下来将在 Golang 中实践它
-
写入一些测试数据
//... insert some data func TestInsertVideoCache(t *testing.T) { bootstrap.InitAll() for i := 0; i < 10; i++ { time.Sleep(100 * time.Millisecond) util.ZAdd2Redis( "test:"+global.VideoSeqSetKey, float64(util.TimeNowInt64()), i, ) } } -
范围查询数据
func TestZSetRangeByScore(t *testing.T) { bootstrap.InitAll() res, err := util.ZSetRangeByScoreStrings("test:"+global.VideoSeqSetKey, &redis.ZRangeBy{ Max: strconv.FormatInt(util.TimeNowInt64(), 10), Min: "-inf", Offset: 0, Count: 5, }) if err != nil { zap.L().Error("", zap.Error(err)) return } global.Logf.Infof("ZSetRange >> %#v\n", res) } // ZSetRange >> []string{"0", "1", "2", "3", "4"} // util.func func ZSetRangeByScoreStrings(key string, z *redis.ZRangeBy) ([]string, error) { return global.RedisDB.ZRangeByScore(global.RedisDB.Context(), key, z).Result() }
接下来是一些失败的测试,我没有找到原因(暂时将它记下来,期待未来某天它可以被解决
应用到业务场景中,我的 Redis 会缓存 video_id 数据,而主键是以 int64 存进 redis 的,我希望它可以被以 redis 的格式取出来,而默认的 zrangebyscore 返回的是 []string,因此我开始了自定义 Redis 指令的尝试。
首先我阅读了 redisC.ZRangeByScore 的源码,发现它其实只是拼接了一些 args,然后交个 cmdable 去执行这条指令。
-
Go-Redis 源码流程如下
func (c cmdable) ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd { return c.zRangeBy(ctx, "zrangebyscore", key, opt, false) } func (c cmdable) zRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy, withScores bool) *StringSliceCmd { args := []interface{}{zcmd, key, opt.Min, opt.Max} if withScores { args = append(args, "withscores") } if opt.Offset != 0 || opt.Count != 0 { args = append( args, "limit", opt.Offset, opt.Count, ) } cmd := NewStringSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd }
起初,我仿照该流程写了一遍,直到 _ = c(ctx, cmd) 发现 redis.C 的 cmdable 没有导出,不能直接使用该语法去调用。
然后我翻阅了 go-redis 官方文档中关于如何使用自定义指令的部分,知晓了使用 Redis.Do 方法可以自定义拼接 args 然后执行该指令。
-
封装调用方法
func ZSetRangeByScoreInt(key string, z *redis.ZRangeBy) ([]int64, error) { // func ZSetRangeByScoreInt(key string, z *redis.ZRangeBy) ([]string, error) { // global.RedisDB. args := []interface{}{"zrangebyscore", key, z.Min, z.Max} if z.Offset != 0 || z.Count != 0 { args = append(args, "limit", z.Offset, z.Count) } // 一下仿照: global.RedisDB.Do() ctx := global.RedisDB.Context() cmd := redis.NewIntSliceCmd(ctx, args...) // cmd := redis.NewStringSliceCmd(ctx, args...) _ = global.RedisDB.Process(ctx, cmd) return cmd.Result() }
但测试结果让我比较失望,看起来 redis 返回的数据并不能被正确的序列化。
main_cache_test.go:61 {"error": "redis: can't parse int reply: "$1""}
-
测试代码
func TestZSetRangeByScoreInt(t *testing.T) { bootstrap.InitAll() res, err := util.ZSetRangeByScoreInt("test:"+global.VideoSeqSetKey, &redis.ZRangeBy{ Max: strconv.FormatInt(util.TimeNowInt64(), 10), Min: "-inf", Offset: 0, Count: 5, }) if err != nil { zap.L().Error("", zap.Error(err)) return } global.Logf.Infof("ZSetRange >> %#v\n", res) }
💡 最后:我决定向 Redis 妥协,优先完成项目正常工作 关于为什么 Go-Redis 的 ZRangeByScore 方法为啥只返回 []string 猜测可能是由于 Redis 实现的原因?该 value 只能以 string 返回?