并发控制
- 深入解析
PostgreSQL 系列整理自 The Internals of PostgreSQL 等系列文章,从碎片化地阅读到体系化地学习,感觉对数据库有了更深入地了解;触类旁通,相互印证,也是有利于掌握MySQL 等其他的关系型数据库或者NoSQL 数据库。
深入解析PostgreSQL 系列之并发控制与事务机制
并发控制旨在针对数据库中对事务并行的场景,保证
Isolation Level | Dirty Reads | Non-repeatable Read | Phantom Read | Serialization Anomaly |
---|---|---|---|---|
READ COMMITTED | Not possible | Possible | Possible | Possible |
REPEATABLE READ | Not possible | Not possible | Not possible in PG; See Section 5.7.2. (Possible in ANSI SQL) | Possible |
SERIALIZABLE | Not possible | Not possible | Not possible | Not possible |
Tuple 结构
Transaction ID
当某个事务开启时,txid_current()
函数来获取当前的
testdb=# BEGIN;
BEGIN
testdb=# SELECT txid_current();
txid_current
--------------
100
(1 row)

鉴于实际系统中的
HeapTupleHeaderData

其中
(TransactionId)t_xmin: 存放插入该Tuple 时的txid (TransactionId)t_xmax: 存放删除或者更新该Tuple 时的txid ,如果还没更新或者删除,那么置0 ,表示无效(CommandId)t_cid: 存放Command ID ,即 创建该Tuple 的命令在该事务内执行的所有SQL 命令中的编号;譬如BEGIN; INSERT; INSERT; INSERT; COMMIT;
这个事务,如果是首个INSERT 命令创建的Tuple ,那么其t_cid 值为0 ,第二个就是1 (ItemPointerData)t_ctid: 当某个Tuple 更新时,该值就指向新创建的Tuple ,否则指向自己
Tuple 的插入、删除与更新
如上所述,

插入
在执行插入操作时,

假如某个
t_xmin 与创建该Tuple 的事务的txid 保持一致,即99 t_xmax 被设置为0 ,因为其还未被删除或者更新t_cid 被设置为0 ,因为该Tuple 是由事务中的首个Insert 命令创建的t_ctid 被设置为了(0, 1)
,即指向了自己
testdb=# CREATE EXTENSION pageinspect;
CREATE EXTENSION
testdb=# CREATE TABLE tbl (data text);
CREATE TABLE
testdb=# INSERT INTO tbl VALUES('A');
INSERT 0 1
testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid
FROM heap_page_items(get_raw_page('tbl', 0));
tuple | t_xmin | t_xmax | t_cid | t_ctid
-------+--------+--------+-------+--------
1 | 99 | 0 | 0 | (0,1)
删除
在删除操作中,目标

当该事务被提交之后,
更新
在更新操作时,

上图所示的行被
Free Space Map
当插入某个
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
....
Commit Log

如果当前的
Transaction Snapshot | 事务快照
事务快照即是存放了当前全部事务是否为激活状态信息的数据结构,xmin:xmax:xip_list’
;譬如 “100:100:",其意味着所有
testdb=# SELECT txid_current_snapshot();
txid_current_snapshot
-----------------------
100:104:100,102
(1 row)
xmin: 最早的仍处在激活状态的txid ,所有更早之前的事务要么处于被提交之后的可见态,要么就是被回滚之后的假死态。xmax: 首个至今仍未分配的事务编号,所有txid 大于或者等于该值的事务,相对于该快照归属的事务都是尚未发生的,因此是不可见的。xip_list: 快照时候处于激活状态的txids ,仅会包含在xmin 与xmax 之间的txids 。
以 100:104:100,102
为例,其示意图如下所示:

事务快照主要由事务管理器(Transaction Manager)提供,在

