我记得有一次发现 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 实现。