Redis缓存更新
本文为redis学习笔记的第十篇文章。redis缓存更新策略学习。
更新缓存的的Design Pattern
有四种:Cache aside
, Read through
, Write through
, Write behind caching
,我们下面一一来看一下这四种Pattern
。这里,我们先不讨论更新缓存和更新数据这两个事是一个事务的事,或是会有失败的可能,我们先假设更新数据库和更新缓存都可以成功的情况(我们先把成功的代码逻辑先写对)。
先来看看缓存可能存在的一些问题,目的是突出缓存使用策略选择的重要性。
1.缓存穿透
缓存穿透是说访问一个缓存中没有的数据,但是这个数据数据库中也不存在。
解决方案是:
- 缓存空对象。如果缓存未命中,而数据库中也没有这个对象,则可以缓存一个空对象到缓存。如果使用
Redis
,这种key
需设置一个较短的时间,以防内存浪费。 - 缓存预测。预测
key
是否存在。如果缓存的量不大可以使用hash
来判断,如果量大可以使用布隆过滤器来做判断。采用布隆,将所有可能存在的数据哈希到一个足够大的BitSet
中,不存在的数据将会被拦截掉,从而避免了对存储系统的查询压力。
2.缓存并发
多个客户端同时访问一个没有在cache
中的数据,这时每个客户端都会执行从DB
加载数据set
到缓存,就会造成缓存并发。
- 缓存预热。提前把所有预期的热数据加到缓存。定位热数据还是比较复杂的事情,需要根据自己的服务访问情况去评估。这个方案只能减轻缓存并发的发生次数不能全部抵制。
- 缓存加锁。 如果多个客户端访问不存在的缓存时,在执行加载数据并
set
缓存这个逻辑之前先加锁,只能让一个客户端执行这段逻辑。
3.缓存雪崩
缓存雪崩是缓存服务暂时不能提供服务,导致所有的请求都直接访问DB。
解决方案:
- 构建高可用的缓存系统。目前常用的缓存系统
Redis
和Memcache
都支持高可用的部署方式,所以部署的时候不防先考虑是否要以高可用的集群方式部署。 - 限流。
Netflix
的Hystrix
是非常不错的工具,在用缓存时不妨搭配它来使用。
4.Cache Aside Pattern
一种错误的做法是:先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,直到这个缓存失效为止。
Cache Aside Pattern
是最常用最常用的pattern
了。其具体逻辑如下:
- 失效:应用程序先从
cache
取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。 - 命中:应用程序从
cache
中取数据,取到后返回。 - 更新:先把数据存到数据库中,成功后,再让缓存失效。
注意,我们的更新是先更新数据库,成功后,让缓存失效。那么,这种方式是否可以没有文章前面提到过的那个问题呢?
一个是查询操作,一个是更新操作的并发,首先,没有了删除cache
数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。
但还是存在问题的。比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。不过,实际上出现的概率可能非常低.
所以,这也就是Quora
上的那个答案里说的,要么通过2PC
或是Paxos
协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook
使用了这个降低概率的玩法,因为2PC
太慢,而Paxos
太复杂。当然,最好还是为缓存设置上过期时间。
5.Read/Write Through Pattern
Read Through
:读取数据的时候如果当前缓存中没有数据,惯常的操作都是应用程序去DB
加载数据,然后加入到缓存中。Read Through
与之不同的是我们不需要在应用程序自己加载数据了,缓存层会帮忙做件事。Write Through
:更新数据的时候,如果命中缓存,则先更新缓存然后缓存在负责把数据更新到数据库;如果没有命中缓存则直接更新数据库。
这种方式缓存层直接屏蔽了DB,应用程序只需要更缓存打交道。优点是应用逻辑简单了,而且更高效了;缺点是缓存层的实现相对复杂一些。
6.Write Back Pattern
Write Back
套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O
操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg
还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux
非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。
另外,Write Back
实现逻辑比较复杂,因为他需要track
有哪数据是被更新了的,需要刷到持久层上。操作系统的write back
会在仅当这个cache
需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write
。
7.实际使用的一些策略
业务方(调用者)更新
传统上,更新缓存都是由业务方来做,也就是由调用者负责更新DB和缓存。
DB中间件监听DB变化,更新缓存
现在有种新的办法就是利用DB
中间件监听DB
变化(比如阿里的Canal
中间件,点评的Puma
),从而对缓存进行更新。
这种办法的一个好处就是:把缓存的更新逻辑,和业务逻辑解藕。业务只更新DB,缓存的更新被放在另外一个专门的系统里面。
8.总结
一句话,无论谁先谁后,只要更新缓存和更新DB不是原子的,就可能导致不一致。
总之,只是从实际业务来讲,一般缓存也都是保持“最终一致性“,而不是和DB
的强一致性。
并且一般建议先更新DB,再更新缓存,优先保证DB数据正确。
9.一致性问题
上面,我们没有考虑缓存(Cache
)和持久层(Repository
)的整体事务的问题。比如,更新Cache
成功,更新数据库失败了怎么吗?或是反过来。关于这个事,如果你需要强一致性,你需要使用“两阶段提交协议”——prepare
, commit/rollback
.后续再探讨。