PostgreSQL鎖機制詳解:從并發控制到死鎖檢測

PostgreSQL鎖詳解

————向逍xiangxiao@highgo.com

首先要講鎖的話,必須得先了解并發控制。數據庫中的對象都是共享的,如果同時間不同的用戶對同一個對象進行修改,就會出現數據不一致的情況。所以如果要實現并發訪問,就需要對這個過程加以控制。

PostgreSQL針對并發訪問控制有兩種方法,一是基于鎖的機制,也就是兩階段鎖機制(Two phase lock,2PL),二是基于時間戳的機制,即MVCC機制。而本文要提到的就是基于鎖的機制,兩階段鎖機制簡單來說就是把對鎖的操作分為兩階段:加鎖階段(增長階段)和解鎖階段(收縮階段),并且強制要求加鎖階段不能釋放鎖,在解鎖階段不能加鎖。

同時,在PostgreSQL中鎖分為一下3中層次。

  • 自旋鎖(Spin Lock):是一種和硬件結合的互斥鎖。它借用了硬件提供的原子操作來對一些共享變量進行加鎖,通常適用于臨界資源比較小的情況。

  • 輕量鎖(Lightweight Lock):負責保存PostgreSQL中的大量共享內存和數據結構。是一種讀寫鎖,具有共享和排他兩種模式。

  • 常規鎖(Regular Lock):給數據庫對象進行加鎖,針對不同的數據庫對象,鎖的粒度也不一樣,例如分為表、頁面、元組和事務ID等等。常規鎖分為八個等級,不同的操作需要不同級別的鎖。

1、PostgreSQL自旋鎖

1.1、什么是自旋鎖

自旋鎖,顧名思義,就是不斷旋轉的鎖。如果一個進程需要訪問臨界區,則必須先獲取鎖資源,如果不能獲得鎖資源,就會一直在原地忙等,即旋轉,直到獲取鎖資源。

自旋鎖的這種忙等,也就是自旋非常浪費CPU資源,所以一般用于臨界資源非常小的情況,持有這個鎖的進程能夠很快的釋放鎖,在這個時候自旋對CPU資源的浪費小于釋放CPU資源帶來的上下文切換的資源消耗,那就是適合自旋鎖的情況。

但是需要注意的是這里的自旋鎖的實現必須依賴原子操作,也就是說這個操作可能在c語言是一條語句,但是轉換為匯編指令則可能是多條指令才能達到的效果,當進程在訪問的時候可能出現同時獲取資源的情況。比如*lock != 0這條語句轉換為匯編指令為movq -0x8(%rbq), %rax和cmpl $0x0, (%rax)這就不是原子操作。

PG采用了兩種方式去實現自旋鎖,一種是硬件提供的原子操作去實現,另一種是如果機器沒有TAS指令集,則通過自己定義的信號量PGSemaphore來仿真SpinLock,但是后者的效率不如前者,本文這里暫時只介紹通過硬件提供的原子操作實現。

1.2、自旋鎖的申請

打開spin.h文件可以發現,PG是通過宏定義HAVE_SPINLOCKS判斷機器是否由TAS指令集。

自旋鎖的申請使用的是SpinLockAcquire,但是這是一個宏定義,它最終調用的是tas函數,函數的定義如下:

static inline int
tas(volatile slock_t *lock)
{register slock_t _res = 1;asm _volatile_("   lock            \n""   xchgb   %0,%1   \n"
:       "+q"(_res), "+m"(*lock)
:       /* no inputs */
:       "memory", "cc");return (int) _res;
}

這里舉的例子是x86架構下的TAS實現方法,對于不同的架構,PostgreSQL提供不同的方法實現,具體可以查看src/include/storage/s_lock.h這個文件下的內容。

1.3、自旋鎖的釋放

自旋鎖的釋放就是將這里的*lock的值置為1,具體代碼實現如下:

#define S_UNLOCK(lock)  \do { asm _volatile_("" : : : "memory");  *(lock) = 0; } while (0)

2、PostgreSQL輕量鎖

2.1、什么是輕量鎖

自旋鎖是一個互斥鎖,對于臨界資源比較小的情況,能保證多個進程對臨界區的訪問是互斥的,但是在數據庫的實際使用過程中,會出現多個進程同時讀取共享內存,在這種情況下大量的讀操作是不沖突的,因為他們并不會修改數據,所以就需要輕量鎖來解決這個問題。

輕量級鎖有兩種模式,共享和排他,在讀操作的時候就加上共享鎖,這樣保證在其他進程讀的時候不沖突,而又杜絕了其他寫操作。在寫操作的時候就加上排他鎖,這樣保證在寫操作執行的過程中,其他進程既不可以讀也不可以寫。

輕量級鎖的相容性矩陣如下:

輕量級鎖類型定義在lwlocknames.h中,這個文件是在編譯的時候由lwlocknames.txt生成的,在這里面定義了PG的輕量級鎖的類型。

#define ShmemIndexLock (&MainLWLockArray[1].lock)
#define OidGenLock (&MainLWLockArray[2].lock)
#define XidGenLock (&MainLWLockArray[3].lock)
...
#define NotifyQueueTailLock (&MainLWLockArray[47].lock)#define NUM_INDIVIDUAL_LWLOCKS      48

從這個宏定義可以看出,在這個文件中所有的輕量級鎖的類型都存儲在MainLWLockArray里面。在上面這種為individual lwlock,每個individual lwlock都有自己固定要保護的對象。individual lwlock的使用方法如下:

LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE);
//這里對需要保護的變量進行寫操作
LWLockRelease(ShmemIndexLock);

在individual lwlock中,每個lwlock都對應一個tranche id,它具有全局唯一性,在MainLWLockArray中前NUM_INDIVIDUAL_LWLOCKS - 1都是individual lwlock。

除了individual lwlock之外,還有Builtin tranche,每一個builtin tranche可能對應多個lwlock,他代表的是一組lwlocks,這組lwlocks雖然各自封鎖各自的內容,但是他們的功能相同。builtin tranche包含如下類型,類型定義在lwlock.h文件的BuiltinTrancheIds枚舉類型中。

typedef enum BuiltinTrancheIds
{LWTRANCHE_XACT_BUFFER = NUM_INDIVIDUAL_LWLOCKS,LWTRANCHE_COMMITTS_BUFFER,LWTRANCHE_SUBTRANS_BUFFER,LWTRANCHE_MULTIXACTOFFSET_BUFFER,
...
}

從這里可以看出來,對于Builtin tranche,他的計數是從NUM_INDIVIDUAL_LWLOCKS開始的,也就是individual lwlock的最后。

這些Builtin Tranche對應的鎖一部分保存在MainLWLockArray中,另一半部分被保存在使用他們的結構體中,如下所示:

//存儲在MainLWLockArray中
/* Register named extension LWLock tranches in the current process. */
for (int i = 0; i < NamedLWLockTrancheRequests; i++)LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,NamedLWLockTrancheArray[i].trancheName);
//存儲在自己的結構體中 xlog.c:4465 (pg15.8)
//這里初始化了NUM_XLOGINSERT_LOCKS個輕量鎖
//這些輕量鎖的tranche id都是lwtranche_wal_insert
//這些輕量鎖沒有保存在MainLWLockArray里面
for (i = 0; i < NUM_XLOGINSERT_LOCKS; i++)
{LWLockInitialize(&WALInsertLocks[i].l.lock, LWTRANCHE_WAL_INSERT);WALInsertLocks[i].l.insertingAt = InvalidXLogRecPtr;WALInsertLocks[i].l.lastImportantAt = InvalidXLogRecPtr;
}

但是無論是individual lwlocks還是builtin tranches,他們都被保存在共享內存中,只是保存的方式不相同而已。

這里先給出輕量級鎖的結構體定義:

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_DEBUG pg_atomic_uint32 nwaiters; /* number of waiters 等待者的數量 *///最后一次獲取排他鎖的進程struct PGPROC *owner;  /* last exclusive owner of the lock */#endif} LWLock;

在早期輕量級鎖是通過自旋鎖實現的,但是隨著性能的提升,輕量級鎖的實現開始借助原子操作來實現,pg_atomic_uint32 state;關注這個狀態的變量,這是一個32位的無符號整型,其中他的低24位作為存儲共享鎖的計數器,因此一個輕量級鎖最多擁有2^24個共享鎖持有者。有1位作為排他鎖的標記,因為同一時刻最多只能擁有一個持鎖者。

2.2、輕量鎖的申請

接下來介紹輕量鎖的申請,主要分為兩種,一是申請共享鎖,二是申請排他鎖。

(1)當申請一個共享鎖的時候:

a如果里面是共享鎖或者無鎖 —-> 直接獲取

b如果里面排他鎖 —-> 進入等待隊列

(2)當申請一個排他鎖的時候:

a如果里面是共享鎖或者排他鎖 —-> 進入等待隊列

b如果里面沒有鎖 —-> 直接獲取

在比較極端的情況下,如果申請一個排他鎖,然后目前持有的是共享鎖,則進入等待隊列,在這個過程中不斷的申請共享鎖,那么這個排他鎖將持續等待,出現排他鎖饑餓的情況。

具體申請輕量鎖的詳細細節如下:

獲取鎖的調用LWLockAcquire方法,釋放調用LWLockRelease方法。 獲取的步驟大致為:

  • 判斷鎖的持有數量是否超出能記錄的持有列表大小。

if (num_held_lwlocks >= MAX_SIMUL_LWLOCKS)elog(ERROR, "too many LWLocks taken");
  • 嘗試獲取,成功則返回。

mustwait = LWLockAttemptLock(lock, mode);if (!mustwait){LOG_LWDEBUG("LWLockAcquire", lock, "immediately acquired lock");break;    /* got the lock 獲取成功則返回 */}
  • 失敗則進入等待隊列。

/* add to the queue */
LWLockQueueSelf(lock, mode);
  • 再次嘗試獲取,成功則出隊列,并返回。

