美文网首页go
Go内存缓存

Go内存缓存

作者: Go语言由浅入深 | 来源:发表于2022-04-30 18:42 被阅读0次

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

Map

第一种方法是简单的缓存实现。通常,使用map存储结构体。此外,还需要监控元素的过期时间和缓存大小。

package go_cache

import (
    "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 map[int64]cachedUser  //map存储用户信息
}

//创建缓存对象,cleanupInterval设置定期清除缓存过期元素
func newLocalCache(cleanupInterval time.Duration) *localCache {
    lc := &localCache{
        users: make(map[int64]cachedUser),
        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[u.Id] = 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[id]
    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_cache

import (
    "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_cache

import (
    "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: darwin
goarch: amd64
pkg: go-cache
cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
Benchmark_bigCache
Benchmark_bigCache-8         1751281           688.0 ns/op       390 B/op          6 allocs/op
Benchmark_gCache
Benchmark_gCache-8            772846          1699 ns/op         373 B/op          8 allocs/op
Benchmark_localCache
Benchmark_localCache-8       1534795           756.6 ns/op       135 B/op          0 allocs/op
PASS
ok      go-cache    6.044s

BigCache是最快的缓存库。gCache的性能主要受到interface{}对象转换上。

总结

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

相关文章

  • 内存缓存-go

    GO 内存缓存 CPU 有缓存:L1,L2,L3 不同等级缓存执行速度不一样,空间也不一样。 内存缓存:有栈有堆,...

  • Go内存缓存

    基本上每个项目都有加快服务响应或复杂计算的需求。简单快速的解决方案是使用缓存。通常,有Redis或Memcache...

  • LRU Cache_leetcode_go实现一个LRU缓存,c

    LRU Cache_leetcode_go实现一个LRU缓存,container/list.Remove()内存释...

  • 内存缓存 - cache2go分析

    最近接触到一个内存缓存的例子 cache2go描述了最简单的缓存机制 : 维护一个缓存对象map,设置到期时间. ...

  • YYCache 源码学习总结

    YYCache 内存分为内存缓存和磁盘缓存 内存缓存 1.内存缓存实现通过CFMutableDictionaryR...

  • 区分SDWebImage的三种缓存

    SDWebImage的三种缓存分为:内存图片缓存、磁盘图片缓存、内存操作缓存步骤如下1、先查看内存图片缓存,内存图...

  • 内存缓存那些事

    内存缓存 缓存分为2类,内存缓存和磁盘缓存,今天说的是内存缓存。 内存缓存实现很多种方式,最简单的是就是用NSMu...

  • Java数据结构_LinkedHashMap 的工作原理

    缓存算法的基本概念 源码基于JDK1.7 缓存机制 内存缓存 本地缓存 网络缓存 本节记录的是内存缓存 什么是内存...

  • Java数据结构_LinkedHashMap 的工作原理

    缓存算法的基本概念 源码基于JDK1.7 缓存机制 内存缓存 本地缓存 网络缓存 本节记录的是内存缓存 什么是内存...

  • glide缓存之ActiveResources

    glide 缓存分为内存缓存和硬盘缓存,内存缓存是用Lru算法缓存和弱引用缓存(ActiveResources),...

网友评论

    本文标题:Go内存缓存

    本文链接:https://www.haomeiwen.com/subject/hapyyrtx.html