我记得曾经发现 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实现,可能存在大量错误信息。以下是需要翻译的内容: