PostgreSQL 随读笔记-事务


7. 事务处理与并发控制

7.1 事务系统简介

  • 事务管理器
    是事务系统的中枢,本事是一个FSM。通过接受外部系统的命令或者信号,再根据当前事务所处的状态决定事务的下一步执行过程。
  • 锁管理器
    读阶段采用了MVCC。保证读写互不阻塞,写阶段需要由各种锁保证事务隔离级别。
  • 日志
    事务提交日志CLOG(记录执行结果和状态)和事务日志XLOG(保持了一定的冗余数据)

PostgreSQL在接到语句后,会fork出一个子进程postgres处理。首先会从总控进入事务块环境中,如果没有一个事务信息,调用底层事务处理函数启动一个事务,返回上层。Begin、RollBack、end会改变事务块状态。

7.2 事务系统上层

事务块实现数据库内部底层事务和终端命令的交互。一般来说,大家所认为的事务对应着数据库理论中的命令概念,反应一个SQL(命令)的执行状态。实际上“事务块”才对应着数据库理论中的“事务”。一个事务块包含着多个事务,所以事务块状态数量要比底层事务的状态数量多得多。

  • pg中事务块:DB理论中的事务
  • pg中事务:事务块中sql语句

执行一条SQL语句前会调用:StratTransactionCommand。执行结束调用CommitTransactionCommand,如果命令执行失败,调用AbortCurrentTransaction。这三个函数根据事务块的状态,执行不同的操作,调用不同的底层事务执行函数。

7.2.1 事务块状态

默认情况下,PostgreSQL中一个事务处理一条SQL语句,事务块使得一个事务能够处理多条SQL语句。每一个Postgres进程中,至始至终只存在一个事务块,当用户开始一个新的事务时,并不进入新的事务块,而是修改事务块的状态使之重新开始记录用户“事务”的状态。

事务块状态是指整个事务系统的状态,反应当前用户输入命令的变化过程和事务底层执行状态的变化。Begin、RollBack、end会改变事务块状态,底层事务提交或者终止也会改变该状态。事务块状态:TBlockState

7.2.2 事务块操作

任何语句通过事务块入口函数进入并执行,执行完毕后,通过事务块出口函数退出。

  1. 事务块基本操作
    3个:
  • StartTransactionCommand
    每条语句执行前,都需要执行该函数,通过判断当前事务块状态进入不同的底层事务执行函数。例如当前事务块状态为TBLOCK_DEFAULT,调用当前函数时,启动一个事务设置事务块状态为TBLOCK_STARTED。
  • CommitTransactionCommand
    每条语句执行后,都需要执行该函数,通过判断当前事务块状态进入不同的底层事务执行函数。例如当前TBLOCK_DEFAULT,调用该函数时说明此事系统没有任何事务信息,由于每个语句都是在事务中执行的,所以调用当前函数处于一个非法状态,打印错误信息并返回。
  • AbortCurrentTransaction
    系统遇到错误时,会返回调用点执行该函数。假设【事务块】当前TBLOCK_DEFAULT,系统执行遇到错误,返回到sigsetjmp函数,然后调用该函数进行出错处理,如果底层事务的状态是TRANS_DEFAULT,说明系统中没有任何事务信息,不需要进行事务出错处理,直接返回;如果当前底层事务状态为TRANS_START,则退出事务块需要调用AbortTransaction,再CleanupTransaction。
  1. 事务块状态改变
  • BeginTransactionBlock (Begin语句执行)
    执行begin命令,会进入事务块,此时调用该函数,将事务块状态从 TBLOCK_STARTED 到 TBLOCK_BEGIN,如果遇到其他状态都是不合理的。

  • EndTransactionBlock (End语句执行/commit)
    接受到end命令,调用该函数,事务块结束。可能由commit导改致,也可能是rollback,根据返回值来确定系统动作。
    该函数调用:判断当前事务块状态。如果事务块当前状态TBLOCK_INPROGRESS则把当前事务块状态改为TBLOCK_END,返回值设为TRUE;如果当前事务失败,则事务块状态为TBLOCK_ABORT,则需要退出该事务块,事务块状态设置为TBLOCK_ABORT_END,返回false。

  • UserAbortTransactionBlock (Rollback语句执行)
    执行用户提交的一个rollback命令会进入该函数。

    其他错误场景:

chazhangtu

总结:事务块状态

事务块状态 标志
默认 TBLOCK_DEFAULT
已开始 TBLOCK_STARTED
事务块开启 TBLOCK_BEGIN
事务块运行中 TBLOCK_INPROGRESS
事务块结束 TBLOCK_END
回滚 TBLOCK_ABORT
回滚结束 TBLOCK_ABORT_END
回滚等待 TBLOCK_ABORT_PENDING

