进程内缓存助你提高并发能力!
前言
缓存,设计的初衷是为了减少繁重的 IO 操作,增加系统并发能力。不管是 CPU多级缓存
,page cache
,还是我们业务中熟悉的 redis
缓存,本质都是将有限的热点数据存储在一个存取更快的存储介质中。
计算机本身的缓存设计就是 CPU 采取多级缓存。那对我们服务来说,我们是不是也可以采用这种多级缓存的方式来组织我们的缓存数据。同时 redis
的存取都会经过网络 IO,那我们能不能把热点数据直接存在本进程内,由进程自己缓存一份最近最热的这批数据呢?
这就引出了我们今天探讨的:local cache
,本地缓存,也叫进程缓存。
本文带你一起探讨下 go-zero
中进程缓存的设计。Let’s go!
快速入门
作为一个进程存储设计,当然是 crud
都有的:
我们先初始化
local cache
其中参数的含义:
expire
:key 统一的过期时间CacheOption
:cache 设置。比如 key 的上限设置等
基础操作缓存
Set(key, value)
设置缓存value, ok := Get(key)
读取缓存Del(key)
删除缓存
高级操作
前面的 Set(key, value)
是单纯将 <key, value>
加入缓存;Take(key, setFunc)
则是在 key 对于的 value 不存在时,执行传入的 fetch
方法,将具体读取逻辑交给开发者实现,并自动将结果放到缓存里。
到这里核心使用代码基本就讲完了,其实看起来还是挺简单的。也可以到 https://github.com/tal-tech/go-zero/blob/master/core/collection/cache_test.go 去看 test 中的使用。
解决方案
首先缓存实质是一个存储有限热点数据的介质,面临以下的这些问题:
有限容量
热点数据统计
多线程存取
下面来说说这 3 个方面我们的设计实践。
有限容量
有限就意味着满了要淘汰,这个就涉及到淘汰策略。cache
中使用的是:LRU
(最近最少使用)。
那淘汰怎么发生呢? 有几个选择:
开一个定时器,不断循环所有 key,等到了预设过期时间,执行回调函数(这里是删除 map 中过的 key)
惰性删除。访问时判断该键是否被删除。缺点是:如果未访问的话,会加重空间浪费。
而 cache
中采取的是第一种 主动删除。但是,主动删除中遇到最大的问题是:
不断循环,空消耗 CPU 资源,即使在额外的协程中这么做,也是没有必要的。
cache
中采取的是时间轮记录额外过期通知,等过期 channel
中有通知时,然后触发删除回调。
有关 时间轮 更多的设计文章:https://go-zero.dev/cn/timing-wheel.html
热点数据统计
对于缓存来说,我们需要知道这个缓存在使用额外空间和代码的情况下是否有价值,以及我们想知道需不需要进一步优化过期时间或者缓存大小,所有这些我们就很依赖统计能力了, go-zero
中 sqlc
和 mongoc
也同样提供了统计能力。所以我们在 cache
中也加入的缓存,为开发者提供本地缓存监控的特性,在接入 ELK
时开发者可以更直观的监测到缓存的分布情况。
而设计其实也很简单,就是:Get() 命中,就在统计 count 上加 1 即可。
多线程存取
当多个协程并发存取的时候,对于缓存来说,涉及的问题以下几个:
写-写冲突
LRU
中元素的移动过程冲突并发执行写入缓存时,造成流量冲击或者无效流量
这种情况下,写冲突好解决,最简单的方法就是 加锁 :
而并发执行写入逻辑,这个逻辑主要是开发者自己传入的。而这个过程:
而 sharedCalls
通过共享返回结果,节省了多次执行函数,减少了协程竞争。
总结
本篇文章讲解了本地缓存设计实践。从使用到设计思路,你也可以根据你的业务动态修改 缓存的过期策略,加入你想要的统计指标,实现自己的本地缓存。
甚至可以将本地缓存和 redis
结合,给服务提供多级缓存,这个就留到我们下一篇文章:缓存在服务中的多级设计。
关于 go-zero
更多的设计和实现文章,可以关注『微服务实践』公众号。
项目地址
https://github.com/tal-tech/go-zero
欢迎使用 go-zero 并 star 支持我们!
微信交流群
关注『微服务实践』公众号并点击 进群 获取社区群二维码。
版权声明: 本文为 InfoQ 作者【万俊峰Kevin】的原创文章。
原文链接:【http://xie.infoq.cn/article/cafa80b39e5b9d0a55e6a429a】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论