[TOC]
缓存
缓存同步策略
典型的缓存模式,一般有如下几种:
- Cache Aside
- Read/Write Through
- Write Behind
Cache Aside
st=>start: 开始
e=>end: 结束
op_db=>operation: 从DB加载数据
op_cache=>operation: 将DB数据放入Cache中
cond_re_wr=>condition: 是否是读?
cond_cache=>condition: 缓存是否有数据
io=>inputoutput: 输出数据
op_db_write=>operation: 更新DB数据
op_cache_write=>operation: 删除缓存
st->cond_re_wr
cond_re_wr(no)->op_db_write
op_db_write(right)->op_cache_write->e
cond_re_wr(yes)->cond_cache
cond_cache(no)->op_db(right)->op_cache
cond_cache(yes)->io
op_cache->io
io->e
经常用到的一种策略模式。这种模式主要流程如下:
- 失效:应用程序先从 Cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从 Cache 中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后,再删除缓存。
这种缓存策略会在如下情况出现脏数据:
title: 脏数据产生场景
participant A线程
participant B线程
participant Cache
A线程->Cache:查询数据
Cache-->>A线程:不存在
A线程->A线程:加载DB数据
B线程->B线程:更新DB数据
B线程->Cache:删除缓存
A线程->Cache:将DB数据放入缓存
- 线程 A 查询一个数据,在缓存中没有找到,于是去数据库查,拿到数据之后还没放到缓存中。
- 这时候线程 B 过来更新这一条数据,先更新数据库之后准备令缓存失效,由于缓存还没放进来所以不需要失效。
- 线程 A 把数据放到了缓存中。但是这个数据其实已经是脏数据了,跟数据库中不同步。
这种情况发生概率要稍微低,因为需要在数据库的查询操作和缓存的更新操作之间,插入一个数据库的修改和缓存的修改操作,一般情况下,后者需要的时间是要远高于前者的。当然也不能忽视这种情况确实存在。
Read/Write Through
Read/Write Through 套路是把更新数据库的操作由缓存自己代理了,对应用层来说是透明的,应用不再需要关心数据同步问题。
st=>start: 开始
e=>end: 结束
op_db=>operation: 开辟缓存空间
op_cache=>operation: 将低速存储器数据放入Cache中
cond_re_wr=>condition: 是否是读?
cond_cache_read=>condition: 缓存是否有数据
io=>inputoutput: 输出数据
cond_cache_write=>condition: 缓存是否有数据
op_db_write=>operation: 更新低速存储器数据
op_cache_write=>operation: 将数据写入Cache
st->cond_re_wr
cond_re_wr(yes)->cond_cache_read
cond_cache_read(no)->op_db(right)->op_cache
cond_cache_read(yes)->io
op_cache->io
io->e
cond_re_wr(no)->cond_cache_write
cond_cache_write(yes)->op_cache_write->op_db_write
cond_cache_write(no)->op_db_write
op_db_write->e
Read Through
Read Through 就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或 LRU 换出),Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对应用方是透明的。
Write Through
Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由 Cache 自己更新数据库(这是一个同步操作)。
这种方式的风险也显而易见,如果追求落库之后再返回成功,效率必然降低很多,缓存的意义就不大了。如果追求落到缓存上就算成功,那问题又抛给了缓存的丢失风险上。
Write Behind
Write Behind 又叫 Write Back。就是 Linux 文件系统的 Page Cache 的算法。Write Back 套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作飞快无比(因为直接操作内存嘛)。
因为异步,Write Back 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。性能很强悍,问题也很明显,数据不是强一致性的,而且可能会丢失。另外 Write Behind 实际上的逻辑还比较复杂,因为需要追踪定位哪些数据需要做持久化。
缓存常见问题
一致性问题
首先需要明确,除了把整个缓存和持久化存储都做串行化,或者操作一个的时候直接锁死另一个,或者通过消息队列操作缓存以达到最终一致性,不然必然会有产生数据不一致的可能。选择哪种缓存策略是根据自己的业务场景进行取舍。
强一致性
利用锁机制,串行化Cache、Db,且做好事务、缓存回滚机制,具体思路可以参考分布式事务的实现。
最终一致性
一般缓存偏向于最终一致性,常用的解决方案有:
-
基于Cache Aside策略
- 延迟双删
- 消息删除缓存
- 订阅业务消息
- 订阅Db的Log消息
PS:当然Cache的主要目的是为了性能,如果必须强一致性,则一定会带来性能损失,未必是个好主意,凡事需要结合业务以及实际应用场景来平衡。
缓存穿透
请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去。
可能引发的问题:数据库压力过大,扛不住。
解决办法:
-
缓存短时间的空值
-
布隆过滤器:可以用来判断超大数据量中单个数据的存在性问题。但是存在误差,如果布隆过滤器判断不存在,那肯定不存在,但是如果判断存在,也有一定的可能误判。
缓存雪崩
是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩。
可能引发的问题:数据库压力过大,扛不住。
解决办法:
- 将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值。
缓存击穿
对于一些设置了过期时间的Key,如果这些Key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,雪崩则是很多 key。
可能引发的问题:单个 key 过期的瞬间大量请求过来,导致数据库压力过大崩了。
解决办法:
- 加锁,单个请求去更新缓存就行了,别的排队等着。
- 双层的超时值,保存一个用于更新缓存的超时值,到达这个值之后去延长一下真是的超时时间。
热点Key
缓存中的某些Key(可能对应用与某个促销商品)对应的 value 存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈。
解决办法:
- 客户端缓存(多级缓存),将热点 key 对应 value 并缓存在客户端本地,并且设置一个失效时间。
- 将单个热点分散为多个子 key,时期请求的时候 hash 到不同的机器上处理。