19

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

但是,作为一名非计算机专业的毕业生,我想知道 ACID是如何在非常低的水平上实际工作的例如,我知道 Oracle 一直将数据写入“在线重做日志”,然后在应用程序发出应提交事务的信号时执行“提交”。

我想放大并理解正是这个“提交”阶段。是只是将“多一个字节”写入磁盘,还是将 a 翻转0为 a1以表示给定行已成功存储?

4

3 回答 3

12

我记得有一次发现 BerkeleyDB 文档实际上对于了解这些实现如何工作非常有用,因为那是/曾经是一个非常低级的数据库,它在没有整个关系/查询计划基础架构的情况下实现了事务。

并非所有数据库(即使只是您提到的那些)都以完全相同的方式工作。PostgreSQL 的底层实现与 Oracle 和 SQL Server 的快照实现完全不同,尽管它们都基于相同的方法(MVCC:多版本并发控制)。

实现 ACID 属性的一种方法是将您(“您”这里是一些进行事务更改)对数据库所做的所有更改写入“事务日志”,并锁定每一行(原子性单位)以确保在您提交或回滚之前,没有其他事务可以改变它。在事务结束时,如果提交,您只需在日志中写入一条记录,说明您已提交并释放锁. 如果您回滚,则需要回溯事务日志以撤消所有更改——因此写入日志文件的每个更改都包含数据最初外观的“前映像”。(实际上,它还将包含“后映像”,因为事务日志也会重放以进行崩溃恢复)。通过锁定您正在更改的每一行,并发事务在结束事务后释放锁定之前不会看到您的更改。

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 完成,通过尝试对事务 ID进行共享锁定。我要强调这一点,这就是为什么 PostgreSQL 通常没有这样的“行锁”,只有事务锁:每行更改都没有内存锁。(锁定使用select ... for update是通过设置 xmax 和一个指示 xmax 的标志来完成的,它只是表示锁定而不是删除)

Oracle... 做了一些类似的事情,但我对细节的了解要模糊得多。在 Oracle 中,每个事务都会发出一个系统更改编号,并记录在每个块的顶部。当一个块发生变化时,它的原始内容被放入撤消空间,新块指向旧块:所以你基本上有一个块版本的链表 N - 数据文件中的最新版本,以及逐渐变旧的版本在撤消表空间中。在块的顶部是一个“感兴趣的事务”列表,它以某种方式实现了锁定(再次没有为每一行更改的内存锁),我不记得除此之外的细节。

我相信 SQL Server 的快照隔离机制与 Oracle 的很相似,使用 tempdb 来存储正在更改的块而不是专用文件。

希望这个漫无边际的答案是有用的。这一切都来自内存,因此可能存在大量错误信息,特别是对于非 postgresql 实现。

于 2012-02-09T14:06:04.453 回答
2

Oracle 的高级概述:

每个 Oracle 会话都是唯一的,每个会话可以有 1* 个活动事务。当事务开始时,Oracle 为其分配一个单调递增的系统更改编号 (SCN)。当 Oracle 更新/插入/删除行时,Oracle 通过更新正在写入的块中的标题以及将“原始”块保存到 oracle 的回滚(撤消)空间来锁定表中感兴趣的行和支持索引。Oracle 还将重做日志条目写入内存缓冲区,描述对表和索引块以及撤消块所做的更改。请注意,所做的更改是在内存中进行的,而不是直接在磁盘中进行的。

提交时,Oracle 确保在将事务控制权返回给客户端之前,已将整个日志缓冲区(包括事务的 SCN)写入磁盘。

回滚时,Oracle 使用回滚(撤消)中的信息来消除所做的更改。

那么这是如何实现 ACID 的:

原子性:我的会话,我的交易,要么全部完成,要么都不完成。当我提交时,在提交完成之前我什么都做不了。

一致性:Oracle 检查日期是否为日期,字符数据是否为字符数据,数字是否有效。与检查约束相同。外键约束依赖于检查以确保被引用的父键是有效的——并且没有被正在运行的事务更新或删除。如果父键已被更新或删除,您的语句将挂起 - 它实际上处于不确定状态 - 等待影响父记录的语句提交或回滚。

独立性:还记得那些系统变更号码吗?如果您不进行更改,Oracle 会在您启动语句或声明游标时知道 SCN 是什么。因此,如果您有一个长时间运行的语句,其中数据正在从您下面更改,Oracle 会在您的语句开始运行时检查以获取数据,因为它是提交的。它是多版本的一致性控制,而且相当复杂。Oracle 并未实现各种 SQL 标准所要求的所有隔离级别——例如,Oracle 绝不允许脏读或幻读。

持久性:刷新到磁盘的重做日志缓冲区是持久性的根级别。当重做日志文件已满时,Oracle 会强制执行检查点。此过程导致 Oracle 将所有已修改的表和索引块从内存中写入磁盘,无论它们是否已提交。如果实例崩溃并且数据文件中的数据包含未提交的更改,Oracle 使用重做日志回滚这些更改,因为撤消信息也包含在重做日志中。

*暂时忽略自治事务,因为它们是一个严重的并发症。

于 2012-02-09T17:46:12.633 回答
0

Ayende在 Twitter 上向我建议我研究Munin,这是他用于RavenDBRaven MQ的实际数据存储机制。

于 2012-02-09T15:05:47.873 回答