数据库如何执行原子I/O操作?

20

像Oracle、SQL Server等数据库非常擅长数据完整性。如果我想编写一个数据存储,我知道它要么存储一些数据,要么失败(即遵循ACID原则),那么我会在其下使用类似MySQL这样的数据库作为实际存储,因为这些问题已经得到了解决。

然而,作为一名非计算机科学专业毕业生,我想知道ACID在非常低层次上是如何工作的。例如,我知道Oracle会将数据不断地写入“在线重做日志”,然后在应用程序发出事务提交信号时执行“提交”操作。

我想深入了解这个“提交”阶段。这是仅仅向磁盘写入“一个字节”,还是将0变成1以表示给定行已成功存储?


9
我尊重地不同意。如果我只是“管理数据库”,那没问题,但如果我要“编写”一个数据库,那就是一个编程问题,需要有软件工程或计算机科学学位的人来回答。 - Neil Barnwell
好的。这个问题似乎是关于当前实现的(即Oracle如何做到这一点),因此我发表了评论。 - Oded
你可以查看PostgreSQL源代码,了解他们是如何做到的。显然,这段代码非常干净,相对容易理解。 - user330315
是的,我其实在考虑看一下 RavenDB。 - Neil Barnwell
你可能想阅读《SQLite中的原子提交》(http://sqlite.org/atomiccommit.html),该文解释了SQLite如何实现原子提交。 - John Bartholomew
显示剩余4条评论
3个回答

13
我记得曾经发现 BerkeleyDB 文档非常有用,可以了解这些实现的工作原理,因为它是一个实现事务但没有整个关系/查询规划基础设施的相当低级别的数据库。并不是所有的数据库(即使只是你提到的那些)都以相同的方式工作。PostgreSQL 的底层实现与 Oracle 和 SQL Server 的快照实现非常不同,尽管它们都基于相同的方法(MVCC:多版本并发控制)。实现 ACID 特性的一种方法是将你所做的所有对数据库的更改写入“事务日志”,并锁定每行(原子单元),以确保没有其他事务可以在你提交或回滚之前对其进行变异。在事务结束时,如果提交,你只需在日志中写入一条记录,说明你已经提交并释放锁定。如果回滚,你需要返回事务日志,撤消所有更改-- 因此写入日志文件的每个更改都包含数据最初的“before image”。(实际上它也会包含一个“after image”,因为事务日志也会被重放以进行崩溃恢复)。通过锁定每行正在更改的并发事务,直到你结束事务后释放锁定,其他并发事务就无法看到你的更改。
MVCC是一种方法,通过该方法,想要读取行而不被您更新阻塞的并发事务可以访问“之前的图像”。每个事务都有一个标识,并且有一种确定它可以“看到”哪些事务数据以及哪些不能的方法:使用不同的规则来生成此集合以实现不同的隔离级别。因此,要获得“可重复读”语义,事务必须找到由在其之后启动的事务更新的任何行的“之前的图像”。您可以天真地通过使事务回溯事务日志以获取之前的图像来实现此操作,但实际上它们存储在其他地方:因此,Oracle具有单独的重做和撤消空间-重做是事务日志,撤消是供并发事务使用的块的之前的图像;SQL Server将之前的图像存储在tempdb中。相比之下,PostgreSQL在更新行时总是创建一个新的副本,因此之前的图像存在于数据块本身中:这具有一些优点(提交和回滚都是非常简单的操作,没有额外的空间管理)和权衡(那些过时的行版本必须在后台中清除)。
在PostgreSQL中(这是我最熟悉的数据库内部),每个磁盘上的行版本都有一些额外属性,事务必须检查这些属性才能决定是否对它们可见。为了简单起见,考虑它们具有“xmin”和“xmax”——“xmin”指定创建行版本的事务ID,“xmax”指定删除它的(可选)事务ID(可能包括创建新的行版本以表示对行的更新)。所以你从由txn#20创建的行开始:
xmin xmax id value
20   -    1  FOO

然后txn#25执行update t set value = 'BAR' where id = 1

20   25   1  FOO
25   -    1  BAR

直到txn#25完成,新的事务将知道将其更改视为不可见。因此,扫描此表的事务将采用“FOO”版本,因为它的xmax是一个不可见事务。
如果txn#25被回滚,新的事务不会立即跳过它,而是会考虑txn#25是否提交或回滚。(PostgreSQL管理一个“提交状态”查找表来服务此操作,pg_clog)由于txn#25已回滚,其更改不可见,因此再次采用“FOO”版本。(并且跳过了“BAR”版本,因为它的xmin事务是不可见的)
如果txn#25被提交,则现在不会采用“FOO”行版本,因为它的xmax事务是可见的(也就是说,该事务所做的更改现在是可见的)。相比之下,“BAR”行版本会被采用,因为它的xmin事务是可见的(并且它没有xmax)。
当txn#25仍在进行中时(这可以从pg_clog 中读取),任何想要更新该行的其他事务都将等待txn#25完成,通过尝试获取 transaction ID的共享锁。我强调了这一点,这就是为什么PostgreSQL通常没有“行锁”而只有事务锁的原因:每个更改的行都没有内存中的锁。(使用 select ... for update 进行锁定是通过设置xmax和指示xmax仅表示锁定而不是删除的标志来完成的)。
Oracle...做了类似的事情,但我对细节的了解要模糊得多。在Oracle中,每个事务都会发出一个系统更改号码,并记录在每个块的顶部。当一个块发生变化时,它的原始内容将放入撤消空间中,并且新块会指向旧块:因此,您实际上拥有块N的版本的链接列表-数据文件中的最新版本,逐渐变老的版本在撤消表空间中。在块的顶部是“感兴趣的事务”的列表,它以某种方式实现锁定(再次没有为每个更改的行设置内存中的锁),除此之外我记不起来更多的细节了。
SQL Server的快照隔离机制与Oracle的类似,使用tempdb存储正在更改的块而不是专用文件。希望这个冗长的答案有用。由于非PostgreSQL实现,可能存在大量错误信息。以下是需要翻译的内容:

实际上,我认为MS SQL Server、PostgreSQL和Firebird/Interbase在实现MVCC方面是近亲。这种基本技术是由Jim Starkey开创的,最终发布为"Interbase",并且基本上是面向行的。Oracle使用不同的(更"面向页面")算法,据我所知,这是受专利保护的,因此使竞争对手无法进入。 - Branko Dimitrijevic

2
Oracle的高级概述:
每个Oracle会话都是独一无二的,每个会话可以具有1个活动事务。当事务开始时,Oracle为其分配一个单调递增的系统更改号(SCN)。随着Oracle更新/插入/删除行,Oracle通过更新正在写入的块中的标头以及将“原始”块保存到Oracle的回滚(撤消)空间来锁定感兴趣的表和支持索引的行。 Oracle还将重做日志条目写入内存缓冲区,描述对表和索引块以及撤消块进行的更改。需要注意的是,正在进行的更改是在内存中进行的,而不是直接在磁盘上进行。
提交时,在将控制权返回给客户端之前,Oracle确保整个日志缓冲区直到包括事务的SCN在内的部分已写入磁盘。
在回滚时,Oracle使用回退(撤消)中的信息来消除所做的更改。
那么,这如何实现ACID:
原子性:我的会话、我的事务,全部执行或全部不执行。当我提交时,在提交完成之前我不能做任何事情。
一致性:Oracle检查日期是否为日期,字符数据是否为字符数据,数字是否有效。检查约束条件也是同样的方式。外键约束依赖于检查以确保被引用的父键是有效的,并且未被正在进行的事务更新或删除。如果父键已更新或删除,则您的语句将挂起——实际上处于不稳定状态——等待影响父记录的语句提交或回滚。
独立性:还记得那些系统更改号吗?如果你没有进行更改,Oracle就知道在你开始运行语句或声明游标时SCN是什么。因此,如果您有一个长时间运行的语句,在数据在您下面更改的情况下,Oracle会检查获取在您的语句开始运行时提交的数据。这是多版本一致性控制,而且非常复杂。 Oracle并不实现各种SQL标准所要求的所有隔离级别——例如,Oracle从不允许脏读或幻读。
耐久性:刷新到磁盘的重做日志缓冲区是耐久性的根源。当重做日志文件被填满时,Oracle会强制执行一个检查点。此过程会导致Oracle将所有修改的表和索引块从内存中写入磁盘,无论它们是否已提交。如果实例崩溃且数据文件中包含未提交的更改,则Oracle使用重做日志来回滚这些更改,因为撤消信息也包含在重做日志中。
*暂时忽略自主事务,因为它们会带来严重的复杂性。

在提交时,Oracle 确保整个日志缓冲区直到事务的 SCN 已写入磁盘...它是如何做到的?这是我问题的核心 - 它是否开始将日志缓冲区和 SCN 的内容写入磁盘,然后执行最后一件小事,例如将磁盘上的位从 0 设置为 1,以表示一切正常? - Neil Barnwell
1
它使用fwritefflush调用(我相信Oracle内核仍然是C语言),并依赖于从这些调用返回的状态来知道它是否起作用。在某个时候,Oracle必须相信操作系统已经按照其所说的做了。 - Adam Musch

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接