mustwait = LWLockAttemptLock(lock, mode);/* ok, grabbed the lock the second time round, need to undoqueueing */
if (!mustwait){LOG_LWDEBUG("LWLockAcquire", lock, "acquired, undoing queue");LWLockDequeueSelf(lock);//從隊列中出來,獲取成功,直接返回break;}
  • 失敗則報告等待事件開始,并持續等待被喚醒,然后報告等待事件結束。

LWLockReportWaitStart(lock);if (TRACE_POSTGRESQL_LWLOCK_WAIT_START_ENABLED())TRACE_POSTGRESQL_LWLOCK_WAIT_START(T_NAME(lock), mode);for (;;){PGSemaphoreLock(proc->sem);if (proc->lwWaiting == LW_WS_NOT_WAITING)break;extraWaits++;}/* Retrying, allow LWLockRelease to release waiters again. 使用原子操作獲取更新鎖的狀態 */pg_atomic_fetch_or_u32(&lock->state, LW_FLAG_RELEASE_OK);if (TRACE_POSTGRESQL_LWLOCK_WAIT_DONE_ENABLED())TRACE_POSTGRESQL_LWLOCK_WAIT_DONE(T_NAME(lock), mode);LWLockReportWaitEnd();
  • 然后持續再次嘗試獲取鎖,回到1,直到獲取成功。

  • 獲取成功之后將這個鎖增加到held_lwlocks中去。

held_lwlocks[num_held_lwlocks].lock = lock;
held_lwlocks[num_held_lwlocks++].mode = mode;

2.3、輕量鎖的釋放

釋放輕量級鎖的步驟大致如下:

  • 將這個鎖在held_lwlocks中定位,并將數組該位置之后的鎖前移。

for (i = num_held_lwlocks; --i >= 0;)if (lock == held_lwlocks[i].lock)break;if (i < 0)elog(ERROR, "lock %s is not held", T_NAME(lock));mode = held_lwlocks[i].mode;num_held_lwlocks--;//釋放之后數組前移for (; i < num_held_lwlocks; i++)held_lwlocks[i] = held_lwlocks[i + 1];PRINT_LWDEBUG("LWLockRelease", lock, mode);
  • 釋放鎖。

if (mode == LW_EXCLUSIVE)oldstate = pg_atomic_sub_fetch_u32(&lock->state, LW_VAL_EXCLUSIVE);
elseoldstate = pg_atomic_sub_fetch_u32(&lock->state, LW_VAL_SHARED);
  • 獲取修改前的狀態,判斷是否需要喚醒等待隊列的進程。

//如果鎖未被持有并且等待隊列為空則不需要喚醒
if ((oldstate & (LW_FLAG_HAS_WAITERS | LW_FLAG_RELEASE_OK)) ==(LW_FLAG_HAS_WAITERS | LW_FLAG_RELEASE_OK) &&(oldstate & LW_LOCK_MASK) == 0)check_waiters = true;
elsecheck_waiters = false;
  • 如果需要喚醒,則喚醒。

if (check_waiters){/* XXX: remove before commit? */LOG_LWDEBUG("LWLockRelease", lock, "releasing waiters");LWLockWakeup(lock);}

2.4、Extension拓展輕量級鎖

在插件中拓展輕量級鎖有兩種方法。

方法一、通過RequestNamedLWLockTranche函數和GetNamedLWLockTranche函數來實現。其中RequestNamedLWLockTranche函數負責注冊Tranche的名字以及自己所需要的輕量級鎖的數量,GetNamedLWLockTranche負責根據Tranche Name來獲得對應的鎖。每個tranche都有自己唯一的id,全局唯一,tranche id和tranche name一一對應。

方法二、通過LWLockNewTrancheId函數獲取新的Tranche ID,然后將Tranche ID和Tranche Name通過LWLockRegisterTranche函數建立聯系,然后再由LWLockInitialize函數進行初始化。

3、PostgreSQL常規鎖

常規鎖是用于給數據庫對象,例如表,頁面,元組等加鎖的事務鎖,它也是一種讀寫鎖,但是和LWLock不同的在于它將鎖等級分為了八個等級,所以他的相容性矩陣就比較麻煩。后續也會接著介紹是怎么演變到八個等級鎖來的。

3.1、常規鎖的鎖模式

常規鎖的鎖模式分為八種,也就是八級鎖,前三種鎖稱為弱鎖,第四級既不是弱鎖也不是強鎖,后面四級為強鎖。具體劃分如下圖所示:

由最開始的輕量鎖的兩種鎖模式,共享和排他,到現在八級鎖,各種鎖之間的相容度也是隨之變化的,常規鎖的弱鎖之間是相容的,強鎖之前是不相容的,第四級鎖于弱鎖相容于強鎖不相容,且于自己不相容。具體各級鎖之前的沖突關系由下圖的沖突矩陣表示:

3.2、常規鎖的演變

最開始只有兩種鎖,就是讀鎖和寫鎖,即Share和Exclusive。

然后PG引入了多版本控制,多版本控制要求讀寫相互不阻塞,所以又引入了AccessShare和AccessExclusive鎖,引入MVCC之后insert\update\delete操作仍然使用原來的Exclusive鎖,但是普通的select操作則使用AccessShare鎖,這樣子就和原來的Exclusive不沖突了,這個時候讀寫之間互不阻塞,而之前的Share鎖的主要應用場景為創建索引,而AccessExclusive鎖就用于除了原來的增刪改之外的其他的排他性變更(drop/truncate/reindex/vacuum full),但是寫和寫之間還是會有沖突,根據相容性矩陣可以看出在并發寫入使用Exclusive鎖的時候,這是個表級鎖,粒度太大了,會影響并發操作。

所以就引入了意向鎖,即RowShare和RowExclusive,意向鎖之間都不沖突。 意向讀與讀鎖兼容,但是與寫鎖沖突,因為在寫的時候去讀是徒勞的,用于select for update/share操作 意向寫他與讀鎖和寫鎖都沖突,兩者都阻止并發修改,用于insert,update,delete。

意向鎖用于協調表鎖和行鎖之間的關系,比如說當某個進程想要修改某一行的時候,首先會在這個表上面加一把意向寫鎖,表示自己會在某一行上進行寫操作,然后再在某一行上面增加寫鎖。舉個例子,假設不存在意向鎖。假設進程A獲取了表上某行的行鎖,持有行上的排他鎖意味著進程A可以對這一行執行寫入;同時因為不存在意向鎖,進程B很順利地獲取了該表上的表級排他鎖,這意味著進程B可以對整個表,包括A鎖定對那一行進行修改,這就違背了常識邏輯。因此A需要在獲取行鎖前先獲取表上的意向鎖,這樣后來的B就意識到自己無法獲取整個表上的排他鎖了(但B依然可以加一個意向鎖,獲取其他行上的行鎖)。

RowShare就是行級共享鎖對應的表級意向鎖(SELECT FOR SHARE|UPDATE命令獲取),而RowExclusive(INSERT|UPDATE|DELETE獲取)則是行級排他鎖對應的表級意向鎖。

這個時候由有六把鎖了,但是后面為什么又引入了另外兩把鎖,即ShareUpdateExclusiveLock 和ShareRowExclusiveLock這兩把鎖。首先ShareUpdateExclusiveLock 鎖可以讓 VACUUM 操作不應該阻止數據寫入,但是又不能出現在一張表并發執行VACUUM操作,所以他又必須自斥。ShareRowExclusiveLock這種鎖級別是介于 SHARE 鎖和 EXCLUSIVE 鎖之間的一種平衡選擇,它比 SHARE 鎖更嚴格,因為它阻止了更新操作,但比 EXCLUSIVE 鎖更寬松,因為它允許并發讀取,比如說在創建觸發器的時候,應該阻止數據寫入,因為這個時候無法判斷是否需要觸發觸發器,但是又必須防止其他進程修改觸發器,所以他又需要互斥。所以又引入了兩把自斥鎖。

3.3、常規鎖的實現

3.3.1、常規鎖的數據結構

在數據庫啟動的階段,PostgreSQL通過InitLocks函數來初始化保存鎖對象的共享內存空間,在共享內存中,有兩個鎖表用于保存鎖對象,LockMethodLockHash主鎖表和LockMethodProcLockHash進程鎖表。

【共享內存】主鎖表:LockMethodLockHash(存儲所有鎖對象)

【共享內存】鎖進程關系表:LockMethodProcLockHash(查詢當前進程阻塞了哪些進程,死鎖檢測)

【本地內存】本地鎖表:對重復申請的鎖計數,避免頻繁訪問(本地快速加鎖,鎖緩存)

【共享內存】本地弱鎖fastpath(需要共享內存中查詢是否有強鎖FastPathStrongRelationLockData)

首先看一下主鎖表在內存中的存儲結構,具體如下:

然后是進程鎖表,它用于保存當前進程的事務鎖狀態,保存的是PROCLOCK結構體,主要是為了建立鎖與申請鎖的會話之間的關系:

除此之外還有個結構,那就是本地鎖表LocalLock。

最后就是快速路徑(Fast Path),這個快速路徑分為強鎖和弱鎖,弱鎖的訪問保存到本進程,避免頻繁的訪問主鎖表和進程鎖表。 而對于強鎖是存儲在共享內存中的,這里的count數量是2(10)是1024,具體如下:

然后對于弱鎖,是存儲在進程的變量中的,即PGPROC結構體的fpLockBits和fpRelId。存儲的結構也就是存儲在fpLockBits這個64位的位圖中的,每三位一組,標識八級鎖對應的鎖級別,而根據fpRelId的大小可以看出來,其實最多只能存儲了16個鎖,fpRelId顧名思義就是表的oid,也就是鎖對象即鎖表的oid。

3.3.2、常規鎖的申請

加鎖的API是LockAcquire,但是在里面還是調用的是LockAcquireExtended,這個函數接受四個參數:分別是鎖tag,申請鎖模式,會話鎖或事務鎖,是否等待。詳細的加鎖流程如下:

(1)查找本地鎖表

在Acquire Lock的時候首先查找的是本地鎖表LocalLock,通過hash_search函數搜索,locktag作為查找的key值,返回一個LOCALLOCK*。如果在本地鎖表沒有找到的話就構造一條放入本地鎖表。

如果已經持有鎖的話直接將持有的數量加1。

(2)查找fast path,判斷弱鎖。

在查找本地鎖表之后,如果本地鎖表內沒有,則開始查找fast path,首先是通過EligibleForRelationFastPath宏定義進行判斷:要求必須是非咨詢鎖 locktag_lockmethodid == DEFAULT_LOCKMETHOD,要求鎖的對象為表 locktag_type == LOCKTAG_RELATION,要求必須是當前數據庫的鎖 locktag_field1 == MyDatabaseId,要求鎖的級別必須是弱鎖 (mode) < ShareUpdateExclusiveLock。

并且還有個條件需要判斷的是,當前fastpath里面已經使用的數量是否超過了最大數。FastPathLocalUseCount < FP_LOCK_SLOTS_PER_BACKEND

總的流程大致為用tag算的hashcode在%1024,落到1024數組FastPathStrongRelationLocks->count的某個位置上。這個位置上存儲的是這個對象所持有的強鎖,有強鎖的話則加鎖失敗,否則使用FastPathGrantRelationLock函數加鎖,這里傳進去的參數locktag->locktag_field2是表的id,還有鎖的模式。

FastPathGrantRelationLock邏輯:

  • 3個bit一組按順序查位圖是不是空的,是空的就記錄下來位置,不是空的就看下oid里面記的是不是需要的,如果正好Oid也是需要的,把當前請求鎖模式或進去就可以返回了。

  • 如果查了一遍位圖,所有Oid都不是需要的,那就找一個空的位置,把鎖級別記錄到位圖,OID記錄到數組,然后返回。

  • 如果查了一遍位圖,沒有一個空余位置,就返回false了。

  • 如果申請的是強鎖的情況。

強鎖的判斷和前面弱鎖的判斷比較相似,也是通過一個宏定義去判斷的,這里給出宏定義如下:

如果判斷出來是強鎖的話,那就開始處理強鎖,走強鎖的邏輯。首先就是通過BeginStrongLockAcquire函數去申請強鎖,之后就將強鎖對應的加鎖對象的OID鎖持有的弱鎖,存儲在fastpath中的,如果有的話就移動到主鎖表和進程鎖表中去,用于后期的鎖沖突判斷和死鎖檢測。

這里給出BeginStrongLockAcquire的實現邏輯:

  • 共享內存強鎖表對應位置++,FastPathStrongRelationLocks->count[fasthashcode]++;(重要!相當于禁用了后面這個對象后來的所有fastpath)

  • 本地鎖記錄強鎖存在locallock->holdsStrongLockCount = true

  • 全局記錄本地強鎖StrongLockInProgress = locallock

然后給出將fastpath中對于弱鎖轉移到主鎖表和進程鎖表中去的代碼邏輯:

  • 輪詢PGPROC,用表oid在fpRelId16數組中查詢,如果找到了,用找到的鎖級別去主鎖表中加鎖。

  • 清理fastpath對應的位置。

  • 完成轉換:PGPROC記錄的fastpath鎖換成主鎖表的鎖,便于死鎖檢查。

  • 注意這里只是把fastpath中已經存在弱鎖換到主鎖表了,并沒有給當前請求LockAcquire的對象加鎖。

這一塊比較復雜,給出完整代碼注釋如下:

static bool
FastPathTransferRelationLocks(LockMethod lockMethodTable, const LOCKTAG *locktag,uint32 hashcode)
{LWLock     *partitionLock = LockHashPartitionLock(hashcode);Oid         relid = locktag->locktag_field2;uint32      i;/** Every PGPROC that can potentially hold a fast-path lock is present in* ProcGlobal->allProcs.  Prepared transactions are not, but any* outstanding fast-path locks held by prepared transactions are* transferred to the main lock table.* 每個可能持有快速路徑鎖的PGPROC都存在于ProcGlobal->allProcs中。* 已準備事務不是,但由已準備事務持有的任何未完成的快速路徑鎖都會轉移到主鎖表。*/for (i = 0; i < ProcGlobal->allProcCount; i++)//遍歷所有進程{PGPROC     *proc = &ProcGlobal->allProcs[i];uint32      f;LWLockAcquire(&proc->fpInfoLock, LW_EXCLUSIVE);/** If the target backend isn't referencing the same database as the* lock, then we needn't examine the individual relation IDs at all;* none of them can be relevant.** proc->databaseId is set at backend startup time and never changes* thereafter, so it might be safe to perform this test before* acquiring &proc->fpInfoLock.  In particular, it's certainly safe to* assume that if the target backend holds any fast-path locks, it* must have performed a memory-fencing operation (in particular, an* LWLock acquisition) since setting proc->databaseId.  However, it's* less clear that our backend is certain to have performed a memory* fencing operation since the other backend set proc->databaseId.  So* for now, we test it after acquiring the LWLock just to be safe.* 如果目標后端沒有引用與鎖相同的數據庫,那么我們根本不需要檢查各個關系id;沒有一個是相關的。* proc->databaseId在后端啟動時設置,此后永遠不會改變,因此在獲取&proc->fpInfoLock之前執行此測試可能是安全的。* 特別是,可以肯定地假設,如果目標后端持有任何快速路徑鎖,那么它在設置proc->databaseId之后一定執行了內存隔離操作(特別是LWLock獲取)。* 然而,由于另一個后端設置了proc->databaseId,我們的后端是否確定執行了內存隔離操作就不太清楚了。* 所以現在,為了安全起見,我們在獲得LWLock后進行測試。*/if (proc->databaseId != locktag->locktag_field1)//如果進程數據庫id和當前數據庫id不一致則跳過{LWLockRelease(&proc->fpInfoLock);continue;}for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)//如果是當前數據庫id,則開始遍歷16個槽{uint32      lockmode;/* Look for an allocated slot matching the given relid. */if (relid != proc->fpRelId[f] || FAST_PATH_GET_BITS(proc, f) == 0)//表的id不正確或者未加鎖則跳過continue;/* Find or create lock object. 循環檢查三種弱鎖*/LWLockAcquire(partitionLock, LW_EXCLUSIVE);for (lockmode = FAST_PATH_LOCKNUMBER_OFFSET;lockmode < FAST_PATH_LOCKNUMBER_OFFSET + FAST_PATH_BITS_PER_SLOT;++lockmode){PROCLOCK   *proclock;if (!FAST_PATH_CHECK_LOCKMODE(proc, f, lockmode))//檢查某個進程是否持有某種鎖,檢查是否有這個是否持有弱鎖continue;/* 如果持有弱鎖,則將這個弱鎖移動到主鎖表和進程鎖表中去 */proclock = SetupLockInTable(lockMethodTable, proc, locktag,hashcode, lockmode);if (!proclock)//如果失敗{LWLockRelease(partitionLock);LWLockRelease(&proc->fpInfoLock);return false;}GrantLock(proclock->tag.myLock, proclock, lockmode);//增加本地鎖表計數FAST_PATH_CLEAR_LOCKMODE(proc, f, lockmode);//清理fastpath,也就是pgproc里面}LWLockRelease(partitionLock);/* No need to examine remaining slots. */break;}LWLockRelease(&proc->fpInfoLock);}return true;
}

如果前面都成功了,就可以申請強鎖了。

如果申請失敗則報錯,但是如果申請成功就可以開始檢測申請鎖與等待隊列中的鎖是否沖突,比如說這里的lockmode是1,那么對應的lockMethodTable->conflicTab[lockmode]=10000000,表示1級鎖與8級鎖沖突。如果等待隊列中不沖突的話,就要去檢查是否與當前的其他事務所持有的鎖模式是否存在沖突,這里是由LockCheckConflicts函數去實現的。

這里對LockCheckConflicts函數進行解釋:

首先先直接拿出與當前鎖模式沖突的鎖模式與當前對象已經持有的鎖模式進行比較,如果沒有沖突則直接獲取。

如果有沖突,則需要更進一步的檢測,統計持有的模式中,沖突的數量,并且還得減去當前會話持有的數量,自己和自己不沖突。

然后再檢查一遍,是否由沖突,沒有沖突則直接獲取。

然后如果還是有沖突的話則需要再檢測是否可能是組鎖沖突,也就是可能這里面存在并行事務,同屬一個鎖組,不算沖突。

首先檢查一下是否是單進程事務,不存在組鎖,又存在沖突那么一定是存在沖突的。

如果是并行事務,且是針對表進行拓展操作,即鎖的LOCKTAG是LOCKTAG_RELATION_EXTEND則為沖突,因為如果組鎖的時候,一個進程要去拓展表的話,其他進程不能去拓展。

然后就是去檢測并行操作, 如果其他進程與當前進程具有相同的groupleader,則不沖突,需要減去沖突計數。

否則的話就意味著不能馬上拿到鎖,如果在申請的時候指明不等待的話,這個時候直接刪除前面的操作,返回失敗,如果指明等待的話則進入等待函數WaitOnLock(locallock, owner);。

在插入等待隊列的時候需要注意注意一點,通常來說,如當前事務應該插入等待隊列的末尾,但是如果本事務A除了當前申請的鎖模式之外,已經持有了這個對象的其他鎖模式,而且等待隊列中某個事務B所等待的鎖模式與當前事務A所持有的鎖模式沖突,這個時候如果把事務A插入到這個等待者B的后面,則容易出現死鎖,所以考慮將事務A插入到等待者的前面。

插入到等待隊列之后,就需要進行死鎖檢測,這里給出調用邏輯:waitOnLock->ProcSleep->CheckDeadLock,詳細的死鎖檢測在后面介紹。

3.3.3、常規鎖的釋放

相較于常規鎖的申請,常規鎖的釋放就比較簡單了。

  • 從LockMethodLocalHash 查找LOCALLOCK,在它的owner數組里找到與當前進程對應的元素,解除LOCALLOCK與ResourceOwner之間的關聯,并減小相應計數值。

  • 將LOCALLOCK的加鎖次數(nLocks)減1,如果減為0了,表示當前進程不再持有鎖,則后續有兩種解鎖途徑,分別是快速鎖和主鎖表。

  • 對于快速鎖,調用FastPathUnGrantRelationLock清除快速鎖的加鎖標記。如果解鎖成功即可返回。

  • 如果沒找到快速鎖,且LOCALLOCK.lock為空,說明被轉移到了主鎖表里(在加鎖邏輯中,當申請快速鎖成功時,會把LOCALLOCK.lock置空)。

  • 查找主鎖表LockMethodLockHash 以及 LockMethodProcLockHash,如果沒有找到,則blog-error。找到了 調用UnGrantLock更新LOCK和PROCLOCK信息。

  • 調用CleanUpLock:要么刪除主表項,要么調用ProcLockWakeup()喚醒等待者,這里會按照 WFG 中構建的 soft-edge 以及 hard-edge 依賴關系進行喚醒。

3.5、死鎖檢測

在postgresql數據庫中,對申請事務鎖的順序沒有嚴格的限制,在申請鎖時也沒有嚴格的檢查,因此不可避免的會發生死鎖。PG使用“樂觀鎖”來檢測死鎖,即鎖等待的時間超過了時間deadlock_timeout(postgresql.conf中配置,deadlock_timeout = 1s),那么開始進行死鎖檢測,如果沒有死鎖繼續等待。

如果有一個事務A已經持有某個鎖,這個時候事務B來申請這個鎖,但是和事務A持有的鎖沖突,需要等待A釋放之后才能獲取,這個時候就產生了等待,可以作為B->A,表示B等待事務A。而在數據庫內核中事務是有限的,每個事務都在等待另一個事務,在這個等待的集合中就會出現環,也就是死鎖,所以死鎖的檢測也就變成了環的檢測。

還有另一個概念就是實邊和虛邊,其中實邊定義為如果A持有某個對象的共享或者排他鎖,這個時候B申請排他鎖,就需要進入等待過程,即B——>A。而需邊則為,如果事務A持有共享鎖,B申請排他鎖,然后進入等待隊列,這個時候事務C又來申請共享鎖,但是雖然C與A不沖突,但是和等待隊列中B沖突,也需要進入等待過程。這個時候就產生了事務C等待事務B,即C--------->B。

所以如果產生死鎖即檢測到環的存在,如果這個環上全部都是實邊,則必須結束掉一個事務來接觸這個環,如果存在虛邊,則可以通過調整等待隊列實現解決死鎖,即可以調整等待隊列中C到B的前面,這個時候C和A不沖突,即可獲取鎖。

3.5.1、死鎖檢測中的數據結構

首先是死鎖的狀態定義:

其他方面的話用PGPROC作為節點,EDGE作為邊。

如果在換種存在虛邊,則調整等待隊列消除環,新的等待隊列保存在waitOrders數組中,每個數組都是一個等待隊列。

而在這個過程中,可以被調整的虛邊記錄在possibleConstraints中,每次都是從possibleConstraints找到一個虛邊進行探索,而被探索的虛邊則保存在curConstrains中,這兩個數組的長度必須足夠大,curConstrains的長度為MaxBackends,被調整的虛邊數量不能超過這個值。

3.5.2、死鎖的檢測過程

首先進入到的是CheckDeadLock函數,死鎖檢測的過程是互斥的,就是說在死鎖檢測開始的時候,鎖表就需要被保護起來,防止被其他人修改。

然后如果只有當前對話的話,就不會構成死鎖。

否則就開始檢測死鎖,即檢測是否有環的存在。進入DeadLockCheck函數,在從中進入DeadLockCheckRecurse進行檢測,接下來詳解該函數:

進入函數之后首先進行是否有環的判斷,通過TestConfiguration進行判斷,該函數沒有環則返回0,有實環則返回-1,其他情況則返回大于0的數。

然后檢測對虛邊的調整是否達到了最大值,如果達到了最大值,則認為產生了死鎖。

而如果還有檢測的空間,則將檢測產生的虛邊加入到考慮的范圍。

然后遞歸調用DeadLockCheckRecurse函數,判斷是否有死鎖。

在上面的TestConfiguration函數的作用就是調用FindLockCycle去檢測是否有環。

并調用ExpandConstraints函數嘗試對等待隊列進行重排來嘗試解除虛邊死鎖,如果存在環的話又將虛邊保存到possibleConstraints數組中。

再接著對這里檢測死鎖所用到的函數FindLockCycle進行詳解,但是其實際調用的又是FindLockCycleRecurse函數,接下來對其進行詳解:

遞歸查找是否存在環,FindLockCycleRecurse函數查找的原理就是:在查找環時,先檢查待測進程是否在visitedProcs數組中出現過,如果沒出現過,就將待測進程存入到visitedProcs數組中,如果出現過,而且是在等待隊列的起始處,則表明出現了死鎖的環,返回死鎖信息。

如果不存在環則將待檢測進程存入visitedProcs數組中。

如果要檢查的進程處于等待狀態,那么就遞歸檢測他的等待隊列。

如果待測進程是鎖組中的一部分,遍歷鎖組的每個成員進程,檢查是否存在環。

而在這里又提到了一個FindLockCycleRecurseMember函數,這個函數通過遞歸地檢查進程的鎖等待依賴關系來檢測死鎖循環。它區分了硬邊(直接阻塞關系)和軟邊(可調整的等待關系),并在檢測到死鎖時記錄相關信息。函數的設計確保了能夠準確地識別死鎖場景,并為可能的重排等待隊列提供支持,以嘗試解決死鎖問題。

參考連接或文獻

  • 《PostgreSQL技術內幕 事務處理深度探索》,張樹杰

  • 《PostgreSQL數據庫內核分析》,彭智勇

  • PostgreSQL · 內核特性 · 死鎖檢測與解決

  • PostgreSQL的全局死鎖檢測原理

  • 11-pg內核之鎖管理器(六)死鎖檢測

  • PostgreSQL中的表鎖

  • PostgreSQL 并發控制 -- 鎖體系實現原理

  • [自學分享]Postgres數據庫鎖機制(部分)

  • 37.1 經典知識庫:PostgreSQL中的鎖 - 張樹杰

  • postgresql源碼學習(九)—— 常規鎖②-強弱鎖與Fast Path

  • postgresql源碼學習(八)—— 常規鎖①-加鎖步驟與本地鎖表

  • Postgresql源碼(69)常規鎖細節分析

  • postgresql源碼學習(七)—— 自旋鎖與輕量鎖

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/91520.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/91520.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/91520.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【啟發式算法】RRT*算法詳細介紹(Python)

&#x1f4e2;本篇文章是博主人工智能&#xff08;AI&#xff09;領域學習時&#xff0c;用于個人學習、研究或者欣賞使用&#xff0c;并基于博主對相關等領域的一些理解而記錄的學習摘錄和筆記&#xff0c;若有不當和侵權之處&#xff0c;指出后將會立即改正&#xff0c;還望諒…

Docker架構深度解析:從核心概念到企業級實踐

Docker架構深度解析&#xff1a;從核心概念到企業級實踐一、Docker架構全景圖1.1 整體架構示意圖二、核心組件深度解析2.1 Docker Daemon工作機制三、鏡像與容器原理3.1 鏡像分層結構3.2 容器生命周期四、網絡架構詳解4.1 網絡模式對比4.2 Bridge網絡實現原理五、存儲架構與實踐…

PPT自動化 python-pptx - 8: 文本(text)

在使用 python-pptx 庫操作 PowerPoint 文檔時&#xff0c;理解文本的結構和處理方式至關重要。本文將深入探討文本在形狀中的組織層級、訪問方式以及各級別的格式化選項。文本容器與層級結構可容納文本的形狀&#xff1a; 只有自動形狀 (Auto shapes) 和表格單元格 (table cel…

使用realsense進行目標檢測并標識目標深度

涉及知識點都在代碼中注釋了&#xff0c;直接看代碼 // This example is derived from the ssd_mobilenet_object_detection opencv demo // and adapted to be used with Intel RealSense Cameras // Please see https://github.com/opencv/opencv/blob/master/LICENSE#includ…

OpenWrt Network configuration

OpenWrt Network configuration device 和 interface 關系device device 表示底層的網絡設備&#xff0c;如物理網卡、橋接設備&#xff08;bridge&#xff09;、VLAN 設備等。 通過 config device 定義&#xff0c;描述設備類型、端口成員、VLAN 等屬性。 例如&#xff1a;br…

VuePress 使用詳解

一、核心概念 VuePress 是 Vue.js 團隊開發的靜態網站生成器&#xff0c;專為技術文檔優化&#xff0c;具備以下特性&#xff1a; Markdown 優先&#xff1a;原生支持 Markdown 語法擴展Vue 驅動&#xff1a;可在 Markdown 中使用 Vue 組件默認主題優化&#xff1a;內置響應式…

AI大模型前沿:Muyan-TTS開源零樣本語音合成技術解析

AI大模型前沿&#xff1a;Muyan-TTS開源零樣本語音合成技術解析引言&#xff1a;語音合成技術的演進與Muyan-TTS的突破性意義語音合成&#xff08;Text-to-Speech, TTS&#xff09;技術作為人機交互的核心接口之一&#xff0c;自20世紀30年代貝爾實驗室首次嘗試電子語音合成以來…

c# everthing.exe 通信

1 獲取everthing進程 調用 Everything 搜索創建SearchWithEverything函數using Microsoft.Win32; using System; using System.Diagnostics; using System.IO; using System.Management; using System.Text;class EverythingHelper {// 方法 1&#xff1a;從進程獲取路徑publi…

Gitee:中國企業級DevOps平臺的本土化突圍之路

Gitee&#xff1a;中國企業級DevOps平臺的本土化突圍之路 在國內數字化轉型浪潮下&#xff0c;DevOps平臺作為企業研發效能提升的核心引擎&#xff0c;正在經歷從工具到生態的全面升級。作為國內領先的一站式DevOps解決方案&#xff0c;Gitee憑借其本土化優勢與全鏈路服務能力&…

C++法則22:運算符 ::* 和 ->* 和 ::* 是獨特的整體運算符,是不可分的。

C法則22&#xff1a;運算符 ::* 和 ->* 和 ::* 是獨特的整體運算符&#xff0c;是不可分的。1. ::*&#xff08;成員指針聲明符&#xff09;作用&#xff1a;用于聲明一個指向類成員的指針。語法&#xff1a;ReturnType (ClassName::*pointerName) &ClassName::MemberN…

Linux系統管理習題

Linux 系統管理練習題 1.請為此虛擬機配置以下網絡參數&#xff1a; 1&#xff09;主機名&#xff1a;chenyu.example.com &#xff08;將chenyu改成自己名字的全拼&#xff09; 2&#xff09;IP 地址&#xff1a;192.168.100.100/24 3&#xff09;默認網關&#xff1a;192.168…

SQL166 每天的日活數及新用戶占比

SQL166 每天的日活數及新用戶占比 題目理解 本SQL查詢旨在分析用戶活躍數據&#xff0c;計算兩個關鍵指標&#xff1a; 每日活躍用戶數(DAU)每日新增用戶占比(新用戶占活躍用戶的比例) 解題思路 1. 數據準備階段 首先我們需要獲取所有用戶的活躍記錄&#xff0c;包括&…

【33】C# WinForm入門到精通 ——表格布局器TableLayoutPanel【屬性、方法、事件、實例、源碼】

WinForm 是 Windows Form 的簡稱&#xff0c;是基于 .NET Framework 平臺的客戶端&#xff08;PC軟件&#xff09;開發技術&#xff0c;是 C# 語言中的一個重要應用。 .NET 提供了大量 Windows 風格的控件和事件&#xff0c;可以直接拿來使用。 本專欄內容是按照標題序號逐漸…

uv使用教程

以下是使用 Python 包管理工具 uv 的常見命令指南。uv 是由 Astral&#xff08;Ruff 的開發者&#xff09;開發的高性能 Python 包安裝器和解析器&#xff0c;旨在替代 pip 和 pip-tools&#xff1a; 1. 安裝 uv uv官網倉庫 # Linux/macOS curl -Ls https://astral.sh/uv/in…

SpringBoot3.x入門到精通系列:1.1 簡介與新特性

SpringBoot 3.x 簡介與新特性 &#x1f4d6; 什么是SpringBoot SpringBoot是由Pivotal團隊提供的全新框架&#xff0c;其設計目的是用來簡化Spring應用的初始搭建以及開發過程。SpringBoot集成了大量常用的第三方庫配置&#xff0c;SpringBoot應用中這些第三方庫幾乎可以零配…

二、搭建springCloudAlibaba2021.1版本分布式微服務-Nacos搭建及服務注冊和配置中心

nacos介紹 1、Nacos簡介 Nacos 是阿里巴巴推出來的一個新開源項目&#xff0c;這是一個更易于構建云原生應用的動態服務發現、配置管理和服務管理平臺。 Nacos 致力于幫助您發現、配置和管理微服務。Nacos 提供了一組簡單易用的特性集&#xff0c;幫助您快速實現動態服務發現、…

淺談物聯網嵌入式程序開發源碼技術方案

在物聯網蓬勃發展的時代&#xff0c;嵌入式程序作為連接硬件與軟件的橋梁&#xff0c;發揮著至關重要的作用。以“邊緣智能 云協同”為核心&#xff0c;為工業、醫療、家居、農業、智慧城市五大場景提供穩定、低功耗、可擴展的物聯網終端與平臺一體化解決方案。以下董技叔軟件…

【筆記】重學單片機(51)

為學習嵌入式做準備&#xff0c;重新拿起51單片機學習。此貼為學習筆記&#xff0c;僅記錄易忘點&#xff0c;實用理論基礎&#xff0c;并不是0基礎。 資料參考&#xff1a;清翔零基礎教你學51單片機 51單片機學習筆記1. C語言中的易忘點1.1 數據類型1.2 位運算符1.3 常用控制語…

C++現代Redis客戶端庫redis-plus-plus詳解

&#x1f680; C現代Redis客戶端庫redis-plus-plus詳解&#xff1a;告別繁瑣的hiredis&#xff0c;擁抱現代C的Redis操作 &#x1f4c5; 更新時間&#xff1a;2025年07月28日 &#x1f3f7;? 標簽&#xff1a;C | Redis | redis-plus-plus | 現代C | 后端開發 文章目錄&#x…

Redis存儲原理與數據模型(上)

一、Redis數據模型 1.1、查看Redis數據定義&#xff1a; typedef struct redisDb {kvstore *keys; /* The keyspace for this DB 指向鍵值存儲的指針&#xff0c;用于快速訪問和修改數據庫中的鍵值對*/kvstore *expires; /* Timeout of keys with a t…