这步中,查询管理器正在执行查询并需要从表和索引中获取数据。它这会要求数据管理器给它数据,但这有两个问题:
在这部分,我们会看到关系数据库是如何解决这两个问题。我不会谈及数据管理器获取数据的方式,因为这不太重要(这篇文章已经够长了)
就像我之前说的,数据库的主要瓶颈是磁盘 I/O。为了提高性能,现代数据库使用缓存管理器。
查询管理器不会直接从系统中拿数据,而是去缓存管理器请求数据。缓存管理器有个叫缓冲池(buffer pool)的内存缓存。从内存中获取数据会大大加快数据库的速度 。但这很难给出一个具体的数量级,因为这取决于你需要的是哪种操作:
数据库用的是什么磁盘
但我还是要说内存比磁盘快100到100k倍。 这又导致另一个问题的出现(数据库总是这样。。。),缓存管理器需要在查询执行器使用数之前,从内存中获取数据;所以查询管理器需要等待数据从慢磁盘中获取
这个问题叫预读取。查询管理器知道将会需要数据了,因为它知道查询的完整流程和磁盘上的数据的统计信息。构思如下:
缓存管理器会在缓冲区中存储所有数据。为了知道数据是否仍然需要,缓存管理器会为数据添加了一个缓存日期(叫闩latch) 有时查询执行器不知道需要什么数据,有时候数据库也不提供功能。相反他们会用推测预读(例如:如果查询执行器要数据 1,3,5,它可能在不久的将来会要数据 7,9,11) 又或者一个顺序的预读取(在这种情况下,缓存管理器在一次请求后,简单地加载下一个连续的数据)
注意:缓存命中率不高并不总是意味着缓存不正常。有关更多信息,请阅读 Oracle文档
但,缓冲是的内存是有限的。因此,它需要将一些数据移走并加载新的数据。加载和清理缓存需要一点磁盘和网络的I/O 成本。如果你有一个查询要经常执行,使用这查询的时候总是要加载数据清理数据,这也未免太没效率的。为了解决这个问题,现代数据库使用一种缓冲区替换策略
LRU是指(Least Recently Used)最近用得最少的。这个算法背后的构想是在缓存中保留最近使用,这些数据更有可能会再次使用 下面是个直观的例子
为了便于理解,我会设计这些在缓冲区的数据没有被闩(latch)锁住(所以能被移除)。在这个简单的例子中,这个缓冲区可以存储3个元素
1) 缓冲管理器用了数据1,然后把数据放到一个空的缓冲区
2) 缓冲管理器用了数据4,然后把数据放半满载缓冲区
3) 缓冲管理器用了数据3,然后把数据放到半满载缓冲
4) 缓冲管理器用了数据9,缓冲区已满,* 于是将数据1移除,因为它是最早使用的数据* 。然后把数据9加入到缓冲区 5) 缓冲管理器用了数据4,而数据4之前已经在缓冲区存在了,所有数据3成了缓冲区最早使用的数据
6) 缓冲管理器用了数据1,缓冲区已满,于是数据3被清除因为它是最早使用的数据,数据1 被添加到缓冲区中。 这算法能很好地工作,但也存在一些局限性。如果在一个大表中进行全局搜索呢?换句话说,如果表/索引的大小比缓冲区还大会发生什么事呢?使用这算法会把之前在缓冲中的值全部移走,但是全局扫描可能只会使用一次
为了防止上述的情况,某些数据库会添加特定的规则。如果根据Oracle 文档所言
对于很大的表,数据库会直接用路径读,这直接加载块... 以避免填满缓冲区缓存。对于中等大小的表,数据库可以使用直接读取或者是读缓存。如果它决定要读缓存,数据库会把这块放到 LRU列表的最后,来防止扫描有效地清除缓存区缓存
也有很多其他的办法像是一个LRU的高级版本叫 LRU-K。像 SQL Server 就用了 LRU-k 而 K = 2 这算法背后的思想是要考虑更多的历史。使用简单的 LRU(K=1时的LRU-K),算法只用考虑上次使用数据的时间。而LRU-K:
而计算权重的成本是很大的,这就是 SQL Server 只用到 K = 2。这个值在可接受的成本范围内性能不错。 关于 LRU-K 的更深入的学习,你可以阅读最原始的研究论文(1993):《用于数据库磁盘缓冲与的LRU-K页替换算法》
当然啦,还有很多其他的用于管理缓存的算法,像是:
有些数据库可能允许使用其他的算法而不是默认算法
我只讲过在去缓冲区要在使用前先加载。但在数据库中,有写缓冲区的操作,这用来存储数据,把数据串联起来刷新磁盘数据。而不是逐个逐个地写数据,产生很多的单次磁盘访问。
请记住,buffer 存储的是页(page,数据的最小单元)而不是 row(逻辑上/人性化观察数据的)。一个页在缓冲池被修改但没有写入到磁盘是肮脏的。有很多算法能决定脏页写入磁盘的最佳时间,它和事务概念关系很密切,那是下一部分的内容。
最后,但也很重要,这部分会讲事务管理器。我们将看到进程是如何确保每个查询都在自己的事务中执行。在此之前,我们需要明白事务的
一个ACID事务是一个工作单元,它要保证4个属性:
在同一事务期间,您可以运行多个SQL查询来读取,创建,更新和删除数据。当两个事务使用相同的数据时,开始混乱了。典型的例子是从账户A到账户B的汇款。想象一下,您有2笔事务:
如果我们回到ACID属性:
[如果你愿意,可以跳到下一部分,我要说的对于文章的其余部分并不重要]
许多现代数据库不使用纯隔离作为默认行为,因为它带来了巨大的性能开销。 SQL规范定义了4个级别的隔离:
例如,如果事务A执行 “TABLE_X中的SELECT count(1)”,然后由事务B在TABLE_X中添加并提交新数据,如果事务A再次执行count(1),则该值将不是相同。 这称为幽灵读取(phantom read)
多数数据库添加了自己的自定义的隔离级别(比如 PostgreSQL、Oracle、SQL Server的使用快照隔离),而且并没有实现SQL规范里的所有级别(尤其是读取未提交级别)。
默认的隔离级别可以由用户/开发者在建立连接时覆盖(只需要增加很简单的一行代码)。
确保隔离性,一致性和原子性的真正问题是对相同数据(添加,更新和删除)的写操作:
这种问题叫 并发控制
解决问题的最简单的方式是每个事务逐一运行(按顺序)。但这根本就没有伸缩性的,一个多进程/多核心的服务器上只有一个核,这太没效率了
解决这个问题的方法是,每次创建或取消事务:
更正规地说,这是一个调度冲突的问题。更具体地讲,这是个非常难的且CPU开销大的优化的问题。企业级数据库无法负担等待数小时,为新的事务找寻最佳的调度。因此,他们用不太理想的方法,它会让更多的时间花费在处理事务冲突上。
为了解决这个问题,大部分数据库使用 锁 和/或 数据版本控制。由于这是个大话题,我关注点会在锁的部分,然后我会说一小点数据版本控制
这锁背后的思想是:
这种叫排他锁(exclusive lock) 但对事务只是要读取数据,使用排他锁就很昂贵了。因为它强制让那些只想读一些数据的事务去等待。 这就是为什么会有另外一种锁,共享锁(share lock) 共享锁是这样的:
但是,如果数据在用排它锁,而事务只需要读数据,也不得不等到排他锁结束才能用共享锁锁住数据
锁管理器是提供和释放锁的进程。在内部,它用哈希表(key是被锁的数据)存储了锁,并且知道每个数据
但是使用锁可能导致2个事务永远等待数据的情况:
在这图中:
这叫做 死锁 。 在死锁中,锁管理器选择要取消(回滚)事务来删除死锁,这个决定也不太容易啊
但在做出这个选择之前,需要检查是否存在死锁。 哈希表可以看成是一张图表(像前面的那张图)。如果图中有个循环就会出现死锁。由于检查循环(因为所有锁的图标是相当的大)是成本是很昂贵的,所以一个更简单的方法会被经常使用:使用 时间超时(timeout) 。 如果在给定超时范围内未能锁定,就说明事务进入了死锁状态。 锁管理器也可以在加锁之前检查该锁会不会变成死锁,但要完美做到这点成本也是很昂贵的。因此这些预检经常设置一些基本规则。
确保纯粹的隔离的 最简单方式 是在事务开始的时候加锁,在事务结束的时候释放锁。这意味着事务在开始前不得不等待它的所有锁,然后为事务持有锁,当结束时释放锁。它可以工作的,但是在等待所有锁的时候回浪费很多的时间
一个更快的方法是 两段锁协议(由DB2和SQL Server使用),其中事务分为两个阶段:
这两条简单规则背后的思想是:
这协议能很好地工作,除非是那个事务修改后的数据并释放锁后,事务被取消或者回滚了。你可能遇到一种情况是,一个事务读了另一个事务修改后的值,而这个事务要被回滚的。要避免此问题,必须在事务结束时释放所有独占锁。
当然,真正的数据库会用更复杂的系统,涉及更多类型的锁(如意向锁 intention lock )和更多粒度(行级锁,页级锁,分区锁,表锁,表空间锁)但是这个道理都是一样的。 我只探讨纯粹基于锁的方法,数据版本控制是解决这个问题的另一个方法。 版本控制背后的思想是:
它提高了性能,因为:
一切都比锁更好,除了两个事务写入相同的数据(因为总有一个被回滚)。只是,你的磁盘空间会被快速增大。
数据版本控制和锁定是两种不同的简介:乐观锁定与悲观锁定。他们都有利有弊;它实际上取决于应用场景(更多读取与更多写入)。有关数据版本控制的演示文稿,我推荐这篇关于PostgreSQL如何实现多版本并发控制,是非常好的演示文稿。
某些数据库(如DB2(直到DB2 9.7)和SQL Server(快照隔离除外))仅使用锁。其他像PostgreSQL,MySQL和Oracle使用涉及锁和数据版本控制的混合方法。我不知道只使用数据版本控制的数据库(如果您知道基于纯数据版本的数据库,请随时告诉我)。
[2015年8月20日更新]读者告诉我: Firebird和Interbase使用没有锁的版本控制。 版本控制对索引有一个有趣的影响:有时一个唯一索引包含重复项,索引可以有比表有行更多的条目,等等。
如果你在不同的隔离级别上读过那部分,你会发现增加隔离级别时,会增加锁的数量,从而增加事务等待锁定所浪费的时间。这就是大多数数据库默认情况下不使用最高隔离级别(Serializable)的原因。
与往常一样,您可以自己检查主数据库的文档(例如 MySQL,PostgreSQL或Oracle)。
我们已经看到,为了提高性能,数据库将数据存储在内存缓冲区中。但是如果服务器在提交事务时崩溃,那么在崩溃期间你将丢失在内存中的数据,这会破坏事务的持久性。
你可以在磁盘上写入所有内容,但如果服务器崩溃,你最终会将数据可能只有部分写入磁盘,这会破坏事务的原子性。
任何事务的修改都只有撤销和已完成两个状态 要解决这个问题,
有两种方法:
在涉及许多事务的大型数据库上使用时,影子副本/页面会产生巨大的磁盘开销。 这就是现代数据库使用事务日志的原因。事务日志必须存储在稳定的存储中。我不会深入研究存储技术,但必须使用(至少)RAID磁盘来防止磁盘故障。
大多数数据库(至少Oracle,SQL Server,[DB2,PostgreSQL,MySQL和 SQLite)使用Write-Ahead Logging协议(WAL)处理事务日志。
WAL协议是一组3条规则:
1)数据库的每次修改都会生成一条日志记录,并且 必须在将数据写入磁盘之前将日志记录写入事务日志。
2)日志记录必须按顺序写入;日志记录A在日志记录B之前发生就必须在B之前写入
3)提交事务时,必须在事务成功结束之前,在事务日志中写入提交顺序。
这个工作由日志管理器完成。一种简单的方法是在缓存管理器和数据访问管理器(在磁盘上写入数据)之间,日志管理器在将事务日志写入磁盘之前将每个更新/删除/创建/提交/回滚写入事务日志。容易,对吗? 错误的答案!都讲了这么多了,你应该知道与数据库相关的所有内容都受到“数据库效应”的诅咒。认真地是,问题是找到一种在保持良好性能的同时编写日志的方法。如果事务日志上的写入速度太慢,则会降低所有内容的速度。
1992年,IBM研究人员“发明了”一种名为ARIES的WAL增强版。ARIES或多或少地被大多数现代数据库使用。逻辑可能不一样,但ARIES背后的理念随处可见。我给发明加了引号是因为,是因为根据麻省理工学院的这门课程,IBM的研究人员“只不过是编写事务恢复的良好实践”。自从我5岁时ARIES论文发表以来,我并不关心来自辛酸研究者的这个古老八卦。事实上,在我们开始这个最后的技术部分之前,我只是把这些信息给你一个休息时间。 我已经阅读了关于ARIES的大量研究论文,我发现它非常有趣!在这部分中,我将仅向你概述ARIES,但如果你需要真正的知识,我强烈建议您阅读本文。 ARIES 表示的是恢复和利用语义隔离算法(Algorithms for Recovery and Isolation Exploiting Semantics)。 这项技术的目的是有两个的:
1) 写日志时有良好的性能
2) 有快速可靠的恢复 数据库必须回滚事务有多种原因:
有时候(比如网络出现故障),数据库可以恢复事务。 怎么可能?要回答这个问题,我们需要了解日志记录中的存储的信息。
事务期间的每个 *操作(添加/删除/修改)都会生成一个日志* 。该日志记录包括:
比如,如果操作是更新,UNDO将会回到元素更新前的值或状态(物理UNDO),或者回到原来状态的反向状态(逻辑UNDO)
此外,磁盘上的每个页面(存储数据,而不是日志)具有修改数据的最后一个操作的日志记录(LSN)的id。
给出LSN的方式更复杂,因为它与日志的存储方式有关。但这个背后的思想仍然是一样的。 ARIES仅使用逻辑UNDO,因为处理物理UNDO真是一团糟。
注意:据我所知,只有PostgreSQL没有使用UNDO。它使用垃圾收集器守护程序来删除旧版本的数据。这与PostgreSQL中数据版本控制的实现有关。
为了更好地说明这点,这里是查询“UPDATE FROM PERSON SET AGE = 18;”生成的日志记录的可视化和简化示例。假设此查询在事务18中执行。
每个日志都有一个唯一的LSN。连接的日志属于同一事务。日志按时间顺序链接(链接列表的最后一个日志是最后一个操作的日志)。
为避免日志写入成为主要瓶颈,使用 日志缓冲区 。
当查询执行程序要求修改时:
1) 缓存管理器将修改存储其缓冲区中
2) 日志管理器将关联的日志存储在其缓冲区中
3) 到了这一步,查询执行器认为操作完成了(因此可以请求做另一次修改);
4)然后(稍后)日志管理器将日志写入事务日志。何时写日志的决定是由算法完成的。
5)然后(稍后)缓存管理器将修改写入磁盘。何时在磁盘上写入数据是由算法完成的。 当事务被提交,这意味着对于事务中的每个操作,步骤1,2,3,4,5也做完了。 在事务日志中写入很快,因为它只是“在事务日志中的某处添加日志”,而在磁盘上写入数据则更复杂,因为它要 “以能快速读取数据的方式写入数据”。
出于性能原因,步骤5可能在提交之后完成 ,因为在崩溃的情况下,仍然可以使用REDO日志恢复事务。这称为NO-FORCE政策 。 数据库可以选择FORCE策略(即必须在提交之前完成步骤5)以降低恢复期间的工作负载。 另一个问题是选择是否在磁盘上逐步写入数据(STEAL策略),或者缓冲区管理器是否需要等到提交顺序一次写入所有内容(NO-STEAL)。STEAL和NO-STEAL之间的选择取决于您的需求:使用UNDO日志快速写入长时间恢复或快速恢复? 以下是这些影响恢复策略摘要:
注意:我在多篇研究论文和课程中读到了这个事实,但我没有(明确地)在官方文件中找到它。
好的,我们有很好的日志,让我们使用它们! 假设新实习生让数据库崩溃了(规则1:永远是实习生的错误)。你重启数据库并开始恢复进程 ARIES在3个关卡中让崩溃恢复过来:
1) 分析关卡:恢复进程读全部的事务日志去创建奔溃期间发生的事情的时间线。它会确定哪些事务要回滚(所有事务没有提交的都会回滚)和哪些在奔溃期间的数据需要写入到磁盘
2) redo关卡:这关从分析期间确定一条日志记录开始,并使用 REDO 来将数据库更新到崩溃之前的状态。
在 REDO 阶段,REDO日志按时间顺序处理(使用LSN)。 对于每个日志,恢复过程将读取包含要修改数据的磁盘每页上的 LSN 如果 LSN(磁盘的页)>= LSN(日志记录),则表明数据在奔溃之前已经写入磁盘了(值已经被日志之后、奔溃之前的某个操作覆盖)所以不用做什么 如果LSN(磁盘的页)< LSN(日志记录),那么磁盘上的页将被更新。 即使对于要回滚的事务,重做也会完成,因为它简化了恢复过程(但我确信现代数据库不会这样做)。
3) undo关卡: 此过程将回滚崩溃时未完成的所有事务。回滚从每个事务的最后日志开始,并按照反时间顺序处理UNDO日志(使用日志记录的PrevLSN)。
在恢复期间,事务日志必须留意中恢复过程的操作,以便写入磁盘上的数据与事务日志中写入的数据同步。解决方案可能是移除被 undone的事务日志记录,这是很困难的。相反,ARIES在事务日志中写入补偿日志,逻辑上删除被取消的事务日志记录。 当事务被手动取消,或者被锁管理器取消(为了消除死锁),或仅仅因为网络故障而取消,那么分析阶段就不需要了。实际上,有关 REDO 和 UNDO 的信息在 2 个内存表中:
当新的事务产生时,这两个表由缓存管理器和事务管理器更新。因为它们是在内存中,当数据库崩溃时它们也被破坏掉了。 分析阶段的任务就是在崩溃之后,用事务日志中的信息重建上述的两个表。为了加快分析阶段,ARIES提出了一个概念:检查点(check point),就是不时地把事务表和脏页表的内容,还有此时最后一条LSN写入磁盘。那么在分析阶段当中,只需要分析这个LSN之后的日志即可。
转载于:.html
本文发布于:2024-02-03 23:59:16,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170698053151902.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |