背景
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实现
- 使用装饰器方法初始化Client
- SetHitRate 打开Metrics记录开关
- 当使用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()
}
输出
优化
笔者只是抛砖引玉,通过demo的装饰器+闭包特性,演示实现了丐版Set/Get 命中次数自动统计
还有很多未实现,以及可优化的,如:
- 封装引用上,目前初始化直接调用闭包;可以直接将现有Client初始化嵌套在闭包内、简化初始化。等
- 触发机制:也可以使用redis hook实现触发记录metrics