LiteralliJeff 发表于 2022-5-1 15:29

Go内存缓存


基本上每个项目都有加快服务响应或复杂计算的需求。简单快速的解决方案是使用缓存。通常,有Redis或Memcached,但我们在单实例微服务中不需要使用它们。有时候在你的Go应用程序中使用一个简单的内存缓存会更好,今天我想介绍实现内存缓存的方法。
Map


第一种方法是简单的缓存实现。通常,使用map存储结构体。此外,还需要监控元素的过期时间和缓存大小。
package go_cacheimport (    "errors"    "sync"    "time")//这里缓存的对象是user结构体,包含id、email两个字段type user struct {    Id    int64`json:"id"`    Email string `json:"email"`}//实际存到map中的结构体内容type cachedUser struct {    user    expireAtTimestamp int64}//创建缓存结构体,包含对象的增加、删除等方法type localCache struct {    stop chan struct{}    wg    sync.WaitGroup    mu    sync.RWMutex   //读写锁    users mapcachedUser//map存储用户信息}//创建缓存对象,cleanupInterval设置定期清除缓存过期元素func newLocalCache(cleanupInterval time.Duration) *localCache {    lc := &localCache{      users: make(mapcachedUser),      stop:make(chan struct{}),    }    //启动goroutine后台定期清除过期元素    lc.wg.Add(1)    go func(cleanupInterval time.Duration) {      defer lc.wg.Done()      lc.cleanupLoop(cleanupInterval)    }(cleanupInterval)    return lc}func (lc *localCache) cleanupLoop(interval time.Duration) {    t := time.NewTicker(interval)    defer t.Stop()    for {      select {      case <-lc.stop://主动退出            return      case <-t.C:            lc.mu.Lock()            for uid, cu := range lc.users {                if cu.expireAtTimestamp <= time.Now().Unix() {                  delete(lc.users, uid)                }            }            lc.mu.Unlock()      }    }}func (lc *localCache) stopCleanup() {    close(lc.stop)    lc.wg.Wait()}//更新元素过期时间func (lc *localCache) update(u user, expireAtTimestamp int64) {    lc.mu.Lock()    defer lc.mu.Unlock()    lc.users = cachedUser{      user:            u,      expireAtTimestamp: expireAtTimestamp,    }}var (    errUserNotInCache = errors.New("the user isn't in cache"))//读缓存func (lc *localCache) read(id int64) (user, error) {    lc.mu.RLock()    defer lc.mu.RUnlock()    cu, ok := lc.users    if !ok {      return user{}, errUserNotInCache    }    return cu.user, nil}//删除缓存元素func (lc *localCache) delete(id int64) {    lc.mu.Lock()    defer lc.mu.Unlock()    delete(lc.users, id)}
上面的例子我们使用用户ID作为缓存元素的Key。使用map,所有update/read/delete操作时间复杂度都是O(1)
优点

实现简单性能高
缺点

存储每一类结构体都需要实现缓存需要单独测试缓存单独的bug修复
gCache库


gCache库对缓存实现进行抽象,包含各种配置。例如,可以很简单地设置缓存淘汰规则,缓存元素最大长度,过期时间TTL等。
package go_cacheimport (    "errors"    "fmt"    "github.com/bluele/gcache"    "time")type gCache struct {    users gcache.Cache //该对象可以缓存任何类型数据}const (    cacheSize = 1_000_000    cacheTTL= 1 * time.Hour // default expiration)//创建缓存对象,使用ARC算法淘汰缓存元素func newGCache() *gCache {    return &gCache{      users: gcache.New(cacheSize).Expiration(cacheTTL).ARC().Build(),    }}//更新缓存元素过期时间func (gc *gCache) update(u user, expireIn time.Duration) error {    return gc.users.SetWithExpire(u.Id, u, expireIn)}//读取缓存func (gc *gCache) read(id int64) (user, error) {    val, err := gc.users.Get(id)    if err != nil {      if errors.Is(err, gcache.KeyNotFoundError) {            return user{}, errUserNotInCache      }      return user{}, fmt.Errorf("get: %w", err)    }    return val.(user), nil}//删除缓存元素func (gc *gCache) delete(id int64) {    gc.users.Remove(id)}优点

可直接投入生产环境中使用接口适用任意类型不同的缓存淘汰算法:LRU,LFU,ARC
缺点

去缓存都需要做类型转换性能差这个库有一点时间没有维护
BigCache库


BigCache库高性能、支持并发、缓存淘汰,可存储大量元素而不影响性能。BigCache将元素放在堆中忽略GC。
package go_cacheimport (    "encoding/json"    "errors"    "fmt"    "github.com/allegro/bigcache"    "strconv"    "time")type bigCache struct {    users *bigcache.BigCache}func newBigCache() (*bigCache, error) {    bCache, err := bigcache.NewBigCache(bigcache.Config{      // 分片数量 (必须是2的幂次方)      Shards: 1024,      // 存活时间,过了该时间才会删除元素      LifeWindow: 1 * time.Hour,      //删除过期元素的时间间隔(清理缓存).      // 如果设置为<= 0,则不执行任何操作      // 设置为< 1秒会适得其反— bigcache只能精确到1秒.      CleanWindow: 5 * time.Minute,      // rps * lifeWindow, 仅用于初始内存分配      MaxEntriesInWindow: 1000 * 10 * 60,      // 以字节为单位的元素大小最大值,仅在初始内存分配时使用      MaxEntrySize: 500,      // 打印内存分配信息      Verbose: false,      // 缓存分配的内存不会超过这个限制, MB单位      // 如果达到值,则可以为新条目覆盖最旧的元素      // 0值表示没有限制      HardMaxCacheSize: 256,      // 当最旧的元素由于过期时间或没有剩余空间而被删除时,触发回调      // 对于新元素,或者因为调用了delete。将返回一个表示原因的位掩码.      // 默认值为nil,这意味着没有回调.      OnRemove: nil,      // OnRemoveWithReason当因为过期时间或没有空间时,最老一条元素被删除会触发该回调。会返回删除原因。      // 默认值为nil。      OnRemoveWithReason: nil,    })    if err != nil {      return nil, fmt.Errorf("new big cache: %w", err)    }    return &bigCache{      users: bCache,    }, nil}func (bc *bigCache) update(u user) error {    bs, err := json.Marshal(&u)    if err != nil {      return fmt.Errorf("marshal: %w", err)    }    return bc.users.Set(userKey(u.Id), bs)}func userKey(id int64) string {    return strconv.FormatInt(id, 10)}func (bc *bigCache) read(id int64) (user, error) {    bs, err := bc.users.Get(userKey(id))    if err != nil {      if errors.Is(err, bigcache.ErrEntryNotFound) {            return user{}, errUserNotInCache      }      return user{}, fmt.Errorf("get: %w", err)    }    var u user    err = json.Unmarshal(bs, &u)    if err != nil {      return user{}, fmt.Errorf("unmarshal: %w", err)    }    return u, nil}func (bc *bigCache) delete(id int64) {    bc.users.Delete(userKey(id))}
我们使用JSON编码/解码元素,但也可以使用任何数据格式。例如,一种二进制格式Protobuf可以显著提高性能。
优点

可用户生产环境丰富的缓存配置维护当中缓存不会触发GC,在大元素存储性能高
缺点

需要自己实现元素编解码。
性能测试

goos: darwingoarch: amd64pkg: go-cachecpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmark_bigCacheBenchmark_bigCache-8         1751281         688.0 ns/op       390 B/op          6 allocs/opBenchmark_gCacheBenchmark_gCache-8            772846          1699 ns/op         373 B/op          8 allocs/opBenchmark_localCacheBenchmark_localCache-8       1534795         756.6 ns/op       135 B/op          0 allocs/opPASSok      go-cache    6.044s
BigCache是最快的缓存库。gCache的性能主要受到interface{}对象转换上。
总结


我们调研了Golang的不同内存缓存。记住没有最好的解决方案,需要根据实际应用场景来决定。 使用本文来比较解决方案,并决定哪一个适合您的项目需要。
页: [1]
查看完整版本: Go内存缓存