Skip to the content.

[TOC]

缓存

缓存同步策略

典型的缓存模式,一般有如下几种:

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

经常用到的一种策略模式。这种模式主要流程如下:

这种缓存策略会在如下情况出现脏数据:

title: 脏数据产生场景
participant A线程
participant B线程
participant Cache

A线程->Cache:查询数据
Cache-->>A线程:不存在
A线程->A线程:加载DB数据
B线程->B线程:更新DB数据
B线程->Cache:删除缓存
A线程->Cache:将DB数据放入缓存

这种情况发生概率要稍微低,因为需要在数据库的查询操作和缓存的更新操作之间,插入一个数据库的修改和缓存的修改操作,一般情况下,后者需要的时间是要远高于前者的。当然也不能忽视这种情况确实存在。

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,且做好事务、缓存回滚机制,具体思路可以参考分布式事务的实现。

最终一致性

一般缓存偏向于最终一致性,常用的解决方案有:

PS:当然Cache的主要目的是为了性能,如果必须强一致性,则一定会带来性能损失,未必是个好主意,凡事需要结合业务以及实际应用场景来平衡。

缓存穿透

请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去。

可能引发的问题:数据库压力过大,扛不住。

解决办法:

缓存雪崩

是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩。

可能引发的问题:数据库压力过大,扛不住。

解决办法:

缓存击穿

对于一些设置了过期时间的Key,如果这些Key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,雪崩则是很多 key。

可能引发的问题:单个 key 过期的瞬间大量请求过来,导致数据库压力过大崩了。

解决办法:

热点Key

缓存中的某些Key(可能对应用与某个促销商品)对应的 value 存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈。

解决办法: