源码阅读是2020年开始的一个长期计划,主要目的有两个:1.提高自己对GO语言的理解,2.理解功能设计的原理。关于第二点,说的详细点就是,不仅要了解怎么做的,还要知道为什么这么做,有哪些好处,在什么场景下适用。最终提高自己对代码的敏感度,丰富自己的工具箱,让自己面对业务问题时能够从容不迫。
源码地址:go-zero/cache.go at master · tal-tech/go-zero · GitHub
代码主要分成三个模块:缓存操作,LRU淘汰算法,命中统计模块。
type (
CacheOption func(cache *Cache) //用来操作缓存的函数,用在实例化缓存时
Cache struct {
name string //缓存名称
lock sync.Mutex //并发锁
data map[string]interface{}//缓存内容
expire time.Duration //过期时间
timingWheel *TimingWheel //框架封装的定时器
lruCache lru //LRU组件
barrier syncx.SharedCalls//缓存并发安全组件,可以解决缓存击穿的问题
unstableExpiry mathx.Unstable //生成随机数的插件
stats *cacheStat //统计命中率模块
}
)
func NewCache(expire time.Duration, opts ...CacheOption) (*Cache, error) {
cache := &Cache{//声明缓存结构,初始化内容
data: make(map[string]interface{}),
expire: expire,
lruCache: emptyLruCache, //默认是一个空的LRU结构,可以通过opts来控制
barrier: syncx.NewSharedCalls(),//解决缓存击穿的核心方法
unstableExpiry: mathx.NewUnstable(expiryDeviation),//框架自己做的一个并发安全的随机数
}
for _, opt := range opts {
opt(cache) //执行预加载函数
}
...
cache.stats = newCacheStat(cache.name, cache.size)//缓存命中统计模块,初始化
timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
key, ok := k.(string)
if !ok {
return
}
cache.Del(key)
})//定时器模块,初始化
...
return cache, nil
}
这里有三个模块是调用的go-zero中实现好的模块,这些代码块也很小,实现的功能很强大。可以参考:
1.syncx.NewSharedCalls
2.mathx.NewUnstable
3.NewTimingWheel
func (c *Cache) Del(key string) {
c.lock.Lock() //上锁
delete(c.data, key) // 删除元素
c.lruCache.remove(key) //移除LRU
c.lock.Unlock() // 解锁
c.timingWheel.RemoveTimer(key) //移除定时器, 注意先解锁,后移除。
}
func (c *Cache) Get(key string) (interface{}, bool) {
...
if ok { //统计命中率
c.stats.IncrementHit()
} else {
c.stats.IncrementMiss()
}
...
}
func (c *Cache) Set(key string, value interface{}) {
c.lock.Lock() //上锁
_, ok := c.data[key] //判断KEY是否存在
c.data[key] = value //赋值
c.lruCache.add(key) //添加到LRU
c.lock.Unlock() //解锁
expiry := c.unstableExpiry.AroundDuration(c.expire) //设置过期值
if ok {
c.timingWheel.MoveTimer(key, expiry)
} else {
c.timingWheel.SetTimer(key, value, expiry)
}
}
func (c *Cache) doGet(key string) (interface{}, bool) {
...
value, ok := c.data[key]
if ok {
c.lruCache.add(key) //添加到LRU中
}
...
}
这一块代码,这一块代码大同小异,都是加锁,操作,再解锁的方式来进行并发管理的。通过这一块代码的阅读,基本可以明确LRU模块和命中率统计模块是如何工作的。
//这个方法可以获取KEY的值,如果这个值不存在,那么执行fetch方法,拿到返回值设置到缓存里,并返回。当出现并发情况时,barrier方法会保证并发安全。
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
if val, ok := c.doGet(key); ok {//直接获取KEY的值
c.stats.IncrementHit() //记录命中
return val, nil
}
var fresh bool
//核心方法,主要是用了syncx.NewSharedCalls实现的功能
val, err := c.barrier.Do(key, func() (interface{}, error) {
//这里进行了一次dobble check。解决并发时,有些协程可能已经把数据查出来并加载到缓存了。
if val, ok := c.doGet(key); ok {
return val, nil
}
v, e := fetch()//执行方法,获取CACHE。这个方法应该尽量的保证效率
if e != nil {
return nil, e
}
fresh = true
c.Set(key, v) //设置缓存
return v, nil
})
...
if fresh {
//fetch 获取到数据为空,记录miss次数
c.stats.IncrementMiss()
return val, nil
}
// 直接把之前查到的数据返回,并记录命中次数
c.stats.IncrementHit()
return val, nil
}
这里实现缓存击穿安全的方法主要是依赖了syncx.NewSharedCalls,他的主要作用是:使用SharedCalls可以使得同时多个请求只需要发起一次拿结果的调用,其他请求”坐享其成”,这种设计有效减少了资源服务的并发压力,可以有效防止缓存击穿。
详细的可以直接看源码,或者参考文章:更简的并发代码,更强的并发控制 - 知乎
LRU的核心思想:
具体实现方法:
type (
lru interface { //声明一个接口
add(key string)
remove(key string)
}
emptyLru struct{} //默认的空结构,newCache的时候默认用的就这个
keyLru struct {//带有限制的结构,ewCache的时候通过调用withLimit方法设置
limit int //总长度
evicts *list.List //元素链
elements map[string]*list.Element//存放的是元素在链表中的地址。利用MAP操作O1的特性,不用遍历链表即可找到需要的元素的地址。
onEvict func(key string) //删除的后置操作
}
)
func newKeyLru(limit int, onEvict func(key string)) *keyLru {
...
}
func (klru *keyLru) add(key string) {
if elem, ok := klru.elements[key]; ok {
klru.evicts.MoveToFront(elem)//如果新增元素已存在,就直接移到最前面
return
}
elem := klru.evicts.PushFront(key)//在链表最前面增加一个元素
klru.elements[key] = elem //记录这个元素的地址
if klru.evicts.Len() > klru.limit {
klru.removeOldest()//如果链表的最大长度超过配置,移除最老的元素
}
}
func (klru *keyLru) remove(key string) {
...
}
func (klru *keyLru) removeOldest() {
elem := klru.evicts.Back()//取链表最后一个元素
if elem != nil {
klru.removeElement(elem) //移除元素
}
}
func (klru *keyLru) removeElement(e *list.Element) {
klru.evicts.Remove(e)//移除链表中的元素
key := e.Value.(string)//获取Key
delete(klru.elements, key)//移除MAP中的元素
klru.onEvict(key) //执行删除的后置操作
}
这块代码的结构非常简单。每个函数的声明和实现都非常的简洁、标准。整个LRU的代码风格,具体实现的方式,非常值得学习。 参考地址缓存淘汰算法—LRU算法 - 知乎
这块代码的逻辑很简单,这里不做详细展开。
type cacheStat struct {
name string //名称,最后打印日志记录是要用到
hit uint64 //命中缓存次数
miss uint64 //未命中次数
sizeCallback func() int //自定义回调函数,会在打印结果的时候用到
}
func newCacheStat(name string, sizeCallback func() int) *cacheStat {...}
func (cs *cacheStat) IncrementHit() {
atomic.AddUint64(&cs.hit, 1) //记录命中次数
}
func (cs *cacheStat) IncrementMiss() {
atomic.AddUint64(&cs.miss, 1)
}
func (cs *cacheStat) statLoop() {
...
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。