Redis脚手架

什么是脚手架?

对外部成熟的框架再进行包装。

创建连接对象

package gedis

import (
   "context"
   "fmt"
   "log"
   "sync"
   "time"

   "github.com/go-redis/redis/v8"
)

var redisClient *redis.Client

//单例模式
var redisClient_Once sync.Once

func Redis() *redis.Client {
   redisClient_Once.Do(func() {
      redisClient = redis.NewClient(&redis.Options{
         Network:  "tcp",
         Addr:     "127.0.0.1:6379",
         Password: "", //密码
         DB:       0,  // redis数据库

         //连接池容量及闲置连接数量
         PoolSize:     15, // 连接池数量
         MinIdleConns: 10, //好比最小连接数
         //超时
         DialTimeout:  5 * time.Second, //连接建立超时时间
         ReadTimeout:  3 * time.Second, //读超时,默认3秒, -1表示取消读超时
         WriteTimeout: 3 * time.Second, //写超时,默认等于读超时
         PoolTimeout:  4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。

         //闲置连接检查包括IdleTimeout,MaxConnAge
         IdleCheckFrequency: 60 * time.Second, //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。
         IdleTimeout:        5 * time.Minute,  //闲置超时
         MaxConnAge:         0 * time.Second,  //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接

         //命令执行失败时的重试策略
         MaxRetries:      0,                      // 命令执行失败时,最多重试多少次,默认为0即不重试
         MinRetryBackoff: 8 * time.Millisecond,   //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔
         MaxRetryBackoff: 512 * time.Millisecond, //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔

      })
      pong, err := redisClient.Ping(context.Background()).Result()
      if err != nil {
         log.Fatal(fmt.Errorf("connect error:%s", err))
      }
      log.Println(pong)
   })
   return redisClient
}

封装类

Redis常用类型

string、hash、list、set、zset五种

image-20220321202958324

type StringOperation struct {
   ctx context.Context
}

func NewStringOperation() *StringOperation {
   return &StringOperation{
      ctx: context.Background(),
   }
}

func (so *StringOperation) Set() {

}

func (so *StringOperation) Get(key string) *StringResult {
   return NewStringResult(Redis().Get(so.ctx, key).Result())
}

异常处理

type StringResult struct {
   Result string
   Error  error
}

func NewStringResult(result string, error error) *StringResult {
   return &StringResult{Result: result, Error: error}
}

func (sr *StringResult) Unwrap() string {
   if sr.Error != nil {
      panic(sr.Error)
   }
   return sr.Result
}
func (sr *StringResult) Unwrap_or(str string) string {
   if sr.Error != nil {
      return str
   }
   return sr.Result
}

使用

func main() {
   fmt.Println(gedis.NewStringOperation().Get("as").Unwrap_or("dfasd"))
}

获取多值和实现迭代器

//stringOperation
func (so *StringOperation) MGet(key ...string) *SliceResult {
   return NewSliceResult(Redis().MGet(so.ctx, key...).Result())
}
type SliceResult struct {
   Result []interface{}
   Error  error
}

func NewSliceResult(result []interface{}, error error) *SliceResult {
   return &SliceResult{Result: result, Error: error}
}

func (sr *SliceResult) Unwrap() []interface{} {
   if sr.Error != nil {
      panic(sr.Error)
   }
   return sr.Result
}
func (sr *SliceResult) Unwrap_or(v []interface{}) []interface{} {
   if sr.Error != nil {
      return v
   }
   return sr.Result
}
func (sr *SliceResult) Iter() *Iterator {
   return NewIterator(sr.Result)
}
package gedis

type Iterator struct {
   data  []interface{}
   index int
}

func NewIterator(data []interface{}) *Iterator {
   return &Iterator{data: data}
}

func (i *Iterator) HasNext() bool {
   if i.data == nil || len(i.data) == 0 {
      return false
   }
   return i.index < len(i.data)
}

