Go 每日一库之 go-cache 缓存
<p>什么是 go-cache</p><p> </p>
<p data-tool="mdnice编辑器">go-cache 是一个轻量级的基于内存的 K-V 储存组件,内部实现了一个线程安全的 mapinterface{},适用于单机应用。具备如下功能:</p>
<ul class="list-paddingleft-1" data-tool="mdnice编辑器">
<li>线程安全,多 goroutine 并发安全访问;</li>
<li>每个 item 可以设置过期时间(或无过期时间);</li>
<li>自动定期清理过期的 item;</li>
<li>可以自定义清理回调函数;</li>
</ul>
<p data-tool="mdnice编辑器">这里的 item 指的是 map 里的元素。</p>
<p data-tool="mdnice编辑器">go-cache 一般用作临时数据缓存来使用,而不是持久性的数据存储。对于某些停机后快速恢复的场景,go-cache支持将缓存数据保存到文件,恢复时从文件中将数据加载到内存。</p>
<p data-tool="mdnice编辑器">先来“浅尝”下,好不好用试试就知道。</p>
<h3 data-tool="mdnice编辑器">如何使用</h3>
<pre data-tool="mdnice编辑器"><code>func main() {<br> c := cache.New(10*time.Second, 30*time.Second) // 默认过期时间10s;清理间隔30s,即每30s钟会自动清理过期的键值对<br><br> // 设置一个键值对,过期时间是 3s<br> c.Set("a", "testa", 3*time.Second)<br><br> // 设置一个键值对,采用 New() 时的默认过期时间,即 10s<br> c.Set("foo", "bar", cache.DefaultExpiration)<br><br> // 设置一个键值对,没有过期时间,不会自动过期,需要手动调用 Delete() 才能删除<br> c.Set("baz", 42, cache.NoExpiration)<br><br> v, found := c.Get("a")<br> fmt.Println(v, found) // testa,true<br><br> <-time.After(5 * time.Second) // 延时5s<br><br> v, found = c.Get("a") // nil,false<br> fmt.Println(v, found)<br><br> <-time.After(6 * time.Second)<br> v, found = c.Get("foo") // nil,false<br> fmt.Println(v, found)<br><br> v, found = c.Get("baz") // 42,true<br> fmt.Println(v, found)<br><br> // 完整例子请关注公众号【Golang来啦】,后台发送关键字 gocache 获取<br> //TestCache()<br> //TestCacheTimes()<br> //TestNewFrom()<br> //TestOnEvicted()<br> //TestFileSerialization()<br>}<br><br></code></pre>
<p data-tool="mdnice编辑器">下面我们来看下 go-cache 内部是如何实现前面说的那些功能的,另外阅读完源码我们还能掌握一种优雅地关闭后台 goroutine 的方法。</p>
<h3 data-tool="mdnice编辑器">常量与结构体</h3>
<h4 data-tool="mdnice编辑器">常量</h4>
<pre data-tool="mdnice编辑器"><code>const (<br> <br> NoExpiration time.Duration = -1 // 无有效时间<br><br> DefaultExpiration time.Duration = 0 // 表示采用默认时间<br>)<br></code></pre>
<p data-tool="mdnice编辑器">这两个参数可以用作 New() 函数的第一个入参,则默认过期时间小于0,意味着添加键值对时如果采用默认过期时间,则该键值对不会过期,因为 DeleteExpired() 方法会判断 v.Expiration 是否大于 0,大于 0 时才会自动删除。如果想删除需要手动 Delete() 方法。</p>
<p data-tool="mdnice编辑器">添加键值对,比如执行 Set()、Add() 等操作时,这两个常量也可以作为参数,NoExpiration 表示没有过期时间,DefaultExpiration 表示采用默认的过期时间。</p>
<h4 data-tool="mdnice编辑器">结构体</h4>
<p data-tool="mdnice编辑器">主要的结构体包括下面这些:</p>
<pre data-tool="mdnice编辑器"><code><br>type Item struct { // 键值对<br> Object interface{} // 存放 K-V 的值,可以存放任何类型的值<br> Expiration int64 // 键值对的过期时间(绝对时间)<br>}<br><br>type Cache struct { // 对外使用的 Cache<br> *cache // cache 实例<br>}<br><br>type cache struct {<br> defaultExpiration time.Duration // 默认的过期时间,添加一个键值对时如果设置默认的过期时间(即代码里的 DefaultExpiration)则会使用到该值<br> items mapItem // 存放的键值对<br> mu sync.RWMutex // 读写锁<br> onEvicted func(string, interface{}) // 删除key时的回调函数<br> janitor *janitor // 定期清理器 定期检查过期的 Item<br>}<br><br>type janitor struct { // 清理器结构体<br> Interval time.Duration // 清理时间间隔<br> stop chan bool // 是否停止<br>}<br><br><br></code></pre>
<h3 data-tool="mdnice编辑器">主要流程和功能</h3>
<h4 data-tool="mdnice编辑器">实例化 Cache</h4>
<pre data-tool="mdnice编辑器"><code><br>// 参数:默认过期时间、清理时间间隔<br>func New(defaultExpiration, cleanupInterval time.Duration) *Cache {<br> items := make(mapItem) // 初始化 items<br> return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) // 根据需要是否创建清理器<br>}<br><br>func newCacheWithJanitor(de time.Duration, ci time.Duration, m mapItem) *Cache {<br> c := newCache(de, m) // 实例化cache<br> C := &Cache{c}<br> if ci > 0 { // 清理间隔大于 0 时才会开启自动清除过期item的功能,否则需要手动调用删除方法-DeleteExpired() 删除过期item<br> runJanitor(c, ci) // 开启清理器<br> runtime.SetFinalizer(C, stopJanitor) // 优雅地停止 goroutine 的一种方式,我们放在后面讲<br> }<br> return C<br>}<br><br>// 如果 de 小于等于0,则默认过期时间小于0,意味着添加键值对时如果采用默认过期时间,则该键值对不会过期<br>// 因为 DeleteExpired() 方法会判断 v.Expiration 是否大于 0,大于 0 时才会自动删除。 如果想删除需要手动 Delete() 方法。<br>func newCache(de time.Duration, m mapItem) *cache {<br> if de == 0 {<br> de = -1<br> }<br> c := &cache{<br> defaultExpiration: de,<br> items: m,<br> }<br> return c<br>}<br><br>// 运行清理器<br>func runJanitor(c *cache, ci time.Duration) {<br> j := &janitor{ // 实例化清理器<br> Interval: ci,<br> stop: make(chan bool),<br> }<br> c.janitor = j<br> go j.Run(c) // 开协程,定时清理器跑起来!!<br>}<br><br>func (j *janitor) Run(c *cache) {<br> ticker := time.NewTicker(j.Interval) // 开定时器<br> for {<br> select {<br> case <-ticker.C:<br> c.DeleteExpired() // 定时调用 DeleteExpired() 执行过期删除操作<br> case <-j.stop: // 接收到停止清理器的信号,下面便停止定时器并返回,退出协程<br> ticker.Stop()<br> return<br> }<br> }<br>}<br><br></code></pre>
<h3 data-tool="mdnice编辑器">添加操作</h3>
<p data-tool="mdnice编辑器">添加类操作有四个函数可以使用,分别是:</p>
<p data-tool="mdnice编辑器"><strong>Set()</strong></p>
<pre data-tool="mdnice编辑器"><code>func (c *cache) Set(k string, x interface{}, d time.Duration) {<br> var e int64<br> if d == DefaultExpiration {<br> d = c.defaultExpiration<br> }<br> if d > 0 {<br> e = time.Now().Add(d).UnixNano() // 过期时间,绝对时间<br> }<br> c.mu.Lock()<br> c.items = Item{<br> Object: x,<br> Expiration: e, <br> }<br> c.mu.Unlock()<br>}<br></code></pre>
<p data-tool="mdnice编辑器">函数的逻辑还是比较简单的,需要注意的是过期时间是绝对时间。</p>
<p data-tool="mdnice编辑器"><strong>SetDefault()</strong></p>
<pre data-tool="mdnice编辑器"><code>func (c *cache) SetDefault(k string, x interface{}) {<br> c.Set(k, x, DefaultExpiration)<br>}<br></code></pre>
<p data-tool="mdnice编辑器">SetDefault() 内部调用了 Set() 函数实现,使用默认过期时间。</p>
<p data-tool="mdnice编辑器"><strong>Add()</strong>如果待添加的 K 不存在的话才能添加成功,包括已过期但还未被删除,否则返回错误。</p>
<p data-tool="mdnice编辑器"><strong>Replace()</strong></p>
<p data-tool="mdnice编辑器">如果待添加的 K 已存在的话才能执行成功,否则返回错误。操作条件真好与 Add() 相反。</p>
<p data-tool="mdnice编辑器">上面两个函数逻辑比较简单就不放源码,大家自行查看。</p>
<h3 data-tool="mdnice编辑器">删除</h3>
<p data-tool="mdnice编辑器">删除操作主要有两个,执行删除操作的时候都会判断是否需要执行删除回调函数,下面我们可以看到。</p>
<p data-tool="mdnice编辑器"><strong>Delete()</strong> 常规删除,不管是否过期都会删除。</p>
<p data-tool="mdnice编辑器"><strong>DeleteExpired()</strong> 用于执行批量删除操作,只会删除已过期的键值对。</p>
<pre data-tool="mdnice编辑器"><code>func (c *cache) Delete(k string) {<br> c.mu.Lock()<br> v, evicted := c.delete(k) // v 是 k 对应的值<br> c.mu.Unlock()<br> if evicted { // 是否需要执行删除回调函数<br> c.onEvicted(k, v)<br> }<br>}<br><br>func (c *cache) delete(k string) (interface{}, bool) {<br> if c.onEvicted != nil {<br> if v, found := c.items; found {<br> delete(c.items, k)<br> return v.Object, true<br> }<br> }<br> // 能执行到这里说明有两种情况:<br> // 1.没有设置删除回调函数 2.设置了删除回调函数,但是键值对不存在<br> delete(c.items, k)<br> return nil, false<br>}<br><br>// 将所有过期的键值对从缓存中删除,如果设置有回调函数,则会将键值对作为参数调用回调函数<br>func (c *cache) DeleteExpired() {<br> var evictedItems []keyAndValue // 临时存放已经过期的键值对<br> now := time.Now().UnixNano()<br> c.mu.Lock()<br> for k, v := range c.items {<br> if v.Expiration > 0 && now > v.Expiration { // 是否已经过期<br> ov, evicted := c.delete(k)<br> if evicted { // 是否需要执行删除回调函数<br> evictedItems = append(evictedItems, keyAndValue{k, ov})<br> }<br> }<br> }<br> c.mu.Unlock()<br> for _, v := range evictedItems {<br> c.onEvicted(v.key, v.value) // 执行删除回调函数<br> }<br>}<br><br></code></pre>
<p data-tool="mdnice编辑器">关于删除回调函数的详细用法,请关注公众号【Golang来啦】,后台发送关键字 gocache 获取。</p>
<div class="appmsg_card_context wx_profile_card wx-root wx_tap_card common-webchat">
<div class="wx_profile_card_inner">
<div class="wx_profile_card_bd">
<div class="wx_profile weui-flex">
<div class="wx_profile_hd"><img alt="" class="wx_profile_avatar lazyload" data-src="http://mmbiz.qpic.cn/mmbiz_png/zVM9HMBJAjPLZsNAwoP93y3gJtYPibvKcZWoRVVBW1kGaZ6RL41uL1TaoPGwsc4ohmmIoameiaYrficRo6b9EExrg/0?wx_fmt=png"></div>
<div class="wx_profile_bd weui-flex weui-flex__item">
<div class="weui-flex__item"><span class="wx_profile_nickname"><span class="wx_profile_nickname">Golang来啦</span></span>
<div id="js_a11y_wx_profile_desc" class="wx_profile_desc">欢迎来到Gopher自留地! 专注于分享Golang知识、职场心得和生活感悟。带你入门,完成进阶,顺利掌握Golang ! Golang已来,上车!!</div>
<div id="js_a11y_wx_profile_tips" class="wx_profile_tips"><span class="wx_profile_tips_meta">205篇原创内容</span></div>
</div>
</div>
</div>
</div>
<div id="js_a11y_wx_profile_logo" class="wx_profile_card_ft">公众号</div>
</div>
</div>
<h3 data-tool="mdnice编辑器">备份恢复数据</h3>
<p data-tool="mdnice编辑器">虽然 go-cache 比较倾向于当做缓存数据来使用,但还是提供了备份数据和恢复数据的操作,数据使用 gob 序列化。</p>
<p data-tool="mdnice编辑器">详细用法请关注公众号【Golang来啦】,后台发送关键字 gocache 获取。</p>
<h3 data-tool="mdnice编辑器">其他一些操作</h3>
<p data-tool="mdnice编辑器">其他一些操作包括:</p>
<ul class="list-paddingleft-1" data-tool="mdnice编辑器">
<li>ItemCount(),返回所有数据的条数,这里的条数包括已过期但还未被删除的数量;</li>
<li>Flush(),清空数据;</li>
<li>Items(),返回数据的未过期的数据,可以使用 NewFrom() 恢复数据;</li>
</ul>
<p data-tool="mdnice编辑器">从各种操作的源码就可以看出,go-cache 内部通过使用读写锁(sync.RWMutex)保证并发安全。</p>
<h3 data-tool="mdnice编辑器">优雅地关闭 goroutine</h3>
<p data-tool="mdnice编辑器">这个问题之前有提到过,也是一种优雅停止 goroutine 的一种方式。我们把相关的代码拉出来看下:</p>
<pre data-tool="mdnice编辑器"><code>func newCacheWithJanitor(de time.Duration, ci time.Duration, m mapItem) *Cache {<br> c := newCache(de, m) // 实例化cache<br> C := &Cache{c}<br> if ci > 0 {<br> runJanitor(c, ci) // 开启清理器<br> runtime.SetFinalizer(C, stopJanitor) // 指定调用函数停止后台 goroutine<br> }<br> return C<br>}<br><br>func stopJanitor(c *Cache) {<br> c.janitor.stop <- true<br>}<br><br>type janitor struct { // 清理器结构体<br> Interval time.Duration // 清理时间间隔<br> stop chan bool // 是否停止<br>}<br><br>func (j *janitor) Run(c *cache) {<br> ticker := time.NewTicker(j.Interval) // 开定时器<br> for {<br> select {<br> case <-ticker.C:<br> c.DeleteExpired() // 定时调用 DeleteExpired() 执行过期删除操作<br> case <-j.stop: // 接收到停止清理器的信号,下面便停止定时器并返回,退出协程<br> ticker.Stop()<br> return<br> }<br> }<br>}<br><br>// 运行清理器<br>func runJanitor(c *cache, ci time.Duration) {<br> j := &janitor{ // 实例化清理器<br> Interval: ci,<br> stop: make(chan bool),<br> }<br> c.janitor = j<br> go j.Run(c) // 开协程,定时清理器跑起来!!<br>}<br><br></code></pre>
<p data-tool="mdnice编辑器">这里退出后台 goroutine 使用的是下面这个函数,当 GC 准备释放对象时,会调用该函数指定的方法。</p>
<pre data-tool="mdnice编辑器"><code>runtime.SetFinalizer(obj,func(obj *typeObj))<br></code></pre>
<p data-tool="mdnice编辑器">当我们取消对 C 对象的引用时,如果不退出 runJanitor() 开启的 goroutine 就会造成内存泄漏。当 gc 准备释放 C 时,会调用指定函数 stopJanitor(),Run() 方法便能收到信号,退出协程,gc 也会将 c 释放掉。</p>
<h3 data-tool="mdnice编辑器">总结</h3>
<p data-tool="mdnice编辑器">go-cache 的源码结构清晰,代码量也不多,还能掌握一种优雅退出 goroutine 的防止,推荐阅读,有问题可以发信息给我,相互学习进步!</p>
<p data-tool="mdnice编辑器">项目地址:https://github.com/patrickmn/go-cache</p><br><br>
来源:https://www.cnblogs.com/cheyunhua/p/16802281.html
頁:
[1]