PostgreSQL技术内幕:事务处理深度探索
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.8 行锁

PostgreSQL数据库采用MVCC方式进行并发控制,读写不会互相阻塞,而写和写之间仍然存在冲突,因此如果并发写同一个元组,就只能有一个事务进行修改,其他事务必须等待,直到先修改的事务提交或回滚。虽然用常规锁来对元组加锁也能防止多个事务同时修改同一个元组,但是常规锁被保存在锁表中,锁表在共享内存中的大小是有限的。如果一个事务修改了大量的元组,那么就需要在事务中申请大量的行级常规锁,锁表的规模就会增大,导致性能劣化,这是难以接受的。PostgreSQL采用了常规锁(元组级常规锁)和行锁(xmax)结合的方式来实现行锁。

在PostgreSQL数据库中,通常有两种方式对元组进行加锁,第一种方式是对元组做UPDATE/DELETE操作,另一种方式是通过显式地指定行锁,例如SELECT查询时指定了FOR UPDATE子句。在老版本的PostgreSQL中,只有FOR UPDATE和FOR SHARE两种方式显式地增加行锁;为了提高并发,PostgreSQL又增加了FOR KEY SHARE和FOR NO KEY UPDATE两种“弱”一些的行锁,因此目前显式加行锁有4个等级,这4个等级和LockTupleMode中的锁类型一一对应,如表2-7所示。

表2-7 显式加行锁的子句

行锁的锁相容性矩阵如表2-8所示。

表2-8 行锁的相容性矩阵

UPDATE、DELETE操作同样需要对元组进行加锁,它们的加锁类型如表2-9所示。

表2-9 DELETE和UPDATE对应的行锁

行级锁是由常规锁和xmax相结合实现的。对于常规锁,它需要建立LockTupleMode和常规锁之间的映射关系。

tupleLockExtraInfo中除了保存常规锁对应的锁模式,还保存了MultiXact中的锁模式。通常如果只有一个事务增加行锁,那么直接在xmax处设置这个事务的ID,并且在infomask中设置对应的锁类型即可。但是在显式加行锁的SELECT…FOR…子句中,可以加行级共享锁。这时多个事务可以对一个元组加共享锁,xmax无法同时表达多个事务,因此将多个事务组合在一起形成一个mXactCacheEnt。并为其指定一个唯一的MultiXactId,在xmax处保存的就是MultiXactId。为了区分事务ID和MultiXactId,在使用MultiXactId时会在元组上增加HEAP_XMAX_IS_MULTI标记。

如果元组的xmax是事务ID,则还需要通过元组infomask中的标记位来区分当前元组的加锁情况,行锁的标记位和说明如表2-10所示。

表2-10 行锁的标记位和说明

行锁和标记位的反向对应关系如表2-11所示。

表2-11 行锁和标记位的反向对应关系

如果xmax中是MultiXactId,则每种子句都对应一种锁模式(它们的对应关系通过tupleLockExtraInfo也可以看出来)。

为了保存MultiXactId和事务的映射关系,PostgreSQL使用两个SLRU进行分层映射,它们位于$PGDATA/pg_multixact目录下,分别是offsets目录和members目录,如图2-19所示。

图2-19 pg_multixact中的文件结构

我们以更新操作为例来说明PostgreSQL数据库在更新操作时的可见性判断过程,实际上删除操作和更新操作同理。在并发更新时,会判断当前元组的状态,元组的状态类型如表2-12所示。根据不同的元组状态决定继续进行何种操作,状态的判断是由HeapTupleSatisfiesUpdate函数来完成的。

表2-12 元组的状态类型

元组是否能够被更新,取决于是否可见。不可见的元组显然是无须做更新操作的,例如事务A插入元组t之后异常终止,元组t仍会在数据页面中存在(还没有做Vacuum清理);当事务B做更新操作时,需要判断元组t的可见性,由于事务A最终没有提交,元组t是不可见的,所以事务B也就无须更新元组t。

一个更新操作代表的是一个Command,自然这个更新操作也会有自己事务内部的CommandID。假如更新操作在扫描元组的过程中,发现一个元组的事务ID是当前事务,而它的CommandID却大于快照的CommandID,则代表在启动更新操作之后,又有本事务的其他操作修改了这个元组,此时这个元组的状态是TM_SelfModified。

例如,有一个触发器,在更新每个元组之前,都尝试删除元组,就会导致元组的实际CommandID大于更新操作的CommandID。

假如元组正在被其他事务更新,那么这个元组的状态就是TM_BeingModified。在这个状态下,我们需要等待那个正在操作的事务完成之后,才能继续修改元组。更新元组的事务会在元组的头部设置元组的xmax,那么目前xmax就成了各个进程之间进行通信的行锁。先设置xmax的更新事务将xmax设置为自己的事务ID,也就获得了这个元组资源,其他事务必须等待xmax事务结束,如图2-20所示。

图2-20 行锁的等待关系

如果元组已经被更新,那么就会进入TM_Updated状态,元组的更新可能导致新元组不再满足之前的过滤条件,因此需要重新检查新元组是否还满足我们的更新要求(PostgreSQL采用EPQ即EvalPlanQual机制进行条件检查)。