并发安全本地化存储go-cache读写锁实现多协程并发访问
作者:海生
这篇文章主要介绍了并发安全本地化存储go-cache读写锁实现多协程并发访问,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
简介
go-cache广泛使用在go语言编程中,适合迎来在单机上 存储键值对形式的内存缓存。
在github上地址为 https://github.com/patrickmn/go-cache他在并发的时候,线程安全(读写锁) + map[string]interface{} + 过期时间 来作为go的本地化存储。
这也是他的三大特性:
- 线程安全,通过读写锁支持多个协程并发访问
- 不需要序列化,键值对形式,任意值类型map[string]interface{}
- 自定义每个key的过期时间
数据结构
主要有Cache,以及其组成 cache,Item两个结构。
type Cache struct { *cache // If this is confusing, see the comment at the bottom of New() // 如果这令人困惑,请参阅New()底部的注释。 } type cache struct { defaultExpiration time.Duration items map[string]Item //存储键值对 mu sync.RWMutex // 读写锁,并发安全 onEvicted func(string, interface{}) // 被清除时的回调函数 janitor *janitor // 脚本,定期清理过期数据 } type Item struct { Object interface{} // 存储的值 Expiration int64 // 到期时间 }
创建Cache对象
使用New(defaultExpiration默认过期时间, cleanupInterval定时清理时间)函数来初始化。
传递到两个参数:key的过期时间,以及定时脚本清理过期数据的时间。
// Return a new cache with a given default expiration duration and cleanup interval. // 返回具有给定默认过期和清除时间的 new cache新缓存 // If the expiration duration is less than one (or NoExpiration), // the items in the cache never expire (by default), // 假如 到期时间为-1,永不过期 // and must be deleted manually. // 并且只能手动删除。 // If the cleanup interval is less than one, expired items are not // 如果清除时间小于1,过期item,在call之前是没有被删除的。 // deleted from the cache before calling c.DeleteExpired(). func New(defaultExpiration, cleanupInterval time.Duration) *Cache { items := make(map[string]Item) return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) } func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { c := newCache(de, m) // This trick ensures that the janitor goroutine (which--granted it // 这个代码,确认是否启动 janitor看门人goroutine // was enabled--is running DeleteExpired on c forever) does not keep // the returned C object from being garbage collected. When it is // garbage collected, the finalizer stops the janitor goroutine, after // which c can be collected. C := &Cache{c} if ci > 0 { runJanitor(c, ci) // 调用守卫进程来清理过期数据 runtime.SetFinalizer(C, stopJanitor) // runtime.SetFinalizer(C, stopJanitor)会指定调用函数停止后台 goroutine, // 当 GC 准备释放对象时,会调用stopJanitor方法, // Run函数中j.stop通道会输出一个信号,从而退出协程。 } return C } func runJanitor(c *cache, ci time.Duration) { j := &janitor{ Interval: ci, stop: make(chan bool), } c.janitor = j go j.Run(c) // 调用守卫进程来清理过期数据 } // 开启一个定时器,来定时清理数据。 func (j *janitor) Run(c *cache) { // 创建了一个计时器,时间到时ticker.C通道会输出一个值,调用DeleteExpired()函数 // 该函数会通过遍历cache中的map[string]Item的过期时间,过期则直接从map中删除, // 如果该值有回调函数,则在删除后执行回调函数。 ticker := time.NewTicker(j.Interval) for { select { case <-ticker.C: c.DeleteExpired() case <-j.stop: ticker.Stop() return } } } // Delete all expired items from the cache. // 删除cache中所有的过期items // 此时会加锁,如果定时清理的时间比较长,并且key比较多的话, // 会导致一直被 清理协程锁住。其他的协程没法写入。 func (c *cache) DeleteExpired() { var evictedItems []keyAndValue now := time.Now().UnixNano() c.mu.Lock() // 过期删除的时候,需要上锁。 for k, v := range c.items { // "Inlining" of expired if v.Expiration > 0 && now > v.Expiration { ov, evicted := c.delete(k) if evicted { evictedItems = append(evictedItems, keyAndValue{k, ov}) } } } c.mu.Unlock() // 删除完解锁 for _, v := range evictedItems { c.onEvicted(v.key, v.value) } } func (c *cache) delete(k string) (interface{}, bool) { if c.onEvicted != nil { if v, found := c.items[k]; found { delete(c.items, k) return v.Object, true } } delete(c.items, k) return nil, false }
Get获取数据
// Get an item from the cache. Returns the item or nil, and a bool indicating // whether the key was found. // 从cache中Get一个item.返回item或者nil,和一个bool值。 func (c *cache) Get(k string) (interface{}, bool) { c.mu.RLock() // "Inlining" of get and Expired item, found := c.items[k] if !found { c.mu.RUnlock() return nil, false } // 获取到item,是否能返回还需要判断过期时间 if item.Expiration > 0 { if time.Now().UnixNano() > item.Expiration { c.mu.RUnlock() return nil, false } } c.mu.RUnlock() return item.Object, true }
Set保存数据
set保存数据,d的表达有三种情况:
- 为0,使用默认的过期时间
- 为-1,永不过期
- 大于0的正常值,就是过期时间
// Add an item to the cache, replacing any existing item. If the duration is 0 // 将item添加到缓存中,以更换任何现有item。如果duration持续时间为0 // (DefaultExpiration), the cache's default expiration time is used. If it is -1 // (DefaultExpiration),使用缓存的默认到期时间。如果是-1 // (NoExpiration), the item never expires. // (否开发),该项目永远不会到期。 func (c *cache) Set(k string, x interface{}, d time.Duration) { // "Inlining" of set var e int64 if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { e = time.Now().Add(d).UnixNano() } c.mu.Lock() c.items[k] = Item{ Object: x, Expiration: e, } // TODO: Calls to mu.Unlock are currently not deferred because defer // adds ~200 ns (as of go1.) c.mu.Unlock() }
常见问题
1、高并发
因为是加锁在整个cache上,相比那些加锁在分片上的其余缓存,并发会低一些。
2、关于内存溢出
如果设置的清理时间为0,就是永不清理,或者时间过长,有可能导致缓存越来越多。
因为没有主动清理,占用的缓存越闹越大。
3、关于定时清理
如果时间过长,一次清理太大,又因为加锁整个cache,可能会导致其他的协程无法写入。
4、关于map[string]interface{}存储的值,有可能会变。
interface{},如果存的是数组,或者指针等,当取出使用的时候,修改值,会导致缓存中的原始值变化。
以上就是patrickmn/go-cache源码阅读与分析的详细内容,更多关于patrickmn/go-cache源码阅读与分析的资料请关注脚本之家其它相关文章!