「7」尝试自行封装 Go-Redis ZSet 浅尝命令模式|青训营笔记

405 阅读2分钟

创建时间: May 25, 2022 8:00 PM 标签: Redis

这是我参与「第三届青训营 -后端场」笔记创作活动的的第N篇笔记。

在使用 Go-Redis 完成小组项目开发的过程中,我遇到一些需要合理使用 Redis 才能很好实现的任务。

Feed 视频流中,利用 Redis ZSet 的有序特性,存储 video_id 和时间戳映射,从而实现以时间戳作为游标的有序遍历视频。

在经过几小时的 RTFM ( Read the friendly manual ),我找到可以实现这一需求的指令——zrangebyscore,如果它正常工作,它将如官方文档所说可以提供一个时间复杂度为 O(logN+M)O(logN + M) 的查找算法( 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 然后执行该指令。

redis.uptrace.dev/guide/go-re…

  • 封装调用方法

    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 返回?