事务管理器始终保存有关当前运行的事务的信息。假设三个事务一个接一个地开始,并且
-
T1:
Transaction_A 启动并执行第一个SELECT 命令。执行第一个命令时,Transaction_A 请求此刻的txid 和快照。在这种情况下,事务管理器分配txid 200 ,并返回事务快照'200 :200: ’。
-
T2:
Transaction_B 启动并执行第一个SELECT 命令。事务管理器分配txid 201 ,并返回事务快照'200 :200: ’,因为Transaction_A (txid 200)正在进行中。因此,无法从Transaction_B 中看到Transaction_A 。
-
T3:
Transaction_C 启动并执行第一个SELECT 命令。事务管理器分配txid 202 ,并返回事务快照'200 :200: ’,因此,Transaction_A 和Transaction_B 不能从Transaction_C 中看到。
-
T4:
Transaction_A 已提交。事务管理器删除有关此事务的信息。
-
T5:
Transaction_B 和Transaction_C 执行各自的SELECT 命令。Transaction_B 需要事务快照,因为它处于READ COMMITTED 级别。在这种情况下,Transaction_B 获取新快照'201 :201: ’,因为Transaction_A (txid 200)已提交。因此,Transaction_B 不再是Transaction_B 中不可见的。Transaction_C 不需要事务快照,因为它处于REPEATABLE READ 级别并使用获得的快照,即'200 :200: ’。因此,Transaction_A 仍然是Transaction_C 不可见的。
Visibility Check | 可见性检测
Rules | 可见性检测规则
可见性检测的规则用于根据
t_xmin 对应事务的状态为ABORTED
当某个
/* t_xmin status = ABORTED */
// Rule 1: If Status(t_xmin) = ABORTED ⇒ Invisible
Rule 1: IF t_xmin status is 'ABORTED' THEN
RETURN 'Invisible'
END IF
t_xmin 对应事务的状态为IN_PROGRESS
对于非插入该
/* t_xmin status = IN_PROGRESS */
IF t_xmin status is 'IN_PROGRESS' THEN
IF t_xmin = current_txid THEN
// Rule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible
Rule 2: IF t_xmax = INVALID THEN
RETURN 'Visible'
// Rule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible
Rule 3: ELSE /* this tuple has been deleted or updated by the current transaction itself. */
RETURN 'Invisible'
END IF
// Rule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible
Rule 4: ELSE /* t_xmin ≠ current_txid */
RETURN 'Invisible'
END IF
END IF
t_xmin 对应事务的状态为COMMITTED
此时该
/* t_xmin status = COMMITTED */
IF t_xmin status is 'COMMITTED' THEN
// If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible
Rule 5: IF t_xmin is active in the obtained transaction snapshot THEN
RETURN 'Invisible'
// If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible
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
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible
Rule 7: IF t_xmax = current_txid THEN
RETURN 'Invisible'
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible
Rule 8: ELSE /* t_xmax ≠ current_txid */
RETURN 'Visible'
END IF
ELSE IF t_xmax status is 'COMMITTED' THEN
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible
Rule 9: IF t_xmax is active in the obtained transaction snapshot THEN
RETURN 'Visible'
// If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible
Rule 10: ELSE
RETURN 'Invisible'
END IF
END IF
END IF
可见性检测流程
以简单的双事务更新与查询为例:

上图中
- 当在
T3 时刻执行SELECT 命令时:
根据Tuple_1
是处于可见状态:
# Rule6(Tuple_1) ⇒ Status(t_xmin:199) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible
testdb=# -- txid 200
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)
testdb=# -- txid 201
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)
- 当在
T5 时刻执行SELECT 命令时:
对于Tuple_1
可见而 Tuple_2
不可见:
# Rule7(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 = current_txid:200 ⇒ Invisible
# Rule2(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 = current_txid:200 ∧ t_xmax = INVAILD ⇒ Visible
testdb=# -- txid 200
testdb=# SELECT * FROM tbl;
name
------
Hyde
(1 row)
而对于Tuple_1
是可见的,Tuple_2
是不可见的:
# Rule8(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 ≠ current_txid:201 ⇒ Visible
# Rule4(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 ≠ current_txid:201 ⇒ Invisible
testdb=# -- txid 201
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)
- 当在
T7 时刻执行SELECT 命令时:
如果此时201:201:
,因此 Tuple_1
是不可见的,而 Tuple_2
是可见的:
# Rule10(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) ≠ active ⇒ Invisible
# Rule6(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible
testdb=# -- txid 201 (READ COMMITTED)
testdb=# SELECT * FROM tbl;
name
------
Hyde
(1 row)
如果此时200:200:
,那么Tuple_1
是可见的,而 Tuple_2
是不可见的:
# Rule9(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) = active ⇒ Visible
# Rule5(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ Snapshot(t_xmin:200) = active ⇒ Invisible
testdb=# -- txid 201 (REPEATABLE READ)
testdb=# SELECT * FROM tbl;
name
--------
Jekyll
(1 row)
Preventing Lost Updates | 避免更新丢失
所谓的更新丢失(Lost Update
(1) FOR each row that will be updated by this UPDATE command
(2) WHILE true
/* The First Block */
(3) IF the target row is being updated THEN
(4) WAIT for the termination of the transaction that updated the target row
(5) IF (the status of the terminated transaction is COMMITTED)
AND (the isolation level of this transaction is REPEATABLE READ or SERIALIZABLE) THEN
(6) ABORT this transaction /* First-Updater-Win */
ELSE
(7) GOTO step (2)
END IF
/* The Second Block */
(8) ELSE IF the target row has been updated by another concurrent transaction THEN
(9) IF (the isolation level of this transaction is READ COMMITTED THEN
(10) UPDATE the target row
ELSE
(11) ABORT this transaction /* First-Updater-Win */
END IF
/* The Third Block */
ELSE /* The target row is not yet modified or has been updated by a terminated transaction. */
(12) UPDATE the target row
END IF
END WHILE
END FOR
在上述流程中,

空间整理
- 移除那些被标记为
Dead 的Tuples 与Index Tuples - 移除
clog 中过时的部分 - 冻结旧的
txids - 更新
FSM ,VM 以及其他统计信息
首先讨论下Tuple_1
,则该Tuple_1
期间并未被改变。直到2^31 + 101
时,对于该事务而言,其执行Tuple_1
的,因为
为了解决这个问题,