在无异常情形下,一个事务块的状态按照默认(TBLOCK_DEFAULT)->已开始(TBLOCK_STARTED)->事务块开启(TBLOCK_BEGIN)->事务块运行中(TBLOCK_INPROGRESS)->事务块结束(TBLOCK_END)->默认(TBLOCK_DEFAULT)循环。剩余的状态机是在上述正常场景下的各个状态点的异常处理分支。

(1) 在进入事务块运行中(TBLOCK_INPROGRESS)之前出错,因为事务还没有开启,直接报错并回滚,清理资源回到默认(TBLOCK_DEFAULT)状态。

(2) 在事务块运行中(TBLOCK_INPROGRESS)出错分为2种情形。事务执行失败:事务块运行中(TBLOCK_INPROGRESS)->回滚(TBLOCK_ABORT)->回滚结束(TBLOCK_ABORT_END)->默认(TBLOCK_DEFAULT);用户手动回滚执行成功的事务:事务块运行中(TBLOCK_INPROGRESS)->回滚等待(TBLOCK_ABORT_PENDING)->默认(TBLOCK_DEFAULT)。

(3) 在用户执行COMMIT语句时出错:事务块结束(TBLOCK_END)->默认(TBLOCK_DEFAULT)。由图2可以看出,事务开始后离开默认(TBLOCK_DEFAULT)状态,事务完全结束后回到默认(TBLOCK_DEFAULT)状态。

(4) 隐式事务块,当客户端执行单条SQL语句时可以自动提交,其状态机相对比较简单:按照默认(TBLOCK_DEFAULT)->已开始(TBLOCK_STARTED)->默认(TBLOCK_DEFAULT)循环。

7.3 事务系统底层

底层事务的状态:也就是事务块中一条sql语句真正的状态:

7.3.1 事务状态

/**  transaction states - transaction state from server perspective*/
typedef enum TransState
{TRANS_DEFAULT,             /* idle */TRANS_START,              /* transaction starting */TRANS_INPROGRESS,         /* inside a valid transaction */TRANS_COMMIT,               /* commit in progress */TRANS_ABORT,                /* abort in progress */TRANS_PREPARE                /* prepare in progress */
} TransState;

由于子事务的引入,一个事务中可能会有多个层级的子事务。pg使用一个事务栈来保存每个层级子事务的状态,这个事务栈的结构体是 TransactionStateData:

typedef struct TransactionStateData
{FullTransactionId fullTransactionId;   /* my FullTransactionId */SubTransactionId subTransactionId;    /* my subxact ID */char    *name;           /* savepoint name, if any */int         savepointLevel; /* savepoint level */TransState state;          /* low-level state */TBlockState blockState;        /* high-level state 事务块状态 */int         nestingLevel;   /* transaction nesting depth */int          gucNestLevel;   /* GUC context nesting depth */MemoryContext curTransactionContext; /* my xact-lifetime context */ResourceOwner curTransactionOwner;    /* my query resources */TransactionId *childXids;   /* subcommitted child XIDs, in XID order */int          nChildXids;     /* # of subcommitted child XIDs */int           maxChildXids;   /* allocated size of childXids[] */Oid          prevUser;       /* 记录前一个 CurrentUserId(用户名) */int         prevSecContext; /* previous SecurityRestrictionContext */bool       prevXactReadOnly;   /* entry-time xact r/o state */bool     startedInRecovery;  /* did we start in recovery? */bool     didLogXid;      /* has xid been included in WAL record? */int           parallelModeLevel;  /* Enter/ExitParallelMode counter */bool        chain;          /* start a new block after this one */bool      assigned;       /* assigned to top-level XID */struct TransactionStateData *parent; /* back link to parent */
} TransactionStateData;
  • TBlockState blockState:就是这里介绍的上层(事务块状态层),本质就是一个状态机,不做实质性的操作;
  • TransState state:事务真正状态;
  • nestingLevel: 当前事务嵌套级别,顶层为1,开启子事务PushTransaction,子事务的nestingLevel就在父基础上+1;
  • curTransactionOwner:指针,记录当前事务占有资源
  • 其他:略

7.3.2 事务操作函数

事务实际操作函数由StartTransaction、CommitTransaction、AbortTransaction、CleanupTransaction等函数。

1. 启动事务(StartTransaction)

调用该函数一定是顶层事务,子事务启动会调用StartSubTransaction

1)将全局变量CurrentTransactionState指向TopTransactionStateData(一个初始化好的空间)设置底层事务状态state = TRANS_START;

