近期关于缓存设计的一些思考
我们先来看看我们常说的缓存有哪些
- CPU 的 L1, L2, L3 缓存
- 操作系统内存(相对于CPU来说也是缓存)
- 操作系统 page cache 高速页缓存(缓存磁盘中的数据)
- Redis 用作 MySQL的缓存
- MySQL buffer pool 的数据页缓存
- Java HashMap 实现的堆内缓存
通过上面的例子我们可以知道缓存的实现不一定都是DRAM内存,但他们的作用都一样,解决两个系统或层次的读写能力差距
操作系统的缓存
在极客时间《Java并发编程实战》这样写道
这些年,我们的 CPU、内存、I/O 设备都在不断迭代,不断朝着更快的方向努力。但是,在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异。
CPU 和内存的速度差异可以形象地描述为:CPU 是天上一天,内存是地上一年(假设 CPU 执行一条普通指令需要一天,那么 CPU 读写内存得等待一年的时间)。内存和 I/O 设备的速度差异就更大了,内存是天上一天,I/O 设备是地上十年。
程序里大部分语句都要访问内存,有些还要访问 I/O,根据木桶理论(一只水桶能装多少水取决于它最短的那块木板),程序整体的性能取决于最慢的操作——读写 I/O 设备,也就是说单方面提高 CPU 性能是无效的。
为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
1. CPU 增加了缓存,以均衡与内存的速度差异;
2. 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
3. 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。
同时作者也说了缓存带来的问题:可见性,这也是并发编程Bug的源头之一
一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。
多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。
Redis 用作缓存
下面我们在看看 Redis 用作缓存的场景
在极客时间《Redis核心技术与实战》中作者写道:
一个系统中的不同层之间的访问速度不一样,所以我们才需要缓存
所以,计算机系统中,默认有两种缓存:
1. CPU 里面的末级缓存,即 LLC,用来缓存内存中的数据,避免每次从内存中存取数据;
2. 内存中的高速页缓存,即 page cache,用来缓存磁盘中的数据,避免每次从磁盘中存取数据。
还有一点非常重要,文中也提到了
缓存系统的容量大小总是小于后端慢速系统的,我们不可能把所有数据都放在缓存系统中。
这其实取决于硬件的限制成本,读写越快的存储越贵,容量越大的存储越贵,并且两者不是线性增长的关系。
另外这个限制也说明缓存系统是要有数据淘汰机制的,比如redis就有多种数据淘汰策略
同时还要关注缓存命中率的问题,有限的资源当然要给访问更频繁的数据。
那么Redis用作磁盘DB的缓存又会带来什么问题呢?
就是我们常说的缓存不一致问题,这里不展开讨论了
MySQL中的缓存设计
我们都知道MySQL的数据是存储在磁盘上的,但是在购买MySQL实例时,往往能看到8核32g,64g这样的配置,为什么磁盘数据库还需要这么大的内存呢,如果你查看的内存使用量,发现也是不低的
这是因为MySQL也有自己的数据页内存,和操作系统类似,操作系统对磁盘有page cache
,mysql
也有 buffer pool
另外,MySQL作为一个复杂的数据库系统,在磁盘IO上做了大量的缓存设计,比如写 binlog
有 binlog cache
,写 redo log
有 redo log cache
同时 MySQL 也利用了操作系统的高速页缓存(page cache)来提高读写性能,比如组提交机制
MySQL 涉及到的缓存很多,这里不细讲了,极客上面的课有对上面的内容作解释,MySQL 官方文档也都有对应的描述
总结
- 一个系统中的不同层之间的访问速度不一样,所以我们才需要缓存
- 缓存会带来数据一致性问题,可见性问题
- 缓存往往容量是小于被缓存数据的,所以任何缓存系统都要关注缓存命中率和数据淘汰的问题
这里就不展开说如何解决缓存带来的问题了,每个系统都有自己的解决方案
简单聊聊,最近的一些收获,欢迎指正讨论