深刻解析 PostgreSQL 系列整理自 The Internals of PostgreSQL 等系列文章,从碎片化地阅读到体系化地学习,感受对数据库有了更深刻地了解;举一反三,相互印证,也是有利于掌握 MySQL 等其余的关系型数据库或者 NoSQL 数据库。html
并发控制旨在针对数据库中对事务并行的场景,保证 ACID 中的一致性(Consistency)与隔离(Isolation)。数据库技术中主流的三种并发控制技术分别是: Multi-version Concurrency Control (MVCC), Strict Two-Phase Locking (S2PL), 以及 Optimistic Concurrency Control (OCC),每种技术也都有不少的变种。在 MVCC 中,每次写操做都会在旧的版本之上建立新的版本,而且会保留旧的版本。当某个事务须要读取数据时,数据库系统会从全部的版本中选取出符合该事务隔离级别要求的版本。MVCC 的最大优点在于读并不会阻塞写,写也不会阻塞读;而像 S2PL 这样的系统,写事务会事先获取到排他锁,从而会阻塞读事务。git
PostgreSQL 以及 Oracle 等 RDBMS 实际使用了所谓的 Snapshot Isolation(SI)这个 MVCC 技术的变种。Oracle 引入了额外的 Rollback Segments,当写入新的数据时,老版本的数据会被写入到 Rollback Segment 中,随后再被覆写到实际的数据块。PostgreSQL 则是使用了相对简单的实现方式,新的数据对象会被直接插入到关联的 Table Page 中;而在读取表数据的时候,PostgreSQL 会经过可见性检测规则(Visibility Check Rules)来选择合适的版本。github
SI 可以避免 ANSI SQL-92 标准中定义的三个反常现象:脏读(Dirty Reads),不可重复读(Non-Repeatable Reads)以及幻读(Phantom Reads);在 9.1 版本后引入的 Serializable Snapshot Isolation(SSI)则可以提供真正的顺序读写的能力。sql
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 |
当某个事务开启时,PostgreSQL 内置的 Transaction Manager 会为它分配惟一的 Transaction ID(txid);txid 是 32 位无类型整型值,能够经过 txid_current()
函数来获取当前的 txid:数据库
testdb=# BEGIN;
BEGIN
testdb=# SELECT txid_current();
txid_current
--------------
100
(1 row)
复制代码
PostgreSQL 还保留了三个关键 txid 值做特殊标记:0 表示无效的 txid,1 表示启动时的 txid,仅在 Database Cluster 启动时使用;2 表明了被冻结的(Frozen)txid,用于在序列化事务时候使用。PostgreSQL 选择数值类型做为 txid,也是为了方便进行比较;对于 txid 值为 100 的事务而言,全部小于 100 的事务是发生在过去的,可见的;而全部大于 100 的事务,是发生在将来,即不可见的。数组
鉴于实际系统中的 txid 数目的须要可能会超过最大值,PostgreSQL 实际是将这些 txid 做为环来看待。服务器
Table Pages 中的 Heap Tuples 每每包含三个部分:HeapTupleHeaderData 结构,NULL bitmap 以及用户数据。数据结构
其中 HeapTupleHeaderData 与事物处理强相关的属性有:架构
BEGIN; INSERT; INSERT; INSERT; COMMIT;
这个事务,若是是首个 INSERT 命令建立的 Tuple,那么其 t_cid 值为 0,第二个就是 1如上所述,Table Pages 中的 Tuples 呈以下布局:并发
在执行插入操做时,PostgreSQL 会直接将某个新的 Tuple 插入到目标表的某个页中:
假如某个 txid 为 99 的事务插入了新的 Tuple,那么该 Tuple 的头域会被设置为以下值:
(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)
复制代码
在删除操做中,目标 Tuple 会被先逻辑删除,即将 t_xmax 的值设置为当前删除该 Tuple 的事务的 txid 值。
当该事务被提交以后,PostgreSQL 会将该 Tuple 标记为 Dead Tuple,并随后在 VACUUM 处理过程当中被完全清除。
在更新操做时,PostgreSQL 会首先逻辑删除最新的 Tuple,而后插入新的 Tuple:
上图所示的行被 txid 为 99 的事务插入,被 txid 为 100 的事务连续更新两次;在该事务提交以后,Tuple_2 与 Tuple_3 就会被标记为 Dead Tuples。
当插入某个 Heap Tuple 或者 Index Tuple 时,PostgreSQL 使用相关表的 FSM 来决定应该选择哪一个 Page 来进行具体的插入操做。每一个 FSM 都存放着表或者索引文件相关的剩余空间容量的信息,可使用以下方式查看:
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
....
复制代码
PostgreSQL 使用 Commit Log,亦称 clog 来存放事务的状态;clog 存放于 Shared Memory 中,在整个事务处理的生命周期中都起到了重要的做用。PostgreSQL 定义了四种不一样的事务状态:IN_PROGRESS, COMMITTED, ABORTED, 以及 SUB_COMMITTED。
Clog 有 Shared Memory 中多个 8KB 大小的页构成,其逻辑上表现为类数组结构,数组下标便是关联的事务的 txid,而值就是当前事务的状态:
若是当前的 txid 超过了当前 clog 页可承载的最大范围,那么 PostgreSQL 会自动建立新页。而在 PostgreSQL 中止或者 Checkpoint 进程运行的时候,clog 的数据会被持久化存储到 pg_xact 子目录下,以 0000,0001 依次顺序命名,单个文件的最大尺寸为 256KB。而当 PostgreSQL 重启的时候,存放在 pg_xact 目录下的文件会被从新加载到内存中。而随着 PostgreSQL 的持续运行,clog 中势必会累计不少的过期或者无用的数据,Vacuum 处理过程当中一样会清除这些无用的数据。
事务快照便是存放了当前所有事务是否为激活状态信息的数据结构,PostgreSQL 内部将快照表示为简单的文本结构,xmin:xmax:xip_list’
;譬如 "100:100:",其意味着全部 txid 小于或者等于 99 的事务是非激活状态,而大于等于 100 的事务是处在了激活状态。
testdb=# SELECT txid_current_snapshot();
txid_current_snapshot
-----------------------
100:104:100,102
(1 row)
复制代码
以 100:104:100,102
为例,其示意图以下所示:
事务快照主要由事务管理器(Transaction Manager)提供,在 READ COMMITTED 这个隔离级别,不管是否有 SQL 命令执行,该事务都会被分配到某个快照;而对于 REPEATABLE READ 或者 SERIALIZABLE 隔离级别的事务而言,仅当首个 SQL 语句被执行的时候,才会被分配到某个事务快照用于进行可见性检测。事务快照的意义在于,当某个快照进行可见性判断时,不管目标事务是否已经被提交或者放弃,只要他在快照中被标记为 Active,那么其就会被当作 IN_PROGRESS 状态的事务来处理。
事务管理器始终保存有关当前运行的事务的信息。假设三个事务一个接一个地开始,而且 Transaction_A 和 Transaction_B 的隔离级别是 READ COMMITTED,Transaction_C 的隔离级别是 REPEATABLE READ。
T1:
T2:
T3:
T4:
T5:
可见性检测的规则用于根据 Tuple 的 t_xmin 与 t_xmax,clog 以及自身分配到的事务快照来决定某个 Tuple 相对于某个事务是否可见。
当某个 Tuple 的 t_xmin 值对应的事务的状态为 ABORTED 时候,该 Tuple 永远是不可见的:
/* 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
复制代码
对于非插入该 Tuple 的事务以外的其余事务关联的 Tuple 而言,该 Tuple 永远是不可见的;仅对于与该 Tuple 同属一事务的 Tuple 可见(此时该 Tuple 未被删除或者更新的)。
/* 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
复制代码
此时该 Tuple 在大部分状况下都是可见的,除了该 Tuple 被更新或者删除。
/* 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
复制代码
以简单的双事务更新与查询为例:
上图中 txid 200 的事务的隔离级别是 READ COMMITED,txid 201 的隔离级别为 READ COMMITED 或者 REPEATABLE READ。
根据 Rule 6,此时仅有 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)
复制代码
对于 txid 200 的事务而言,根据 Rule 7 与 Rule 2 可知,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)
复制代码
而对于 txid 201 的事务而言,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)
复制代码
若是此时 txid 201 的事务处于 READ COMMITED 的隔离级别,那么 txid 200 会被当作 COMMITTED 来处理,由于此时获取到的事务快照是 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)
复制代码
若是此时 txid 201 的事务处于 REPEATABLE READ 的隔离级别,此时获取到的事务快照仍是 200:200:
,那么 txid 200 的事务必须被当作 IN_PROGRESS 状态来处理;所以此时 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)
复制代码
所谓的 更新丢失(Lost Update),也就是写冲突(ww-conflict),其出如今两个事务同时更新相同的行;在 PostgreSQL 中,REPEATABLE READ 与 SERIALIZABLE 这两个级别都须要规避这种异常现象。
(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
复制代码
在上述流程中,UPDATE 命令会遍历每一个待更新行,当发现该行正在被其余事务更新时进入等待状态直到该行被解除锁定。若是该行已经被更新,而且隔离级别为 REPEATABLE 或者 SERIALIZABLE,则放弃更新。
Being updated 意味着该行由另外一个并发事务更新,而且其事务还没有终止。由于 PostgreSQL 的 SI 使用 first-updater-win 方案, 在这种状况下,当前事务必须等待更新目标行的事务的终止。假设事务 Tx_A 和 Tx_B 同时运行,而且 Tx_B 尝试更新行;可是 Tx_A 已更新它而且仍在进行中,Tx_B 等待 Tx_A 的终止。在更新目标行提交的事务以后,继续当前事务的更新操做。 若是当前事务处于 READ COMMITTED 级别,则将更新目标行; 不然 REPEATABLE READ 或 SERIALIZABLE,当前事务当即停止以防止丢失更新。
PostgreSQL 的并发控制机制还依赖于如下的维护流程:
首先讨论下 txid 环绕式处理的问题,假设 txid 100 的事务插入了某个 Tuple_1
,则该 Tuple 对应的 t_xmin 值为 100;然后服务器又运行了许久,Tuple_1
期间并未被改变。直到 txid 为 2^31 + 101
时,对于该事务而言,其执行 SELECT 命令时,是没法看到 Tuple_1
的 ,由于 txid 为 100 的事务相对于其是发生在将来的,由其建立的 Tuple 天然也就是不可见的。
为了解决这个问题,PostgreSQL 引入了所谓的 frozen txid(被冻结的 txid),而且设置了 FREEZE 进程来具体处理该问题。前文说起到 txid 2 是保留值,专门表征那些被冻结的 Tuple,这些 Tuple 永远是非激活的、可见的。FREEZE 进程一样由 Vacuum 进程统一调用,它会扫描全部的表文件,将那些与当前 txid 差值超过 vacuum_freeze_min_age 定义的 Tuple 的 t_xmin 域设置为 2。在 9.4 版本以后,则是将 t_infomask 域中的 XMIN_FROZEN 位设置来表征该 Tuple 为冻结状态。
若是但愿深刻浅出 分布式系统,分布式计算,分布式存储,数据库,操做系统,虚拟化 等内容,能够参阅 深刻浅出分布式基础架构, DistributedSystem CheatSheet, Database CheatSheet, Linux CheatSheet, MySQL CheatSheet, Docker CheatSheet, Flink CheatSheet, Kafka CheatSheet 等。
若是想获取分布式系统、虚拟化调度、数据库、分布式存储、分布式计算、操做系统等领域更多资料: Docker List, Kubernetes List, Linux List, HTTP List, Distributed System List, Blockchain List, Flink List, Kafka List, Database List, MySQL List, PostgreSQL List, etc.