业务缓存的基本概念与整体设计思路
缓存的使用流程
请求数据时,先判断缓存是否存在,存在则反序列化数据,然后返回数据,如果缓存不存在,则回源拿到数据 ,序列化后将数据更新到缓存,并返回数据
缓存的4种模式:
Cache Aside:
业务程序先请求cache,如果命中直接返回,不命中,就从db加载数据,然后回写到cache中
Read Through:
业务程序不会直接访问db,而是由cache负责从db加载数据
Write Through(同步写db):
业务程序写cache,然后由cache更新db
Write Behind(异步写db):
跟Write Through类似,业务程序写cache,然后由cache更新db,区别在于Write Behind是异步写db
业务缓存设计应尽量采用Read Through,整体设计思路如下:
其中用户接口我们应该提供最基础的Get和MGet,获取数据和批量获取数据的能力
实现方式1:
实现方式2:
不推荐:方式1,实现起来复杂,用起来有点摸不着头脑 推荐:方式2,清晰明了
缓存设计的例子:
在设计时,应该考虑到轮子的更新迭代速度较快,我们的基础组件设计不应该过于复杂,方便替换,所以在设计之初应该依赖于接口,而非实现
缓存接口设计示例:
序列化设计实例:
接口设计的一些好处:
小结:
业务缓存基础库选型与回源
缓存库很多,我们应该怎么选?只需抓住两个重点,命中率和性能
命中率:
用来做缓存的数据,总量是有限的,如果能全存下来,命中率就是100%,如果不能,就要靠逐出算法来筛选出价值最高的数据,常见的逐出算法如下:
FIFO、ARC、Tiny-LFU是对LRU和LFU的改良算法。
性能:
第一个影响性能的关键因素是锁
锁的颗粒度:
锁的类型:
另一个影响性能的指标是GC
从存储对象上看,缓存分为:
如下是两个带指针和不带指针的例子
例子1:
可以看出紫色的是map[int]*int,携带了指针,GC耗时明显比不带指针的map[int]int要高
例子2:
这个例子要稍微复杂一点,可以看到gc的耗时猛然飙五六十甚至一百多毫秒,而在实际业务中,可能更加复杂,带来的GC耗时也成倍增长。
如何选择相应的存储对象?
字节缓存和对象缓存对内存的控制
序列化包的选择
、
小结:
回源:
抽象,同样的函数,参数,返回值,屏蔽内部实现细节
如何不跟类型绑定?
生成缓存key
回源函数参数
什么是缓存穿透?
一个不存在的key,总能绕过缓存,直接请求数据源。如果遇到恶意请求,可能会把数据源打崩
常见解决方案:
1、布隆过滤器
特点: 如果不存在就一定不存在
正好可以前置检查key是否存在,不存在的话就不请求数据源,可以借助Redis的BitMap实现
缺点: 维护比较麻烦,需要在写操作时更新布隆过滤器,如果我们无法获取全量数据,或者监听写操作,就不太好构建布隆过滤器
2、缓存nil
特点: 简单,更常见
如果key不存在,就在缓存中写一个空值或者nil,空值不一定是0或者字符串,可以是业务含义上的空值
什么是缓存击穿?
一个热key突然失效,导致大量的请求回源,给数据源带来较大的压力
常见解决方案:
1、singleflight
golang.org/x/sync/singleflight:程序内多个goroutine同时回源,只允许一个goroutine回源,其他goroutine阻塞,共享回源结果,借此减少单实例的回源请求数量
分布式singleflight:
2、多层缓存
使用多级缓存,每层的过期时间不一样,缓存miss后,优先从下一层缓存加载数据,都miss再回源
还可以做缓存异构:Redis + 其他分布式KV
小结:
总结: