浅析Postgres中的并发控制(Concurrency Control)与事务特性(下)

上文咱们讨论了PostgreSQL的MVCC相关的基础知识以及实现机制。关于PostgreSQL中的MVCC,咱们只讲了元组可见性的问题,还剩下两个问题没讲。一个是"Lost Update"问题,另外一个是PostgreSQL中的序列化快照隔离机制(SSI,Serializable Snapshot Isolation)。今天咱们就来继续讨论。html


3.2 Lost Update

所谓"Lost Update"就是写写冲突。当两个并发事务同时更新同一条数据时发生。"Lost Update"必须在REPEATABLE READ 和 SERIALIZABLE 隔离级别上被避免,即拒绝并发地更新同一条数据。下面咱们看看在PostgreSQL上如何处理"Lost Update"sql

有关PostgreSQL的UPDATE操做,咱们能够看看ExecUpdate()这个函数。然而今天咱们不讲具体的函数,咱们形而上一点。只从理论出发。咱们只讨论下UPDATE执行时的情形,这意味着,咱们不讨论什么触发器啊,查询重写这些杂七杂八的,只看最"干净"的UPDATE操做。并且,咱们讨论的是两个并发事务的UPDATE操做。并发

请看下图,下图显示了两个并发事务中UPDATE同一个tuple时的处理。函数

  • [1]目标tuple处于正在更新的状态

咱们看到Tx_A和Tx_B在并发执行,Tx_A先更新了tuple,这时Tx_B准备去更新tuple,发现Tx_A更新了tuple,可是尚未提交。因而,Tx_B处于等待状态,等待Tx_A结束(commit或者abort)。post

当Tx_A提交时,Tx_B解除等待状态,准备更新tuple,这时分两个状况:若是Tx_B的隔离级别是READ COMMITTED,那么OK,Tx_B进行UPDATE(能够看出,此时发生了Lost Update)。若是Tx_B的隔离级别是REPEATABLE READ或者是SERIALIZABLE,那么Tx_B会当即被abort,放弃更新。从而避免了Lost Update的发生。this

当Tx_A和Tx_B的隔离级别都为READ COMMITTED时的例子:操作系统

Tx_A Tx_B
postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED ;
START TRANSACTION
postgres=# update test set b = b+1 where a =1;
UPDATE 1
postgres=# commit;
COMMIT


postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION
postgres=# update test set b = b+1;

↓this transaction is being blocked

UPDATE 1

当Tx_A的隔离级别为READ COMMITTED,Tx_B的隔离级别为REPEATABLE READ时的例子:postgresql

Tx_A Tx_B
postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED ;
START TRANSACTION
postgres=# update test set b = b+1 where a =1;
UPDATE 1
postgres=# commit;
COMMIT


postgres=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
postgres=# update test set b = b+1;

↓this transaction is being blocked

ERROR:couldn't serialize access due to concurrent update
  • [2]目标tuple已经被并发的事务更新

咱们看到Tx_A和Tx_B在并发执行,Tx_A先更新了tuple而且已经commit,Tx_B再去更新tuple时发现它已经被更新过了而且已经提交。若是Tx_B的隔离级别是READ COMMITTED,根据咱们前面说的,,Tx_B在执行UPDATE前会从新获取snapshot,发现Tx_A的此次更新对于Tx_B是可见的,所以Tx_B继续更新Tx_A更新过得元组(Lost Update)。而若是Tx_B的隔离级别是REPEATABLE READ或者是SERIALIZABLE,那么显然咱们会终止当前事务来避免Lost Update。code

当Tx_A的隔离级别为READ COMMITTED,Tx_B的隔离级别为REPEATABLE READ时的例子:htm

Tx_A Tx_B
postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED ;
START TRANSACTION
postgres=# update test set b = b+1 where a =1;
UPDATE 1
postgres=# commit;
COMMIT
postgres=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
postgres=# select * from test ;
a b
---+---
1 5
(1 row)
postgres=# update test set b = b+1
ERROR: could not serialize access due to concurrent update
  • [3]更新无冲突

这个很显然,没有冲突就没有伤害。Tx_A和Tx_B照常更新,不会有Lost Update。

从上面咱们也能够看出,在使用SI(Snapshot Isolation)机制时,两个并发事务同时更新一条记录时,先更新的那一方得到更新的优先权。可是在下面提到的SSI机制中会有所不一样,先提交的事务得到更新的优先权。


3.3 SSI(Serializable Snapshot Isolation)

SSI,可序列化快照隔离,是PostgreSQL在9.1以后,为了实现真正的SERIALIZABLE(可序列化)隔离级别而引入的。

对于SERIALIZABLE隔离级别,官方介绍以下:

可序列化隔离级别提供了最严格的事务隔离。这个级别为全部已提交事务模拟序列事务执行;就好像事务被按照序列一个接着另外一个被执行,而不是并行地被执行。可是,和可重复读级别类似,使用这个级别的应用必须准备好由于序列化失败而重试事务。事实上,这个隔离级别彻底像可重复读同样地工做,除了它会监视一些条件,这些条件可能致使一个可序列化事务的并发集合的执行产生的行为与这些事务全部可能的序列化(一次一个)执行不一致。这种监控不会引入超出可重复读以外的阻塞,可是监控会产生一些负荷,而且对那些可能致使序列化异常的条件的检测将触发一次序列化失败。

讲的比较繁琐,个人理解是:

1.只针对隔离级别为SERIALIZABLE的事务;
2.并发的SERIALIZABLE事务与按某一个顺序单独的一个一个执行的结果相同。

条件1很好理解,系统只判断并发的SERIALIZABLE的事务之间的冲突;
条件2个人理解就是并发的SERIALIZABLE的事务不能同时修改和读取同一个数据,不然由并发执行和前后按序列执行就会不一致。

可是这个不能同时修改和读取同一个数据要限制在多大的粒度呢?
咱们分状况讨论下。

  • [1] 读写同一条数据

彷佛没啥问题嘛,根据前面的论述,这里的一致性在REPEATABLE READ阶段就保证了,不会有问题。

以此类推,咱们同时读写2,3,4....n条数据,没问题。

  • [2]读写闭环

啥是读写闭环?这我我造的概念,相似于操做系统中的死锁,即事务Tx_A读tuple1,更新tuple2,而Tx_B偏偏相反,读tuple2, 更新tuple1.

咱们假设事务开始前的tuple1,tuple2为tuple1_1,tuple2_1,Tx_A和Tx_B更新后的tuple1,tuple2为tuple1_2,tuple2_2。

这样在并发下:

Tx_A读到的tuple1是tuple1_1,tuple2是tuple2_1。
同理,Tx_B读到的tuple1是tuple1_1,tuple2是tuple2_1。

而若是咱们以Tx_A,Tx_B的顺序串行执行时,结果为:

Tx_A读到的tuple1是tuple1_1,tuple2是tuple2_1。
Tx_B读到的tuple1是tuple1_2(被Tx_A更新了),tuple2是tuple2_1。

反之,而若是咱们以Tx_B,Tx_A的顺序串行执行时,结果为:

Tx_B读到的tuple1是tuple1_1,tuple2是tuple2_1。
Tx_A读到的tuple1是tuple1_1,tuple2是tuple2_2(被Tx_B更新了)。

能够看出,这三个结果都不同,不知足条件2,即并发的Tx_A和Tx_B不能被模拟为Tx_A和Tx_B的任意一个序列执行,致使序列化失败

其实我上面提到的读写闭环,更正式的说法是:序列化异常。上面说的那么多,其实下面两张图便可解释。

关于这个*-conflicts咱们遇到好几个了。咱们先总结下:

wr-conflicts (Dirty Reads)
ww-conflicts (Lost Updates)
rw-conflicts (serialization anomaly)

下面说的SSI机制,就是用来解决rw-conflicts的。

好的,下面就开始说怎么检测这个序列化异常问题,也就是说,咱们要开始了解下SSI机制了。

在PostgreSQL中,使用如下方法来实现SSI:

  1. 利用SIREAD LOCK(谓词锁)记录每个事务访问的对象(tuple、page和relation);

  2. 在事务写堆表或者索引元组时利用SIREAD LOCK监测是否存在冲突;

  3. 若是发现到冲突(即序列化异常),abort该事务。

从上面能够看出,SIREAD LOCK是一个很重要的概念。解释了这个SIREAD LOCK,咱们也就基本上理解了SSI。

所谓的SIREAD LOCK,在PostgreSQL内部被称为谓词锁。他的形式以下:

SIREAD LOCK := { tuple|page|relation, {txid [, ...]} }

也就是说,一个谓词锁分为两个部分:前一部分记录被"锁定"的对象(tuple、page和relation),后一部分记录同时访问了该对象的事务的virtual txid(有关它和txid的区别,这里就不作多介绍了)。

SIREAD LOCK的实如今函数CheckForSerializableConflictOut中。该函数在隔离级别为SERIALIZABLE的事务中发生做用,记录该事务中全部DML语句所形成的影响。

例如,若是txid为100的事务读取了tuple_1,则建立一个SIREAD LOCK为{tuple_1, {100}}。此时,若是另外一个txid为101的事务也读取了tuple_1,则该SIREAD LOCK升级为{tuple_1, {100,101}}。须要注意的是若是在DML语句中访问了索引,那么索引中的元组也会被检测,建立对应的SIREAD LOCK。

SIREAD LOCK的粒度分为三级:tuple|page|relation。若是同一个page中的全部tuple都被建立了SIREAD LOCK,那么直接建立page级别的SIREAD LOCK,同时释放该page下的全部tuple级别的SIREAD LOCK。同理,若是一个relation的全部page都被建立了SIREAD LOCK,那么直接建立relation级别的SIREAD LOCK,同时释放该relation下的全部page级别的SIREAD LOCK。

