Go 缓存系列之 go-cache

 go

版权

我是一只可爱的土拨鼠,专注于分享 Go 职场、招聘和求职,解 Gopher 之忧!欢迎关注我。

欢迎大家加入Go招聘交流群,来这里找志同道合的小伙伴!跟土拨鼠们一起交流学习。

一句话描述

go-cache [1]基于内存的 K/V 存储/缓存 : (类似于Memcached),适用于单机应用程序

简介

go-cache是什么?

基于内存的 K/V 存储/缓存 : (类似于Memcached),适用于单机应用程序 ,支持删除,过期,默认Cache共享锁,

大量key的情况下会造成锁竞争严重

为什么选择go-cache?

可以存储任何对象(在给定的持续时间内或永久存储),并且可以由多个goroutine安全地使用缓存。

Example

 
  1. package main

  2. import (

  3.  "fmt"

  4.  "time"

  5.  "github.com/patrickmn/go-cache"

  6. )

  7. type MyStruct struct {

  8.  Name string

  9. }

  10. func main() {

  11.  // 设置超时时间和清理时间

  12.  c := cache.New(5*time.Minute, 10*time.Minute)

  13.  // 设置缓存值并带上过期时间

  14.  c.Set("foo", "bar", cache.DefaultExpiration)

  15.  // 设置没有过期时间的KEY,这个KEY不会被自动清除,想清除使用:c.Delete("baz")

  16.  c.Set("baz", 42, cache.NoExpiration)

  17.  var foo interface{}

  18.  var found bool

  19.  // 获取值

  20.  foo, found = c.Get("foo")

  21.  if found {

  22.   fmt.Println(foo)

  23.  }

  24.  var foos string

  25.  // 获取值, 并断言

  26.  if x, found := c.Get("foo"); found {

  27.   foos = x.(string)

  28.   fmt.Println(foos)

  29.  }

  30.  // 对结构体指针进行操作

  31.  var my *MyStruct

  32.  c.Set("foo", &MyStruct{Name: "NameName"}, cache.DefaultExpiration)

  33.  if x, found := c.Get("foo"); found {

  34.   my = x.(*MyStruct)

  35.   // ...

  36.  }

  37.  fmt.Println(my)

  38. }

源码分析

源码分析主要针对核心的存储结构、Set、Get、Delete、定时清理逻辑进行分析。包含整体的逻辑架构

核心的存储结构

 
  1. package cache

  2. // Item 每一个具体缓存值

  3. type Item struct {

  4.  Object     interface{}

  5.  Expiration int64 // 过期时间:设置时间+缓存时长

  6. }

  7. // Cache 整体缓存

  8. type Cache struct {

  9.  *cache

  10. }

  11. // cache 整体缓存

  12. type cache struct {

  13.  defaultExpiration time.Duration // 默认超时时间

  14.  items             map[string]Item // KV对

  15.  mu                sync.RWMutex // 读写锁,在操作(增加,删除)缓存时使用

  16.  onEvicted         func(string, interface{}) // 删除KEY时的CallBack函数

  17.  janitor           *janitor // 定时清空缓存的结构

  18. }

  19. // janitor  定时清空缓存的结构

  20. type janitor struct {

  21.  Interval time.Duration // 多长时间扫描一次缓存

  22.  stop     chan bool // 是否需要停止

  23. }

Set

 
  1. package cache

  2. func (c *cache) Set(k string, x interface{}, d time.Duration) {

  3.  // "Inlining" of set

  4.  var e int64

  5.  if d == DefaultExpiration {

  6.   d = c.defaultExpiration

  7.  }

  8.  if d > 0 {

  9.   e = time.Now().Add(d).UnixNano()

  10.  }

  11.  c.mu.Lock() // 这里可以使用defer?

  12.  c.items[k] = Item{

  13.   Object:     x, // 实际的数据

  14.   Expiration: e, // 下次过期时间

  15.  }

  16.  c.mu.Unlock()

  17. }

Get

 
  1. package cache

  2. func (c *cache) Get(k string) (interface{}, bool) {

  3.  c.mu.RLock() // 加锁,限制并发读写

  4.  item, found := c.items[k] // 在 items 这个 map[string]Item 查找数据

  5.  if !found {

  6.   c.mu.RUnlock()

  7.   return nil, false

  8.  }

  9.  if item.Expiration > 0 {

  10.   if time.Now().UnixNano() > item.Expiration { // 已经过期,直接返回nil,为什么在这里不直接就删除了呢?

  11.    c.mu.RUnlock()

  12.    return nil, false

  13.   }

  14.  }

  15.  c.mu.RUnlock()

  16.  return item.Object, true

  17. }

