《PostgreSQL技术内幕——原理探索》第五章 并发控制

0    78    1

Tags:

👉 本文共约27827个字,系统预计阅读时间或需105分钟。

当多个事务同时在数据库中运行时,并发控制是一种用于维持一致性隔离性的技术,一致性与隔离性是ACID的两个属性。

从宽泛的意义上来讲,有三种并发控制技术:多版本并发控制(Multi-version Concurrency Control, MVCC)严格两阶段锁定(Strict Two-Phase Locking, S2PL)乐观并发控制(Optimistic Concurrency Control, OCC),每种技术都有多种变体。在MVCC中,每个写操作都会创建一个新版本的数据项,并保留其旧版本。当事务读取数据对象时,系统会选择其中的一个版本,通过这种方式来确保各个事务间相互隔离。 MVCC的主要优势在于“读不会阻塞写,而写也不会阻塞读”,相反的例子是,基于S2PL的系统在写操作发生时会阻塞相应对象上的读操作,因为写入者获取了对象上的排他锁。 PostgreSQL和一些RDBMS使用一种MVCC的变体,名曰快照隔离(Snapshot Isolation,SI)

一些RDBMS(例如Oracle)使用回滚段来实现快照隔离SI。当写入新数据对象时,旧版本对象先被写入回滚段,随后用新对象覆写至数据区域。 PostgreSQL使用更简单的方法:新数据对象被直接插入到相关表页中。读取对象时,PostgreSQL根据可见性检查规则(visibility check rules),为每个事务选择合适的对象版本作为响应。

SI中不会出现在ANSI SQL-92标准中定义的三种异常:脏读,不可重复读和幻读。但SI无法实现真正的可串行化,因为在SI中可能会出现串行化异常:例如写偏差(write skew)只读事务偏差(Read-only Transaction Skew)。需要注意的是:ANSI SQL-92标准中可串行化的定义与现代理论中的定义并不相同。为了解决这个问题,PostgreSQL从9.1版本之后添加了可串行化快照隔离(SSI,Serializable Snapshot Isolation),SSI可以检测串行化异常,并解决这种异常导致的冲突。因此,9.1版本之后的PostgreSQL提供了真正的SERIALIZABLE隔离等级(此外SQL Server也使用SSI,而Oracle仍然使用SI)。

本章包括以下四个部分:

  • 第1部分:第5.1~5.3节。

    这一部分介绍了理解后续部分所需的基本信息。

    第5.1和5.2节分别描述了事务标识和元组结构。第5.3节展示了如何插入,删除和更新元组。

  • 第2部分:第5.4~5.6节。

    这一部分说明了实现并发控制机制所需的关键功能。

    第5.4,5.5和5.6节描述了提交日志(clog),分别介绍了事务状态,事务快照和可见性检查规则。

  • 第3部分:第5.7~5.9节。

    这一部分使用具体的例子来介绍PostgreSQL中的并发控制。

    这一部分说明了如何防止ANSI SQL标准中定义的三种异常。第5.7节描述了可见性检查,第5.8节介绍了如何防止丢失更新,第5.9节简要描述了SSI。

  • 第4部分:第5.10节。

    这一部分描述了并发控制机制持久运行所需的几个维护过程。维护过程主要通过清理过程(vacuum processing)进行,清理过程将在第6章详细阐述。

并发控制包含着很多主题,本章重点介绍PostgreSQL独有的内容。故这里省略了锁模式与死锁处理的内容(相关信息请参阅官方文档)。

PostgreSQL中的事务隔离等级

PostgreSQL实现的事务隔离等级如下表所示:

隔离等级脏读不可重复读幻读串行化异常
读已提交不可能可能可能可能
可重复读[1]不可能不可能PG中不可能,见5.7.2小节 但ANSI SQL中可能可能
可串行化不可能不可能不可能不可能

