Skip to content

Commit

Permalink
一致性问题 part2
Browse files Browse the repository at this point in the history
  • Loading branch information
flycash committed Mar 13, 2022
1 parent d4daff0 commit 8f4d086
Showing 1 changed file with 46 additions and 10 deletions.
56 changes: 46 additions & 10 deletions cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@
- 缓存的基本理论
- 缓存中间件的应用

这里我们讨论缓存的一些基本理论,缓存中间件 Redis 等,在 Redis 里面查看
这里我们讨论缓存的一些基本理论,缓存中间件 Redis 等,在对应的中间件章节里面看里面查看

缓存的基本理论,目前来说考察比较多的是:
- 缓存和 DB 一致性的问题
- 缓存模式
- 缓存穿透、缓存击穿、缓存雪崩

首先针对缓存和 DB 一致性的问题,可以说这个问题是无最优解的。无论选择哪个方案,总是会有一些缺点。要想理解这个问题,核心是在于理解更新缓存和 DB 的先后顺序,在并发情况下,会出现哪些问题。
### 缓存和 DB 一致性问题

缓存模式主要是所谓的 cache-aside, read-through, write-through, write-back, refresh ahead 以及更新缓存使用到的 singleflight 模式。这些模式并不是银弹,如果说某个模式优于其它的模式,这就是在扯淡了。因此,选择何种缓存模式,也就是一个业务层面上考虑的问题,大多数时候,选取任何一种模式都不会有问题
为了方便讨论,这里就将问题简化为 DB 和缓存一致性。也就是更新只需要更新 DB 和缓存

缓存穿透、击穿和雪崩,本质上就是一个问题:缓存没起效果。只不过根据不起效的原因进行了进一步的细分。
首先要记住,缓存一致性的问题根源于两个原因:
- 不同线程并发更新 DB 和数据库;
- 即便是同一个线程,更新 DB 和更新缓存是两个操作,容易出现一个成功一个失败的情况;

回答这一类的问题,都要详细解释每一种方案的优缺点,并且要总结出来“不是银弹”这个点
缓存和 DB 一致性的问题可以说是无最优解的。无论选择哪个方案,总是会有一些缺点

首先要说三种常见但是必然会引起不一致的方案,这三种方案大同小异。面试的时候要记住为什么它们会引起不一致。因为很多时候没有完美方案,所以我们实践中还真有可能采用这三者之一:
最常用的是三种必然会引起不一致的方案,这三种方案大同小异。面试的时候要记住为什么它们会引起不一致。这三种方案都是有一个显著特征,就是如果缓存是会过期的,那么它们最终都会一致。

1. 先更新 DB,再更新缓存。不一致的情况:
1. A 更新 DB,DB中数据被更新为1
Expand All @@ -45,9 +47,9 @@

所以本质上,没有完美的解决方案,或者说仅仅考虑这种更新顺序,是不足以解决缓存一致性问题的。

而大多数的方案,无非就是取舍上有一些不同
与这三个类似的一个方案是利用 CDC 接口,异步更新缓存。但是本质上,也是要忍受一段时间的不一致性。比如说典型的,应用只更新 MySQL,然后监听 MySQL 的 binlog,更新缓存

而可能的方案,我比较喜欢的就两个
而如果需求强一致性的话,那么比较好的方案就是
- 第一个是负载均衡算法结合 singleflight
- 第二个是分布式锁。严格来说,分布式锁的方案,我一点都不喜欢,毫无技术含量

Expand All @@ -67,9 +69,43 @@

分布式锁的方案就没什么好说的了,咔嚓一把分布式锁一了百了。分布式锁适用于写请求特别少的例子,因为读是没有必要加分布式锁的。读完全没有必要加分布式锁,即便此时有人正在更新缓存或者 DB,当前的请求要么读到更新前的,要么读到更新后的,不会有什么问题。

