Go Redis中间件「自动注入实现业务Key命中率统计」

228 阅读3分钟

背景

  Redis监控大盘,默认只能展示实例级监控指标;如 命中/失败次数、命中/失败率

如果需要展示业务/项目Key以上指标,并推送至prometheus/grafana dashboard,如何来实现?

封装Redis操作

如 最简单的 Get/Set


import (
   "context"
   "encoding/json"
   "fmt"
   "github.com/redis/go-redis/v9"
   "x.com/infra/common/conf"
   "x.com/infra/common/global"
   "x.com/infra/common/initialize"
   "x.com/infra/common/util"
   "time"
)

type RedisInterfac interface {
   Exists(key string) (bool, error)
   Set(key string, members ...interface{}) error
   Get(key string) (r string, err error)
   ...
}

type RedisInstance struct {
   Client  *redis.Client
   Options *redis.Options
}

func NewRedisOptions() *redis.Options {
   return &redis.Options{}
}

func NewRedisClient(c *conf.RedisConf, args ...interface{}) *RedisInstance {

   var (
      ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
      rdoptions   = &redis.Options{}
      rdb         = &RedisInstance{}
   )

   for _, arg := range args {
      switch v := arg.(type) {
      case *redis.Options:
         rdoptions = v
      }
   }

   defer cancel()

   rdb.Client = redis.NewClient(&redis.Options{
      Addr:         c.Host + ":" + fmt.Sprintf("%v", c.Port),
      Password:     c.Password,   // no password set
      DB:           rdoptions.DB, // use default DB
      DialTimeout:  30 * time.Second,
      ReadTimeout:  30 * time.Second,
      WriteTimeout: 30 * time.Second,
      PoolSize:     100,
      MinIdleConns: 10, 
   })

   _, err := rdb.Client.Ping(ctx).Result()
   if err != nil {
      fmt.Printf("Connect %v Failed! Err: %v\n",
         c.Host, err)
      global.LogEntry.Errorf("主机:%v %v 错误详情:%v", c.Host, initialize.RedisPing.ToString(), err)
   }

   return rdb
}

func (ri *RedisInstance) Get(key string) (r string, err error) {
   ctx := context.Background()

   defer util.TimeCost()

   res, err := ri.Client.Get(ctx, key).Result()
   if err == redis.Nil {
      fmt.Printf("%v does not exist\n",
         key)
   } else if err != nil {
      panic(err)
   }
   return res, err

}

func (ri *RedisInstance) Set(key string, members ...interface{}) error {
   defer util.TimeCost()

   ctx := context.Background()
   val, _ := json.Marshal(members)
   err := ri.Client.Set(ctx, key, val, 0).Err()

   if err != nil {
      //panic(err)
      fmt.Println(err)
   }
   return err
}

func (ri *RedisInstance) Exists(key string) (bool, error) {
   ctx := context.Background()

   defer util.TimeCost()

   res := ri.Client.Exists(ctx, key)
   if res.Err() != nil {
      fmt.Printf("%v does not exist\n",
         key)
   }
   if res.Val() == 1 {
      return true, nil
   }
   return false, nil

}

Metrics定义

type KeyMetric struct {
   KeyName      string
   GetHitCount  int
   GetMissCount int
   GetHitRate   float64
   SetHitCount  int
   SetMissCount int
   SetHitRate   float64
}

实现装饰器「透明装饰器」

这里正常情况下RedisInstanceDecorator需要实现RedisInstance的所有方法,Get/Set...

如果装饰器不需要实现完整的接口,或者希望它透明地传递大部分调用到原始对象,可以使用结构体嵌入来简化实现「透明装饰器」


var RI RedisInstanceDecorator

type RedisInstanceDecorator struct {
   R          *RedisInstance
   KeyMetrics map[string]KeyMetric // 记录详情
   Record     bool                 // 是否记录
}

func (ri *RedisInstanceDecorator) Get(key string) (r string, err error) {
   ri.recordMetrics(key, "Get")()
   return ri.R.Get(key)
}

func (ri *RedisInstanceDecorator) Set(key string, members ...interface{}) error {
   ri.recordMetrics(key, "Set")()
   return ri.R.Set(key, members)
}

func (ri *RedisInstanceDecorator) Exists(key string) (bool, error) {
   return ri.R.Exists(key)
}

func (ri *RedisInstanceDecorator) SetHitRate(t bool) {
   if t {
      if ri.KeyMetrics == nil {
         ri.Record = true
         ri.KeyMetrics = make(map[string]KeyMetric)
      } else {
         ri.Record = true
      }
      fmt.Printf("设置记录Metrics开关: %v\n", t)
   }
}

闭包实现计数器

使用闭包特性,修改ri.KeyMetrics计数器;实现Get/Set计数

recordMetrics返回func() *RedisInstanceDecorator


func (ri *RedisInstanceDecorator) recordMetrics(keyName string, opType string) func() *RedisInstanceDecorator {

   return func() *RedisInstanceDecorator {
      if ri.Record {
         if ri.KeyMetrics != nil {

            if _, ok := ri.KeyMetrics[keyName]; !ok {
               v := p(ri.KeyMetrics[keyName], opType)
               v.KeyName = keyName
               ri.KeyMetrics[keyName] = v
            } else {
               v := p(ri.KeyMetrics[keyName], opType)
               v.KeyName = keyName
               ri.KeyMetrics[keyName] = v
            }
         }
      }
      return ri
   }
}

func p(km KeyMetric, opType string) KeyMetric {
   switch opType {
   case "Get":
      km.GetHitCount += 1
   case "Set":
      km.SetHitCount += 1
   }

   return km
}

func (ri *RedisInstanceDecorator) GetHitRate() {
   if ri.Record {
      for k, v := range ri.KeyMetrics {
         fmt.Printf("Key:%v KeySet命中次数:%v KeyGet命中次数:%v \n", k, v.SetHitCount, v.GetHitCount)
      }
   }
}

调用Demo实现

  1. 使用装饰器方法初始化Client
  2. SetHitRate 打开Metrics记录开关
  3. 当使用Set/Get方法时,自动计算命中次数

func TestRedisDecorator(t *testing.T) {
   rc := conf.GetInfrastructureRedisConf()
   rdoptions := RCV3.NewRedisOptions()
   r := RCV3.NewRedisClient(rc, rdoptions)

   v := RCV3.RedisInstanceDecorator{R: r}

   v.SetHitRate(true)

   v.Set("TestRedisKey", "TestRedisValue")
   val1, _ := v.Get("TestRedisKey")
   fmt.Printf("Key:%v 获取到Vale:%v\n", "TestRedisKey", val1)
   v.GetHitRate()

   val2, _ := v.Get("TestRedisKey")
   fmt.Printf("Key:%v 获取到Vale:%v\n", "TestRedisKey", val2)
   v.GetHitRate()

   val3, _ := v.Get("TestRedisKey")
   fmt.Printf("Key:%v 获取到Vale:%v\n", "TestRedisKey", val3)
   v.GetHitRate()
}

输出

image.png

优化

笔者只是抛砖引玉,通过demo的装饰器+闭包特性,演示实现了丐版Set/Get 命中次数自动统计

还有很多未实现,以及可优化的,如:

  • 封装引用上,目前初始化直接调用闭包;可以直接将现有Client初始化嵌套在闭包内、简化初始化。等
  • 触发机制:也可以使用redis hook实现触发记录metrics