2.1 旁路缓存
Cache Aside Pattern(旁路缓存)适合读请求比较多的场景
Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
2.1.1 写
- 先更新db
- 直接删除缓存
2.1.2 读
- 先读缓存
- 有,则从缓存返回。
- 没有,从db中读取返回。
- 再将读取的数据写入缓存
2.1.3 常见问题
1.写,写DB后咋是删缓存不是更新缓存?
写N次,只有最后一次数据的有效,前面写的都会被覆盖。
读不到的时候自然会重建缓存,我们直接删除了,等读的时候重建即可。
2.写,可不可以先删除缓存,再更新DB?
不可以。
前面已经讲了,我们更新的时候,db更新没问题,但是缓存呢不做更新,直接删除。
现在的问题就是,更新db和删除缓存这两个操作的先后问题。
假设我们数据库中有个值name=yang
,我们准备更新成yang37
。
2.1 (×)先删除缓存,再更新db。
这个情况,主要是考虑并发场景,在删除缓存 -> 更新db期间,db中的始终是旧数据。
如果线程2发生了读取请求,会导致数据不一致。
- 线程1:尝试更新name从yang到yang37
- 线程2:尝试读取name的值
最终我们可以看到,缓存中的数据还是旧数据yang,而db中是正确的yang37,数据不一致。
问题的根源在哪里,在于你线程2读取缓存是很快的,缓存中没值,必然触发缓存的重建。
- 线程1的db更新完了,那就皆大欢喜,即左边线程1整个期间没有并发问题。
- 线程1的db没更新完,db中的必定是旧数据,重建的缓存值也必定成旧数据,导致出问题。
注意图最下面的两个框框。最重要的是,在缓存有效期内,你缓存的一直是脏数据,如果这个缓存没过期时间,那么缓存中的数据始终有问题。
这个问题出现的概率大吗?即左边删除缓存 -> 更新db的期间能不能包裹住右边边。
实际上,db查询数据比更新数据快,这个情况很容易出现。
2.2 (√) 先更新db,再删除缓存。
接着上面的,来看下先更新db的。
嗯,这里,如果更新db -> 删除缓存期间有读取请求,我们的线程2还是会读取到脏数据。
但是,咱们最终缓存中的数据不存在,下次查询会触发重建。
先删除缓存再更新db的方式,逻辑上就存在问题了,咱们这个影响程度小的多。
咱们只是在这期间有短暂的不一致问题,不会导致最终状态下的数据不一致。
但是,还是可能有缓存最终不一致的情况,就是刚好缓存失效了,例如下图。
这里的问题点是什么,线程2的更新缓存操作覆盖了线程1的更新db+删除缓存
的操作。
它必须要完整的包裹住线程1的更新操作和删除操作。
- 如果线程1更新操作在线程2查询db的操作之前,那么此时查询到的数据已经被更新了,重建的缓存值刚好是正确的。
- 如果线程1的删除操作发生在线程2的更新缓存之后,那么此时缓存中的数据已经被删除了,下次重建会成为正确的值,也符合我们的预期。
我们注意下哈,先决条件是线程2从db查到旧数据之后,线程1开始更新db了。
然后,必须完完整整的包裹住这整个操作,必须得下面这样。
即,哪怕我像下面这样两种情况,它也不会有问题,必须满足上面包裹住的场景,才可能有问题。
实际上,db查询数据比更新数据快,所以这个更新db+删除缓存的时间
很难比查询db+更新缓存的时间长
。
别忘了我们还有并发+缓存失效的背景,这所有因素组合在一起才可能有这个数据不一致的情况,所以出现问题的概率是很低的。
你说,咱们方案1中过期时间的场景都还没讨论呢。哈哈,其实不用讨论了,你想啊,方案1还没说过期就有很大隐患了,还不要说咱们现在这里的讨论的方案2+过期的场景。
综上呢,咱们的正确方案是:先更新db,再删除缓存。