1.什么是一致性
顾名思义,就是数据的一致性,如果是在分布式系统中,那就是各节点中的数据保持一致。
一致性分为
- 强一致性
- 弱一致性
- 最终一致性
强一致性:这种是符合用户直观感觉的,就是系统写入什么,读出来的就是什么。读写是实时的,用户体验行好,但是对系统的性能影响非常大。
弱一致性:这种级别约束了系统再写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据可以达到一致,但会尽可能的保证在某个时间级别后达到一致状态。
最终一致性:这种级别约束了系统在一定时间内,数据可以达到一致性。是业界的大型分布式系统比较推崇的模型。
2.三种缓存模式
缓存可以提升性能,缓解数据库压力,但是使用缓存可能会导致数据不一致性的问题。有三种缓存模式比较常用。
- Cache-Aside Pattern
- Reah/Write-Through Pattern
- Write behind Pattern
Cache-Aside Pattern:旁路缓存模式
读的过程:
1.读数据时,先读缓存,缓存如果命中直接返回数据;
2.缓存如果不命中,就去读数据库,从数据库中取出数据,然后放入缓存,返回数据。
写的过程:先更新数据库,再删除缓存。
Reah/Write-Through Pattern:读写穿透
这个模式和第一种很像,但是把缓存和数据库认为是一体的,应用的读写都针对于缓存,缓存来维护更新数据库。这样可以让程序代码变得更加简洁。
Write behind Pattern:异步缓存写入模式
和第二种类似,但是第二种缓存和数据库的更新是事务性的,保证了强一致性;而第三种是异步的,先更新缓存,通过异步的方式来更新数据库,缓存和数据库的一致性不强。
3.操作缓存的时候,删除还是更新
在第一种模式中,采用了删除缓存,为什么不是更新缓存呢?
如下例子,有两个线程A和B同时发出一个写操作,
- 线程A先写,更新数据库
- 线程B后写,更新数据库
- 由于网络等原因,线程B先更新了缓存
- 线程A后更新了缓存
结果,数据库里为B的值,缓存里为A的值,就成了脏数据,如果后续不再执行写操作,则读出去的数据都是脏数据。
如果是删除缓存则
- 线程A先写,更新数据库
- 线程B后写,更新数据库
- 由于网络等原因,线程B先删除了缓存
- 线程A后删除缓存
结果,数据库里为B的值,缓存中没有值,一旦有读操作进来,cache miss,然后去数据库中读,读到的是B的值。
因此,更新的副作用是数据不一致性,删除的副作用仅仅是多一次cache miss。
另外,更新缓存相对于删除缓存还有两点劣势:
- 如果写入的缓存值,是经过复杂计算才得到的,更新缓存频率高的话就浪费了大量的性能
- 在写场景多,读场景少的情况下,数据不需要读的时候,可能发生了很多次写,太浪费性能。
4.双写情况下,先操作数据库还是缓存
如果是先操作缓存,则出现如下情况
线程A做写操作,线程B做读操作。
- 线程A发起写操作,删除缓存
- 线程B发起读操作,cache miss
- 线程B去读数据库,写入缓存并返回
- 线程A更新数据库
结果,数据库和缓存出现数据不一致性。
如果是先操作数据库,则出现如下情况:
- 线程A发起写操作,更新数据库
- 线程B发起读操作,返回
- 线程A删除缓存
结果,线程B读的是过期数据,但是接下来的读操作则会MISS,然后去数据库读取正确数据并放入缓存中。
先删缓存的解决方法
缓存延时双删:
- 先删除缓存
- 更新数据库
- 休眠一会(读业务耗时+几百毫秒,保证读请求结束),再删除缓存
但是,无论是先更新数据库在删除缓存,还是延时双删,如果最后一步的删除缓存操作失败,就会导致脏数据。
因此,可以引入删除缓存重试机制:
- 写请求更新数据库
- 缓存因为某些原因,删除失败
- 把删除失败的key放入消息队列
- 消费消费队列的信息,获得删除失败的key
- 重试删除缓存操作
既可以保证所有缓存删除成功。