另外一个分布式锁方案的优化是在单机上引入 singleflight,确保一个实例针对一个特定的 key 只会有一个线程去参与抢全局锁。
注意我说的写,是写缓存的写。也就是说,如果要是缓存过期,然后用 DB 的数据更新缓存,同样要参与抢夺这个分布式锁。

另外,一个可行的分布式锁方案的优化是在单机上引入 singleflight,确保一个实例针对一个特定的 key 只会有一个线程去参与抢全局的分布式锁。

注意!前面的这些方案,我们都有一个基本的假设,就是更新 DB 和更新缓存两个步骤都会成功。但是很显然这个假设是站不住脚的,也就是说,真正寻求强一致性,还要进一步解决更新 DB 和更新缓存一个成功一个失败的问题。

这里,也就是只有三个选项:
- 追求强一致性,选用分布式事务;
- 追求最终一致性,可以引入重试机制;
- 如果可以使用本地事务,那么应该是:开启本地事务-更新DB-更新缓存-提交事务

然后一个问题是:我用了分布式事务,我还需要分布式锁吗?答案是,要的。因为分布式事务既解决不了多个线程同时更新的问题,也解决不了一个线程更新,一个线程从数据库读数据刷缓存的问题。

### 缓存模式

缓存模式主要是所谓的 cache-aside, read-through, write-through, write-back, refresh ahead 以及更新缓存使用到的 singleflight 模式。这些模式并不是银弹,如果说某个模式优于其它的模式,这就是在扯淡了。因此,选择何种缓存模式,也就是一个业务层面上考虑的问题,大多数时候,选取任何一种模式都不会有问题。

- write-back:这个稍微有点意思。因为标准的 write-back 是在缓存过期的时候,然后再将缓存刷新到 DB 里面。因此,它的弊端就是,在缓存刷新到 DB 之前,如果缓存宕机了,比如说 Redis 集群崩溃了,那么数据就永久丢失了;但是好处就在于,因为过期才把数据刷新到 DB 里面,因为读写都操作的是缓存。如果缓存是 Redis 这种集中式的,那么意味着大家读写的都是同一份数据,也就没有一致性的问题。但是,如果你设置了过期时间,那么缓存过期之后重新从数据库里面加载的同时,又有一个线程更新缓存,那么两者就会冲突,出现不一致的问题;
- refresh ahead 这种其实就是前面说的利用 CDC 的方案

### 缓存异常场景

缓存穿透、击穿和雪崩,本质上就是一个问题:缓存没起效果。只不过根据不起效的原因进行了进一步的细分。

我一直觉得这三个东西的命名特别沙雕,因为穿透和击穿在中文语境下区别就不大,也不知道是哪个卧龙凤雏搞出来的名字。

其实,这三个就是描述了三种场景:
- 你数据库本来就没数据
- 你数据库有,但是缓存里面没有
- 你缓存本来有,但是突然一大批缓存集体过期了

数据库本来就没数据,所以请求来的时候,肯定是查询数据库的。但是因为数据库里面没有数据,所以不会刷新回去,也就是说,缓存里面会一直没有。因此,如果有一些黑客,一直发一些请求,这些请求都无法命中缓存,那么数据库就会崩溃。

如果数据库有,但是缓存里面没有。理论上来说,只要有人请求数据,就会刷新到缓存里面。问题就在于,如果突然来了一百万个请求,一百万个线程都尝试从数据库捞数据,然后刷新到缓存,那么数据库也会崩溃。

缓存本来都有,但是过期了。一般情况下都不会有问题,但是如果突然之间几百万个 key 都过期了,那么接下来的请求也几乎全部命中数据库,也会导致数据库崩溃。

还有一种思路是利用 CDS 接口,异步更新缓存,但是本质上,也是要忍受一段时间的不一致性。比如说典型的,应用只更新 MySQL,然后监听 MySQL 的 binlog,更新缓存。

## 面试题

Expand Down

0 comments on commit 8f4d086

Please sign in to comment.