现在使用得比较多的是 Cache Aside Pattern(旁路缓存模式)。
首先说明这个保证的是在最终一致性的基础上尽可能减少不一致的时间,并不是保证强一致性。
1、Cache Aside Pattern
缓存就三种情况:
(1)命中:程序先从缓存中读取数据,如果命中,则直接返回。
(2)失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后将数据放入缓存中。
(3)更新:先更新数据库,再删除缓存(Cache Aside Pattern
就是使用这种模式)。
我们讨论的一致性主要保证的就是缓存的更新。各种更新策略我们来分析一下,为什么 先更新数据库,再删除缓存 这种策略好。
1.1 先更新缓存,再更新数据库
先看一幅图:
假如我们缓存更新成功了,但是数据库更新失败了,数据库会回滚操作,这就意味着我们缓存中的数据和数据库中的数据不一致。
因为我们的持久化是由数据库来保证的,也就是数据的正确性是以数据库为主,缓存中的数据和数据库中的数据不一致,这肯定是不行的。
1.2 先更新数据库,再更新缓存
先看一幅图:
假如线程 A 首先更新了数据库,但它正准备要更新缓存;此时发生了线程上下文切换,这个时候线程 B 它更新了数据库,同时也把缓存也更新了;此时线程 A 执行,它又把缓存给更新了。
最后的结果就是:缓存的数据和数据库中的数据又不一致了,因为线程 B 更新的数据才是数据库中最新的数据,但缓存中存的却是线程 A 更新的旧数据。
1.3 先删除缓存,再更新数据库
先看一幅图:
还是同样的问题,缓存和数据库中的数据又不一致了。
1.4 先更新数据库,再删除缓存
先看一幅图:
为什么这里的更新缓存会比删除缓存的时间点快呢?
是因为查询数据库的数据是不用加锁的,而更新数据库会加锁,所以更新的速度肯定是比查询缓存慢的。就算更新的速度比查询的速度快,这种也是概率非常小的,偶尔的数据不一致是可以接受的,毕竟我们保证的不是强一致性。
还有一种降低概率的方式:延时删除。就是你更新完数据库后,删除缓存这个逻辑可以待会在执行,就是让线程睡一会,然后再更新缓存。
2、读写分离架构
改进后:
可以使用阿里的中间件 Canel 来实现。
更新缓存失败如何处理
使用消息中间件(这个暂时还不会,有兴趣的自己去了解一下)