缓存,设计的初衷是为了减小繁重的IO操做,增长系统并发能力。无论是 CPU多级缓存
,page cache
,仍是咱们业务中熟悉的 redis
缓存,本质都是将有限的热点数据存储在一个存取更快的存储介质中。html
计算机自己的缓存设计就是 CPU 采起多级缓存。那对咱们服务来讲,咱们是否是也能够采用这种多级缓存的方式来组织咱们的缓存数据。同时 redis
的存取都会通过网络IO,那咱们能不能把热点数据直接存在本进程内,由进程本身缓存一份最近最热的这批数据呢?git
这就引出了咱们今天探讨的:local cache
,本地缓存,也叫进程缓存。github
本文带你一块儿探讨下 go-zero
中进程缓存的设计。Let’s go!redis
做为一个进程存储设计,固然是 crud
都有的:sql
local cache
// 先初始化 local cache cache, err = collection.NewCache(time.Minute, collection.WithLimit(10)) if err != nil { log.Fatal(err) }
其中参数的含义:缓存
expire
:key统一的过时时间CacheOption
:cache设置。好比key的上限设置等// 1. add/update 增长/修改都是该API cache.Set("first", "first element") // 2. get 获取key下的value value, ok := cache.Get("first") // 3. del 删除一个key cache.Del("first")
Set(key, value)
设置缓存value, ok := Get(key)
读取缓存Del(key)
删除缓存cache.Take("first", func() (interface{}, error) { // 模拟逻辑写入local cache time.Sleep(time.Millisecond * 100) return "first element", nil })
前面的 Set(key, value)
是单纯将 <key, value>
加入缓存;Take(key, setFunc)
则是在 key 对于的 value 不存在时,执行传入的 fetch
方法,将具体读取逻辑交给开发者实现,并自动将结果放到缓存里。微信
到这里核心使用代码基本就讲完了,其实看起来仍是挺简单的。也能够到 https://github.com/tal-tech/g... 去看 test 中的使用。网络
首先缓存实质是一个存储有限热点数据的介质,面临如下的这些问题:多线程
下面来讲说这3个方面咱们的设计实践。并发
有限就意味着满了要淘汰,这个就涉及到淘汰策略。cache
中使用的是:LRU
(最近最少使用)。
那淘汰怎么发生呢? 有几个选择:
而 cache
中采起的是第一种 主动删除。可是,主动删除中遇到最大的问题是:
不断循环,空消耗CPU资源,即便在额外的协程中这么作,也是没有必要的。
cache
中采起的是时间轮记录额外过时通知,等过时 channel
中有通知时,而后触发删除回调。
有关 时间轮 更多的设计文章: https://go-zero.dev/cn/timing...
对于缓存来讲,咱们须要知道这个缓存在使用额外空间和代码的状况下是否有价值,以及咱们想知道需不须要进一步优化过时时间或者缓存大小,全部这些咱们就很依赖统计能力了, go-zero
中 sqlc
和 mongoc
也一样提供了统计能力。因此咱们在 cache
中也加入的缓存,为开发者提供本地缓存监控的特性,在接入 ELK
时开发者能够更直观的监测到缓存的分布状况。
而设计其实也很简单,就是:Get() 命中,就在统计 count 上加1便可。
func (c *Cache) Get(key string) (interface{}, bool) { value, ok := c.doGet(key) if ok { // 命中hit+1 c.stats.IncrementHit() } else { // 未命中miss+1 c.stats.IncrementMiss() } return value, ok }
当多个协程并发存取的时候,对于缓存来讲,涉及的问题如下几个:
LRU
中元素的移动过程冲突这种状况下,写冲突好解决,最简单的方法就是 加锁 :
// Set(key, value) func (c *Cache) Set(key string, value interface{}) { // 加锁,而后将 <key, value> 做为键值对写入 cache 中的 map c.lock.Lock() _, ok := c.data[key] c.data[key] = value // lru add key c.lruCache.add(key) c.lock.Unlock() ... } // 还有一个在操做 LRU 的地方时:Get() func (c *Cache) doGet(key string) (interface{}, bool) { c.lock.Lock() defer c.lock.Unlock() // 当key存在时,则调整 LRU item 中的位置,这个过程也是加锁的 value, ok := c.data[key] if ok { c.lruCache.add(key) } return value, ok }
而并发执行写入逻辑,这个逻辑主要是开发者本身传入的。而这个过程:
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) { // 1. 先获取 doGet() 中的值 if val, ok := c.doGet(key); ok { c.stats.IncrementHit() return val, nil } var fresh bool // 2. 多协程中经过 sharedCalls 去获取,一个协程获取多个协程共享结果 val, err := c.barrier.Do(key, func() (interface{}, error) { // double check,防止屡次读取 if val, ok := c.doGet(key); ok { return val, nil } ... // 重点是执行了传入的缓存设置函数 val, err := fetch() ... c.Set(key, val) }) if err != nil { return nil, err } ... return val, nil }
而 sharedCalls
经过共享返回结果,节省了屡次执行函数,减小了协程竞争。
本篇文章讲解了本地缓存设计实践。从使用到设计思路,你也能够根据你的业务动态修改 缓存的过时策略,加入你想要的统计指标,实现本身的本地缓存。
甚至能够将本地缓存和 redis
结合,给服务提供多级缓存,这个就留到咱们下一篇文章:缓存在服务中的多级设计。
关于 go-zero
更多的设计和实现文章,能够关注『微服务实践』公众号。
https://github.com/tal-tech/go-zero
欢迎使用 go-zero 并 star 支持咱们!
关注『微服务实践』公众号并点击 进群 获取社区群二维码。
go-zero 系列文章见『微服务实践』公众号