PostgreSQL 并发控制机制(1):隔离级别

并发控制是多个事务在并发运行时,数据库保证事务一致性(Consistency)和隔离性(Isolation)的一种机制。主流商用关系数据库使用的并发控制技术主要有两种:严格两阶段封锁(S2PL)和多版本并发控制(MVCC)。
PostgreSQL使用了多版本并发控制技术的一种变体:快照隔离Sanpshot Isolation(简称SI)。通过SI,PostgreSQL提供了与ANSI SQL标准相应的四个隔离级别,但实现与标准定义有一些差异。

一、基础知识

本节介绍用于抽象表达的形式化语言和标准隔离级别。

1、形式化语言

为了更好的描述,使用以下更一般化(抽象)的形式化语言来描述数据库事务操作。

读写操作
w和r,示例:w1(x),r2(y)
其中w和r分别表示写和读,1和2分别表示事务T1和T2,x和y表示数据项。
注意:这里的w(写)泛指对数据变更的操作,包括但不限于插入、删除等;数据项是一个宽泛的概念,包括但不限于元组tuple、页page甚至是整个关系relation。

事务操作
c和a,示例:c1,a2
c表示commit提交,其后的数字指代事务,如c1表示事务T1提交
a表示abort回滚,a2表示事务T2回滚

谓词
谓词P,通常与读写操作一起出现,示例:r1[P],w2[P]
r1[P]表示事务T1读满足谓词P的结果集,
w2[P]表示事务T2对满足谓词P的结果集进行变更(写)。

2、ANSI SQL标准隔离级别

ANSI SQL标准通过现象(Phenomena)定义(除此之外还可以通过锁、数据流图定义),下面首先使用形式化语言来定义以下几种现象:
脏写P0 – (Dirty Write)
w1(x) w2(x) (c1 or a1)

脏读P1 – Dirty Read
w1(x) r2(x) (c1 or a1)

不可重复读P2 – Fuzzy or Non-Repeatable Read
r1(x) w2(x) (c1 or a1)

幻读P3 – Phantom
r1(P)w2(y in P) (c1 or a1)
这里的P表示逻辑术语谓词(Predicate)

ANSI SQL标准通过禁止上述现象的形式定义了四个隔离级别,分别是读未提交、读已提交、可重复读和可串行化:

隔离级别 P0 Dirty Write P1 Dirty Read P2 Fuzzy Read P3 Phantom
Read Uncommitted Not Possible Possible Possible Possible
Read Committed Not Possible Not Possible Possible Possible
Repeatable Read Not Possible Not Possible Not Possible Possible
Serializable Not Possible Not Possible Not Possible Not Possible

可串行化:禁止所有并发现象,效果如同事务以串行的方式执行
可重复读:除幻读外,禁止其他现象
读已提交:禁止脏读和脏写
读未提交:禁止脏写

二、PostgreSQL隔离级别

PostgreSQL与ANSI SQL标准相应,也定义了四种隔离级别

testdb=# \help set transaction
Command:     SET TRANSACTION
Description: set the characteristics of the current transaction
Syntax:
SET TRANSACTION transaction_mode [, ...]
...

where transaction_mode is one of:

    ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED }
...

1、READ COMMITTED vs READ UNCOMMITTED

基于SI实现的并发控制,READ UNCOMMITTED和READ COMMITTED这两个级别在PostgreSQL中没有任何区别。
见下面例子,session 1启动事务,插入数据到表t1中,但不提交事务;这时候session 2读取t1的数据,按READ UNCOMMITTED的定义,应可“脏读”t1的数据,但实际上并没有获取脏数据。

-- session 1
testdb=# \d t1
                 Table "public.t1"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           |          |

testdb=# select * from t1;
 id
----
(0 rows)

testdb=# START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION
testdb=# insert into t1 values(1);
INSERT 0 1
testdb=#

-- session 2
testdb=# START TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION
testdb=#  select * from t1;
 id
----
(0 rows)

详细可参考PostgreSQL手册[1]。

2、REPEATABLE READ

在PostgreSQL 9.0或以下版本并没有该隔离级别,在9.1之后,PostgreSQL实现了Serializable Snapshot Isolation(简称SSI),原来的SERIALIZABLE变为新增的REPEATABLE READ,新的SSI成为SERIALIZABLE。实质上,PostgreSQL中的REPEATABLE READ隔离级别是Snapshot Isolation,比起ANSI SQL标准定义的可重复读,除了P2 Non-Repeatable Read现象之外,还可以禁止P3 Phantom。

-- session 1
[local]:5432 postgres@testdb=# insert into t1 select generate_series(1,5);      
INSERT 0 5
Time: 3.638 ms
[local]:5432 postgres@testdb=# select * from t1 where id <= 10;                                 
id
----
  1
  2
  3
  4
  5
(5 rows)

Time: 0.607 ms
[local]:5432 postgres@testdb=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
Time: 0.292 ms
[local]:5432 postgres@testdb=#* select txid_current();
 txid_current
--------------
          502
(1 row)

Time: 0.284 ms

-- session 2
[local]:5432 postgres@testdb=# insert into t1 select generate_series(6,10);
INSERT 0 5
Time: 1.511 ms

[local]:5432 postgres@testdb=# select * from t1;
 id
----
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
(10 rows)

Time: 0.434 ms

-- session 1
[local]:5432 postgres@testdb=#* select * from t1 where id <= 10;                                
id
----
  1
  2
  3
  4
  5
(5 rows)

Time: 0.536 ms

如上所示,得益于快照隔离提供的一致性读,session 1查询数据表t1,返回1-5,session 2插入5行数据(6-10)。session 1再次查询表t1,返回的数据仍然是1-5(事务启动时的快照)。

从理论上来说,快照隔离SI要求满足以下两个规则:
Rule 1:事务T读取数据对象x,其中x是T启动前已提交事务产生的最新版本
Rule 2:并发事务的写集合之间不相交。使用形式化语言描述,wset(T)表示事务T的写集合,设有两个并发事务T1和T2,SI要求wset(T1) ∩ wset(T2) = ∅,否则出现冲突,其中一个事务必须回滚。

冲突处理常用的协议包括FCW(First Commit Wins,先提交者胜)和FUW(First Updater Wins,先更新者胜)。
FCW:事务Ti准备提交时,检查是否存在其他已提交的事务变更了数据对象x,如存在,则回滚,否则提交
FUW:如事务Tj已持有数据对象x的锁,同时Ti希望变更x,则Ti必须等待直至Tj提交或回滚;如Tj提交,则Ti回滚,如Tj回滚,则Ti可以获取x的写锁,可以继续执行
PostgreSQL在REPEATABLE READ级别使用FUW协议。

时间点 T1 T2
t1 START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
t2 START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
t3 update t1 set id = 1 where id = 5;
t4 update t1 set id = 11 where id = 5;
t5 commit;
t6 提示出错

执行输出如下:

-- T1
[local]:5432 postgres@testdb=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
Time: 0.197 ms

-- T2
[local]:5432 postgres@testdb=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
Time: 0.181 ms

-- T1
[local]:5432 postgres@testdb=#* update t1 set id = 1 where id = 5;
UPDATE 1
Time: 0.430 ms

-- T2
[local]:5432 postgres@testdb=#* update t1 set id = 11 where id = 5;
---------->wait

-- T1
[local]:5432 postgres@testdb=#* commit;
COMMIT
Time: 3.241 ms
    
-- T2
[local]:5432 postgres@testdb=#* update t1 set id = 11 where id = 5;
ERROR:  could not serialize access due to concurrent update
Time: 3172.768 ms (00:03.173)

3、SERIALIZABLE

REPEATABLE READ隔离级别已然禁止了P3 Phantom幻读,PostgreSQL为何还提供SERIALIZABLE隔离级别?原因是可重复读(其实是SI)不能实现可串行化,还存在不可串行化的写偏序(Write Skew)现象。
写偏序(Write Skew)
使用形式化语言来表示:设rset(Tn)是事务Tn读数据涉及的数据项集合,wset(Tn)是事务Tn写数据涉及的数据项集合。
现有两个并发事务Ti和Tj,
1.Ti和Tj执行前,rset(Ti)和rset(Tj)之间满足某种业务约束C
2.wset(Ti) ∩ rset(Tj) ≠ ∅ 且 rset(Ti) ∩ wset(Tj) ≠ ∅
3.Ti和Tj执行后,rset(Ti)和rset(Tj)不再满足业务约束C
以上现象,称为Write Skew现象。

一个经典的例子,现有数据表doctor,业务约束要求处于oncall状态的医生人数不能少于1人,但如果存在并发事务同时更新不同医生的状态,则能会出现Write Skew现象。下面是一个例子,测试数据如下:

name oncall remark
Alice true
Bob true
Jacky false
Susan false

测试脚本:

drop table if exists doctor;
create table doctor(name varchar,oncall boolean,remark varchar);
insert into doctor(name,oncall,remark) values('Alice',true,'');
insert into doctor(name,oncall,remark) values('Bob',true,'');
insert into doctor(name,oncall,remark) values('Jacky',false,'');
insert into doctor(name,oncall,remark) values('Susan',false,'');

CREATE OR REPLACE FUNCTION change_doctor_status(pi_name in varchar) 
RETURNS boolean 
AS
$$ 
declare 
  v_count int := 0;
begin  
    -- 获取处于oncall状态的医生数量
    select count(*) from doctor into v_count where oncall = true;
    -- 大于等于2,则设置医生oncall为false
    if (v_count >= 2) then
      update doctor 
      set oncall = false
      where name = pi_name;
      -- 成功返回t
      return true;
    end if;
    -- 失败返回f
    return false;
end;
$$  
LANGUAGE plpgsql;

并发事务T1和T2的操作序列:

时间点 T1 T2
t1 START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
t2 START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
t3 select change_doctor_status('Alice');
t4 select change_doctor_status('Bob');
t5 commit;
t6 commit;

T1和T2完成后,数据如下:

name oncall remark
Alice false
Bob false
Jacky false
Susan false

处于oncall状态的医生数变为0,违反了oncall状态医生人数不能少于1人的业务约束。

Serializable Snapshot Isolation
PostgreSQL在9.1之后实现了SSI,设置隔离级别为SERIALIZABLE可禁止Write Skew现象,出现冲突时遵循FCW协议。

重新初始化数据,仍然使用上述操作序列,隔离级别设置为SERIALIZABLE:

时间点 T1 T2
t1 START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
t2 START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
t3 select change_doctor_status('Alice');
t4 select change_doctor_status('Bob');
t5 commit;
t6 commit;

处于SERIALIZABLE隔离级别下,T2在执行commit的时候会报错:

-- T1
[local]:5432 postgres@testdb=# START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION

-- T2
[local]:5432 postgres@testdb=# START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION

-- T1
[local]:5432 postgres@testdb=#* select change_doctor_status('Alice');
 change_doctor_status
----------------------
 t
(1 row)
Time: 0.682 ms

-- T2
[local]:5432 postgres@testdb=#* select change_doctor_status('Bob');
 change_doctor_status
----------------------
 t
(1 row)

-- T1
[local]:5432 postgres@testdb=#* commit;
COMMIT

-- T2
[local]:5432 postgres@testdb=#* commit;
ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.
Time: 0.289 ms

SSI的实现原理,简单来说,PostgreSQL的SSI机制会在读操作时对读对象加SIRead锁,但该锁不会阻塞写也不会阻塞读仅用于判断是否会出现不可串行化的事务调度,通过该锁跟踪事务之间的冲突关系(rw依赖冲突),如可能出现环型结构,则终止其中一个事务,由于实现算法已超出本文的范畴,在此不再展开。

值得一提的是,如果数据表上没有索引,PostgreSQL会在整个relation上加SIRead锁,这意味着对relation的所有写操作只能串行执行,极大的降低并发度。

时间点 T1 T2
t1 START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
t2 START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
t3 update tbl set c1 = 'x' where id = 1;
t4 update tbl set c1 = 'x' where id = 50000;
t5 commit;
t6 commit;

示例表tbl没有索引,有10万行数据,值为1和50000的tuple不在同一个page中,在t6时刻T2提交时会报错。

三、参考资料

[1].PostgreSQL Manual
[2].A Critique of ANSI SQL Isolation Levels
[3].Concurrency Control

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

推荐阅读更多精彩内容