[1]:在9.0及更早版本中,该级别被当做SERIALIZABLE,因为它不会出现ANSI SQL-92标准中定义的三种异常。 但9.1版中SSI的实现引入了真正的SERIALIZABLE级别,该级别已被改称为REPEATABLE READ

PostgreSQL对DML(SELECT, UPDATE, INSERT, DELETE等命令)使用SSI,对DDL(CREATE TABLE等命令)使用2PL。

5.1 事务标识

每当事务开始时,事务管理器就会为其分配一个称为事务标识(transaction id, txid)的唯一标识符。 PostgreSQL的txid是一个32位无符号整数,总取值约42亿。在事务启动后执行内置的txid_current()函数,即可获取当前事务的txid,如下所示。

PostgreSQL保留以下三个特殊txid

  • 0表示无效(Invalid)txid
  • 1表示初始启动(Bootstrap)txid,仅用于数据库集群的初始化过程。
  • 2表示冻结(Frozen)txid,详情参考第5.10.1节。

txid可以相互比较大小。例如对于txid=100的事务,大于100的txid属于“未来”,且对于txid=100的事务而言都是不可见(invisible)的;小于100的txid属于“过去”,且对该事务可见,如图5.1(a)所示。

图5.1 PostgreSQL中的事务标识

《PostgreSQL技术内幕——原理探索》第五章 并发控制

因为txid在逻辑上是无限的,而实际系统中的txid空间不足(4字节取值空间约42亿),因此PostgreSQL将txid空间视为一个环。对于某个特定的txid,其前约21亿个txid属于过去,而其后约21亿个txid属于未来。如图5.1(b)所示。

所谓的txid回卷问题将在5.10.1节中介绍。

请注意,txid并非是在BEGIN命令执行时分配的。在PostgreSQL中,当执行BEGIN命令后的第一条命令时,事务管理器才会分配txid,并真正启动其事务。

5.2 元组结构

可以将表页中的堆元组分为两类:普通数据元组与TOAST元组。本节只会介绍普通元组。

堆元组由三个部分组成,即HeapTupleHeaderData结构,空值位图,以及用户数据,如图5.2所示。

图5.2 元组结构

《PostgreSQL技术内幕——原理探索》第五章 并发控制

HeapTupleHeaderData结构在src/include/access/htup_details.h中定义。

虽然HeapTupleHeaderData结构包含七个字段,但后续部分中只需要了解四个字段即可。

  • t_xmin保存插入此元组的事务的txid
  • t_xmax保存删除或更新此元组的事务的txid。如果尚未删除或更新此元组,则t_xmax设置为0,即无效。
  • t_cid保存命令标识(command id, cid)cid意思是在当前事务中,执行当前命令之前执行了多少SQL命令,从零开始计数。例如,假设我们在单个事务中执行了三条INSERT命令BEGIN;INSERT;INSERT;INSERT;COMMIT;。如果第一条命令插入此元组,则该元组的t_cid会被设置为0。如果第二条命令插入此元组,则其t_cid会被设置为1,依此类推。
  • t_ctid保存着指向自身或新元组的元组标识符(tid)。如第1.3节中所述,tid用于标识表中的元组。在更新该元组时,其t_ctid会指向新版本的元组;否则t_ctid会指向自己。

5.3 元组的增删改

本节会介绍元组的增删改过程,并简要描述用于插入与更新元组的自由空间映射(Free Space Map, FSM)

这里主要关注元组,页首部与行指针不会在这里画出来,元组的具体表示如图5.3所示。

图5.3 元组的表示

《PostgreSQL技术内幕——原理探索》第五章 并发控制

5.3.1 插入

在插入操作中,新元组将直接插入到目标表的页面中,如图5.4所示。

图5.4 插入元组

《PostgreSQL技术内幕——原理探索》第五章 并发控制

假设元组是由txid=99的事务插入页面中的,在这种情况下,被插入元组的首部字段会依以下步骤设置。

