pg并发控制

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

ACID指数据库事务正确执行的四个基本要素的缩写:

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(lsolation)
  • 持久性(Durability)

从宽泛的意义上讲,有三种并发控制技术,分别是多版本并发控制、严格两阶段锁定和乐观并发控制。每项技术都有多种变体。

PostgreSQL中的事务隔离等级

事务隔离等级

事务标识

每当事务开始时,事务管理器就会为其分配一个成为事务标识的唯一标识符。pg的txid是一个32位无符号整数,取之空间大小约为42亿。在事务启动后执行内置的txid_current()函数,即可获取当前事务的txid。

获取事务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页面组成。数组的序号索引对应着相应事务的标识,其内容则是相应事务的状态。

CLOG结构

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容