2)重置事务状态变量:

 s->nestingLevel = 1;s->gucNestLevel = 1;s->childXids = NULL;s->nChildXids = 0;s->maxChildXids = 0;GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext); // 继承context初始化事务内计数器等...

3)分配内存和资源跟踪器初始化:AtStart_Memory 和 AtStart_ResourceOwner。前者CurrentTransactionState->curTransactionContext指向内存管理器新分配的内存,后者初始化CurrentTransactionState->curTransactionOwner,里面包括打开的锁、snapshot的引用等。

4)产生事务标识XID给当前事务。

通常只读事务不会申请事务ID,只有涉及写操作时才会分配事务ID。事务会在执行第一个含有写操作的语句时分配事务ID。不过,即使没有事务id,事务也会用一个虚拟事务id来代表自己。虚拟事务id由两部分组成:backendId(后台进程id,会话独有)+ localTransactionId(进程维护的本地事务id,也就是说vxid是在本进程中递增,而不是全局进程递增),以下结构体代码在 lock.h:

typedef struct
{BackendId  backendId;      /* backendId from PGPROC */LocalTransactionId localTransactionId;   /* lxid from PGPROC */
} VirtualTransactionId;

backendId是通过MyBackendId继承(与select pg_backend_pid()一一对应);localTransactionId通过GetNextLocalTransactionId进行分配,就是一个全局变量一直+++。

MyBackendId在初始化Postgres进程被赋值:

创建完成之后把localTransactionId插入当前进程队列PGPROC中:

void
VirtualXactLockTableInsert(VirtualTransactionId vxid)
{Assert(VirtualTransactionIdIsValid(vxid));LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);Assert(MyProc->backendId == vxid.backendId);Assert(MyProc->fpLocalTransactionId == InvalidLocalTransactionId);Assert(MyProc->fpVXIDLock == false);MyProc->fpVXIDLock = true;MyProc->fpLocalTransactionId = vxid.localTransactionId;LWLockRelease(&MyProc->fpInfoLock);
}

5)设置时间戳
设置事务开始时间=命令开始时间,并初始化事务结束时间=0

6)初始化GUC,cache等结构

7)把事务状态设置为TRANS_INPROGRESS

2. 提交事务(CommitTransaction)

1)检查事务状态:确保事务状态为TRANS_INPROGRESS
2)触发所有延迟触发器:SQL中after触发器
3)关闭大对象AtEOXact_LargeObject
4)如果改了pg_database、pg_authid、pg_auth_members,则执行更新操作通知Postmaster进程
5)提交事务,TRANS_COMMIT,执行RecordTransactionCommit,将日志写会磁盘,返回最后处理完的当前事务或者子事务的xid
6)清理,略

3. 退出事务(AbortTransaction)

主要是系统遇到错误时调用,关中断,释放所有资源设置TRANS_ABORT等

4. 清理事务

7.4 事务保存点和子事务

savepoint,一种机制,用于回滚部分事务。必要的时候可以rollback transaction to语句回滚到该保存点。

具体来说在执行definesavepoint这个函数调用PushTransaction:

保存点涉及到的函数

xact.c:
static void PushTransaction(void); // 为子事务创建一个TransactionState并压入事务状态堆栈中,currentTransactionState切换到新创建的事务状态
static void PopTransaction(void); //出栈

这里PushTransaction会把block的状态改成 TBLOCK_SUBBEGIN;

子事务涉及函数:

static void StartSubTransaction(void);
static void CommitSubTransaction(void);
static void AbortSubTransaction(void);
static void CleanupSubTransaction(void);

在执行savepoint语句时(CommitTransactionCommand)时会调用StartSubTransaction设置blockstate为TBLOCK_SUBINPROGRESS,TBLOCK_SUBBEGIN这里在pushTransaction的时候设置的。

 /** We were just issued a SAVEPOINT inside a transaction block.* Start a subtransaction.  (DefineSavepoint already did* PushTransaction, so as to have someplace to put the SUBBEGIN* state.)*/
case TBLOCK_SUBBEGIN:StartSubTransaction();s->blockState = TBLOCK_SUBINPROGRESS;break;

通常上层事务在创建过程中可以得到一个XID,用于标志自己,但不对数据库写操作,就没必要单独获得事务或者子事务XID。只有GetCurrentTransactionId函数才分配XID。

根据CurrentTransactionState的vxid决定分配:
GetCurrentTransactionId->AssignTransactionId
注意:子任务分配XID时,会递归分配为父亲分配xid,这样保证子事务的xid大于parent:

AssignTransactionId:
/** Ensure parent(s) have XIDs, so that a child always has an XID later* than its parent.  Mustn't recurse here, or we might get a stack* overflow if we're at the bottom of a huge stack of subtransactions none* of which have XIDs yet.*/if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId)){TransactionState p = s->parent;TransactionState *parents;size_t       parentOffset = 0;parents = palloc(sizeof(TransactionState) * s->nestingLevel);while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId)){parents[parentOffset++] = p;p = p->parent;}/** This is technically a recursive call, but the recursion will never* be more than one layer deep.*/while (parentOffset != 0)AssignTransactionId(parents[--parentOffset]);pfree(parents);}

7.5 两阶段提交

pg只支持分布式数据库的两阶段提交协议,即提供相关操作接口,并没有实现整个协议。
暂不深入

7.6 PostgreSQL的并发控制

隔离级别,最小实体是元组,对元组需要实施访问控制,通过锁操作和MVCC操作。

7.7 PostgreSQL的三种锁

SpinLocl

s_lock.c

是一种和硬件结合的互斥锁,借用了硬件提供的原子操作的原语来对一些共享变量进行封锁,通常适用于临界区比较小的情况。特点是:封锁时间很短、无死锁检测机制和等待队列、事务结束时不会自动释放SpinLock。

自旋锁顾名思义就是一直原地旋转等待的锁。一个进程如果想要访问临界区,必须先获得锁资源,如果不能获得,就会一直自旋(进程不停止,一直在循环),直到获取到锁资源。

显然这种自旋会造成CPU浪费,但是通常它保护的临界区非常小,封锁时间很短,因此通常自旋比释放CPU资源带来的上下文切换消耗要小。

作为底层锁,利用其来实现LWlock轻量锁

LWLock

负责保护共享内存中的数据结构,有共享和排他两种模式,类似Oracle中的latch。特点是:封锁时间较短、无死锁检测机制、有等待队列、事务结束时会自动释放。

pg中的轻量锁类型定义在lwlocknames.h文件中(这个文件是在编译时由lwlocknames.txt生成的),在pg 14中,目前有48种轻量锁。

#define ShmemIndexLock (&MainLWLockArray[1].lock)
#define OidGenLock (&MainLWLockArray[2].lock)
#define XidGenLock (&MainLWLockArray[3].lock)
#define ProcArrayLock (&MainLWLockArray[4].lock)
#define SInvalReadLock (&MainLWLockArray[5].lock)
#define SInvalWriteLock (&MainLWLockArray[6].lock)
#define WALBufMappingLock (&MainLWLockArray[7].lock)
#define WALWriteLock (&MainLWLockArray[8].lock)
#define ControlFileLock (&MainLWLockArray[9].lock)

可以看出,各锁被保存在MainLWLockArray数组中(前48个值),每种LWLocks都有自己固定要保护的对象,使用方式如下:

LWLockAcquire(BtreeVacuumLock, LW_SHARED);
LWLockRelease(BtreeVacuumLock);

其定义如下:

typedef struct LWLock
{uint16     tranche;        /* tranche ID */pg_atomic_uint32 state;     /* state of exclusive/nonexclusive lockers */proclist_head waiters;     /* list of waiting PGPROCs */
#ifdef LOCK_DEBUGpg_atomic_uint32 nwaiters; /* number of waiters */struct PGPROC *owner;        /* last exclusive owner of the lock */
#endif
} LWLock;

State变量有32位,其中低24位作为共享锁的计数器,因此一个轻量锁最多可以有2^24个共享锁持锁者。有1位作为排他锁的标记,因为同一时间最多只能有一个持锁者。

申请:
如果等待队列中第一个申请的是排他锁,则只有这一个申请者被唤醒,其他申请者继续等待。
如果等待队列中第一个申请的是共享锁,则所有共享锁申请者都被唤醒,其他排他锁申请者继续等待。

RegularLock

常规锁(Regular Lock):就是通常说的对数据库对象的锁。按照锁粒度,可以分为表锁、页锁、行锁等;按照等级,pg锁一共有8个等级。特点是:封锁时间可以很长、有死锁检测机制和等待队列、事务结束时会自动释放。

我们平时说的表锁、页锁、咨询锁等等(行锁除外),实际上都是常规锁根据不同锁定对象划分的子类。

1. Regularlock的锁方法

pg提供两种加regular锁的方法:

/** map from lock method id to the lock table data structures*/
static const LockMethod LockMethods[] = {NULL,&default_lockmethod,&user_lockmethod
};

这是个静态的,postmaster启动时作为,分配一块内存区作为regularlock方法表并初始化。其中 LockMethod 定义:

lock.c

typedef struct LockMethodData
{int            numLockModes;const LOCKMASK *conflictTab;const char *const *lockModeNames;const bool *trace_flag;
} LockMethodData;

lockdefs.h文件

这里可以看到LOCKMODE是当整型用的,而LOCKMASK是当位图用的,所以前面的const LOCKMASK *conflictTab,这个冲突数组其实相当于是个二维数组。

/** LOCKMODE is an integer (1..N) indicating a lock type.  LOCKMASK is a bit* mask indicating a set of held or requested lock types (the bit 1<<mode* corresponds to a particular lock mode).*/
typedef int LOCKMASK;
typedef int LOCKMODE;

2. RegularLock支持的锁模式

这里是锁模式(Lockmode):

对应源码:
/** These are the valid values of type LOCKMODE for all the standard lock* methods (both DEFAULT and USER).*//* NoLock is not a lock mode, but a flag value meaning "don't get a lock" */
#define NoLock                  0#define AccessShareLock            1       /* SELECT */
#define RowShareLock            2       /* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock        3       /* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock 4      /* VACUUM (non-FULL),ANALYZE, CREATE INDEX CONCURRENTLY */
#define ShareLock               5       /* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock   6       /* like EXCLUSIVE MODE, but allows ROW SHARE */
#define ExclusiveLock           7       /* blocks ROW SHARE/SELECT...FOR UPDATE */
#define AccessExclusiveLock     8       /* ALTER TABLE, DROP TABLE, VACUUM FULL, and unqualified LOCK TABLE */

带exclusive的锁表示在事务执行期间阻止其他任何类型锁作用于词此表,带share的锁表示允许其他用户共享此锁,事务执行期间阻止排他锁的使用。

挨着分析:

  • AccessShareLock:内部锁模式,select自动施加在表上
  • AccessExclusiveLock:被alter table、drop table 或者 vacuum full 使用 (最强的锁)
  • ShareLock:使用不带concurrently选项对create index语句请求时用该锁对表加锁(也就是会停止该表上的所有dml,vacuum等操作,只允许同一张表select,select for update/for share,create index操作)
  • ExclusiveLock:比sharelock多阻塞 sharelock和rowsharelock,即除了阻止dml、vaccum等,同时阻塞在该表上创建索引,阻止select for update/for share操作,只允许查,Q:啥时候用,A:比如向pg_lock系统表插入一个数据,比如去extend FSM等(一般涉及extend)。

下面四个锁是pg特有的锁:

  • RowShareLock 锁用于 select for update/for share语句。

    • select for update 就是将select出来的行被锁住,要更新;所以避免它们这些行在当前事务结束前被其他事务修改或者删除。所以这里需要阻塞其他update、delete、select for update这些行的事务(注意这里没有insert),直到当前事务结束。同时,如果一个其他事务的update、delete、select for update已经锁住了某个或者某些行,select for update会等到那些事务结束,并且随后锁住并返回更新的行。
    • for share类似,只是在每个检索出来的行上要求一个共享锁,而不是一个排他锁?一个共享锁阻塞update、delete、select for update但不阻塞select for share。
      【注意这里为啥只和7、8级冲突,保证一致性靠的是在行上标记事务,并非锁,对1级锁来说多了和7级的冲突】
  • RowExclusiveLock:insert/update/delete操作
  • ShareUpdateExclusiveLock:vacuum,回收已删除行、或update过时的行占用的空间
  • ShareRowExclusiveLock:?
                 

3. RegularLock的存储

三种基本的锁结构:lock,proclock以及locallock结构。

  • lock:为了存储每个可锁对象、可以保持锁或者请求锁

  • prolock:存储进程和锁之间的关系(pg_locks的grant中的f等)

  • locallock:locallock是对每个backend的锁操作的加速。对于某进程的加锁要求,首先在locallock中查找当前进程是否已获得该锁,已获得就直接引用计数+1。不用LWlock。进对自己进程可见。

三张hashtable

static HTAB *LockMethodLockHash;
static HTAB *LockMethodProcLockHash;
static HTAB *LockMethodLocalHash;

4. RegularLock数据结构

按照上面三个最主要的结构体进行细分(lock.h):

1. Lock结构体:为了存储每个可锁对象、可以保持锁或者请求锁
/*
typedef struct LOCK
{/* hash key */LOCKTAG      tag;            1. /* unique identifier of lockable object *//* data */LOCKMASK grantMask;      2. /* bitmask for lock types already granted */LOCKMASK waitMask;       2. /* bitmask for lock types awaited */SHM_QUEUE    procLocks;      /* list of PROCLOCK objects assoc. with lock */PROC_QUEUE   waitProcs;      /* list of PGPROC objects waiting on lock */int         requested[MAX_LOCKMODES];   /* counts of requested locks */int          nRequested;     /* total of requested[] array */int         granted[MAX_LOCKMODES]; /* counts of granted locks */int            nGranted;       /* total of granted[] array */
} LOCK;
  • LOCKTAG 标志唯一被锁对象,是一个hashkey,是key information对应着一个lock item在lock hashtable里面,即描述被锁的是一个什么东西、什么类型、用什么方法锁。
typedef struct LOCKTAG
{uint32     locktag_field1; /* a 32-bit ID field */uint32       locktag_field2; /* a 32-bit ID field */uint32       locktag_field3; /* a 32-bit ID field */uint16       locktag_field4; /* a 16-bit ID field */uint8        locktag_type;   /* see enum LockTagType */uint8     locktag_lockmethodid;   /* lockmethod indicator */
} LOCKTAG;

拆解一下,这是什么东西,怎么将一个需要锁的对象包装成一个唯一的locktag?

主要掉下面的几个宏:

#define SET_LOCKTAG_RELATION(locktag,dboid,reloid)
#define SET_LOCKTAG_RELATION_EXTEND(locktag,dboid,reloid)
#define SET_LOCKTAG_DATABASE_FROZEN_IDS(locktag,dboid)
#define SET_LOCKTAG_PAGE(locktag,dboid,reloid,blocknum)
#define SET_LOCKTAG_TUPLE(locktag,dboid,reloid,blocknum,offnum)
#define SET_LOCKTAG_TRANSACTION(locktag,xid)
#define SET_LOCKTAG_VIRTUALTRANSACTION(locktag,vxid)
...

这里的LOCKTAG中的locktag_type标志被锁对象的类型(可以看到万物都可锁):

typedef enum LockTagType
{LOCKTAG_RELATION,          /* whole relation */LOCKTAG_RELATION_EXTEND,    /* the right to extend a relation */LOCKTAG_DATABASE_FROZEN_IDS,    /* pg_database.datfrozenxid */LOCKTAG_PAGE,             /* one page of a relation */LOCKTAG_TUPLE,              /* one physical tuple */LOCKTAG_TRANSACTION,        /* transaction (for waiting for xact done) */LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */LOCKTAG_SPECULATIVE_TOKEN,    /* speculative insertion Xid and token */LOCKTAG_OBJECT,                /* non-relation database object */LOCKTAG_USERLOCK,         /* reserved for old contrib/userlock code */LOCKTAG_ADVISORY            /* advisory user locks */
} LockTagType;

这里好奇一下LOCKTAG_RELATION_EXTEND是啥:

  • LOCKMASK grantMask 和 LOCKMASK waitMask(32位);
    前者是这个对象已经被加锁的所有类型,后面是这个对象所有正在被等待加锁的锁类型。这两个标志为了判断已持有锁与请求锁是否冲突。
typedef int LOCKMASK;
  • SHM_QUEUE procLocks; PROC_QUEUE waitProcs;
    下面这两个就是持有该对象锁的proc(进程)队列(信息用PROCLOCK存),和等待该对象锁的proc(进程)队列(信息用PGPROC存)
2. 锁持有者信息描述体

对于一个可加锁的对象,可能有几个不同事物持有或等待锁,我们需要将持有/等待锁和进程联系起来。这些信息应该保存起来PROCLOCK:

typedef struct PROCLOCK
{/* tag */PROCLOCKTAG tag;          /* unique identifier of proclock object *//* data */PGPROC     *groupLeader;    /* proc's lock group leader, or proc itself */LOCKMASK holdMask;       /* bitmask for lock types currently held */LOCKMASK releaseMask;    /* bitmask for lock types to be released */SHM_QUEUE    lockLink;       /* list link in LOCK's list of proclocks */SHM_QUEUE   procLink;       /* list link in PGPROC's list of proclocks */
} PROCLOCK;
  • PROCLOCKTAG tag 同样是为了在HTAB的LockMethodProcLockHash中查找:一个被锁对象和一个等待/持有进程标志唯一一个PROCLOCKTAG。
typedef struct PROCLOCKTAG
{/* NB: we assume this struct contains no padding! */LOCK      *myLock;         /* link to per-lockable-object information */PGPROC    *myProc;         /* link to PGPROC of owning backend */
} PROCLOCKTAG;
  • SHM_QUEUE lockLink:在上一个lock结构体中,记录了一个持有该对象的锁的进程的queue,这里的locklink就是记录本ProcLock在queue的位置;

  • SHM_QUEUE procLink:同样每一个pgproc对象都有一个proclock链表,这里的proclink就是记录本proclock在pgproc链表的位置。

