Go语言 go-redis与watch

3,821 阅读4分钟

对redis 命令很熟悉的 可以直接跳过,因为这个库的api 命名基本上都和命令一一对应上了,很容易记

redis 数据库连接

github.com/go-redis/re…

var rdb *redis.Client
var ctx = context.Background()

func initClient() (err error) {
   rdb = redis.NewClient(&redis.Options{
      Addr:     "localhost:6379",
      Password: "",
      DB:       0,
   })
   _, err = rdb.Ping(ctx).Result()
   if err != nil {
      return err
   }
   return nil
}

初学者要注意 外部声明了一个 rdb, init方法里面 一定得是rdb= 千万别写成rdb:=了 否则rdb就是个局部变量

外部引用就是nil了

另外还有redis的哨兵模式以及集群模式 的两种连接方法 这里就不演示了。有需要的可以自行查询

基本使用


func redisDemo() {
   err := rdb.Set(ctx, "score", 100, 0).Err()
   if err != nil {
      panic(err)
   }
   val, err := rdb.Get(ctx, "score").Result()
   if err != nil {
      panic(err)
   }
   fmt.Println("score:", val)

   val2, err := rdb.Get(ctx, "keytest").Result()
   if err == redis.Nil {
      // 这里主要是看这个key不存在的判定方法就可以了
      fmt.Println("keytest does not exist")
   } else if err != nil {
      fmt.Println("get keytest failed")
   } else {
      fmt.Println("keytest:", val2)
   }

}

总体上 redis的操作 还是挺简单的,可以自行探索,直接在goland中.一下就能看到对应的api 这里就不再一一演示了

redis-类似排行榜的操作

这个例子会比上面的例子稍微复杂一点,很多网站的类似的排行榜的操作 其实就是个zset, 写法就是这:

func redisDemo2() {
   zsetkey := "language_rank"
   languages := []*redis.Z{
      {Score: 90, Member: "java"},
      {Score: 80, Member: "go"},
      {Score: 70, Member: "js"},
      {Score: 60, Member: "rust"},
      {Score: 50, Member: "c++"},
   }

   num, err := rdb.ZAdd(ctx, zsetkey, languages...).Result()
   if err != nil {
      panic(err)
   }
   fmt.Println("num:", num)
   // 增加值
   newScore, err := rdb.ZIncrBy(ctx, zsetkey, 10, "go").Result()
   if err != nil {
      panic(err)
   }
   fmt.Println("newScore:", newScore)

   // 取分数最高的3个
   ret := rdb.ZRevRangeWithScores(ctx, zsetkey, 0, 2).Val()
   for _, z := range ret {
      fmt.Println("name:", z.Member,
         "  score:", z.Score)
   }

   // 取分数在一定范围之内的
   op := redis.ZRangeBy{
      Min: "80",
      Max: "110",
   }

   ret = rdb.ZRangeByScoreWithScores(ctx, zsetkey, &op).Val()
   for _, z := range ret {
      fmt.Println(z.Member,"  ",z.Score)
   }

}

pipeline

pipeline 主要就是网络优化,可以节省rtt,并不是事务,千万别搞错了,比如你要执行3个命令,那你正常操作就是需要3个rtt的网络时间,但是你可以把这3个 放到一个pipeline里面执行 那就只需要1个rtt的网络时间即可

同一时间有大量命令要执行的时候 就可以用这个pipeline了

pipeline也不是万能的,比如后面的操作依赖前面的操作的时候 就不适合了,例如我们要取一个值,然后根据这个值 来决定 set一个新的值,这2个操作 set操作 就依赖前面的get操作了

这种场景是没办法用pipeline的

事务 TxPipeline

redis是单线程的,单个命令是原子操作,但是来自不同客户端的命令是可以依次执行的,这个时候 我们就需利用 TxPipeline来确保我们的2个命令之间 不会有来自其他客户端的命令插入进来。

watch

这种场景也是常用的之一,比如我们下单抢购显卡,显卡现在这么少,你怎么确保用户下单的那一刻一定有库存呢? 那其实就是你下单的时候 watch一下库存的这个key,如果发现下单的过程中这个库存被crud了 那就直接返回呗 操作失败

注意key是可以传多个的

举个例子吧:

func watchDemo() {
   key := "watch_count"
   err := rdb.Watch(ctx, func(tx *redis.Tx) error {
      n, err := tx.Get(ctx, key).Int64()
      if err != nil && err != redis.Nil {
         return err
      }
      _, err = tx.TxPipelined(ctx, func(pipeliner redis.Pipeliner) error {
         pipeliner.Set(ctx, key, n+1, 0)
         return nil
      })
      return err
   }, key)

   if err != nil {
      fmt.Println("tx exec failed:",err)
      return
   }
   fmt.Println("tx exec success")
}

这个函数单独执行是可以的,没问题

image.png

那如果我稍微改一下,在这个函数里面 sleep 几秒钟

然后在这个期间 我在redis-cli里面 去修改一下这个值 看看

func watchDemo() {
   key := "watch_count"
   err := rdb.Watch(ctx, func(tx *redis.Tx) error {
      n, err := tx.Get(ctx, key).Int()
      if err != nil && err != redis.Nil {
         return err
      }
      _, err = tx.TxPipelined(ctx, func(pipeliner redis.Pipeliner) error {
         time.Sleep(5 * time.Second)
         pipeliner.Set(ctx, key, n+1, 0)
         return nil
      })
      return err
   }, key)

   if err != nil {
      fmt.Println("tx exec failed:", err)
      return
   }
   fmt.Println("tx exec success")
}

注意在这个时候 我们在事务的执行过程中 加了一个time sleep的操作 以保证我们可以有充足的时间 在redis-cli里面 set一下这个key 结果也是显而易见 这次操作肯定是失败的

注意,这段关于watch的代码 网上很多都是tx.Pipelined 这个是不对的,千万别被这个误导了,tx.Pipelined里面执行的不是事务 所以代码在这里是会失效的,一定得是 tx.TxPipelined 才行