当咱们执行SQL语句使用的是sequential scan时,会直接建立一个relation 级别的SIREAD LOCK,而使用的是index scan时,只会对heap tuple和index page建立SIREAD LOCK。

同时,我仍是要说明的是,对于index的处理时,SIREAD LOCK的最小粒度是page,也就是说你即便只访问了index中的一个index tuple,该index tuple所在的整个page都被加上了SIREAD LOCK。这个特性经常会致使意想不到的序列化异常,咱们能够在后面的例子中看到。

有了SIREAD LOCK的概念,咱们如今使用它来检测rw-conflicts。

所谓rw-conflicts,简单地说,就是有一个SIREAD LOCK,还有分别read和write这个SIREAD LOCK中的对象的两个并发的Serializable事务。

这个时候,另一个函数闪亮登场:CheckForSerializableConflictIn()。每当隔离级别为Serializable事务中执行INSERT/UPDATE/DELETE语句时,则调用该函数判断是否存在rw-conflicts。

例如,当txid为100的事务读取了tuple_1,建立了SIREAD LOCK : {tuple_1, {100}}。此时,txid为101的事务更新tuple_1。此时调用CheckForSerializableConflictIn()发现存在这样一个状态: {r=100, w=101, {Tuple_1}}。显然,检测出这是一个rw-conflicts。

下面是举例时间。

首先,咱们有这样一个表:

testdb=# CREATE TABLE tbl (id INT primary key, flag bool DEFAULT false);
testdb=# INSERT INTO tbl (id) SELECT generate_series(1,2000);
testdb=# ANALYZE tbl;

并发执行的Serializable事务像下面那样执行:

假设全部的SQL语句都走的index scan。这样,当SQL语句执行时,不只要读取对应的heap tuple,还要读取heap tuple 对应的index tuple。以下图:

执行状态以下:
T1: Tx_A执行SELECT语句,该语句读取了heap tuple(Tuple_2000)和index page(Pkey2);

T2: Tx_B执行SELECT语句,该语句读取了heap tuple(Tuple_1)和index page(Pkey1);

T3: Tx_A执行UPDATE语句,该语句更新了Tuple_1;

T4: Tx_B执行UPDATE语句,该语句更新了Tuple_2000;

T5: Tx_A commit;

T6: Tx_B commit; 因为序列化异常,commit失败,状态为abort。

这时咱们来看一下SIREAD LOCK的状况。

T1: Tx_A执行SELECT语句,调用CheckForSerializableConflictOut()建立了SIREAD LOCK:L1={Pkey_2,{Tx_A}} 和 L2={Tuple_2000,{Tx_A}};

T2: Tx_B执行SELECT语句,调用CheckForSerializableConflictOut建立了SIREAD LOCK:L3={Pkey_1,{Tx_B}} 和 L4={Tuple_1,{Tx_B}};

T3: Tx_A执行UPDATE语句,调用CheckForSerializableConflictIn(),发现并建立了rw-conflict :C1={r=Tx_B, w=Tx_A,{Pkey_1,Tuple_1}}。这很显然,由于Tx_B和TX_A分别read和write这两个object。

T4: Tx_A执行UPDATE语句,调用CheckForSerializableConflictIn(),发现并建立了rw-conflict :C1={r=Tx_A, w=Tx_B,{Pkey_2,Tuple_2000}}。到这里,咱们发现C1和C2构成了precedence graph中的一个环。所以,Tx_A和Tx_B这两个事务都进入了non-serializable状态。可是因为Tx_A和Tx_B都未commit,所以CheckForSerializableConflictIn()并不会abort Tx_B(为何不abort Tx_A?所以PostgreSQL的SSI机制中采用的是first-committer-win,即发生冲突后,先提交的事务保留,后提交的事务abort。)

T5: Tx_A commit;调用PreCommit_CheckForSerializationFailure()函数。该函数也会检测是否存在序列化异常。显然此时Tx_A和Tx_B处于序列化冲突之中,而因为发现Tx_B仍然在执行中,因此,容许Tx_A commit。

T6: Tx_B commit; 因为序列化异常,且和Tx_B存在序列化冲突的Tx_A已经被提交。所以commit失败,状态为abort。

更多更复杂的例子,能够参考这里.

前面在讨论SIREAD LOCK时,咱们谈到对于index的处理时,SIREAD LOCK的最小粒度是page。这个特性会致使意想不到的序列化异常。更专业的说法是"False-Positive Serialization Anomalies"。简而言之实际上并无发生序列化异常,可是咱们的SSI机制不完善,产生了误报。

下面咱们来举例说明。

对于上图,若是SQL语句走的是sequential scan,情形以下:

若是是index scan呢?仍是有可能出现误报:


这篇就是这样。依然仍是有不少问题没有讲清楚。留待下次再说吧(拖延症晚期)。

相关文章
相关标签/搜索