赫赫有名的双刃剑:缓存(上)
缓存的本质
缓存,简单说就是为了节约对原始资源重复获取的开销,而将结果数据副本存放起来以供获取的方式。
-
缓存往往针对的是“资源”. 操作是幂等的
-
缓存数据必须是“重复”获取的. 缓存的命中率
-
缓存是为了解决“开销”的问题
-
时间
-
CPU
-
网络
-
I/O
-
数据库资源
-
缓存的存取其实不一定是“更快”的
-
分摊压力
-
快是相对的
缓存无处不在
缓存应用模式
Cache-Aside
数据获取策略
- 应用先去查看缓存是否有所需数据;
- 如果有,应用直接将缓存数据返回给请求方;
- 如果没有,应用执行原始逻辑,例如查询数据库得到结果数据;
- 应用将结果数据写入缓存。

数据读取的异常情形
- 如果数据库读取异常,直接返回失败,没有数据不一致的情况发生;
- 如果数据库读取成功,但是缓存写入失败,那么下一次同一数据的访问还将继续尝试写入,因此这时也没有不一致的情况发生。
数据更新策略 (注意顺序)
- 应用先更新数据库;
- 应用再令缓存失效。
最重要的一点是必须先更新数据库,而不是先令缓存失效,即这个顺序不能倒过来。原因在于,如果先令缓存失效,那么在数据库更新成功前,如果有另外一个请求访问了缓存,发现缓存数据库已经失效,于是就会按照数据获取策略,从数据库中使用这个已经陈旧的数值去更新缓存中的数据,这就导致这个过期的数据会长期存在于缓存中,最终导致数据不一致的严重问题。
第二个关键点是,数据库更新以后,需要令缓存失效,而不是更新缓存为数据库的最新值。为什么呢?你想一下,如果两个几乎同时发出的请求分别要更新数据库中的值为 A 和 B,如果结果是 B 的更新晚于 A,那么数据库中的最终值是 B。但是,如果在数据库更新后去更新缓存,而不是令缓存失效,那么缓存中的数据就有可能是 A,而不是 B。因为数据库虽然是“更新为 A”在“更新为 B”之前发生,但如果不做特殊的跨存储系统的事务控制,缓存的更新顺序就未必会遵从“A 先于 B”这个规则,这就会导致这个缓存中的数据会是一个长期错误的值 A。
数据更新的异常情形
- 如果数据库操作失败,那么直接返回失败,没有数据不一致的情况发生;
- 如果数据库操作成功,但是缓存失效操作失败,这个问题很难发生,但一旦发生就会非常麻烦,缓存中的数据是过期数据,需要特殊处理来纠正。
Read-Through
这种情况下缓存系统彻底变成了它身后数据库的代理,二者成为了一个整体,应用的请求访问只能看到缓存的返回数据,而数据库系统对它是透明的。

数据获取策略
- 应用向缓存要求数据;
- 如果缓存中有数据,返回给应用,应用再将数据返回;
- 如果没有,缓存查询数据库,并将结果写入自己;
- 缓存将数据返回给应用。
数据读取异常的情况分析和 Cache-Aside 类似,没有数据不一致的情况发生。
Write-Through
和 Read-Through 类似,图示同上,但 Write-Through 是用来处理数据更新的场景。
数据更新策略
- 应用要求缓存更新数据;
- 如果缓存中有对应数据,先更新该数据;
- 缓存再更新数据库中的数据;
- 缓存告知应用更新完成。
这里的一个关键点是,缓存系统需要自己内部保证并发场景下,缓存更新的顺序要和数据库更新的顺序一致。
数据更新的异常情形
- 如果缓存更新失败,直接返回失败,没有数据不一致的情况发生;
- 如果缓存更新成功,数据库更新失败,这种情况下需要回滚缓存中的更新,或者干脆从缓存中删除该数据。
还有一种和 Write-Through 非常类似的数据更新模式,叫做 Write-Around。它们的区别在于 Write-Through 需要更新缓存和数据库,而 Write-Around 只更新数据库(缓存的更新完全留给读操作)。
Write-Back
对于 Write-Back 模式来说,更新操作发生的时候,数据写入缓存之后就立即返回了,而数据库的更新异步完成。这种模式在一些分布式系统中很常见。
这种方式带来的最大好处是拥有最大的请求吞吐量,并且操作非常迅速,数据库的更新甚至可以批量进行,因而拥有杰出的更新效率以及稳定的速率,这个缓存就像是一个写入的缓冲,可以平滑访问尖峰。另外,对于存在数据库短时间无法访问的问题,它也能够很好地处理。
但是它的弊端也很明显,异步更新一定会存在着不可避免的一致性问题,并且也存在着数据丢失的风险(数据写入缓存但还未入库时,如果宕机了,那么这些数据就丢失了)。