Delete

 
  1. package cache

  2. // Delete an item from the cache. Does nothing if the key is not in the cache.

  3. func (c *cache) Delete(k string) {

  4.  c.mu.Lock()

  5.  v, evicted := c.delete(k)

  6.  c.mu.Unlock()

  7.  if evicted {

  8.   c.onEvicted(k, v) // 删除KEY时的CallBack

  9.  }

  10. }

  11. func (c *cache) delete(k string) (interface{}, bool) {

  12.  if c.onEvicted != nil {

  13.   if v, found := c.items[k]; found {

  14.    delete(c.items, k)

  15.    return v.Object, true

  16.   }

  17.  }

  18.  delete(c.items, k)

  19.  return nil, false

  20. }

定时清理逻辑

 
  1. package cache

  2. func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {

  3.  c := newCache(de, m)

  4.  C := &Cache{c}

  5.  if ci > 0 {

  6.   runJanitor(c, ci) // 定时运行清除过期KEY

  7.   runtime.SetFinalizer(C, stopJanitor) // 当C被GC回收时,会停止runJanitor 中的协程

  8.  }

  9.  return C

  10. }

  11. func runJanitor(c *cache, ci time.Duration) {

  12.  j := &janitor{

  13.   Interval: ci,

  14.   stop:     make(chan bool),

  15.  }

  16.  c.janitor = j

  17.  go j.Run(c) // 新的协程做过期删除逻辑

  18. }

  19. func (j *janitor) Run(c *cache) {

  20.  ticker := time.NewTicker(j.Interval)

  21.  for {

  22.   select {

  23.   case <-ticker.C: // 每到一个周期就全部遍历一次

  24.    c.DeleteExpired() // 实际的删除逻辑

  25.   case <-j.stop:

  26.    ticker.Stop()

  27.    return

  28.   }

  29.  }

  30. }

  31. // Delete all expired items from the cache.

  32. func (c *cache) DeleteExpired() {

  33.  var evictedItems []keyAndValue

  34.  now := time.Now().UnixNano()

  35.  c.mu.Lock()

  36.  for k, v := range c.items { // 加锁遍历整个列表

  37.   // "Inlining" of expired

  38.   if v.Expiration > 0 && now > v.Expiration {

  39.    ov, evicted := c.delete(k)

  40.    if evicted {

  41.     evictedItems = append(evictedItems, keyAndValue{k, ov})

  42.    }

  43.   }

  44.  }

  45.  c.mu.Unlock()

  46.  for _, v := range evictedItems {

  47.   c.onEvicted(v.key, v.value)

  48.  }

  49. }

思考

Lock 的使用

  • 在go-cache中,涉及到读写cache,基本上都用到了锁,而且在遍历的时候也用到锁,当cache的数量非常多时,读写频繁时, 会有严重的锁冲突。

使用读写锁?

  • sync.RWMutex, 在读的时候加RLock, 可以允许多个读。在写的时候加Lock,不允许其他读和写。

锁的粒度是否可以变更小?

  • 根据KEY HASH 到不同的map中

使用sync.map?

  • 减少锁的使用

runtime.SetFinalizer

在实际的编程中,我们都希望每个对象释放时执行一个方法,在该方法内执行一些计数、释放或特定的要求, 以往都是在对象指针置nil前调用一个特定的方法, golang提供了runtime.SetFinalizer函数,当GC准备释放对象时,会回调该函数指定的方法,非常方便和有效。

对象可以关联一个SetFinalizer函数, 当gc检测到unreachable对象有关联的SetFinalizer函数时, 会执行关联的SetFinalizer函数, 同时取消关联。这样当下一次gc的时候, 对象重新处于unreachable状态 并且没有SetFinalizer关联, 就会被回收。

Doc

https://pkg.go.dev/github.com/patrickmn/go-cache

比较

相似的库

  • https://github.com/golang/groupcache

  • https://github.com/allegro/bigcache

  • https://github.com/coocood/freecache

参考资料

[1] 

go-cache : https://github.com/patrickmn/go-cache


资料分享,公众号后台回复暗号即可获取

  • 【100】Go资深工程师简历模板

  • 【101 】Go全网最全的面试集锦

  • 【102】Go超级简历


推荐阅读


欢迎关注我!获取更多招聘相关信息。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值