当多个事务同时在数据库中运行时,并发控制是一种用于维持一致性与隔离性的技术,一致性与隔离性是ACID的两个属性。
ACID指数据库事务正确执行的四个基本要素的缩写:
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(lsolation)
- 持久性(Durability)
从宽泛的意义上讲,有三种并发控制技术,分别是多版本并发控制、严格两阶段锁定和乐观并发控制。每项技术都有多种变体。
PostgreSQL中的事务隔离等级
事务标识
每当事务开始时,事务管理器就会为其分配一个成为事务标识的唯一标识符。pg的txid是一个32位无符号整数,取之空间大小约为42亿。在事务启动后执行内置的txid_current()函数,即可获取当前事务的txid。
pg保留三个特殊的txid:
- 0表示无效的txid
- 1表示初始起动的txid,仅用于数据库集群的初始化过程
- 2表示冻结的txid,仅用于数据库集群的初始化过程
txid可以相互比较大小。且在逻辑上无限。
txid并非是在begin命令执行时分配的。在pg中当执行begin命令后的第一条命令时,事务管理器才会分配txid,并真正启动其事务。
元组结构
堆元组可分为普通元组和TOAST元组两类。下面介绍普通元组。
堆元组由三个部分组成,即HeapTupleHeaderData结构,控制位图及用户数据。
其中有四个字段需要了解:
- t_xmin保存插入此元组的事务的txid。
- t_xmax保存删除或更新此元组的事务的txid。如果尚未删除或更新此元组,则t_xmax设置为0,即无效。
- t_cid保存命令标识(cid) ,cid的意思是在当前事务中,执行当前命令之前执行了多少sql命令,从0开始计数。
- t_ctid保存着指向自身或新元组的元组标识符(tid)。tid用于标识表中的元组。在更新该元组时,t_ctid会指向新版本的元组,否则t_ctid会指向自己。
元组的增、删、改
元组的具体表示如图:
插入
在插入操作中,新元组将直接插入目标表的页面中:
假设元组时由txid=99的事务插入页面中的,在这种情况下,被插入元组的首部字段会依一下步骤设置。
Tuple_1:
- t_xmin设置为99,因为此远足由txid=99的事务所插入。
- t_xmax设置为0,因为此元组尚未被删除或更新。
- t_cid设置为0,因为此元组时由txid=99的事务所执行的第一条命令插入的。
- t_ctid设置为(0,1),指向自身,因为这是该元组的最新版本。
pg自带了一个第三方贡献的扩展模块pageinsepect,可以用于检查数据库页面的具体内容。
删除
在删除操作中,目标元组只是在逻辑上被标记为删除。目标元组的t_xmax字段将被设置为执行delete命令事务的txid。
假设Tuple_1被txid=111的事务删除。在这种情况下,Tuple_1首部字段t_xmax被设为111。
如果txid=111的事务已经提交,就不一定要Tuple_1。通常不需要的元组在pg中被称为死元组。
死元组最终将从页面中被移除。清除死元组的过程被称为清理(VACUUM)过程。
更新
在更新操作中,pg在逻辑上实际执行的是删除最新的元组,并插入一条新的元组。
过程如图所示,就不再详细讨论。
需要理解的是:如果txid=100的事务已经提交,那么Tuple_1和Tuple_2就成了死元组,而如果txid=100的事务种植,Tuple_2和Tuple_3就成了死元组。
空闲空间映射
插入堆或索引元组时,pg使用表与索引相应的FSM来选择可供插入的页面。
表和索引都有各自的FSM。每个FSm存储着相应表或索引文件中每个页面可用空间容量的信息。
所有FSM都以后缀存储,在需要时他们会被加载到共享内存中。
扩展pg_freespacemap能提供特定表或索引上的空闲空间信息。一下查询列出了特性表中每个页面的空闲率。
testdb=# CREATE EXTENSION pg_freespacemap; CREATE EXTENSION testdb=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('accounts'); blkno | avail | freespace ratio -------+-------+----------------- 0 | 7904 | 96.00 1 | 7520 | 91.00 2 | 7136 | 87.00 3 | 7136 | 87.00 4 | 7136 | 87.00 5 | 7136 | 87.00 ....
提交日志
pg在提交日志(CLOG)中保存事务的状态。提交日志分配与共享内存中,并用于事务处理过程的全过程。
事务状态
pg定义了4中事务状态,即
- IN_PROGRESS
- COMMITTED
- ABORTED
- SUB_COMMITTED
前三种状态很好理解。而SUB_COMMITTED状态用于子事务。这里不详细描述。
提交日志如何工作
提交日志(CLOG)在逻辑上是一个数组,由共享内存中一路系列8KB页面组成。数组的序号索引对应着相应事务的标识,其内容则是相应事务的状态。
T1:txid 200提交;txid 200的状态从IN_PROGRESS变为COMMITTED。
T2:txid 201终止;txid 201的状态从INPROGRESS变为ABORTED。
txid不断前进,当CLOG空间耗尽无法存储新的事务状态时,就会追加分配一个新的页面。
当需要获取事务的状态时,pg将调用相应内部函数读取CLOG,并返回所请求的事务的状态。
提交日志的维护
当pg关机或执行存档过程时,CLOG数据会写入pg_clog子目录下的文件中。这些文件被命名为0000,0001等。文件最大尺寸为256KB。
当pg启动时会加载存储在pg_clog中的文件,用其数据初始化CLOG。
CLOG的大小会不断增长,因为只要CLOG一天慢就会追加新的页面。但并非所有数据都是必要的。
事务快照
事务快照是一个数据集,存储着某个特定事务在某个时间点所看到的状态信息:哪些事务处于活跃状态。活跃状态意味着事务正在进行中或还没有开始。
内置函数txid_current_snapshot及其文本表示
txid_current_snapshot的文本标识是xmin:xmax:xip_list,各部分描述如下。
-
xmin
最早仍然活跃的事务的txid。所有比它更早的事务(txid < xmin),要么已经提交并可见,要么已经回滚并生成死元组。
-
xmax
第一个尚未分配的txid。所有txid>=xmax的事务在获取快照时尚未启动,因此结果对当前事务不可见。
-
xip_list
获取快照时活跃事务的txid列表。该列表仅包括xmin和xmax之间的txid。
例如,在快照100 : 104 : 100, 102中,xmin是100,xmax是104, 而xip_list为100,102。
事务快照时由事务管理器提供的。在READ COMMITTED隔离级别,事务在执行每条SQL时都会获取快照,在其他情况下,事务只会在执行第一条SQL命令时获取一次快照。获取的事务快照用于元组的可见性检查。
使用获取的快照进行可见性检查时,所有活跃的事务都必须被当成IN_PROGRESS的事务同等对待,无论他们实际上是否已经提交或终止。这条规则非常重要,因为它正是READ COMMITTED和REPEATABLE READ/SERIALIZABLE隔离级别中表现差异的根本来源。
可见性检查规则
可见性检查规则是一组规则,用于确定一条元组是否对一个事务可见,可见性检查会用到元组的t_min和t_xmax,提交日志CLOG,以及已获取的事务快照。所选规则有10调,可以分为三种情况。
t_xmin的状态为ABORTED
t_xmin的状态为ABORTED的元组始终不可见,因为插入此元组的事务已中止。
/* t_xmin status == ABORTED */
Rule 1: IF t_xmin status is 'ABORTED' THEN
RETURN 'Invisible'
END IF
该规则明确表示为以下数学表达式。
规则1: If Status(t_xmin) = ABORTED ⇒ Invisible
t_xmin的状态为IN_PROGRESS
t_xmin状态为INPROGRESS的元组基本上是不可见的(规则3和规则4),但在一个条件下例外。
/* t_xmin status == IN_PROGRESS */
IF t_xmin status is 'IN_PROGRESS' THEN
IF t_xmin = current_txid THEN
Rule 2: IF t_xmax = INVALID THEN
RETURN 'Visible'
Rule 3: ELSE /* this tuple has been deleted or updated by the current transaction itself. */
RETURN 'Invisible'
END IF
Rule 4: ELSE /* t_xmin ≠ current_txid */
RETURN 'Invisible'
END IF
END IF
如果该元组被另一个进行中的事务插入,则该元组显然是不可见的。(规则3)
如果t_min等于当前事务的txid(即当前事务插入了该元组),且t_xmax != 0,则该元组是不可见的,因为它已被当前事务更新或删除。(规则2)
有个例外是,当前事务插入此元组且t_xmax = 0(当前元组尚未被更新或删除)。在这种情况下,此元组对当前事务可见。
规则2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible
规则3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible
规则4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible
t_xmin状态为COMMITTED
t_xmin状态为COMMITTED的元组时可见的,但在三个条件下除外。
/* t_xmin status == COMMITTED */
IF t_xmin status is 'COMMITTED' THEN
Rule 5: IF t_xmin is active in the obtained transaction snapshot THEN
RETURN 'Invisible'
Rule 6: ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN
RETURN 'Visible'
ELSE IF t_xmax status is 'IN_PROGRESS' THEN
Rule 7: IF t_xmax = current_txid THEN
RETURN 'Invisible'
Rule 8: ELSE /* t_xmax ≠ current_txid */
RETURN 'Visible'
END IF
ELSE IF t_xmax status is 'COMMITTED' THEN
Rule 9: IF t_xmax is active in the obtained transaction snapshot THEN
RETURN 'Visible'
Rule 10: ELSE
RETURN 'Invisible'
END IF
END IF
END IF
规则5:If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible
规则6:If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible
规则7:If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible
规则8:If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible
规则9: If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible
规则10:If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible