PostgreSQL的四種程式間鎖

T1YSL發表於2022-05-30

在PostgreSQL裡有四種型別的程式間鎖:

Spinlocks:自旋鎖,其保護的物件一般是資料庫內部的一些資料結構,是一種輕量級的鎖。
LWLocks:輕量鎖,也是主要用於保護資料庫內部的一些資料結構,支援獨佔和共享兩種模式。
Regular locks:又叫heavyweight locks,也就是我們常說的表鎖、行鎖這些。
SIReadLock predicate locks:謂詞鎖,主要是用來表示資料庫物件和事務間的一個特定關係。

image.png

一、Spinlocks-自旋鎖

自旋鎖顧名思義就是一直原地旋轉等待的鎖。一個程式如果想要訪問 臨界區,必須先獲得鎖資源,如果不能獲得,就會一直自旋,直到獲取到鎖資源,它是最輕量級的鎖,不需要做記憶體上下文轉換的。

所謂臨界區(也稱為臨界段)就是訪問和操作共享資料的程式碼段

這種自旋會造成CPU浪費,但是通常它保護的臨界區非常小,封鎖時間很短,因此通常 自旋比釋放CPU資源帶來的上下文切換消耗要小。 它是一種和硬體結合的互斥鎖,借用了硬體提供的原子操作的原語來對一些共享變數進行封鎖,通常適用於臨界區比較小的情況。

特點是:持有鎖時間很短、無死鎖檢測機制和等待佇列、事務結束時不會自動釋放SpinLock。

Spinlocks來自於Linux核心,自旋鎖簡單來說是一種低階的同步機制,表示了一個變數可能的兩個狀態: Acquired 、Released 。在PostgreSQL的原始碼裡其實經常可以看到這種spinlocks的用法,拿出PostgreSQL-15beta1的WalRcvForceReply()函式來簡單舉例的話,

/*
 * Wake up the walreceiver main loop.
 *
 * This is called by the startup process whenever interesting xlog records
 * are applied, so that walreceiver can check if it needs to send an apply
 * notification back to the primary which may be waiting in a COMMIT with
 * synchronous_commit = remote_apply.
 */
void
WalRcvForceReply(void)
{
	Latch	   *latch;
	WalRcv->force_reply = true;
	/* fetching the latch pointer might not be atomic, so use spinlock */
	SpinLockAcquire(&WalRcv->mutex);
	latch = WalRcv->latch;
	SpinLockRelease(&WalRcv->mutex);
	if (latch)
		SetLatch(latch);
}

可以看到在latch = WalRcv->latch; 這裡想用指標取出結構體中的資料 。  WalRcv->latch,其中WalRcv是指向結構體的指標,latch是這個結構體型別的一個成員。表示式 WalRcv->latch引用了指標WalRcv指向的結構體的成員latch。而我們找到latch對應的結構體,看它的定義(如果是VScode的話直接左鍵雙擊,摁F12),在結構體裡的定義為  Latch      *latch; 表示latch是一個指標,*latch表示latch指標指向的相應的額外的結構體。這裡的Latch是另外的一個結構體,如下圖所示,但是我們只看上述內容以及Latch      *latch的話就可以看出這裡想實現的功能是一個賦值的過程。

typedef struct Latch
{
	sig_atomic_t  is_set;
	sig_atomic_t  maybe_sleeping;
	bool	is_shared;
	int	owner_pid;
#ifdef WIN32
	HANDLE	event;
#endif
} Latch;

這個過程的上下兩側,有SpinLockAcquire()和SpinLockRelease()兩個函式。因為每一個想要獲取自旋鎖的處理,必須為這個變數寫入一個表示自旋鎖獲取 (spinlock acquire)狀態的值,並且為這個變數寫入鎖釋放 (spinlock released)狀態。如果一個處理程式嘗試執行受自旋鎖保護的程式碼,那麼程式碼將會被鎖住,直到佔有鎖的處理程式釋放掉。在這個例子裡,為了避免同時訪問臨界區,阻止 競態條件狀態,所以操作必須是原子的。 因此用到了Spinlocks,從註釋裡也可以看見—— “獲取閂鎖指標可能不是原子的,所以使用自旋鎖”。

競態條件是指在併發環境中,當有多個事件同時訪問同一個臨界資源時,由於多個事件的併發執行順序的不確定,從而導致程式輸出結果的不確定,這種情況我們稱之為競態條件 (Race Conditions)或者競爭冒險(race hazard)。

二、LWLocks-輕量鎖

LWLocks 負責保護共享記憶體中的資料結構,通常有共享和排他兩種模式。

在幾乎所有具有並行處理的軟體中,都會採用一種輕量級鎖(比如Oracle 叫latch,Mysql叫rw-lock,PG叫LwLock)來做序列化控制 ,這種輕量級鎖,我們一般也統稱為閂鎖 。oracle的latch和PG的LwLocks都是在系統的spinlock之上實現的輕量級鎖。

還記得原來維護ORACLE資料庫的時候,資料庫裡的“latch: cache buffers chains ” 等待事件引起了我的好奇,分析邏輯讀產生CBC latch分析了好久。

LWLocks一般由Spinlocks來保護。LWLocks比Spinlocks多一種共享模式。因此比Spinlocks稍微重了點,但是和其他的鎖相比,還是比較輕的。

特點是:持有鎖時間較短、無死鎖檢測機制、有等待佇列、事務結束時會自動釋放

LWLocks通常用於對共享記憶體中資料結構的聯鎖訪問。LWLocks支援獨佔和共享鎖模式(用於讀/寫和只讀)訪問共享物件)。獲取或釋放LWLock的速度非常快。

看了下PG-15beta1版本src/backend/storage/lmgr/lwlocknames.h裡輕量鎖型別定義 ,發現和14.2的沒有什麼調整。

而對於LWLocks 的模式,我說有共享和排他兩種模式時,加了個通常,因為我在看原始碼的時候發現LWLocks 的模式,除了原本LW_EXCLUSIVE和LW_SHARED之外,還有一個LW_WAIT_UNTIL_FREE,表示PGPROC->lwWaitMode中使用的一種特殊模式,是等待鎖變空閒時的狀態。它是不能用作LWLockAcquire()的引數來請求LWLocks 的,所以大家平時一般說兩種模式。

    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;

如下,則是使用排他和共享模式LWLocks 的部分的相應舉例:

	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
	LWLockRelease(ControlFileLock);
	LWLockAcquire(XidGenLock, LW_SHARED);
	ctx->next_fxid = ShmemVariableCache->nextXid;
	ctx->oldest_xid = ShmemVariableCache->oldestXid;
	LWLockRelease(XidGenLock);

三、Regular locks-普通鎖

就是通常說的對資料庫物件的鎖。按照鎖粒度,可以分為表鎖、頁鎖、行鎖等,這應該是我們最熟悉的了;按照等級,鎖一共有8個等級。

特點是:持有鎖時間可以很長、有死鎖檢測機制和等待佇列、事務結束時會自動釋放。

如下,是這八個級別鎖的定義,相應的,其特定場景使用鎖的情況在註釋部分已經列舉出來了。

/*
 * 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 */
#define MaxLockMode			8	/* highest standard lock mode */

如下LOCK的結構體,lock的獲取與釋放,都有佇列來維護 ,

typedef struct LOCK
{
	/* hash key */
	LOCKTAG		tag;			/* unique identifier of lockable object */
	/* data */
	LOCKMASK	grantMask;		/* bitmask for lock types already granted */
	LOCKMASK	waitMask;		/* 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;

tag——唯一標識被鎖定的物件
grantMask——當前在該物件上授予的所有鎖型別的位掩碼。
waitMask——當前該物件上等待的所有鎖型別的位掩碼。
procLocks——該鎖的PROCLOCK物件列表。
waitProcs——等待該鎖的程式佇列。
requested——該鎖當前請求的每種鎖型別的數量(包括已經被批准的請求)。
nRequested——所有型別請求的鎖總數。
granted——當前在該鎖上授予的每種鎖型別的計數。
nGranted——所有型別被授予的鎖總數。

上面結構體裡的LOCKTAG,與PG中的資料庫,relation等強相關也就是我們可以可以鎖定的不同型別的物件,例如表鎖、行鎖等等。透過註釋我們就可以看到可以鎖的物件。

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;

下邊是一個申請AccessShareLock和釋放鎖的舉例

/* Get tuple descriptor from relation OID */
rel = relation_open(relid, AccessShareLock);
	
...	
relation_close(rel, AccessShareLock);

四、SIReadLock predicate locks-謂詞鎖

謂詞鎖,主要是用來表示資料庫物件和事務間的一個特定關係 。

PostgreSQL裡用PREDICATELOCK結構體代表一個單獨的鎖。

typedef struct PREDICATELOCK
{
	/* hash key */
	PREDICATELOCKTAG tag;		/* unique identifier of lock */
	/* data */
	SHM_QUEUE	targetLink;		/* list link in PREDICATELOCKTARGET's list of
								 * predicate locks */
	SHM_QUEUE	xactLink;		/* list link in SERIALIZABLEXACT's list of
								 * predicate locks */
	SerCommitSeqNo commitSeqNo; /* only used for summarized predicate locks */
} PREDICATELOCK;

然後用一個物件和事務作為此謂詞鎖的標識( tag )來標識一個謂詞鎖,如下PREDICATELOCKTAG結構體用來標識一個單獨的謂詞鎖。

typedef struct PREDICATELOCKTAG
{
	PREDICATELOCKTARGET *myTarget;
	SERIALIZABLEXACT *myXact;
} PREDICATELOCKTAG;

PREDICATELOCK結構體註釋的內容翻譯過來如下 “當讀取相關的資料庫物件時,或者透過提升多個細粒度目標,可以在這裡建立一個條目。當可序列化事務被清除時,與該可序列化事務相關的所有條目將被刪除。當條目被組合成一個粗粒度的鎖條目時,也可以刪除它們。”

PREDICATELOCKTAG結構體註釋的內容翻譯過來如下“它是謂詞鎖目標(這是一個可鎖定物件)和已獲得該目標上的鎖的可序列化事務的組合。”

這表明謂詞鎖是資料庫物件和事務間的一個特定關係,這樣的關係是用以表示讀寫衝突的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69990629/viewspace-2897982/,如需轉載,請註明出處,否則將追究法律責任。

相關文章