所以可以这么理解:proclock是pgproc和lock的链接。

proclock的所有者可能有两种:事务(由后端pgproc+该事务的事务id标识)或者会话(由后端pgproc和事务的invalidTransactionId标识)

注意proclock为用户锁和vacuum持有的跨事务锁使用。变量holdmask标识该proclock锁表示的已经分配的锁,如果一个进程正在等待锁,那么该进程和所等待的锁构成的proclock中holdmask为0.

3. 本地锁表

HTAB *LockMethodLocalHash;
每一个后台进程(会话)维护一个它自己感兴趣的每一个锁的本地hash表,每个都有一个hashtable 当事务重复在同一个对象上申请同类型锁时,无须做冲突检测,只要将这个锁记录在本地即可,避免频繁访问主锁表和进程锁表。

typedef struct LOCALLOCK
{/* tag */LOCALLOCKTAG tag;         /* unique identifier of locallock entry *//* data */uint32      hashcode;       /* copy of LOCKTAG's hash value */LOCK    *lock;           /* associated LOCK object, if any */PROCLOCK   *proclock;       /* associated PROCLOCK object, if any */int64       nLocks;         /* 持锁数量 */int           numLockOwners;  /* # 锁持有者数量 */int           maxLockOwners;  /* allocated size of array */LOCALLOCKOWNER *lockOwners; /* 锁持有者列表 */bool       holdsStrongLockCount;   /* bumped FastPathStrongRelationLocks */bool        lockCleared;    /* we read all sinval msgs for lock */
} LOCALLOCK;

标志就不说了:

typedef struct LOCALLOCKTAG
{LOCKTAG        lock;           /* identifies the lockable object */LOCKMODE    mode;           /* lock mode for this table entry */
} LOCALLOCKTAG;

5. Regularlock的主要操作

366页
lock.c文件
(1)空间计算LockShmemSize

#define NLOCKENTS() \mul_size(max_locks_per_xact, add_size(MaxBackends, max_prepared_xacts))/* lock hash table */max_table_size = NLOCKENTS();size = add_size(size, hash_estimate_size(max_table_size, sizeof(LOCK)));/* proclock hash table */max_table_size *= 2;size = add_size(size, hash_estimate_size(max_table_size, sizeof(PROCLOCK)));/** Since NLOCKENTS is only an estimate, add 10% safety margin.*/size = add_size(size, size / 10);

(2)初始化
初始三个hashtable

(3)regularlock加锁

LockAcquire加锁:

LockAcquireResult LockAcquire(const LOCKTAG *locktag,LOCKMODE lockmode,bool sessionLock,bool dontWait);

四个参数:

  • LOCKTAG :被锁对象唯一标识

  • LOCKMODE :获得的锁模式
    只有三种:

typedef enum LWLockMode
{LW_EXCLUSIVE,LW_SHARED,LW_WAIT_UNTIL_FREE          /* A special mode used in PGPROC->lwWaitMode,* when waiting for lock to become free. Not* to be used as LWLockAcquire argument */
} LWLockMode;
  • sessionlock 表示为会话加锁;如果false是为当前事务申请锁;
  • dontwait 表示申请锁是否允许等待

返回结果

/* Result codes for LockAcquire() */
typedef enum
{LOCKACQUIRE_NOT_AVAIL,     /* lock not available, and dontWait=true */LOCKACQUIRE_OK,             /* lock successfully acquired */LOCKACQUIRE_ALREADY_HELD,   /* incremented count for lock already held */LOCKACQUIRE_ALREADY_CLEAR  /* incremented count for lock already clear */
} LockAcquireResult;
  • 申请加锁流程:
    在pg中,锁获取最重要的函数是LockAcquire(核心是调用LockAcquireExtended),这是非常高频的操作,性能相当重要。因此在LockAcquire中,对封锁的性能做了大量优化。

常规锁主要保存在以下4个位置:

本地锁表(LOCALLOCK结构体):对于重复申请的锁进行计数,避免频繁访问主锁表和进程锁表,相当于一层缓存,如果查到了对应锁,也就是这个锁对象和模式已经授予本事务,给本地锁表对应锁增加引用计数即可,这个工作由GrantLockLocal函数完成。
快速路径(fast path):对弱锁的访问保存到本进程,避免频繁访问主锁表和进程锁表
主锁表(LOCK结构体):保存一个锁对象所有相关信息
进程锁表(PROCLOCK结构体):保存一个锁对象中与当前会话(进程)相关的信息

  • 释放:
    LockRelease
    有唤醒

Q:会话 backend 进程 事务这都啥关系
A:前三是一样,包含很多事务?

