如果再有人问你数据库的原理,把这篇文章给他(14)


为了监控预读的工作状况,现代数据库引入了一个度量叫缓冲/缓存命中率,用来显示请求的数据在缓存中找到而不是从磁盘读取的频率 。
注:糟糕的缓存命中率不总是意味着缓存工作状态不佳 。更多信息请阅读Oracle文档 。
缓冲只是容量有限的内存空间,因此,为了加载新的数据,它需要移除一些数据 。加载和清除缓存需要一些磁盘和网络I/O的成本 。如果你有个经常执行的查询,那么每次都把查询结果加载然后清除,效率就太低了 。现代数据库用缓冲区置换策略来解决这个问题 。
缓冲区置换策略
多数现代数据库(至少 SQL Server, MySQL, Oracle 和 DB2)使用 LRU 算法 。
LRU
LRU代表最近最少使用(Least Recently Used)算法,背后的原理是:在缓存里保留的数据是最近使用的,所以更有可能再次使用 。
图解:

如果再有人问你数据库的原理,把这篇文章给他

文章插图
 
为了更好的理解,我假设缓冲区里的数据没有被闩锁锁住(就是说是可以被移除的) 。在这个简单的例子里,缓冲区可以保存 3 个元素:
  • 1:缓存管理器(简称CM)使用数据1,把它放入空的缓冲区
  • 2:CM使用数据4,把它放入半载的缓冲区
  • 3:CM使用数据3,把它放入半载的缓冲区
  • 4:CM使用数据9,缓冲区满了,所以数据1被清除,因为它是最后一个最近使用的,数据9加入到缓冲区
  • 5:CM使用数据4,数据4已经在缓冲区了,所以它再次成为第一个最近使用的 。
  • 6:CM使用数据1,缓冲区满了,所以数据9被清除,因为它是最后一个最近使用的,数据1加入到缓冲区
  • ……
这个算法效果很好,但是有些限制 。如果对一个大表执行全表扫描怎么办?换句话说,当表/索引的大小超出缓冲区会发生什么?使用这个算法会清除之前缓存内所有的数据,而且全扫描的数据很可能只使用一次 。
改进
为了防止这个现象,有些数据库增加了特殊的规则,比如Oracle文档中的描述:
『对非常大的表来说,数据库通常使用直接路径来读取,即直接加载区块[……],来避免填满缓冲区 。对于中等大小的表,数据库可以使用直接读取或缓存读取 。如果选择缓存读取,数据库把区块置于LRU的尾部,防止清空当前缓冲区 。』
还有一些可能,比如使用高级版本的LRU,叫做 LRU-K 。例如,SQL Server 使用 LRU-2 。
这个算法的原理是把更多的历史记录考虑进来 。简单LRU(也就是 LRU-1),只考虑最后一次使用的数据 。LRU-K呢:
  • 考虑数据最后第K次使用的情况
  • 数据使用的次数加进了权重
  • 一批新数据加载进入缓存,旧的但是经常使用的数据不会被清除(因为权重更高)
  • 但是这个算法不会保留缓存中不再使用的数据
  • 所以数据如果不再使用,权重值随着时间推移而降低
计算权重是需要成本的,所以SQL Server只是使用 K=2,这个值性能不错而且额外开销可以接受 。
关于LRU-K更深入的知识,可以阅读早期的研究论文(1993):数据库磁盘缓冲的LRU-K页面置换算法
其他算法
当然还有其他管理缓存的算法,比如:
  • 2Q(类LRU-K算法)
  • CLOCK(类LRU-K算法)
  • MRU(最新使用的算法,用LRU同样的逻辑但不同的规则)
  • LRFU(Least Recently and Frequently Used,最近最少使用最近最不常用)
  • ……
写缓冲区
我只探讨了读缓存 —— 在使用之前预先加载数据 。用来保存数据、成批刷入磁盘,而不是逐条写入数据从而造成很多单次磁盘访问 。
要记住,缓冲区保存的是页(最小的数据单位)而不是行(逻辑上/人类习惯的观察数据的方式) 。缓冲池内的页如果被修改了但还没有写入磁盘,就是脏页 。有很多算法来决定写入脏页的最佳时机,但这个问题与事务的概念高度关联,下面我们就谈谈事务 。
事务管理器最后但同样重要的,是事务管理器,我们将看到这个进程是如何保证每个查询在自己的事务内执行的 。但开始之前,我们需要理解ACID事务的概念 。
“I’m on acid”
一个ACID事务是一个工作单元,它要保证4个属性: