最近搞了一个公众号PostgreSQL运维技术,欢迎来踩~
悄悄放一张:
PostgreSQL运维技术
原文:https://www.interdb.jp/pg/pgsql5.html
本章主要介绍PostgreSQL中的并发控制。
注:本文是The Internals of PostgreSQL的第5章
(https://www.interdb.jp/pg/pgsql05.html)
并发控制是一种当多个事务在数据库中并发运行时,它可以维护一致性和隔离性的机制。一致性和隔离性我们都知道,这是ACID的两个属性。
一般来说,有三种广泛的并发控制技术,即多版本并发控制(MVCC)、严格两阶段锁定(S2PL)和乐观并发控制(OCC)。
每种技术都有许多变体。在MVCC中,每次写操作都会创建一个数据项的新版本,同时保留旧版本。当事务读取数据项时,系统选择其中一个版本,以确保各个事务的隔离。MVCC的主要优点是“读取器不会阻塞写入器,写入器也不会阻塞读取器”,相反,当写入器写入一个条目时,基于s2pl的系统必须阻塞读取器,因为写入器获得了该条目的排他锁。PostgreSQL和一些rdbms使用MVCC的变体,称为快照隔离(SI)。
为了实现SI,一些rdbms,例如Oracle,使用回滚段。当写入一个新的数据项时,该项的旧版本被写入回滚段,随后新项被覆盖到数据区域。PostgreSQL使用一种更简单的方法。一个新的数据项被直接插入到相关的表页中。当读取条目时,PostgreSQL通过应用可见性检查规则来选择条目的适当版本以响应单个事务。
SI不允许ANSI SQL-92标准中定义的三种异常,即脏读、不可重复读和幻读。然而,SI不能实现真正的可序列化性,因为它允许序列化异常,比如写倾斜和只读事务倾斜。请注意,基于经典的可串行性定义的ANSI SQL-92标准并不等同于现代理论中的定义。为了解决这个问题,在9.1版中添加了可序列化快照隔离(SSI)。SSI可以检测序列化异常,并解决由此类异常引起的冲突。因此,PostgreSQL 9.1及更高版本提供了一个真正可序列化的隔离级别。(另外,SQL Server也使用SSI, Oracle仍然只使用SI。)
本章包含以下几个主题:
5.1~5.3 事务ID和元组结构等基本概念,以及元组是如何插入、删除和更新的。
5.4~5.6 介绍提交日志(clog)
5.7~5.9 检查可见性、如何防止更新丢失,SSI
5.10 VACUUM
本章主要关注PG中的MVCC实现,关于死锁预防和锁定模式的内容按下不表。
PostgreSQL使用SSI实现DML(数据操作语言,如SELECT、UPDATE、INSERT、DELETE),使用2PL实现DDL(数据定义语言,如CREATE TABLE等)。
5.1. 事务ID
每当事务开始时,事务管理器就会分配一个被称为事务id (txid)的唯一标识符。PostgreSQL的txid是一个32位无符号整数,大约是42亿(万亿)。如果在事务开始后执行内置的txid_current()函数,该函数将返回当前的txid,如下所示。
testdb=# BEGIN;BEGIN testdb=# SELECT txid_current(); txid_current -------------- 100(1 row)
0表示无效txid。
1表示引导txid,它只用于数据库集群的初始化。
2表示冻结的txid,如章节5.10.1所述。
Txids可以相互比较。例如,在txid 100的点上,大于100的txid是“在未来”,它们在txid为100中是不可见的;txids小于100是“过去”和可见(图5.1 a))。
图5.1 PostgreSQL中的事务id
图片来源:
https://www.interdb.jp/pg/pgsql05.html
由于txid空间在实际系统中不足,PostgreSQL将txid空间视为一个圆。之前的21亿txids是“在过去”,接下来的21亿txids是“在未来”(图5.1 b)。
注:txid回卷问题在章节5.10.1中描述。
另外:BEGIN命令没有分配txid。在PostgreSQL中,当BEGIN命令执行后第一个命令执行时,事务管理器分配一个tixd,然后它的事务开始。
5.2. 元组结构
表页中的堆元组分为普通数据元组和TOAST元组。本节只介绍常用的元组。
一个堆元组由三部分组成,即HeapTupleHeaderData结构、 NULL bitmap和用户数据(图5.2)。
图5.2 元组结构
图片来源:
https://www.interdb.jp/pg/pgsql05.html
注:HeapTupleHeaderData结构在src/include/access/htup_details.h中定义。
虽然HeapTupleHeaderData结构包含7个字段,但在MVCC只需要以下4个字段。
t_xmin:保存插入该元组的事务的txid。
t_xmax:保存删除或更新该元组的事务的txid。如果这个元组没有被删除或更新,t_xmax将被设置为0,这意味着无效。
t_cid:保存命令id (cid),这意味着在从0开始的当前事务中执行这个命令之前执行了多少SQL命令。例如,假设我们在一个事务中执行三个INSERT命令:'BEGIN;插入;插入;插入;Commit;”。如果第一个命令插入这个元组,t_cid被设置为0。如果第二个命令插入这个,t_cid将被设置为1,依此类推。
t_ctid:保存着元组标识符(tid),它指向自己或一个新的元组。第1.3节中描述的tid用于标识表中的元组。当这个元组被更新时,这个元组的t_ctid指向新的元组;否则,t_ctid指向自身。
5.3. 插入、删除、更新元组
本节介绍元组如何插入、删除和更新。然后,简要介绍了用于元组插入和更新的自由空间映射(FSM)。
为了关注元组,页头和行指针不在下面表示。图5.3展示了元组如何表示的示例。
图5.3 元组的表示
5.3.1 插入
通过插入操作,一个新的元组被直接插入到目标表的页面中(图5.4)。
图5.4 元组插入
假设一个元组被txid为99的事务插入到一个页面中。在本例中,插入的元组的头字段设置如下。
Tuple_1:
t_xmin被设置为99,因为这个元组是由txid 99插入的。
t_xmax设置为0,因为这个元组还没有被删除或更新。
t_cid被设置为0,因为这个元组是txid 99插入的第一个元组。
t_ctid被设置为(0,1),它指向自身,因为这是最新的元组。
PostgreSQL提供了一个pageinspect 扩展,它是一个 contribution 模块,用来显示数据库页面的内容。
testdb=# CREATE EXTENSION pageinspect; CREATE EXTENSION t