SQLite剖析之異步IO模式、共享緩存模式和解鎖通知
原文轉(zhuǎn)載于:https://www.cnblogs.com/5211314jackrose/p/5816066.html
1、異步I/O模式
?? ?通常,當(dāng)SQLite寫一個(gè)數(shù)據(jù)庫(kù)文件時(shí),會(huì)等待,直到寫操作完成,然后控制返回到調(diào)用程序。相比于CPU操作,寫文件系統(tǒng)是非常耗時(shí)的,這是一個(gè)性能瓶頸。異步I/O后端是SQLite的一個(gè)擴(kuò)展模塊,允許SQLite使用一個(gè)獨(dú)立的后臺(tái)線程來(lái)執(zhí)行所有的寫請(qǐng)求。雖然這并不會(huì)減少整個(gè)系統(tǒng)的資源消耗(CPU、磁盤帶寬等),但它允許SQLite在正在寫數(shù)據(jù)庫(kù)時(shí)立刻返回到調(diào)用者,從用戶角度看,無(wú)疑提高了前端的響應(yīng)速度。對(duì)異步I/O,寫請(qǐng)求在一個(gè)獨(dú)立的后臺(tái)線程中被處理,這意味著啟動(dòng)數(shù)據(jù)庫(kù)寫操作的線程不必等待磁盤I/O的發(fā)生。寫操作看起來(lái)似乎很快就發(fā)生了,但實(shí)際上速度跟通常是一樣的,只不過(guò)在后臺(tái)進(jìn)行。
?? ?異步I/O似乎提供了更好的響應(yīng)能力,但這是有代價(jià)的。你會(huì)失去ACID中的持久性(Durable)屬性。在SQLite的缺省I/O后端中,一旦寫操作完成,你知道更改的數(shù)據(jù)已經(jīng)安全地在磁盤上了。而異步I/O卻不是這樣的情況。如果應(yīng)用程序在數(shù)據(jù)寫操作之后,異步寫線程完成之前發(fā)生崩潰或掉電,則數(shù)據(jù)庫(kù)更改可能根本沒有被寫到磁盤,下一次使用數(shù)據(jù)庫(kù)時(shí)就看不到更改。
?? ?異步I/O失去了持久性,但仍然保持ACID的其他三個(gè)屬性:原子性(Atomic)、一致性(Consistent)和隔離性(Isolated)。很多應(yīng)用程序沒有持久性也能很好地工作。
?? ?我們通過(guò)創(chuàng)建一個(gè)SQLite VFS對(duì)象并且用sqlite3_vfs_register()注冊(cè)它來(lái)使用異步I/O模式。當(dāng)用這個(gè)VFS打開數(shù)據(jù)庫(kù)文件并進(jìn)行寫操作時(shí)(使用vfs的xWrite()方法),數(shù)據(jù)不會(huì)立刻寫到磁盤,而是放在由后臺(tái)線程維護(hù)的寫隊(duì)列中。當(dāng)用異步VFS打開數(shù)據(jù)庫(kù)文件并進(jìn)行讀操作時(shí)(使用vfs的xRead()方法),數(shù)據(jù)從磁盤讀出,而寫隊(duì)列從vfs讀進(jìn)程的角度看,其xWrite()已經(jīng)完成了。異步I/O的虛擬文件系統(tǒng)(VFS)通過(guò)sqlite3async_initialize()來(lái)注冊(cè),通過(guò)sqlite3async_shutdown()來(lái)關(guān)閉。
?? ?為了積累經(jīng)驗(yàn),異步I/O的實(shí)現(xiàn)有意保持簡(jiǎn)單。更多的功能會(huì)在將來(lái)的版本中添加。例如,在當(dāng)前的實(shí)現(xiàn)中,如果寫操作正在一個(gè)穩(wěn)定的流上發(fā)生,而這個(gè)流超過(guò)了后臺(tái)寫線程的I/O能力,則掛起的寫操作隊(duì)列將會(huì)無(wú)限地增長(zhǎng),可能會(huì)耗盡主機(jī)系統(tǒng)的內(nèi)存。復(fù)雜一點(diǎn)的模塊則可以跟蹤掛起的寫操作數(shù)量,在超過(guò)一定數(shù)目后停止接收新的寫請(qǐng)求。
?? ?在單個(gè)進(jìn)程中、使用異步IO的多個(gè)連接可以并發(fā)地訪問(wèn)單個(gè)數(shù)據(jù)庫(kù)。從用戶的角度看,如果所有連接都位于單個(gè)進(jìn)程中,則正常SQLite和使用異步IO的SQLite,其并發(fā)性并沒有什么不同。如果文件鎖是激活的(缺省是激活的),來(lái)自多個(gè)進(jìn)程的連接都要讀和寫數(shù)據(jù)庫(kù)文件,則并發(fā)性在下面的情況下會(huì)減弱:
?? ?(1)當(dāng)使用異步IO的連接啟動(dòng)一個(gè)數(shù)據(jù)庫(kù)事務(wù)時(shí),數(shù)據(jù)庫(kù)會(huì)立刻被鎖住。然而鎖只有在寫隊(duì)列中的所有操作已經(jīng)刷新到磁盤后才能釋放。這意味著有時(shí)即使在一個(gè)"COMMIT"或"ROLLBACK"執(zhí)行完后,數(shù)據(jù)庫(kù)可能仍然處于鎖住狀態(tài)。
?? ?(2)如果應(yīng)用程序使用異步IO連續(xù)地執(zhí)行多個(gè)事務(wù),其他數(shù)據(jù)庫(kù)用戶可能會(huì)因?yàn)閿?shù)據(jù)庫(kù)一直被鎖住而不能使用數(shù)據(jù)庫(kù)。這是因?yàn)楫?dāng)一個(gè)BEGIN執(zhí)行后,數(shù)據(jù)庫(kù)鎖會(huì)立刻建立起來(lái)。但當(dāng)對(duì)應(yīng)的COMMIT或ROLLBACK發(fā)生時(shí),鎖不一定釋放了,要到后臺(tái)寫隊(duì)列全部刷新到磁盤后才能釋放。如果后臺(tái)寫隊(duì)列還沒刷新完,數(shù)據(jù)庫(kù)就一直處于鎖住狀態(tài),其他進(jìn)程不能訪問(wèn)數(shù)據(jù)庫(kù)。
?? ?文件鎖可以在運(yùn)行時(shí)通過(guò)sqlite3async_control()函數(shù)禁用。對(duì)NFS這可以提高性能,因?yàn)榭梢员苊鈱?duì)服務(wù)器的來(lái)回異步操作建立文件鎖。但是如果多個(gè)連接嘗試訪問(wèn)同一個(gè)數(shù)據(jù)庫(kù),而文件鎖被禁用了,則應(yīng)用程序崩潰和數(shù)據(jù)庫(kù)損壞就可能發(fā)生。
?? ?異步IO擴(kuò)展模塊由單個(gè)源文件sqlite3async.c,和一個(gè)頭文件sqlite3async.h組成,位于源碼樹的ext/async/子目錄下。應(yīng)用程序可以用其中定義的C API來(lái)激活和控制這個(gè)模塊的功能。為了使用異步IO擴(kuò)展,把sqlite3async.c編譯成使用SQLite的應(yīng)用程序的一部分,然后使用sqlite3async.h中定義的API來(lái)初始化和配置這個(gè)模塊。這些API在sqlite3async.h的注釋中有詳細(xì)說(shuō)明,使用這些API通常有以下步驟:
?? ?(1)調(diào)用sqlite3async_initialize()來(lái)給SQLite注冊(cè)異步IO VFS(虛擬文件系統(tǒng))。
?? ?(2)創(chuàng)建一個(gè)后臺(tái)線程來(lái)執(zhí)行寫操作,并調(diào)用sqlite3async_run()。
?? ?(3)通過(guò)異步IO VFS,使用正常的SQLite API來(lái)讀寫數(shù)據(jù)庫(kù)。
?? ?當(dāng)前的異步IO擴(kuò)展兼容win32系統(tǒng)和支持pthread接口的系統(tǒng),包括Mac OS X, Linux和其他Unix變體。為了移植異步IO擴(kuò)展到其他的平臺(tái),用戶必須在新平臺(tái)上實(shí)現(xiàn)互斥鎖和條件變量原語(yǔ)。當(dāng)前并沒有外部可用接口來(lái)允許做這樣的控制,但是修改sqlite3async.c中的代碼以包含新平臺(tái)的并發(fā)控制原語(yǔ)是相當(dāng)容易的,更多細(xì)節(jié)可搜索sqlite3async.c中的注釋串"PORTING FUNCTIONS"。然后實(shí)現(xiàn)下面這些函數(shù)的新版本:
static void async_mutex_enter(int eMutex); static void async_mutex_leave(int eMutex); static void async_cond_wait(int eCond, int eMutex); static void async_cond_signal(int eCond); static void async_sched_yield(void);
?? ?上面這些函數(shù)的功能在sqlite3async.c的注釋中有詳細(xì)描述。
2、共享緩存模式
?? ?從3.3.0版開始,SQLite包含一個(gè)特別的“共享緩存”模式(缺省情況下禁用),主要用在嵌入式服務(wù)器中。如果共享緩存模式激活,并且一個(gè)線程在同一個(gè)數(shù)據(jù)庫(kù)上建立多個(gè)連接,則這些連接共享一個(gè)數(shù)據(jù)和模式緩存。這能夠顯著減少系統(tǒng)的內(nèi)存和IO消耗。在3.5.0版中,共享緩存模式被修改以便同一緩存的共享可以跨越整個(gè)進(jìn)程而不只是單個(gè)線程。在這個(gè)修改之前,在線程間傳遞數(shù)據(jù)連接是受限制的。從3.5.0版開始這個(gè)限制就消除了。
?? ?從另一個(gè)進(jìn)程或線程的角度看,使用共享緩存的兩個(gè)或多個(gè)數(shù)據(jù)庫(kù)連接看起來(lái)就像是一個(gè)連接。鎖協(xié)議用來(lái)在多個(gè)共享緩存或數(shù)據(jù)庫(kù)用戶之間進(jìn)行仲裁。
圖1 共享緩存模式
??? 圖1描述一個(gè)運(yùn)行時(shí)配置的例子,有三個(gè)數(shù)據(jù)庫(kù)連接。連接1是一個(gè)正常的SQLite數(shù)據(jù)庫(kù)連接,連接2和3共享一個(gè)緩存。正常的鎖協(xié)議用來(lái)在連接1和共享緩存之間串行化數(shù)據(jù)庫(kù)訪問(wèn)。而連接2和連接3對(duì)共享緩存訪問(wèn)的串行化則有專門的內(nèi)部協(xié)議。見下面的描述。
?? ?有三個(gè)級(jí)別的共享緩存加鎖模型,事務(wù)級(jí)別的加鎖,表級(jí)別的加鎖和模式級(jí)別的加鎖。
?? ?(1)事務(wù)級(jí)別的加鎖
?? ?SQLite連接可能打開兩種類型的事務(wù),讀事務(wù)和寫事務(wù)。這不是顯式完成的,一個(gè)事務(wù)隱式地含有一個(gè)讀事務(wù),直到它首次寫一個(gè)數(shù)據(jù)庫(kù)文件,這時(shí)成為一個(gè)寫事務(wù)。在任何時(shí)候共享緩存上最多只能有一個(gè)連接打開一個(gè)寫事務(wù),這個(gè)寫事務(wù)可以和任何數(shù)量的讀事務(wù)共存。這與非共享緩存模式不同,非共享緩存模式下有讀操作時(shí)不允許有寫操作。
?? ?(2)表級(jí)別的加鎖
?? ?當(dāng)兩個(gè)或更多的連接使用一個(gè)共享緩存,用鎖來(lái)串行化每個(gè)表格的并發(fā)訪問(wèn)。表支持兩種類型的鎖,讀鎖和寫鎖。鎖被授予連接,任何時(shí)候每個(gè)數(shù)據(jù)庫(kù)連接上的每個(gè)表格可以有讀鎖、寫鎖或沒有鎖。一個(gè)表格上可以任何數(shù)量的讀鎖,但只能有一個(gè)寫鎖。讀數(shù)據(jù)庫(kù)表格時(shí)必須首先獲得一個(gè)讀鎖。寫表格時(shí)必須獲得一個(gè)寫鎖。如果不能獲取需要的鎖,查詢失敗并返回SQLITE_LOCKED給調(diào)用者。表級(jí)別的鎖在獲取之后,要到當(dāng)前事務(wù)(讀或?qū)懀┙Y(jié)束時(shí)才釋放。
?? ?如果使用read_uncommitted pragma指令把事務(wù)隔離模式從串行(serialized,缺省模式,即查詢數(shù)據(jù)時(shí)會(huì)加上共享瑣,阻塞其他事務(wù)修改真實(shí)數(shù)據(jù))改成允許臟讀(read-uncommitted,即SELECT會(huì)讀取其他事務(wù)修改而還沒有提交的數(shù)據(jù)),則上面描述的行為會(huì)有稍許的變化。事務(wù)隔離模式還有另外兩種,無(wú)法重復(fù)讀read-comitted是同一個(gè)事務(wù)中兩次執(zhí)行同樣的查詢語(yǔ)句,若在第一次與第二次查詢之間時(shí)間段,其他事務(wù)又剛好修改了其查詢的數(shù)據(jù)且提交了,則兩次讀到的數(shù)據(jù)不一致??梢灾貜?fù)讀read-repeatable是指同一個(gè)事務(wù)中兩次執(zhí)行同樣的查詢語(yǔ)句,得到的數(shù)據(jù)始終都是一致的。
/* Set the value of the read-uncommitted flag: ** ** True -> Set the connection to read-uncommitted mode. ** False -> Set the connection to serialized (the default) mode. */ PRAGMA read_uncommitted =; /* Retrieve the current value of the read-uncommitted flag */ PRAGMA read_uncommitted;
? ? 允許臟讀模式的數(shù)據(jù)庫(kù)連接在讀數(shù)據(jù)庫(kù)表時(shí)不會(huì)獲取讀鎖,如果這時(shí)另外一個(gè)數(shù)據(jù)庫(kù)連接修改了正在被讀的表數(shù)據(jù),則可能導(dǎo)致查詢結(jié)果不一致,因?yàn)樵试S臟讀模式的讀事務(wù)不會(huì)被打斷。允許臟讀模式不會(huì)影響寫事務(wù),它必須獲取寫鎖,因此數(shù)據(jù)庫(kù)寫操作可以被阻塞。允許臟讀模式也不會(huì)影響sqlite_master級(jí)別的鎖。
?? ?(3)模式(sqlite_master)級(jí)別的加鎖
?? ?sqlite_master表支持與其他數(shù)據(jù)庫(kù)表相同的共享緩存讀鎖和寫鎖。還會(huì)使用下面的特殊規(guī)則:
?? ?* 在訪問(wèn)任何數(shù)據(jù)庫(kù)表格或者獲取任何其他的讀鎖和寫鎖之前,連接必須先獲取一個(gè)sqlite_master表上的讀鎖。
?? ?* 在執(zhí)行修改數(shù)據(jù)庫(kù)模式的語(yǔ)句(例如CREATE TABLE或DROP TABLE)之前,連接必須先獲取一個(gè)sqlite_master表上的寫鎖。
?? ?* 如果任何其他的連接持有關(guān)聯(lián)數(shù)據(jù)庫(kù)(包括缺省的主數(shù)據(jù)庫(kù))的sqlite_master表上的寫鎖,則連接不可以編譯一個(gè)SQL語(yǔ)句。
?? ?在SQLite 3.3.0到3.4.2之間,數(shù)據(jù)庫(kù)連接只能被調(diào)用sqlite3_open()創(chuàng)建它的線程使用,一個(gè)連接只能與同一線程中的其他連接共享緩存。從SQLite 3.5.0開始,這個(gè)限制消除了。在老版本的SQLite上,共享緩存模式不能使用在虛擬表上,從SQLite 3.6.17開始,這個(gè)限制消除了。
?? ?共享緩存模式在每個(gè)進(jìn)程級(jí)別上激活。C接口int sqlite3_enable_shared_cache(int)用來(lái)全局地激活或禁用共享緩存模式。每次調(diào)用sqlite3_enable_shared_cache()影響后續(xù)的使用sqlite3_open(), sqlite3_open16()或sqlite3_open_v2()創(chuàng)建的數(shù)據(jù)庫(kù)連接,已經(jīng)存在的數(shù)據(jù)庫(kù)連接則不受影響。每次sqlite3_enable_shared_cache()的調(diào)用覆蓋進(jìn)程上的前面各次調(diào)用。
?? ?使用sqlite3_open_v2()創(chuàng)建的單個(gè)數(shù)據(jù)庫(kù)連接,通過(guò)在第三個(gè)參數(shù)上使用SQLITE_OPEN_SHAREDCACHE或SQLITE_OPEN_PRIVATECACHE標(biāo)志,可能選擇參與或不參與共享緩存模式。在該數(shù)據(jù)庫(kù)連接上這些標(biāo)志會(huì)覆蓋全局的sqlite3_enable_shared_cache()設(shè)置。如果同時(shí)使用這兩個(gè)標(biāo)志,則行為是未定義的。
?? ?當(dāng)使用URI文件名時(shí),"cache"查詢參數(shù)可以用來(lái)指定連接是否使用共享緩存模式。"cache=shared"激活共享緩存,"cache=private"禁用共享緩存。例如:
?? ?ATTACH 'file:aux.db?cache=shared' AS aux;
?? ?從SQLite 3.7.13開始,倘若數(shù)據(jù)庫(kù)使用URI文件名創(chuàng)建,共享緩存模式可以在內(nèi)存數(shù)據(jù)庫(kù)上使用。為了向后兼容,使用未修飾的":memory:"名稱打開內(nèi)存數(shù)據(jù)庫(kù)時(shí)缺省是禁用共享緩存的。而在SQLite 3.7.13之前,無(wú)論使用的內(nèi)存數(shù)據(jù)庫(kù)名、當(dāng)前系統(tǒng)的共享緩存設(shè)置、以及查詢參數(shù)或標(biāo)志是什么,內(nèi)存數(shù)據(jù)庫(kù)上共享緩存總是被禁用的。
?? ?在內(nèi)存數(shù)據(jù)庫(kù)上激活共享緩存,會(huì)允許同一進(jìn)程上的兩個(gè)或更多數(shù)據(jù)庫(kù)連接訪問(wèn)同一段內(nèi)存。當(dāng)最后一個(gè)連接關(guān)閉時(shí),內(nèi)存數(shù)據(jù)庫(kù)會(huì)自動(dòng)刪除,這段內(nèi)存也會(huì)被重置。
?? ?
3、解鎖通知
?? ?當(dāng)多個(gè)連接在共享緩存模式下訪問(wèn)同一個(gè)數(shù)據(jù)庫(kù)時(shí),單個(gè)表上的讀鎖和寫鎖(即共享鎖和排他鎖)用來(lái)確保并發(fā)執(zhí)行的事務(wù)是隔離的。如果連接不能獲取到需要的鎖,sqlite3_step()調(diào)用返回SQLITE_LOCKED。如果不能獲取到每個(gè)關(guān)聯(lián)數(shù)據(jù)庫(kù)的sqlite_master表上的讀鎖(雖然這種情況并不常見),sqlite3_prepare()或sqlite3_prepare_v2()調(diào)用也會(huì)返回SQLITE_LOCKED。
?? ?通過(guò)使用SQLite的sqlite3_unlock_notify()接口,我們可以讓sqlite3_step()或sqlite3_prepare_v2()調(diào)用阻塞直到獲得需要的鎖,而不是立刻返回SQLITE_LOCKED。下面的例子展示解鎖通知的使用。
/* 本例子使用pthreads API */ #include/* ** 當(dāng)注冊(cè)一個(gè)解鎖通知時(shí),傳遞本結(jié)構(gòu)實(shí)例的指針,以作為用戶上下文中的實(shí)例 */ typedef struct UnlockNotification UnlockNotification; struct UnlockNotification { int fired; /* 在解鎖事件發(fā)生后為True */ pthread_cond_t cond; /* 要等待的條件變量 */ pthread_mutex_t mutex; /* 保護(hù)本結(jié)構(gòu)的互斥量 */ }; /* ** 解鎖通知回調(diào)函數(shù) */ static void unlock_notify_cb(void **apArg, int nArg){ int i; for(i=0; i mutex); /* 對(duì)臨界區(qū)加鎖 */ p->fired = 1; /* 觸發(fā)解鎖事件,本變量只能互斥訪問(wèn) */ pthread_cond_signal(&p->cond); pthread_mutex_unlock(&p->mutex); } } /* ** 本函數(shù)假設(shè)SQLite API調(diào)用(sqlite3_prepare_v2()或sqlite3_step())返回SQLITE_LOCKED。 ** 參數(shù)為關(guān)聯(lián)的數(shù)據(jù)庫(kù)連接。 ** 本函數(shù)調(diào)用sqlite3_unlock_notify()注冊(cè)一個(gè)解鎖通知回調(diào)函數(shù),然后阻塞直到 ** 回調(diào)函數(shù)執(zhí)行完并返回SQLITE_OK。調(diào)用者應(yīng)該重試失敗的操作。 ** 或者,如果sqlite3_unlock_notify()指示阻塞將會(huì)導(dǎo)致系統(tǒng)死鎖,則本函數(shù)立刻 ** 返回SQLITE_LOCKED。調(diào)用者不應(yīng)該重試失敗的操作,而是回滾當(dāng)前事務(wù) */ static int wait_for_unlock_notify(sqlite3 *db){ int rc; UnlockNotification un; /* 初始化UnlockNotification結(jié)構(gòu) */ un.fired = 0; pthread_mutex_init(&un.mutex, 0); pthread_cond_init(&un.cond, 0); /* 注冊(cè)一個(gè)解鎖通知回調(diào)函數(shù) */ rc = sqlite3_unlock_notify(db, unlock_notify_cb, (void *)&un); assert( rc==SQLITE_LOCKED || rc==SQLITE_OK ); /* sqlite3_unlock_notify()調(diào)用總是返回SQLITE_LOCKED或SQLITE_OK。 ** 如果返回SQLITE_LOCKED,則系統(tǒng)死鎖。本函數(shù)需要返回SQLITE_LOCKED給調(diào)用者以 ** 便當(dāng)前事務(wù)能夠回滾。否則阻塞直到解鎖通知回調(diào)函數(shù)執(zhí)行,然后返回SQLITE_OK */ if( rc==SQLITE_OK ){ pthread_mutex_lock(&un.mutex); if( !un.fired ){ /* 如果解鎖事件沒有發(fā)生,則阻塞 */ pthread_cond_wait(&un.cond, &un.mutex); } pthread_mutex_unlock(&un.mutex); } /* 銷毀互斥量和條件變量 */ pthread_cond_destroy(&un.cond); pthread_mutex_destroy(&un.mutex); return rc; } /* ** 本函數(shù)是SQLite函數(shù)sqlite3_step()的包裝,它的工作方式與sqlite3_step()相同。 ** 但如果沒有獲得共享緩存鎖,則本函數(shù)阻塞以等待鎖可用。 ** 如果本函數(shù)返回SQLITE_LOCKED,調(diào)用者應(yīng)該回滾當(dāng)前事務(wù),之后再嘗試。否則系統(tǒng)可能死鎖了 */ int sqlite3_blocking_step(sqlite3_stmt *pStmt){ int rc; while( SQLITE_LOCKED==(rc = sqlite3_step(pStmt)) ){ rc = wait_for_unlock_notify(sqlite3_db_handle(pStmt)); if( rc!=SQLITE_OK ) break; sqlite3_reset(pStmt); } return rc; } /* ** 本函數(shù)是SQLite函數(shù)sqlite3_prepare_v2()的包裝,它的工作方式與sqlite3_prepare_v2()相同。 ** 但如果沒有獲得共享緩存鎖,則本函數(shù)阻塞以等待鎖可用。 ** 如果本函數(shù)返回SQLITE_LOCKED,調(diào)用者應(yīng)該回滾當(dāng)前事務(wù),之后再嘗試。否則系統(tǒng)可能死鎖了 */ int sqlite3_blocking_prepare_v2( sqlite3 *db, /* 數(shù)據(jù)庫(kù)句柄 */ const char *zSql, /* UTF-8編碼的SQL語(yǔ)句 */ int nSql, /* zSql的字節(jié)數(shù) */ sqlite3_stmt **ppStmt, /* OUT: 指向預(yù)處理語(yǔ)句的指針 */ const char **pz /* OUT: 解析過(guò)的字符串尾部位置 */ ){ int rc; while( SQLITE_LOCKED==(rc = sqlite3_prepare_v2(db, zSql, nSql, ppStmt, pz)) ){ rc = wait_for_unlock_notify(db); if( rc!=SQLITE_OK ) break; } return rc; }
? ? 如果例子中的sqlite3_blocking_step()或sqlite3_blocking_prepare_v2()函數(shù)返回SQLITE_LOCKED,則表明阻塞將導(dǎo)致系統(tǒng)死鎖。
?? ?只有在編譯時(shí)定義預(yù)處理宏SQLITE_ENABLE_UNLOCK_NOTIFY,才能使用sqlite3_unlock_notify()接口。該接口被設(shè)計(jì)成用在這樣的系統(tǒng)中:每個(gè)數(shù)據(jù)庫(kù)連接分配單獨(dú)的線程。如果在一個(gè)線程中運(yùn)行多個(gè)數(shù)據(jù)庫(kù)連接,則不能使用該接口。sqlite3_unlock_notify()接口一次只在一個(gè)線程上工作,因此上面的鎖控制邏輯只能工作于一個(gè)線程的單個(gè)數(shù)據(jù)庫(kù)連接上。
?? ?上面的例子中,在sqlite3_step()或sqlite3_prepare_v2()返回SQLITE_LOCKED后,sqlite3_unlock_notify()被調(diào)用以注冊(cè)一個(gè)解鎖通知回調(diào)函數(shù)。在數(shù)據(jù)庫(kù)連接持有表級(jí)別的鎖后,解鎖通知函數(shù)被執(zhí)行以防止sqlite3_step()或sqlite3_prepare_v2()隨后完成事務(wù)并釋放所有鎖。例如,如果sqlite3_step()嘗試讀表格X,而其他某個(gè)連接Y正持有表格X的寫鎖,sqlite3_step()將返回SQLITE_LOCKED。如果隨后調(diào)用sqlite3_unlock_notify(),解鎖通知函數(shù)將在連接Y的事務(wù)結(jié)束后被調(diào)用。解鎖通知函數(shù)正在等待的連接(這里的Y),被稱為“阻塞式連接”。
?? ?如果sqlite3_step()嘗試寫一個(gè)數(shù)據(jù)庫(kù),但返回SQLITE_LOCKED,則可能有多個(gè)進(jìn)程持有當(dāng)前數(shù)據(jù)庫(kù)表格的讀鎖。這時(shí)SQLite隨意地選擇其中的一個(gè)連接,當(dāng)這個(gè)連接的事務(wù)完成時(shí)執(zhí)行解鎖通知函數(shù)。解鎖通知函數(shù)從sqlite3_step()(或sqlite3_close())里執(zhí)行,它關(guān)聯(lián)有一個(gè)阻塞式進(jìn)程。解鎖通知函數(shù)里面可以調(diào)用任何的sqlite3_XXX()函數(shù),可以向其他等待線程發(fā)信號(hào),或者安排一些在以后要發(fā)生的行為。
?? ?sqlite3_blocking_step()函數(shù)使用的算法描述如下:
?? ?(1)在指定的SQL語(yǔ)句對(duì)象上調(diào)用sqlite3_step(),如果返回除SQLITE_LOCKED之外的值,則直接返回這個(gè)值給調(diào)用者。如果返回SQLITE_LOCKED則繼續(xù)。
?? ?(2)調(diào)用sqlite3_unlock_notify()注冊(cè)一個(gè)解鎖通知回調(diào)函數(shù)。如果sqlite3_unlock_notify()返回SQLITE_LOCKED,說(shuō)明系統(tǒng)死鎖,返回這個(gè)值給調(diào)用者以便回滾。否則繼續(xù)。
?? ?(3)阻塞,直到解鎖通知函數(shù)被另外一個(gè)線程執(zhí)行。
?? ?(4)在SQL語(yǔ)句對(duì)象上調(diào)用sqlite3_reset()。因?yàn)镾QLITE_LOCKED錯(cuò)誤可能只發(fā)生在第一次調(diào)用sqlite3_step()時(shí)(不可能有sqlite3_step()先返回SQLITE_ROW而下一次卻返回SQLITE_LOCKED的情況)。這時(shí)SQL語(yǔ)句對(duì)象會(huì)被重置,從而不會(huì)影響查詢結(jié)果。如果不調(diào)用sqlite3_reset(),下一次調(diào)用sqlite3_step()將返回SQLITE_MISUSE。
?? ?(5)轉(zhuǎn)向步驟(1)。
?? ?sqlite3_blocking_prepare_v2()使用的算法也類似,只不過(guò)第4步(重置SQL語(yǔ)句對(duì)象)忽略。
?? ?對(duì)于“寫?zhàn)囸I”現(xiàn)象,SQLite能幫助應(yīng)用程序避免出現(xiàn)寫?zhàn)囸I的情況。當(dāng)在一個(gè)表上獲取寫鎖的任何嘗試失敗后(因?yàn)橛羞B接一直持有讀鎖),共享緩存上啟動(dòng)新事務(wù)的所有嘗試都會(huì)失敗,直到下面有一種情況變成true為止:
?? ?* 當(dāng)前寫事務(wù)完成,或者
?? ?* 共享緩存上打開的讀事務(wù)數(shù)量減為0。
?? ?啟動(dòng)新的讀事務(wù)失敗會(huì)返回SQLITE_LOCKED給調(diào)用者。如果調(diào)用者然后調(diào)用sqlite3_unlock_notify()注冊(cè)一個(gè)解鎖通知函數(shù),阻塞式連接當(dāng)前在共享緩存上會(huì)有一個(gè)寫事務(wù)。這就避免了寫?zhàn)囸I,因?yàn)闆]有新的讀鎖可以打開了。當(dāng)所有存在的讀鎖完成時(shí),寫操作最終能有機(jī)會(huì)獲得需要的寫鎖。
?? ?在wait_for_unlock_notify()調(diào)用sqlite3_unlock_notify()時(shí),有可能阻塞式線程已經(jīng)完成它的事務(wù),這樣在sqlite3_unlock_notify()返回前解鎖通知函數(shù)會(huì)立刻被調(diào)用。解鎖通知函數(shù)也有可能被另一個(gè)線程調(diào)用,正好發(fā)生在sqlite3_unlock_notify()調(diào)用之后,而在這個(gè)線程開始等待異步信號(hào)之前。這樣的競(jìng)爭(zhēng)條件怎么處理,取決于應(yīng)用程序使用的線程和同步原語(yǔ)。本例子中使用pthread,這是現(xiàn)代Unix風(fēng)格的系統(tǒng)(包括Linux)提供的接口。
?? ?pthread提供pthread_cond_wait()函數(shù),它允許調(diào)用者同時(shí)釋放一個(gè)互斥量并開始等待一個(gè)異步信號(hào)。使用這個(gè)函數(shù)、一個(gè)"fired"標(biāo)志和一個(gè)互斥量,競(jìng)爭(zhēng)狀態(tài)可以消除,如下:
?? ?當(dāng)解鎖通知函數(shù)被調(diào)用時(shí),這可能發(fā)生在調(diào)用sqlite3_unlock_notify()的線程開始等待一個(gè)異步信號(hào)之前,它做下面的工作:
?? ?(1)獲取互斥量。
?? ?(2)設(shè)置"fired"標(biāo)志為true。
?? ?(3)向等待線程發(fā)信號(hào)。
?? ?(4)釋放互斥量。
?? ?當(dāng)wait_for_unlock_notify()線程開始等待解鎖通知函數(shù)到達(dá)時(shí),它:
?? ?(1)獲取互斥量。
?? ?(2)檢查"fired"標(biāo)志是否設(shè)置。如果已設(shè)置,解鎖通知函數(shù)已經(jīng)被調(diào)用,直接釋放互斥量,然后繼續(xù)。
?? ?(3)如果沒設(shè)置,原子性地釋放互斥量,并開始等待異步信號(hào)。當(dāng)信號(hào)到達(dá)時(shí),繼續(xù)。
?? ?通過(guò)這種方式,當(dāng)wait_for_unlock_notify()開始阻塞時(shí),解鎖通知函數(shù)不管是已經(jīng)被調(diào)用,還是正在被調(diào)用,都沒有問(wèn)題。
?? ?本文例子中的代碼至少在以下兩個(gè)方面可以改進(jìn):
?? ?* 能管理線程優(yōu)先級(jí)。
?? ?* 能處理SQLITE_LOCKED的特殊情形,這可能發(fā)生在刪除一個(gè)表或索引時(shí)。
?? ?雖然sqlite3_unlock_notify()只允許調(diào)用者指定單個(gè)的用戶上下文指針,但一個(gè)解鎖通知回調(diào)是傳給這種上下文指針數(shù)組的。這是因?yàn)楫?dāng)一個(gè)阻塞式線程完成它的事務(wù)時(shí),如果有多個(gè)解鎖通知被注冊(cè)用于調(diào)用同一個(gè)C函數(shù),則上下文指針就要排列成一個(gè)數(shù)組。如果每個(gè)線程分配一個(gè)優(yōu)先級(jí),則高優(yōu)先級(jí)的線程就會(huì)比低優(yōu)先級(jí)的線程先得到信號(hào)通知,而不是以任意的順序來(lái)通知線程。
?? ?如果執(zhí)行一個(gè)"DROP TABLE"或"DROP INDEX"命令,而當(dāng)前數(shù)據(jù)庫(kù)連接上有一個(gè)或多個(gè)正在執(zhí)行的SELECT語(yǔ)句,則會(huì)返回SQLITE_LOCKED。如果調(diào)用了sqlite3_unlock_notify(),指定的回調(diào)函數(shù)立刻會(huì)被調(diào)用。重新嘗試"DROP TABLE"或"DROP INDEX"將返回另外一個(gè)SQLITE_LOCKED錯(cuò)誤。在上面的sqlite3_blocking_step()實(shí)現(xiàn)中,這會(huì)導(dǎo)致死循環(huán)。
?? ?調(diào)用者可以使用擴(kuò)展錯(cuò)誤碼來(lái)區(qū)別這種特殊的"DROP TABLE|INDEX"情形和其他情形。當(dāng)它正常調(diào)用sqlite3_unlock_notify()時(shí),擴(kuò)展錯(cuò)誤碼是SQLITE_LOCKED_SHAREDCACHE。在"DROP TABLE|INDEX"情形中,是普通的SQLITE_LOCKED。另外一種解決方法是限制重試單個(gè)查詢的次數(shù)(如100次)。雖然這會(huì)導(dǎo)致效率低一點(diǎn),但我們這里討論的情況并不是經(jīng)常發(fā)生的。