func (i *Iterator) Next() (ret interface{}) {
   ret = i.data[i.index]
   i.index = i.index + 1
   return
}
func main() {
   iter := gedis.NewStringOperation().MGet("name", "age", "id").Iter()

   for iter.HasNext() {
      fmt.Println(iter.Next())
   }
}

set花式封装

从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

	EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
	PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
	NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
	XX :只在键已经存在时,才对键进行设置操作。

    因为 SET 命令可以通过参数来实现和 SETNX 、 SETEX 和 PSETEX 三个命令的效果,所以将来的 Redis 版本可能会废弃并最终移除 SETNX 、 SETEX 和 PSETEX 这三个命令。

const ATTR_EXPR = "expr"

type OperationAttr struct {
   Name  string
   Value interface{}
}

type OperationAttrs []*OperationAttr

func (oas OperationAttrs) Find(name string) interface{} {
   for _, attr := range oas {
      if attr.Name == name {
         return attr.Value
      }
   }
   return nil
}

func WithExpire(t time.Duration) *OperationAttr {
   return &OperationAttr{
      Name:  ATTR_EXPR,
      Value: t,
   }
}
func (so *StringOperation) Set(key string, value interface{}, attrs ...*OperationAttr) *StringResult {
   exp := OperationAttrs(attrs).Find(ATTR_EXPR)
   if exp == nil {
      exp = time.Second * 0
   }
   return NewStringResult(Redis().Set(so.ctx, key, value, exp.(time.Duration)).Result())

}
func main() {
   fmt.Println(gedis.NewStringOperation().Set("name", "lisi", gedis.WithExpire(10*time.Second)))
}

对其进行改造,

type InterfaceResult struct {
   Result interface{}
   Err    error
}

func NewInterfaceResult(result interface{}, err error) *InterfaceResult {
   return &InterfaceResult{Result: result, Err: err}
}


func (sr *InterfaceResult) Unwrap() interface{} {
   if sr.Err != nil {
      panic(sr.Err)
   }
   return sr.Result
}
func (sr *InterfaceResult) Unwrap_or(v interface{}) interface{} {
   if sr.Err != nil {
      return v
   }
   return sr.Result
}

返回值设为包装类型

func (oas OperationAttrs) Find(name string) *InterfaceResult {
   for _, attr := range oas {
      if attr.Name == name {
         return NewInterfaceResult(attr.Value, nil)
      }
   }
   return NewInterfaceResult(nil, fmt.Errorf("operation found err:%s", name))
}
func (so *StringOperation) Set(key string, value interface{}, attrs ...*OperationAttr) *StringResult {
   exp := OperationAttrs(attrs).Find(ATTR_EXPR)
   return NewStringResult(Redis().Set(so.ctx, key, value, exp.Unwrap_or(0*time.Second).(time.Duration)).Result())

}

设置SetNX

func WithNX() *OperationAttr {
   return &OperationAttr{Name: ATTR_NX, Value: empty}
}
func (so *StringOperation) Set(key string, value interface{}, attrs ...*OperationAttr) *InterfaceResult {
   exp := OperationAttrs(attrs).Find(ATTR_EXPR)
   nx := OperationAttrs(attrs).Find(ATTR_NX).Unwrap_or(nil)
   if nx != nil {
      return NewInterfaceResult(Redis().SetNX(so.ctx, key, value, exp.Unwrap_or(0*time.Second).(time.Duration)).Result())
   }
   return NewInterfaceResult(Redis().Set(so.ctx, key, value, exp.Unwrap_or(0*time.Second).(time.Duration)).Result())

}

缓存

type GetterFunc func() string
type SimpleCache struct {
   Operation *StringOperation
   Expire    time.Duration
   Getter    GetterFunc
}

func NewSimpleCache(operation *StringOperation, expire time.Duration) *SimpleCache {
   return &SimpleCache{Operation: operation, Expire: expire}
}

func (sc *SimpleCache) SetCache(key string, value interface{}) {
   sc.Operation.Set(key, value, WithExpire(sc.Expire)).Unwrap()
}

func (sc *SimpleCache) GetCache(key string) (ret interface{}) {
   ret = sc.Operation.Get(key).Unwrap_or_Else(sc.Getter)
   sc.SetCache(key, ret)
   return
}
func (sr *StringResult) Unwrap_or_Else(f func() string) string {
   if sr.Error != nil {
      return f()
   }
   return sr.Result
}
func main() {
   newCache := gedis.NewSimpleCache(gedis.NewStringOperation(), time.Second*15)

   //模拟从数据库查询数据
   newCache.Getter = func() string {
      log.Println("get from db")
      return "data by id = 123"
   }
   fmt.Println(newCache.GetCache("hello"))
   fmt.Println(newCache.GetCache("hello"))
   fmt.Println(newCache.GetCache("hello"))
}

配合Gorm

func main() {
   newCache := gedis.NewSimpleCache(gedis.NewStringOperation(), time.Second*15)

   newsId := 1
   newCache.Getter = func() string {
      log.Println("get from db")
      newsModel := lib.NewNewsModel()
      lib.Gorm.Table("mynews").Where("id = ?", newsId).Find(newsModel)
      b, _ := json.Marshal(newsModel)
      return string(b)
   }
   fmt.Println(newCache.GetCache("news123").(*lib.NewsModel).NewsContent)
}

优化

//这里使用了装饰着模式
func NewsDBGetter(id string) gedis.GetterFunc {
   return func() string {
      log.Println("get from db")
      newsModel := NewNewsModel()
      Gorm.Table("mynews").Where("id = ?", id).Find(newsModel)
      b, _ := json.Marshal(newsModel)
      return string(b)
   }

}

CachePool

var NewsCachePool *sync.Pool

func init() {
   NewsCachePool = &sync.Pool{
      New: func() interface{} {
         return gedis.NewSimpleCache(gedis.NewStringOperation(), time.Second*15)
      },
   }
}

func NewsCache() *gedis.SimpleCache {
   return NewsCachePool.Get().(*gedis.SimpleCache)
}

func ReleaseNewsCache(cache *gedis.SimpleCache) {
   NewsCachePool.Put(cache )
}
func main() {
   r := gin.New()
   r.Handle("GET", "/news/:id", func(context *gin.Context) {
      newCache := lib.NewsCache()
      defer lib.ReleaseNewsCache(newCache)
      newId := context.Param("id")
      newCache.Getter = lib.NewsDBGetter(newId)
      context.Header("Content-type", "application/json")
      context.String(200, newCache.GetCache("news"+newId).(string))
   })
   r.Run(":8080")

}

缓存穿透

  1. 对唯一标识做基本限制,譬如id必须在某些范围内
  2. 如果一旦数据库不存在,则在redis中置入null值,需要设置过期时间,通常这个过期时间要小于普通key的过期时间
  3. 其他手段,比如用户登录,IP限流等
package gedis

import (
	"regexp"
	"time"
)

type CachePolicy interface {
	Before(key string )
	IfNil(key string,v interface{})
	SetOperation(opt *StringOperation) //今日改动点
}

//缓存穿透 策略
type CrossPolicy struct {
	KeyRegx string  //检查key的正则
	Expire time.Duration
	opt *StringOperation
}

func NewCrossPolicy(keyRegx string,expire time.Duration) *CrossPolicy {
	return &CrossPolicy{KeyRegx: keyRegx,Expire:expire}
}

func (this *CrossPolicy) Before(key string )  {
		if !regexp.MustCompile(this.KeyRegx).MatchString(key){
			panic("error cache key")
		}
}
func(this *CrossPolicy) IfNil(key string,v interface{})  {
	 	this.opt.Set(key,v,WithExpire(this.Expire)).Unwrap()

}
func(this *CrossPolicy) SetOperation(opt *StringOperation){
	this.opt=opt
}

Q.E.D.


勤俭节约,艰苦奋斗。