# 缓存一致性

## 谈谈一致性

一致性就是数据保持一致，在分布式系统中，可以理解为多个节点中数据的值是一致的。

* **强一致性**：这种一致性级别是最符合用户直觉的，它要求系统写入什么，读出来的也会是什么，用户体验好，但实现起来往往对系统的性能影响大
* **弱一致性**：这种一致性级别约束了系统在写入成功后，不承诺立即可以读到写入的值，也不承诺多久之后数据能够达到一致，但会尽可能地保证到某个时间级别（比如秒级别）后，数据能够达到一致状态
* **最终一致性**：最终一致性是弱一致性的一个特例，系统会保证在一定时间内，能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来，是因为它是弱一致性中非常推崇的一种一致性模型，也是业界在大型分布式系统的数据一致性上比较推崇的模型

## 场景：秒杀

* 跳过分布式复制，直接就在单节点完成读写，这是保证强一致性最好的办法。
* 性能上需要垂直切分数据，分散各节点压力。也就是每个 `Redis` 都是作为主节点存在，将需要秒杀的商品，做数据上的切分，由每个 `Redis` 承担一部分商品的读写访问，那么这个过程就是分布式计算中无共享的并行化模式了，适合使用 `Redis cluster` 架构

## 场景：数据库 + 缓存

理论上讲，给缓存设置过期时间，就能保证最终一致性。

#### 1. 先更缓存，再更数据库

1. 如果更数据库失败，需要自行实现缓存的回滚逻辑，很麻烦
2. 和下面一样的并发问题

#### 2. 先更数据库，再更缓存

**2.1. 更新失败后的回滚**

如果更缓存失败，会导致缓存和数据库数据不一致。需要业务增加重试逻辑

**2.2. 并发场景下的顺序问题**

如果同时有请求A和请求B进行更新操作，那么以下场景会出现脏数据：

1. 线程A更新了数据库
2. 线程B更新了数据库
3. 线程B更新了缓存
4. 线程A更新了缓存

**2.3 业务场景角度**

如果读较集中于部分热点数据，则大量缓存不会被读到，更新这些缓存浪费资源

**2.4 解决**

要解决 2.2 并发下的顺序问题，想办法保证操作的顺序就可以了。像上面的场景里，起个线程 `C` 通过阿里的 `canal` 中间件从 `DB binlog` 同步数据到缓存，就可以保证顺序。

另一种思路是在更新数据时从缓冲中删除数据，这种方案叫【Cache-Aside】。设计如下：

* 失效：应用程序先从cache取数据，没有得到，则从数据库中取数据，成功后，放到缓存中。
* 命中：应用程序从cache中取数据，取到后返回。
* 更新：先把数据存到数据库中，成功后，再让缓存失效。

但先删缓存，再更新数据库。还是先更新数据库，再删缓存？

#### 3. 先删缓存，再更数据库

该方案会导致不一致的原因是：同时有一个请求A进行更新操作，另一个请求B进行查询操作。那么会出现如下情形：

1. A删缓存
2. B读不到缓存，从 `DB` 加载旧数据到缓存
3. A更新 `DB`

由于读比写的速度快，所以步骤2一般比步骤3快

* 可以采用延时双删策略，即第一步删完缓存后，睡眠过一会儿再删一次

采用这种同步淘汰策略，吞吐量降低怎么办？

* 睡眠和第二次删除新起线程来做

#### 4. 先更数据库，再删缓存

facebook 在论文《Scaling Memcache at Facebook》中提出，他们用的也是先更新数据库，再删缓存的策略。

该方案也有不一致问题：

1. A查缓存，缓存刚好失效；A从DB查出旧数据
2. B更数据库并删除缓存
3. A加载旧数据进缓存

不过一般A读的速度原高于B写的速度，即步骤3通常早于步骤2，所以这种情况很难出现。

假设，有人非要抬杠，有强迫症，一定要解决怎么办？

首先，给缓存设有效时间是一种方案。其次，采用异步延时删除策略，保证读请求完成以后，再进行删除操作。

这个方案还有一种做法：单起一个线程C，通过阿里 `canal` 中间件消费 `DB binlog`，消费后不是更新缓存，而是删除缓存。B站的评论就是这么做的。

也有问题：1. A读取旧数据（但还没写到缓存）、2. C从缓存删除数据、3. B读取新数据到缓存、4. A写旧数据到缓存。不过同样几率很小，步骤4 一般都早于3

#### 删缓存失败的解决方案

这是3和4都存在的一个问题。要解决，提供一个保障的重试机制即可。

1. 写压力不大的场景，将删除失败的数据直接放入内存里，单起一个线程不断重试即可
2. 删除失败的数据，放入消息队列里进行重试
   * 删除失败属于很少见的 `case`，消息队列里不会有太多数据
   * 为此引入消息队列，会对业务线代码造成大量的侵入
3. 接数据库 `binlog`
   * `mysql` 有个阿里中间件叫 `canal`，可以完成订阅 `binlog` 日志的功能
   * 另起一段非业务代码，同步 `binlog` 和 `redis`

## 参考

[读字节 - 对于秒杀、抢购等场景，主从结构，多个redis之间同时读写，怎么确保数据一致性？](https://www.zhihu.com/question/461074637)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wtifs.gitbook.io/diva-notes/redis/huan-cun-yi-zhi-xing.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
