7. 事务处理与并发控制
7.8 锁管理机制
我们平时说的表锁、页锁、咨询锁等等(行锁除外),实际上都是常规锁根据不同锁定对象划分的子类。 例如,如果要对一个表进行操作,通常会通过heap_open打开这个表,并在打开时指定需要的锁模式。之后会有一系列函数将锁模式传递下去,最终通过LockRelationOid函数将表的Oid和lockmode联系在一起。
# define heap_open(r,l)
Relation table_open(Oid relationId, LOCKMODE lockmode)
Relation relation_open(Oid relationId, LOCKMODE lockmode)
void LockRelationOid(Oid relid, LOCKMODE lockmode)
当然,常规锁不仅可以对表加锁,也可以对各类对象加锁。LOCKTAG确定一个对象。
pg里还有一种对于锁管理的设计:
强锁(5-8)弱锁(1-3)和fastpath
https://blog.youkuaiyun.com/Hehuyi_In/article/details/124776984
弱锁只保存在当前会话,从而避免频繁访问共享内存的主锁表,提高数据库的性能。虽然判断是否有强锁也需要访问共享内存中的FastPathStrongRelationLocks,但这种访问粒度比较小。
冲突检测:
LockCheckConflicts:
LockCheckConflicts函数主要是检查当前申请的锁模式,是否与其他事务已持有的锁冲突
如果通过冲突检测发现可以获得锁,则通过GrantLock和GrantLocalLock函数增加锁的引用计数。如果发现不能获得锁,则进入等待状态。
等待状态的判断通过WaitOnLock函数实现,调用关系是WaitOnLock函数 -> ProcSleep函数。ProcSleep函数一方面将当前事务加入等待队列,另一方面还要做死锁检测。
小结一下:
spin锁支持LwLock,Lwlock用来保护共享内存,Regular锁保护的是数据库对象,下层也会作用到一块内存中(即调用LwLock)
下面看一下锁操作:
表锁不继续了:注意的是会话锁是跨事务存在的,应用范围更广。
页粒度锁(暂不了解):
元组粒度(本质上行锁是由常规锁+xmax结合实现):
有两种会用到行锁:
- 对行执行update,delete
- 显示指定行锁 select for update/for share / for no key share / for no key update;
对应源码lockoptions.h
/*
* Possible lock modes for a tuple.
*/
typedef enum LockTupleMode
{
/* SELECT FOR KEY SHARE */
LockTupleKeyShare,
/* SELECT FOR SHARE */
LockTupleShare,
/* SELECT FOR NO KEY UPDATE, and UPDATEs that don't modify key columns */
LockTupleNoKeyExclusive,
/* SELECT FOR UPDATE, UPDATEs that modify key columns, and DELETE */
LockTupleExclusive
} LockTupleMode;
这里需要建立常规所的映射关系:tupleLockExtraInfo
static const struct
{
LOCKMODE hwlock;
int lockstatus;
int updstatus;
}
tupleLockExtraInfo[MaxLockTupleMode + 1] =
{
{ /* LockTupleKeyShare */
AccessShareLock,
MultiXactStatusForKeyShare,
-1 /* KeyShare does not allow updating tuples */
},
{ /* LockTupleShare */
RowShareLock,
MultiXactStatusForShare,
-1 /* Share does not allow updating tuples */
},
{ /* LockTupleNoKeyExclusive */
ExclusiveLock,
MultiXactStatusForNoKeyUpdate,
MultiXactStatusNoKeyUpdate
},
{ /* LockTupleExclusive */
AccessExclusiveLock,
MultiXactStatusForUpdate,
MultiXactStatusUpdate
}
};
其实这些行锁模式的兼容性是怎么来的,就是根据对应的表锁兼容性来的。例如一个delete操作,它在表上加的是3级锁,但在行上加的是8级锁。因此在表上该操作是兼容的(可以同时对一个表进行delete),但在行上是冲突的(不可以同时delete同一行)。
- xmax设置:tuple上的事务信息
- MultiXactId
通常如果只有一个事务增加行锁,那么直接将行的xmax设为事务id,并在infomask中设置对应锁类型即可。
但select…for…语句中可以加行级共享锁(普通的select不行),即可以有多个事务对一个元组加共享锁(保证别的不能更改行,只能读),这时就没法通过将行的xmax设为事务id来表示了。为此,pg将多个事务组成一个mXactCacheEnt(multixact.c文件),并为其指定唯一的MultiXactId,此时在xmax处保存的就是MultiXactId。
为了区分xmax设置的是事务id还是MultiXactId,在使用MultiXactId时会在元组上增加HEAP_XMAX_IS_MULTI标记。
ps: 可以查看tuple的xmin和xmax:
gaussdb=# create table t1(a int primary key, b int);
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
CREATE TABLE
gaussdb=# insert into t1 values(1, 2);
INSERT 0 1
gaussdb=# insert into t1 values(3, 4);
INSERT 0 1
gaussdb=# select xmin, xmax, * from t1;
xmin | xmax | a | b
--------+------+---+---
219833 | 0 | 1 | 2
220116 | 0 | 3 | 4
(2 rows)
- 行锁标记位
- 如果元组的xmax是事务id,需要通过infomask标记位区分元组的加锁情况。
在htup_details.h中
#define HEAP_XMAX_KEYSHR_LOCK 0x0010 /* for key share子句对应的锁 */
#define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker,排他锁标记位 */
/* xmax is a shared locker */
#define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMAX_LOCK_ONLY 0x0080 /* xmax, if valid, is only a locker,显式加行锁 */
#define HEAP_XMAX_IS_MULTI 0x1000 /* t_xmax is a MultiXactId */
#define HEAP_KEYS_UPDATED 0x2000 /* tuple was updated and key cols */
...
- 如果元组的xmax是MultiXactId,则每种子句都对应一种锁模式(它们的对应关系通过tupleLockExtraInfo也可以看出来)
/*
* Possible multixact lock modes ("status"). The first four modes are for
* tuple locks (FOR KEY SHARE, FOR SHARE, FOR NO KEY UPDATE, FOR UPDATE); the
* next two are used for update and delete modes.
*/
typedef enum
{
MultiXactStatusForKeyShare = 0x00,
MultiXactStatusForShare = 0x01,
MultiXactStatusForNoKeyUpdate = 0x02,
MultiXactStatusForUpdate = 0x03,
/* an update that doesn't touch "key" columns */
MultiXactStatusNoKeyUpdate = 0x04,
/* other updates, and delete */
MultiXactStatusUpdate = 0x05
} MultiXactStatus;
7.9 死锁处理机制
暂不深入
7.10 多版本并发控制
基本概念
以update为例:
- 元组状态类型判断:
首先看返回码:
/*
* Result codes for table_{update,delete,lock_tuple}, and for visibility
* routines inside table AMs.
*/
typedef enum TM_Result
{
TM_Ok, // 成功:update/delete performed, lock was acquired
TM_Invisible, // 元组对当前快照根本不可见,自然无法处理
TM_SelfModified, // 元组被当前事务更新过 通常需要自增事务id解决
TM_Updated, // 元组被已提交事务更新过, 这就说明这个tuple可能被移动到了其他地方(ctid指向)
TM_Deleted, // 元组被已提交事务删除过
TM_BeingModified, // 元组正在被其他(session的)事务更新
TM_WouldBlock // lock couldn't be acquired, action skipped. Only used by lock_tuple
} TM_Result;
- 元组更新函数
元组的操作都在heapam.c中
* INTERFACE ROUTINES
* heap_beginscan - begin relation scan
* heap_rescan - restart a relation scan
* heap_endscan - end relation scan
* heap_getnext - retrieve next tuple in scan
* heap_fetch - retrieve tuple with given tid
* heap_insert - insert tuple into a relation
* heap_multi_insert - insert multiple tuples into a relation
* heap_delete - delete a tuple from a relation
* heap_update - replace a tuple in a relation with another tuple
下面分析heap_upadated:
占个坑。太多了。
解决读写冲突
一般MVCC有2种实现方法:
- 写新数据时,把旧数据快照存入其他位置(如oracle的回滚段、sqlserver的tempdb)。当读数据时,读的是快照的旧数据。
- 写新数据时,旧数据不删除,直接插入新数据。PostgreSQL就是使用的这种实现方法。
7.10.1 heaptuple 结构
txid是事务管理器为事务分配的一个txid,唯一标志符。
使用可以查询:
select txid_current();
三个特殊的txid
- 0:InvalidTransactionId,表示无效的事务ID
- 1:BootstrapTransactionId,表示系统表初始化时的事务ID,比任何普通的事务ID都旧。
- 2:FrozenTransactionId,冻结的事务ID,比任何普通的事务ID都旧。
大于2的事务ID都是普通的事务ID。
txid间可以相互比较大小,任何事务只可见txid<其自身txid的事务修改结果。
看一下tuple:
- t_xmin:保存插入该元组的事务txid(该元组由哪个事务插入)
- t_xmax:保存更新或删除该元组的事务txid。若该元组尚未被删除或更新,则t_xmax=0,即invalid
- t_cid:保存命令标识(command id,cid),指在该事务中,执行当前命令之前还执行过几条sql命令(从0开始计算)
- t_ctid:一个指针,保存指向自身或新元组的元组的标识符(tid)。
当更新该元组时,t_ctid会指向新版本元组。若元组被更新多次,则该元组会存在多个版本,各版本通过t_ctid串联,形成一个版本链。通过这个版本链,可以找到最新的版本。t_ctid是一个二元组(页号,页内偏移量),其中页号从0开始,页内偏移量从1开始。
对应结构体:
- DatumTupleFields
/* We store five "virtual" fields Xmin, Cmin, Xmax, Cmax, and Xvac in three
* physical fields. Xmin and Xmax are always really stored, but Cmin, Cmax
* and Xvac share a field.
*/
typedef struct HeapTupleFields
{
TransactionId t_xmin; /* inserting xact ID,插入该元组的事务id */
TransactionId t_xmax; /* deleting or locking xact ID,删除或锁定该元组的事务id */
union
{
CommandId t_cid; /* inserting or deleting command ID, or both,插入或删除(或者两种兼有)该元组的命令id */
TransactionId t_xvac; /* old-style VACUUM FULL xact ID,对该元组执行vacuum full操作的事务id */
} t_field3;
} HeapTupleFields;
- DatumTupleFields
typedef struct DatumTupleFields
{
int32 datum_len_; /* varlena header (do not touch directly!),变长header */
int32 datum_typmod; /* -1, or identifier of a record type,-1或者表示记录类型 */
Oid datum_typeid; /* composite type OID, or RECORDOID,复合类型oid或者记录oid */
} DatumTupleFields;
- HeapTupleHeaderData
struct HeapTupleHeaderData
{
union
{
HeapTupleFields t_heap; // 主要用于元组可见性检查
DatumTupleFields t_datum;
} t_choice;
ItemPointerData t_ctid;
/* Fields below here must match MinimalTupleData! */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2 2
uint16 t_infomask2; /* number of attributes + various flags */
#define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK 3
uint16 t_infomask; /* various flag bits, see below */
#define FIELDNO_HEAPTUPLEHEADERDATA_HOFF 4
uint8 t_hoff; /* sizeof header incl. bitmap, padding */
#define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5
bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs,23 bytes */
/* MORE DATA FOLLOWS AT END OF STRUCT */
};
字段说明:
① t_choice:具有两个成员的联合类型:
t_heap:用于记录对元组执行插入/删除操作的事务ID和命令ID,这些信息主要用于并发控制时检查元组对事务的可见性。
t_datum:当一个新元组在内存中形成的时候,我们并不关心其事务可见性,因此在t_choice中只需用DatumTupleFields结构来记录元组的长度等信息。但在把该元组插入到表文件时,需要在元组头信息中记录插入该元组的事务和命令ID,故此时会把t_choice所占用的内存转换为HeapTupleFields结构并填充相应数据后再进行元组的插入。
② t_ctid:一个指针,保存指向自身或新元组的元组的标识符(tid)。当更新该元组时,t_ctid会指向新版本元组。若元组被更新多次,则该元组会存在多个版本,各版本通过t_cid串联,形成一个版本链。
③ t_infomask2:其低11位表示当前元组的属性个数,其他位则用于包括用于HOT技术及元组可见性的标志位。
④ t_infomask:用于标识元组当前的状态,比如元组是否具有OID、是否有空属性等,t_infomask的每一位对应不同的状态,共16种状态。
⑤ t_hoff:该元组头的大小。
⑥ t_bits[]数组:标识该元组哪些字段为空。
- ItemIdData
typedef struct ItemIdData
{
unsigned lp_off:15, /* offset to tuple (from start of page),元组在页面的偏移量 */
lp_flags:2, /* state of line pointer, see below,元组的line pointer状态,参考下面 */
lp_len:15; /* byte length of tuple,元组长度 */
} ItemIdData;
typedef ItemIdData *ItemId;
7.10.2 快照和隔离级别
快照是记录数据库当前瞬时状态的一个数据结构。pg的快照主要保存:当前所有活跃事务的最小、最大事务ID、当前活跃事务列表、commandID等。
快照可以分为多种类型,每种快照类型都对应了一种判断元组可见性的方法。
- 快照结构体
typedef struct SnapshotData *Snapshot;
typedef struct SnapshotData
{
SnapshotType snapshot_type; /* type of snapshot,快照类型 */
/* 用于MVCC控制 */
TransactionId xmin; /* all XID < xmin are visible to me,若事务ID小于xmin,则对当前事务可见 */
TransactionId xmax; /* all XID >= xmax are invisible to me,若事务ID大于xmax,则对当前事务不可见*/
/* 快照生成时对应的活跃事务列表 */
TransactionId *xip;
uint32 xcnt; /* # of xact ids in xip[],xip数组大小 */
/* 活跃的子事务列表 */
TransactionId *subxip;
int32 subxcnt; /* # of xact ids in subxip[],sub xip数组大小*/
bool suboverflowed; /* has the subxip array overflowed? 当子事务过多时,数组有可能overflow */
bool takenDuringRecovery; /* recovery-shaped snapshot? */
bool copied; /* false if it's a static snapshot,如果是静态快照则为false */
CommandId curcid; /* in my xact, CID < curcid are visible */
/*
* An extra return value for HeapTupleSatisfiesDirty, not used in MVCC
* snapshots.,HeapTupleSatisfiesDirty使用的一个变量,在判断可见性的同时,会借助这个变量将元组上的speculativeToken返回给上层
*/
uint32 speculativeToken;
/*
* For SNAPSHOT_NON_VACUUMABLE (and hopefully more in the future) this is
* used to determine whether row could be vacuumed.元组是否可vacuum(用于SNAPSHOT_NON_VACUUMABLE状态)
*/
struct GlobalVisState *vistest;
/*
* Book-keeping information, used by the snapshot manager
*/
uint32 active_count; /* refcount on ActiveSnapshot stack */
uint32 regd_count; /* refcount on RegisteredSnapshots */
pairingheap_node ph_node; /* link in the RegisteredSnapshots heap */
TimestampTz whenTaken; /* timestamp when snapshot was taken,快照生成时间 */
XLogRecPtr lsn; /* position in the WAL stream when taken,快照生成时事务REDO日志的LSN*/
/*
* The transaction completion count at the time GetSnapshotData() built
* this snapshot. Allows to avoid re-computing static snapshots when no
* transactions completed since the last GetSnapshotData().
*/
uint64 snapXactCompletionCount;
} SnapshotData;
#endif /* SNAPSHOT_H */
- 主要字段含义:
快照中最重要的xmin, xmax,xip,用来表示当前系统中所有事务所处状态。
testdb=# SELECT txid_current_snapshot();
txid_current_snapshot
-----------------------
100:104:100,102
(1 row)
这3个值就是 xmin:xmax:xip[]数组,注意不要与元组的t_xmin和t_xmax弄混
Snapshot->xmin:当前所有活跃事务中最小的事务txid(所以只有一个事务的时候就是自己本身),所有比它更早的事务(xid<xmin),要么已提交要么已回滚,因此均可见。
Snapshot->xmax:Snapshot->xmax=latestCompletedXid+1,latestCompletedXid记录的是最大的已提交事务id,因此Snapshot->xmax就比所有已提交事务的事务id都要大。如果某个事务xid>= Snapshot->xmax,说明在快照创建时这个事务还是活跃事务(或尚未启动)因此其结果对当前事务不可见。
xip[]数组:快照创建时,所有活跃事务的事务id列表,该列表仅包含[xmin,xmax)范围内的活跃事务xid。
例子:
100💯 含义如下
xmin=100,所以txid<100的事务均不活跃
xmax=100,所以txid>=100的事务均活跃或未启动
xip_list为空,表示[xmin,xmax)范围内无活跃事务
100:104:100,102含义如下
xmin=100,所以txid<100的事务均不活跃
xmax=104,所以txid>=104的事务均活跃或未启动
xip_list为100,102,表示[xmin,xmax)范围内100,102为活跃事务
- 快照和隔离级别
pg会根据不同隔离级别设置,获取不同时刻的快照:
- 已提交读(只会读到commited的数据):在该事务的每条SQL执行之前都会重新获取一次快照
- 可重复读和可串行化(保证一个事务中两次读到的东西一样):该事务只在第一条SQL执行之前获取一次快照
注意这里Transaction C这里是可重复读,所以所有sql的快照都是一样的。
查看数据库的隔离级别:
SELECT name, setting FROM pg_settings WHERE name ='default_transaction_isolation';
-- 或
SELECT current_setting('default_transaction_isolation');
4, 快照类型
就是有的快照不是用于常规的MVCC判断的,有一些特殊的snapshot有其他的可见性规则。
typedef enum SnapshotType
{
SNAPSHOT_MVCC = 0,
SNAPSHOT_SELF,
SNAPSHOT_ANY,
SNAPSHOT_TOAST,
SNAPSHOT_DIRTY,
SNAPSHOT_HISTORIC_MVCC,
SNAPSHOT_NON_VACUUMABLE
} SnapshotType;
各类型快照可见性判断条件:
7.10.3 快照管理和获取
地址:snapmgr.c
快照管理器管理快照按照两种方式:“registered” by resowner.c和active snapshot栈。
它们中的所有快照都保存在持久内存中。当快照不再在这些列表中时(由每个快照上的单独引用计数跟踪),可以释放其内存。
两个静态的快照:
static SnapshotData CurrentSnapshotData = {SNAPSHOT_MVCC};
static SnapshotData SecondarySnapshotData = {SNAPSHOT_MVCC};
SnapshotData CatalogSnapshotData = {SNAPSHOT_MVCC};
SnapshotData SnapshotSelfData = {SNAPSHOT_SELF};
SnapshotData SnapshotAnyData = {SNAPSHOT_ANY};
/* Pointers to valid snapshots */
static Snapshot CurrentSnapshot = NULL;
static Snapshot SecondarySnapshot = NULL;
static Snapshot CatalogSnapshot = NULL;
static Snapshot HistoricSnapshot = NULL;
CurrentSnapshot指向在事务快照模式(SNAPSHOT_MVCC类型)下的唯一快照,是reed-commited的最新快照。SecondarySnapshot是一个截至当前时刻始终最新的快照,即使在事务快照方式下也是如此。它只能用于特殊目的代码(例如RI检查)。CatalogSnapshot指向用于系统表扫描的MVCC快照;每当发生系统表更改时,我们都必须使其无效。
- Snapshot GetTransactionSnapshot函数:为事务中的新查询返回合适的快照。
啥是合适的?
取决于隔离级别,读已提交肯定是最新的,可重复读那肯定是事务开始的时候获取的。这里还有一个变量:
- FirstSnapshotSet初始值是false(尚无快照或者不是事务的第一个快照):这是为了加速。
GetTransactionSnapshot函数中,通过FirstSnapshotSet标志来判断当前要获得的是不是事务的第一个快照。如果是,则通过GetSnapshotData获得快照并将快照缓存。在已提交读隔离级别下,直接返回获得的快照;在可重复读及串行化隔离级别下,返回缓存的快照。
这些变量对应gaussdb都保存在u_sess->utils_cxt中(会话级的)。
u_sess->utils_cxt保存了自己获取的snapshot的信息。
在GetTransactionSnapshot中,如果获得当前最新快照,需要从smgr,也就是快照管理器中获取:GetSnapshotData(对应gaussdb的proc_array_get_snapshot_data,pg内部是一个堆在维护所有事务产生的快照,这很合理,因为事务访问只会访问currentsnapshot(top))
过程先略
7.10.4 heap 可见性判断 (这里其实很重要)
- 可见情况:
在创建快照时所有已提交的事务
本事务之前执行的命令
- 不可见情况:
在创建快照时尚活跃的事务
在创建快照后启动的事务
当前命令造成的变化(changes made by the current command)
- HeapTupleSatisfiesMVCC
主要根据xmin,xmax以及tuple的t_infomask标志位查看元组的提交/未提交,删除/未删除,事务的第几条命令提交的等。
gs里在 heapam_visibility.cpp
* Summary of visibility functions:
*
* visibility_heap_tuple_satisfies_mvcc()
* visible to supplied snapshot, excludes current command
* visibility_heap_tuple_satisfies_now()
* visible to instant snapshot, excludes current command
* visibility_heap_tuple_satisfies_update()
* like visibility_heap_tuple_satisfies_now(), but with user-supplied command
* counter and more complex result
* visibility_heap_tuple_satisfies_self()
* visible to instant snapshot and current command
* visibility_heap_tuple_satisfies_dirty()
* like visibility_heap_tuple_satisfies_self(), but includes open transactions
* visibility_heap_tuple_satisfies_vacuum()
* visible to any running transaction, used by VACUUM
* visibility_heap_tuple_satisfies_toast()
* visible unless part of interrupted vacuum, used for TOAST
* visibility_heap_tuple_satisfies_any()
* all tuples are visible
其他还有一些函数待补充:
register push等
7.11 日志管理
事务状态
pg定义了四种事务状态——IN_PROGRESS, COMMITTED, ABORTED和SUB_COMMITTED,其中SUB_COMMITTED状态用于子事务。
#define TRANSACTION_STATUS_IN_PROGRESS 0x00
#define TRANSACTION_STATUS_COMMITTED 0x01
#define TRANSACTION_STATUS_ABORTED 0x02
#define TRANSACTION_STATUS_SUB_COMMITTED 0x03
四种事务状态仅需两个bit即可记录。以一个块8KB为例,可以存储8KB*8/2 = 32K个事务的状态。内存中缓存CLOG的buffer 大小为Min(128,Max(4,NBuffers/512))。
这块略复杂,先放一下,占坑。
事务实践
进程:
1.查看数据库的进程。
SELECT * FROM pg_stat_activity WHERE datname='死锁的数据库ID';
检索出来的字段中,【wating 】字段,数据为t的那条,就是死锁的进程,找到对应的【procpid 】列的值。
例如:
SELECT procpid FROM pg_stat_activity WHERE datname='数据库ID' and waiting ='t';
查看本身进程:
select pg_backend_pid();
查看当前事务:
select txid_current();
查看事务当前快照:
SELECT txid_current_snapshot();
2.杀掉进程。
kill有两种方式,第一种是:
SELECT pg_cancel_backend(PID);
这种方式只能kill select查询,对update、delete 及DML不生效)
第二种是:
SELECT pg_terminate_backend(PID);
这种可以kill掉各种操作(select、update、delete、drop等)操作
例子:
三个会话:
- 会话1:
gaussdb=# begin;
BEGIN
gaussdb=# select pg_backend_pid();
pg_backend_pid
-----------------
139735972443904
(1 row)
gaussdb=# select txid_current();
txid_current
--------------
17526
(1 row)
gaussdb=# update t1 set a = 2 where a = 1;
UPDATE 1
此时没有commit;
- 会话2:
gaussdb=# begin;
BEGIN
gaussdb=# select pg_backend_pid();
pg_backend_pid
-----------------
139735735465728
(1 row)
gaussdb=#
gaussdb=# select txid_current();
txid_current
--------------
17529
(1 row)
gaussdb=# update t1 set a = 2 where a = 1;
- 会话3:
select locktype as lc,relation::regclass as relname,page||','||tuple as ctid,virtualxid ,transactionid as txid,virtualtransaction,pid,mode,granted from pg_locks where pid = 139735972443904;
lc | relname | ctid | virtualxid | txid | virtualtransaction | pid | mode | granted
---------------+----------+------+------------+-------+--------------------+-----------------+------------------+---------
relation | t1_pkey | , | | | 14/1423 | 139735972443904 | RowExclusiveLock | t
relation | t1 | , | | | 14/1423 | 139735972443904 | RowExclusiveLock | t
virtualxid | | , | 14/1423 | | 14/1423 | 139735972443904 | ExclusiveLock | t
transactionid | | , | | 17526 | 14/1423 | 139735972443904 | ExclusiveLock | t
(4 rows)
gaussdb=# select locktype as lc,relation::regclass as relname,page||','||tuple as ctid,virtualxid ,transactionid as txid,virtualtransaction,pid,mode,granted from pg_locks where pid = 139735735465728;
lc | relname | ctid | virtualxid | txid | virtualtransaction | pid | mode | granted
---------------+---------+------+------------+-------+--------------------+-----------------+---------------------+---------
relation | t1_pkey | , | | | 15/36 | 139735735465728 | AccessShareLock | t
relation | t1_pkey | , | | | 15/36 | 139735735465728 | RowExclusiveLock | t
relation | t1 | , | | | 15/36 | 139735735465728 | AccessShareLock | t
relation | t1 | , | | | 15/36 | 139735735465728 | RowExclusiveLock | t
virtualxid | | , | 15/36 | | 15/36 | 139735735465728 | ExclusiveLock | t
tuple | t1 | 0,1 | | | 15/36 | 139735735465728 | AccessExclusiveLock | t
transactionid | | , | | 17526 | 15/36 | 139735735465728 | ShareLock | f
transactionid | | , | | 17529 | 15/36 | 139735735465728 | ExclusiveLock | t
(8 rows)
几点:
- 每个事务是获得自己virtualxid和transactionid的ExclusiveLock锁的;
- 当某个进程发现需要等待另一个事务结束时,它还会尝试在另一个事务ID上加上share锁(可能是虚拟事务ID,也可能是永久事务ID)。仅当另一个事务终止并释放锁时,操作才会成功。即第二条sql的第7条数据,这里的grant是f,即还没获取到,因为会话1没有commit。