這 部分內容將介紹三個緊密聯系的主題:索引文件的並發訪問、IndexReader和IndexWriter的線程安全性,以及Lucene用於避免索引被 破壞而使用的鎖機制。通常,Lucene的初學者們對這幾個主題都存在一定的誤解。而准確地理解這些內容是十分重要的,因為,當索引應用程序同時服務於大 量不同的用戶時,或為了滿足一些突發性的請求、而需要通過對某些操作進行並行處理時,這些內容會幫助你消除在構建應用程序過程中所遇到的疑問。
2.9.1 並發訪問的規則
Lucene 提供了一些修改索引的方法,例如索引新文檔、更新文檔和刪除文檔;在執行這些操作時,為了避免對索引文件造成損壞,需要遵循一些特定的規則。這類問題通常 會在web應用程序中突顯出來。因為web應用程序是同時為多個請求而服務的。Lucene的並發性規則雖然比較簡單,但我們必須嚴格遵守:
— 任意數量的只讀操作都可以同時執行。例如,多個線程或進程可以並行地對同一個索引進行搜索。
— 在索引正在被修改時,我們也可以同時執行任意數量的只讀操作。例如,當某個索引文件正在被優化,或正在對索引執行文檔的添加、更新或刪除操作時,用戶仍然可以對這個索引進行搜索。
— 在某一時刻,只允許執行一個修改索引的操作。也就是說,在同一時間,一個索引文件只能被一個IndexWriter或IndexReader對象打開。
基於以上的並發性規則,我們可以構造一些關於並發性的更全面的例子,如表2.2中所示。表中說明了是否允許我們對一個索引文件進行各種並發性的操作。
表2.2 是否允許對某個Lucene索引進行並發性操作的舉例
操 作 |
是否允許 |
對同一個索引運行多個並行的搜索進程 |
允許 |
對一個正在生成、被優化或正在與另一索引合並的索引運行多個並行的搜索進程,或該索引正在進行刪除、更新文檔等操作時,對索引運行多個並行的搜索進程 |
允許 |
對同一個索引用多個IndexWriter對象執行添加、更新文檔的操作 |
不允許 |
當一個從索引中刪除文檔的IndexReader對象沒有成功關閉時,打開一個IndexWriter對象用於在這個索引中添加新的文檔 |
不允許 |
IndexWriter對象向索引中添加新文檔后,未成功關閉;在此之后,打開一個IndexReader對象用於從這個索引中刪除文檔 |
不允許 |
注:當正在修改一個索引時,請記住在某一時刻,在同一個索引上只能執行一個修改操作。
2.9.2 線程安全性
盡管 Lucene不允許使用多個IndexWriter或IndexReader實例同時對一個索引進行修改,如表2.2所示,但是這兩個類都是線程安全 (thread-safe)的,了解這一點相當重要。因此,這兩個類的實例都可以被多線程共享,Lucene會對各個線程中所有對索引進行修改的方法的調 用進行恰當的同步處理,以此來確保修改操作能一個接着一個地有序進行。圖2.7描述了這樣的一個場景:
圖2.7 一個IndexWriter或IndexReader對象可以被多個線程所共享
應用程序不需要進行額外的同步處理。盡管IndexReader和IndexWriter這兩 個類都是線程安全的,使用Lucene的應用程序還是必須確保這兩個類的對象對索引的修改操作不能重疊。也就是說,在使用IndexWriter對象將新 文檔被添加至索引中之前,必須關閉所有已經完成在同一個索引上,進行刪除操作的IndexReader實例。同樣地,在IndexReader對象對索引 中的文檔進行刪除和更新之前,必須關閉此前已經打開該索引的 IndexWriter實例。
表2.3是一個關於並發操作的矩陣,它向我們展示了一些具體操作是否能並發地執行。該表假定應 用程序只使用了一個IndexWriter或IndexReader實例。請注意,在此我們並沒有將對索引的更新視為一個單獨的操作列出,因為它實際上可 以被看成是在刪除操作后再進行一個添加操作。
表2.3 使用同一個IndexWriter或IndexReader實例時的並發操作矩陣,表中打叉的部分表示兩個操作不能同時執行
|
查找 |
讀文檔 |
添加 |
刪除 |
優化 |
合並 |
查找 |
|
|
|
|
|
|
讀文檔 |
|
|
|
|
|
|
添加 |
|
|
|
× |
|
|
刪除 |
|
|
× |
|
× |
× |
優化 |
|
|
|
× |
|
|
合並 |
|
|
|
× |
|
|
這個矩陣可以歸納為:
— IndexReader對象在從索引中刪除一個文檔時,IndexWriter對象不能向其中添加文檔。
— IndexWriter對象在對索引進行優化時,IndexReader對象不能從其中刪除文檔。
— IndexWriter對象在對索引進行合並時,IndexReader對象也不能從其中刪除文檔。
從上面的矩陣及其歸納中,我們可以得到這樣一個使用模式:當IndexWriter對象在對索 引進行修改操作時,IndexReader對象不能對索引進行修改。這個操作模式是對稱的:當IndexReader對象正在對索引進行修改操作 時,IndexWriter對象同樣也不能對索引進行修改。
這里讀者可以感到,Lucene的並發性規則和社會中的那些良好的習慣以及合理的法規具有相通 之處。我們不一定非得嚴格地遵守這些規則,但是如果違反這些規則將會造成相應的后果。在現實生活中,違反法律法規也許得鋃鐺入獄。在使用Lucene時, 違背這些規則,則會損壞你的索引文件。Lucene使用者可能會對並發性有錯誤的理解甚至誤用,但Lucene的創造者們對此早已有所預料,因此他們通過 鎖機制盡可能地避免應用程序對索引造成意外的損壞。本書將在2.9.3 節中對Lucene索引的鎖機制進行進一步的介紹。
2.9.3 索引鎖機制
在Lucene 中,鎖機制是與並發性相關的一個主題。在同一時刻只允許執行單一進程的所有代碼段中,Lucene都創建了基於文件的鎖,以此來避免誤用Lucene 的API造成對索引的損壞。各個索引都有其自身的鎖文件集;在默認的情況下,所有的鎖文件都會被創建在計算機的臨時目錄中,這個目錄由Java的 java.io.tmpdir中的系統屬性所指定。
如果在索引文檔時,觀察一下那個臨時目錄,就可以看到Lucene的write.lock文 件;在段(segment)進行合並時,還可以看到 commit.lock文件。你可以通過設定org.apache.lucene.lockDir中的系統屬性,使鎖文件存放的目錄改至指定的位置。這個 系統屬性可以通過使用Java API在程序中進行設定,還可以通過命令行進行設定,如:-Dorg.apache.lucene.lockDir= /path/to/lock/dir。若有多台計算機需要訪問存儲在共享磁盤中的同一個索引,則應該在程序中顯式地設定鎖目錄,這樣位於不同計算機上的應 用程序才能訪問到彼此的鎖文件。根據已知的鎖文件以及網絡文件系統(NFS)出現的問題,鎖目錄應該選擇放在一個不依賴於網絡的文件系統卷上。以下就是上 面提到的兩個鎖文件:
write.lock 文件用於阻止進程試圖並發地修改一個索引。更精確地說,IndexWriter對象在實例化時獲得write.lock文件,直到IndexWriter 對象關閉之后才釋放。當IndexReader對象在刪除、恢復刪除文檔或設定域規范時,也需要獲得這個文件。因此,write.lock會在對索引進行 寫操作時長時間地鎖定索引。
當對段進行讀或合並操作時,就需要用到commit.lock文件。在IndexReader 對象讀取段文件之前會獲取commit.lock文件,在這個鎖文件中對所有的索引段進行了命名,只有當IndexReader對象已經打開並讀取完所有 的段后,Lucene才會釋放這個鎖文件。IndexWriter對象在創建新的段之前,也需要獲得commit.lock文件,並一直對其進行維護,直 至該對象執行諸如段合並等操作,並將無用的索引文件移除完畢之后才釋放。因此,commit.lock的創建可能比write.lock更為頻繁,但 commit.lock決不能過長時間地鎖定索引,因為在這個鎖文件生存期內,索引文件都只能被打開或刪除,並且只有一小部分的段文件被寫入磁盤里。表 2.4對Lucene 中各種使用Lucene API來鎖定索引的情況進行了概括。
表2.4 Lucene中所有鎖及創建和釋放鎖的操作
鎖文件 |
類 |
何時獲取 |
何時釋放 |
描述 |
write.lock |
IndexWriter |
構造函數 |
close() |
在關閉IndexWriter對象時釋放鎖 |
write.lock |
IndexReader |
delete(int) |
close() |
在關閉IndexReader對象時釋放鎖 |
write.lock |
IndexReader |
undeleteAll(int) |
close() |
在關閉IndexReader對象時釋放鎖 |
write.lock |
IndexReader |
setNorms (int,String,byte) |
close() |
在關閉IndexReader對象時釋放鎖 |
commit.lock |
IndexWriter |
構造函數 |
構造函數 |
段信息被讀取或寫入后立即釋放鎖 |
commit.lock |
IndexWriter |
addIndexes (IndexReader[]) |
addIndexes (IndexReader[]) |
寫入新的段時獲取鎖文件 |
commit.lock |
IndexWriter |
addIndexes (Directory[]) |
addIndexes (Directory[]) |
寫入新的段時獲取鎖文件 |
commit.lock |
IndexWriter |
mergeSegment (int) |
mergeSegments (int)) |
寫入新的段時獲取鎖文件 |
commit.lock |
IndexReader |
open(Direcory) |
Open(Direcory) |
所有段被讀取后獲取鎖文件 |
commit.lock |
SegmentReader |
doClose() |
doClose() |
段的文件被寫入或重寫后獲取鎖文件 |
commit.lock |
SegmentReader |
undeleteAll() |
undeleteAll() |
移除段.del文件后獲取鎖文件 |
請注意另外兩個與鎖相關的方法:
— IndexReader的isLocked(Directory) ——這個方法可以判斷參數中指定的索引是否已經被上鎖。在想要對索引進行某種修改操作之前,應用程序需要檢查索引是否被鎖保護時,通過使用這個方法可以很方便地得到結果。
— IndexReader的unlock(Directory) ——該方法的作用正如其命名那樣。盡管通過這個方法可以使你在任意時刻對任意的Lucene索引進行解鎖,然而它的使用具有一定的危險性。因為 Lucene創建鎖自有其理由,此外,在修改一個索引時對其解鎖可能導致這個索引被損壞,從而使得這個索引失效。
雖然知道Lucene使用了哪些鎖文件,何時、為什么要使用它們,以及在文件系統的何處存放這 些鎖文件,但是你不能直接在文件系統對它們進行操作。你應該通過 Lucene的API對它們進行操作。否則,如果將來Lucene開始啟用一種不同的鎖機制,或者Lucene改變了鎖文件的命名或存儲位置時,應用程序 可能會受到影響而不能順利執行。
鎖機制的實例
為了演示鎖是如何使用的,程序2.7演示了Lucene如何利用鎖來避免在同一時刻對同一索引 文件進行多個修改操作。在testWriteLock( )方法中,Lucene對一個已經被IndexWriter對象打開的索引上鎖,阻止第二個IndexWriter對象對這個索引進行修改。
如同我們先前提到的,Lucene的初學者們有時對這一章中介紹的並發性沒有很好理解,從而陷 入到本小節中提到的關於鎖的問題里,以至在程序中出現了上面所示的異常。在你的應用程序中如果出現了類似的異常,而索引的一致性對用戶而言又十分重要,那 么請不要漠視這些異常。與鎖相關的異常通常是誤用了Lucene API的一個標志;若在應用程序中出現了這種異常,應該妥善地處理它們。2.9.4 禁用索引鎖
我們強烈地建議讀者不要對Lucene的鎖機制進行隨意修改,不要漠視與鎖相關的異常。然而在 一些情況下,你也許想禁用Lucene當中的鎖機制,並且這樣做不會破壞索引文件。例如,應用程序可能需要訪問存儲在CD-ROM上的Lucene索引。 因為CD是一種只讀介質,這意味着應用程序對索引的操作也是只讀模式的。換句話說,該應用程序只使用Lucene來搜索索引而不需要對索引進行任何形式的 修改。盡管Lucene已經將鎖文件保存在系統的臨時目錄(這個目錄通常可以被系統的所有用戶打開以用於寫操作)中,但是你仍可以通過將 disableLuceneLocks這個系統屬性設定為“true”,從而禁用 write.lock和commit.lock文件。