Q:Lockmode和LWlockmode啥关系啊。。

PostgreSQL 随读笔记-事务上相关推荐

  1. Java核心卷Ⅱ(原书第10版)笔记(上)

    Java核心卷Ⅱ(原书第10版)笔记(上) 写在最前面,个人认为,卷Ⅱ更适合当手册使用,更多的是讲API的使用,前两章内容比较实际,要是合并到卷一就好了. 文章目录 第1章 Java SE 8 的流库 ...

  2. JDBC学习笔记——事务、存储过程以及批量处理

    2019独角兽企业重金招聘Python工程师标准>>> 1.事务                                                           ...

  3. PostgreSQL函数(存储过程)----笔记

    PostgreSQL函数(存储过程)----笔记 PostgreSQL 函数也称为 PostgreSQL 存储过程. PostgreSQL 函数或存储过程是存储在数据库服务器上并可以使用SQL界面调用 ...

  4. 《MSSQL2008技术内幕:T-SQL语言基础》读书笔记(上)

    索引: 一.SQL Server的体系结构 二.查询 三.表表达式 四.集合运算 五.透视.逆透视及分组 六.数据修改 七.事务和并发 八.可编程对象 一.SQL Server体系结构 1.1 数据库 ...

  5. oracle 幻影读,索引+事务

    这里写目录标题 事务的ACID特性 数据库的事务是什么? 隔离性(Isolation)概念说明 脏读 可重复读 不可重复读:一个事务A,不同时刻读同一数据可能不一样 幻读:事务A,B同时Insert, ...

  6. 读课文|读笔记|读小说|甚至读漫画|教你如何让书开口说话

    以前上课,老师带着我们一起读课文,现在老师会在课件里播放课文朗读的文件:以前得用磁带光盘跟着读英语,学单词,现在人工智能读单词准确又便捷:以前笔记只能用嘴巴读用眼睛看,现在可以用耳朵听还可以分享给别人 ...

  7. 《GUN Make》文档粗读笔记

    前言 在嵌入式工程的编译中,make经常与gcc配合使用,用于对工程进行编译.当然,这只是一份GUN Make文档的阅读笔记,我并不打算在这篇笔记中说太多与文档阅读本身无关的东西,因为我懒.一来我接触 ...

  8. 2020年Yann Lecun深度学习笔记(上)

    2020年Yann Lecun深度学习笔记(上)

  9. CS231n课程笔记翻译:图像分类笔记(上)

    译者注:本文翻译自斯坦福CS231n课程笔记image classification notes,由课程教师Andrej Karpathy授权进行翻译.本篇教程由杜客翻译完成.ShiqingFan对译 ...

最新文章

  1. python科学计算基础教程pdf下载-用Python做科学计算 pdf版
  2. leetcode算法题--可获得的最大点数
  3. DataTransmission:免费薅羊毛,Are you kidding me? 镭速传输 “百日计划”提前大曝光!Raysync传输协议要开放?
  4. s3c2440启动文件详细分析
  5. qmc0转换mp3工具_GoldenRecords for Mac(唱片录音转换软件)
  6. mysql根据月份查询订单销售额
  7. android 键盘遮盖输入框_Android软键盘遮住输入框的解决方法终极适配
  8. PaaS安全:降低企业风险的四条规则
  9. pdf怎么转换成ppt
  10. CocosCreator中TiledMap简单使用
  11. 【微信小程序】事件传参与数据同步
  12. 18个基于Web的代码开发编辑器
  13. win10两台电脑时间同步
  14. cpuz测试分数天梯图_2019年CPU单核跑分天梯图V1.22版(190712)
  15. 爬虫内容保存到txt文件
  16. u8 客户端修改服务器地址,u8服务器地址怎么修改
  17. 手机的唯一标识码 php,android手机获取唯一标识的方法
  18. export PATHONPATH的用法
  19. HTTP的请求头标签 If-Modified-Since与Last-Modified
  20. Ubuntu安装Qt失败

热门文章

  1. 南京邮电大学计算机网络ppt,南京邮电大学计算机网络第三章习题.ppt
  2. python网络爬虫实战——利用逆向工程爬取动态网页
  3. 巨人征途的成功 依赖于不处不在的海报
  4. PRML学习总结(8)——Graphical Models
  5. 【黑盒测试用例设计】正交试验法
  6. Jenkins 打包部署 vue项目
  7. 利用 Ophis 编写 Commodore 64 programs PRG 程序(五)
  8. 联想小新2019电脑wifi图标不见了
  9. 下载S1的ROM的地方
  10. ROS中map,odom坐标系的理解以及acml和robot_pose_ekf的对比和小车漂移方法解决