Tuple_1

  • t_xmin设置为99,因为此元组由txid=99的事务所插入。
  • t_xmax设置为0,因为此元组尚未被删除或更新。
  • t_cid设置为0,因为此元组是由txid=99的事务所执行的第一条命令所插入的。
  • t_ctid设置为(0,1),指向自身,因为这是该元组的最新版本。

pageinspect

PostgreSQL自带了一个第三方贡献的扩展模块pageinspect,可用于检查数据库页面的具体内容。

5.3.2 删除

在删除操作中,目标元组只是在逻辑上被标记为删除。目标元组的t_xmax字段将被设置为执行DELETE命令事务的txid。如图5.5所示。

图5.5 删除元组

《PostgreSQL技术内幕——原理探索》第五章 并发控制 假设Tuple_1txid=111的事务删除。在这种情况下,Tuple_1的首部字段会依以下步骤设置。

Tuple_1

  • t_xmax被设为111。

如果txid=111的事务已经提交,那么Tuple_1就不是必需的了。通常不需要的元组在PostgreSQL中被称为死元组(dead tuple)

死元组最终将从页面中被移除。清除死元组的过程被称为清理(VACUUM)过程第6章将介绍清理过程。

5.3.3 更新

在更新操作中,PostgreSQL在逻辑上实际执行的是删除最新的元组,并插入一条新的元组(图5.6)。

图5.6 两次更新同一行

《PostgreSQL技术内幕——原理探索》第五章 并发控制

假设由txid=99的事务插入的行,被txid=100的事务更新两次。

当执行第一条UPDATE命令时,Tuple_1t_xmax被设为txid 100,在逻辑上被删除;然后Tuple_2被插入;接下来重写Tuple_1t_ctid以指向Tuple_2Tuple_1Tuple_2的头部字段设置如下。

Tuple_1

  • t_xmax被设置为100。
  • t_ctid(0,1)被改写为(0,2)

Tuple_2

  • t_xmin被设置为100。
  • t_xmax被设置为0。
  • t_cid被设置为0。
  • t_ctid被设置为(0,2)

当执行第二条UPDATE命令时,和第一条UPDATE命令类似,Tuple_2被逻辑删除,Tuple_3被插入。Tuple_2Tuple_3的首部字段设置如下。

Tuple_2

  • t_xmax被设置为100。
  • t_ctid(0,2)被改写为(0,3)

Tuple_3

  • t_xmin被设置为100。
  • t_xmax被设置为0。
  • t_cid被设置为1。
  • t_ctid被设置为(0,3)

与删除操作类似,如果txid=100的事务已经提交,那么Tuple_1Tuple_2就成为了死元组,而如果txid=100的事务中止,Tuple_2Tuple_3就成了死元组。

5.3.4 空闲空间映射

插入堆或索引元组时,PostgreSQL使用表与索引相应的FSM来选择可供插入的页面。

如1.2.3节所述,表和索引都有各自的FSM。每个FSM存储着相应表或索引文件中每个页面可用空间容量的信息。

本人提供Oracle(OCP、OCM)、MySQL(OCP)、PostgreSQL(PGCA、PGCE、PGCM)等数据库的培训和考证业务,私聊QQ646634621或微信db_bao,谢谢!
《PostgreSQL技术内幕——原理探索》第五章 并发控制后续精彩内容已被小麦苗无情隐藏,请输入验证码解锁本站所有文章!
验证码:
请先关注本站微信公众号,然后回复“验证码”,获取验证码。在微信里搜索“DB宝”或者“www_xmmup_com”或者微信扫描右侧二维码都可以关注本站微信公众号。

标签:

Avatar photo

小麦苗

学习或考证,均可联系麦老师,请加微信db_bao或QQ646634621

您可能还喜欢...

发表回复

嘿,我是小麦,需要帮助随时找我哦。
  • 18509239930
  • 个人微信

  • DB宝
  • 个人邮箱
  • 点击加入QQ群
  • 个人微店

  • 回到顶部