<audio id="audio" title="11 | MySQL如何应对高并发（一）：使用缓存保护MySQL" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fc/d7/fcbe51f74b37a8bf88ca015c210dcad7.mp3"></audio>

你好，我是李玥。

通过前面几节课的学习，相信你对MySQL这类关系型数据库的能力，已经有了定量的认知。

我们知道，大部分面向公众用户的互联网系统，它的并发请求数量是和在线用户数量正相关的，而MySQL能承担的并发读写的量是有上限的，当系统的在线用户超过几万到几十万这个量级的时候，单台MySQL就很难应付了。

绝大多数互联网系统，都使用MySQL加上Redis这对儿经典的组合来解决这个问题。Redis作为MySQL的前置缓存，可以替MySQL挡住绝大部分查询请求，很大程度上缓解了MySQL并发请求的压力。

Redis之所以能这么流行，非常重要的一个原因是，它的API非常简单，几乎没有太多的学习成本。但是，要想在生产系统中用好Redis和MySQL这对儿经典组合，并不是一件很简单的事儿。我在《[08 | 一个几乎每个系统必踩的坑儿：访问数据库超时](https://time.geekbang.org/column/article/211008)》举的社交电商数据库超时故障的案例，其中一个重要的原因就是，对缓存使用不当引发了缓存穿透，最终导致数据库被大量查询请求打死。

今天这节课，我们就来说一下，在电商的交易类系统中，如何正确地使用Redis这样的缓存系统，以及如何正确应对使用缓存过程中遇到的一些常见的问题。

## 更新缓存的最佳方式

要正确地使用好任何一个数据库，你都需要先了解它的能力和弱点，扬长避短。Redis是一个使用内存保存数据的高性能KV数据库，它的高性能主要来自于：

1. 简单的数据结构；
1. 使用内存存储数据。

上节课我们讲到过，数据库可以分为执行器和存储引擎两部分，Redis的执行器这一层非常的薄，所以Redis只能支持有限的几个API，几乎没有聚合查询的能力，也不支持SQL。它的存储引擎也非常简单，直接在内存中用最简单的数据结构来保存数据，你从它的API中的数据类型基本就可以猜出存储引擎中数据结构。

比如，Redis的LIST在存储引擎的内存中的数据结构就是一个双向链表。内存是一种易失性存储，所以使用内存保存数据的Redis不能保证数据可靠存储。从设计上来说，Redis牺牲了大部分功能，牺牲了数据可靠性，换取了高性能。但也正是这些特性，使得Redis特别适合用来做MySQL的前置缓存。

虽然说，Redis支持将数据持久化到磁盘中，并且还支持主从复制，但你需要知道，**Redis仍然是一个不可靠的存储，它在设计上天然就不保证数据的可靠性**，所以一般我们都使用Redis做缓存，很少使用它作为唯一的数据存储。

即使只是把Redis作为缓存来使用，我们在设计Redis缓存的时候，也必须要考虑Redis的这种“数据不可靠性”，或者换句话说，我们的程序在使用Redis的时候，要能兼容Redis丢数据的情况，做到即使Redis发生了丢数据的情况，也不影响系统的数据准确性。

我们仍然用电商的订单系统来作为例子说明一下，如何正确地使用Redis做缓存。在缓存MySQL的一张表的时候，通常直接选用主键来作为Redis中的Key，比如缓存订单表，那就直接用订单表的主键订单号来作为Redis中的key。

如果说，Redis的实例不是给订单表专用的，还需要给订单的Key加一个统一的前缀，比如“orders:888888”。Value用来保存序列化后的整条订单记录，你可以选择可读性比较好的JSON作为序列化方式，也可以选择性能更好并且更节省内存的二进制序列化方式，都是可以的。

然后我们来说，缓存中的数据要怎么来更新的问题。我见过很多同学都是这么用缓存的：在查询订单数据的时候，先去缓存中查询，如果命中缓存那就直接返回订单数据。如果没有命中，那就去数据库中查询，得到查询结果之后把订单数据写入缓存，然后返回。在更新订单数据的时候，先去更新数据库中的订单表，如果更新成功，再去更新缓存中的数据。

<img src="https://static001.geekbang.org/resource/image/c7/5e/c76155eaf8c6ac1e231d9bfb0e22ba5e.png" alt="">

这其实是一种经典的缓存更新策略: **Read/Write Through**。这样使用缓存的方式有没有问题？绝大多数情况下可能都没问题。但是，在并发的情况下，有一定的概率会出现“脏数据”问题，缓存中的数据可能会被错误地更新成了旧数据。

比如，对同一条订单记录，同时产生了一个读请求和一个写请求，这两个请求被分配到两个不同的线程并行执行，读线程尝试读缓存没命中，去数据库读到了订单数据，这时候可能另外一个读线程抢先更新了缓存，在处理写请求的线程中，先后更新了数据和缓存，然后，拿着订单旧数据的第一个读线程又把缓存更新成了旧数据。

这是一种情况，还有比如两个线程对同一个条订单数据并发写，也有可能造成缓存中的“脏数据”，具体流程类似于我在之前“[如何保证订单数据准确无误？](https://time.geekbang.org/column/article/204673)”这节课中讲到的ABA问题。你不要觉得发生这种情况的概率比较小，出现“脏数据”的概率是和系统的数据量以及并发数量正相关的，当系统的数据量足够大并且并发足够多的情况下，这种脏数据几乎是必然会出现的。

我在“[商品系统的存储该如何设计](https://time.geekbang.org/column/article/204688)”这节课中，在讲解如何缓存商品数据的时候，曾经简单提到过缓存策略。其中提到的Cache Aside模式可以很好地解决这个问题，在大多数情况下是使用缓存的最佳方式。

Cache Aside模式和上面的Read/Write Through模式非常像，它们处理读请求的逻辑是完全一样的，唯一的一个小差别就是，Cache Aside模式在更新数据的时候，并不去尝试更新缓存，而是去删除缓存。

<img src="https://static001.geekbang.org/resource/image/0b/31/0b9c9cb74f017c632136280a63015931.png" alt="">

订单服务收到更新数据请求之后，先更新数据库，如果更新成功了，再尝试去删除缓存中订单，如果缓存中存在这条订单就删除它，如果不存在就什么都不做，然后返回更新成功。这条更新后的订单数据将在下次被访问的时候加载到缓存中。使用Cache Aside模式来更新缓存，可以非常有效地避免并发读写导致的脏数据问题。

## 注意缓存穿透引起雪崩

如果我们的缓存命中率比较低，就会出现大量“缓存穿透”的情况。缓存穿透指的是，在读数据的时候，没有命中缓存，请求“穿透”了缓存，直接访问后端数据库的情况。

少量的缓存穿透是正常的，我们需要预防的是，短时间内大量的请求无法命中缓存，请求穿透到数据库，导致数据库繁忙，请求超时。大量的请求超时还会引发更多的重试请求，更多的重试请求让数据库更加繁忙，这样恶性循环导致系统雪崩。

当系统初始化的时候，比如说系统升级重启或者是缓存刚上线，这个时候缓存是空的，如果大量的请求直接打过来，很容易引发大量缓存穿透导致雪崩。为了避免这种情况，可以采用灰度发布的方式，先接入少量请求，再逐步增加系统的请求数量，直到全部请求都切换完成。

如果系统不能采用灰度发布的方式，那就需要在系统启动的时候对缓存进行预热。所谓的缓存预热就是在系统初始化阶段，接收外部请求之前，先把最经常访问的数据填充到缓存里面，这样大量请求打过来的时候，就不会出现大量的缓存穿透了。

还有一种常见的缓存穿透引起雪崩的情况是，当发生缓存穿透时，如果从数据库中读取数据的时间比较长，也容易引起数据库雪崩。

这种情况我在《[08 | 一个几乎每个系统必踩的坑儿：访问数据库超时](https://time.geekbang.org/column/article/211008)》这节课中也曾经提到过。比如说，我们缓存的数据是一个复杂的数据库联查结果，如果在数据库执行这个查询需要10秒钟，那当缓存中这条数据过期之后，最少10秒内，缓存中都不会有数据。

如果这10秒内有大量的请求都需要读取这个缓存数据，这些请求都会穿透缓存，打到数据库上，这样很容易导致数据库繁忙，当请求量比较大的时候就会引起雪崩。

所以，如果说构建缓存数据需要的查询时间太长，或者并发量特别大的时候，Cache Aside或者是Read/Write Through这两种缓存模式都可能出现大量缓存穿透。

对于这种情况，并没有一种方法能应对所有的场景，你需要针对业务场景来选择合适解决方案。比如说，可以牺牲缓存的时效性和利用率，缓存所有的数据，放弃Read Through策略所有的请求，只读缓存不读数据库，用后台线程来定时更新缓存数据。

## 小结

使用Redis作为MySQL的前置缓存，可以非常有效地提升系统的并发上限，降低请求响应时延。绝大多数情况下，使用Cache Aside模式来更新缓存都是最佳的选择，相比Read/Write Through模式更简单，还能大幅降低脏数据的可能性。

使用Redis的时候，还需要特别注意大量缓存穿透引起雪崩的问题，在系统初始化阶段，需要使用灰度发布或者其他方式来对缓存进行预热。如果说构建缓存数据需要的查询时间过长，或者并发量特别大，这两种情况下使用Cache Aside模式更新缓存，会出现大量缓存穿透，有可能会引发雪崩。

顺便说一句，我们今天这节课中讲到的这些缓存策略，都是非常经典的理论，早在互联网大规模应用之前，这些缓存策略就已经非常成熟了，在操作系统中，CPU Cache的缓存、磁盘文件的内存缓存，它们也都应用了我们今天讲到的这些策略。

所以无论技术发展的多快，计算机的很多基础的理论的知识都是相通的，你绞尽脑汁想出的解决工程问题的方法，很可能早都写在几十年前出版的书里。学习算法、数据结构、设计模式等等这些基础的知识，并不只是为了应付面试。

## 思考题

课后请你想一下，具体什么情况下，使用Cache Aside模式更新缓存会产生脏数据？欢迎你在评论区留言，通过一个例子来说明情况。

感谢阅读，如果你觉得今天的内容对你有帮助，也欢迎把它分享给你的朋友。
