走自己的路

          路漫漫其修遠(yuǎn)兮,吾將上下而求索

            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            50 隨筆 :: 4 文章 :: 118 評論 :: 0 Trackbacks
           

          主要介紹如何周期性盡量實時地從RDBMS爬數(shù)據(jù)然后建索引,不涉及AOPORM Frameworklistener方式。

          先決條件:

          1. Lucene索引是從無到有的,一開始所有數(shù)據(jù)都是存儲在RDBMSOracle)中。
          2. 數(shù)據(jù)表有一列是updateTime或稱為lastModifiedTime用來存儲最后一次更新時間,并建有db索引
          3. 主表必須要有主鍵,這個主鍵也用來唯一確定一個Lucene document

          該策略大致可以分為以下幾個部分:

          1.索引結(jié)構(gòu)

          2.初始化索引

          3.增量索引

          4.補(bǔ)償操作

          5.刪除檢測

          6.備份

          7.注意點

          索引結(jié)構(gòu):

          我們是做全文搜索的,所以db中的用戶需要全文搜索的字段值都會拼接成一個string存儲在lucene的一個字段中稱為content。其他的需要存儲的字段或者字段分析后的token是有意義單詞的我們稱為metadata,每個metadatadocument的一個field

          從索引結(jié)構(gòu)看來,我們有一個主表,不同的主表OID的記錄對應(yīng)不同的lucene document。主表依賴的所有子表中的數(shù)據(jù)如果是需要做全文搜索的就需要append在一起然后用Ngram進(jìn)行分詞處理。有的數(shù)據(jù)是需要存儲的或有特殊用途的,比如訪問控制,就不分詞或者用特殊的analyzer分詞。

          簡單的介紹一下,具體會在以后的文章中談到。

          初始化索引

          初始化索引,是索引從無到有的一個過程。也是系統(tǒng)第一次初始化時做的事情。初始化時我們會首先獲取當(dāng)前時間,然后將updateTime早于當(dāng)前時間的所有數(shù)據(jù)取出來。最后把當(dāng)前時間存儲到timeTrace.properties文件中。作為增量索引的起始依據(jù)。

          取什么?

          用戶預(yù)先在配置文件配置一個主SQL,通過這個sql我們?nèi)〕鲂枳鋈乃饕?/span>contentmetadata。對于某些特定的metadata不希望被build到全文索引中的,可以單獨配置sql

          如何取,多線程分批取。我們會有一個線程池,每個分批都是作為該線程池的一個task被提交執(zhí)行。

          ·         查詢出當(dāng)前時間之前的記錄總數(shù)n

          ·         按照rownum分批。假設(shè)分批的大小是2000,則需要將rownum0, 200040006000…..max(rownum)的所有OID都一次拿回來。

          Select oid from x where rownum=0 or rownum=2000,………….or rownum=n

          如果直接根據(jù)rownum分批取,會出現(xiàn)幻影讀的問題,因為rownum每次查詢都會發(fā)生變化,如果有新數(shù)據(jù)插入,改用當(dāng)時snapshotOID去取,避免這個問題,因為OID是不變的。有人會說,這樣可能查到新插入的數(shù)據(jù),當(dāng)然是這樣的,但是查到新加入的不會有影響,新加入的也是遲早需要爬的,但是用rownum還會丟失數(shù)據(jù)。

          ·         根據(jù)拿回來的斷點的OID,分批取。比如令先前獲取的rownum2000oid10231,拿回來的4000oid14215

          前兩個分批的sql就是:

          Select * from x where oid >= 0 and oid < 10231;

          Select * from x where oid >=10231 and oid < 14215;

          具體的sqloid是哪張表的哪列都是在配置文件中預(yù)先配置的。

          ·         當(dāng)然每個task還要負(fù)責(zé)將從db中爬出的數(shù)據(jù)建索引,建完索引后提交對索引的改動

          ·         所有task都跑完之后,主線程將一開始獲取的當(dāng)前時間更新到timeTrace.properties文件中。

          當(dāng)中可能會出現(xiàn)問題,如果出現(xiàn)問題,就需要重新初始化,初始化之前會清除所有已建好的部分臟索引。因為當(dāng)前時間沒有更新到timeTrace.properties文件中。我們測試下來百萬級的數(shù)據(jù)這個過程大概需要10分鐘。

          增量索引

          在初始化索引成功后,當(dāng)時的時間已經(jīng)被更新到timeTrace.properties。增量索引是從這個時間點開始定期地被觸發(fā)執(zhí)行,可以使用quartz來管理這個timing job。增量索引稱為incrementalIndexService,增量索引服務(wù)的不同任務(wù)調(diào)度之間需要同步執(zhí)行,用quartzstateful job可以實現(xiàn),或者使用內(nèi)存,文件或DB鎖。

          ·         第一步,從timeTrace文件中獲取已爬數(shù)據(jù)的截至?xí)r間Tlast,獲取當(dāng)前時間Tcurrent

          ·         Select count(*) from X where updateTime > Tlast and updateTime <= Tcurrent, 結(jié)果記為n,如果n小于分批的大小就直接爬出這段的索引數(shù)據(jù)。如果n大于分批大小,就需要將這個n個結(jié)果分批.

          ·         這次我們按照時間段分批,如果n/2000 = 3但有余數(shù), 那就說明要分四批拿,將這個時間段Tcurrent-Tlast平均分為四段。每個線程處理其中的某段。

          ·         某線程將它負(fù)責(zé)的某段數(shù)據(jù)拿回來之后,首先判斷這個OID是否在index已經(jīng)存在,如果存在就說明在這個時間段里這條記錄是被用戶update過的,index也做相應(yīng)的update。如果這個OIDindex中不存在,則說明這條記錄是新加入到db中的,index也做add操作。做完之后提交。

          ·         當(dāng)所有分批都完成之后,更新timeTrace文件,把時間更新為Tcurrent

          ·         一旦有分批出現(xiàn)問題失敗,整個時間段就認(rèn)為是不成功的,需要重新爬一遍。

          一般增量服務(wù)我們設(shè)置的間隔都小于1分鐘,因為需要拿出最實時的數(shù)據(jù),而且每次獲取數(shù)據(jù)的結(jié)束時間都是當(dāng)前時間。保證數(shù)據(jù)的實時性。

          補(bǔ)償操作

          補(bǔ)償操作在整個爬蟲策略中是最復(fù)雜的一個環(huán)節(jié)。采用增量索引看似天衣無縫,其實還是有風(fēng)險的。因為記錄到dbupdateTime往往都是有延遲的,一般情況下是java端的時間或是記錄寫入DB的時間,都早于commit時間,但一般數(shù)據(jù)庫的隔離級別都是read committed。只有在數(shù)據(jù)被提交后才可能被增量服務(wù)看到。這樣的話3點跑的增量服務(wù),先前的結(jié)束時間是258分。這時它需要獲取2.583.00之間的數(shù)據(jù),但是此時有可能java端正有一條記錄生成,它的updateTime 2.59,但是它一直沒有commit,因為transaction的超時時間是10分鐘。悲劇了發(fā)生了,這條數(shù)據(jù)將永遠(yuǎn)不會被爬出來,除非遙遠(yuǎn)的將來有人再次更新它。因為這個時間段已經(jīng)被爬過了,按照增量服務(wù),它是永遠(yuǎn)不會再爬timeTrace文件中記錄的時間之前的數(shù)據(jù)的。

          此時,補(bǔ)償服務(wù)隆重登場。它存在的價值就是把所有可能被遺漏的數(shù)據(jù)都查出來。關(guān)鍵點就是要找出在補(bǔ)償服務(wù)運(yùn)行時,哪個時間段的數(shù)據(jù)是可能被遺漏的而哪個時間段的數(shù)據(jù)又是永遠(yuǎn)不會被丟失的。那個永遠(yuǎn)不會丟失的時間段就沒必要再去管它。我們關(guān)心的只是可能被遺漏的那段時間段的數(shù)據(jù)。

          我們來看個例子:

          ·         增量服務(wù)每1分鐘跑一次, 周期記為P(N) = 1

          ·         Transaction Timeout時間3分鐘,記為TO

          ·         補(bǔ)償服務(wù)每x分鐘跑一次,P(C)  = x >= P(N)

          下圖的第一條時間軸T(N)是增量服務(wù)的,第二條是補(bǔ)償服務(wù)的T(C)


           

          對于補(bǔ)償服務(wù)我們需要確定每次的開始時間Ts,結(jié)束時間Te,周期P(C), 算法,初始化值,意外情況,優(yōu)化等方面。

           

          結(jié)束時間Te

          補(bǔ)償服務(wù)的目的是對增量索引進(jìn)行補(bǔ)償,所以它所補(bǔ)償?shù)臅r間區(qū)間一定是增量服務(wù)已經(jīng)處理過的。所以它的結(jié)束時間一定是timeTrace文件中最后一次增量服務(wù)記錄的時間我們記為Last(N). Last(N)之后的數(shù)據(jù)或者正在被增量服務(wù)處理或者沒有被增量服務(wù)處理,如果補(bǔ)償服務(wù)去涉及這些數(shù)據(jù),那肯定全是要補(bǔ)償?shù)模窃隽糠?wù)也會去處理,一個是會重復(fù)處理,一旦需要補(bǔ)償,我們會把這條記錄的所有數(shù)據(jù)都從DB端取過來,建索引,重復(fù)的代價也是很大的。為了避免這個代價: Te < Last(N).

           

          如果Te = Last(N), 我們就會拿到最新的需要補(bǔ)償?shù)臄?shù)據(jù),補(bǔ)償服務(wù)的延遲就最小。Te < Last(N), 每次都沒有不嘗到最新的數(shù)據(jù),遺漏數(shù)據(jù)被檢測到就會有延遲,只有等下一次補(bǔ)償服務(wù)觸發(fā)時才能被檢測出。

           

          開始時間Ts

          上圖中,transaction timeout之前的那個時間段,如果有數(shù)據(jù)生成,T(N) = 1, 要么在圖上標(biāo)出的[T(N) =1T(N) = 4]之間被提交,要么transaction超時該數(shù)據(jù)也不會寫入到DB中。所以Last(N) – TO之前的數(shù)據(jù)在這一次補(bǔ)償服務(wù)的時候已經(jīng)是完全可見的,肯定都會被補(bǔ)償?shù)摹τ谙乱淮蝸碚f這塊也是不需要再被補(bǔ)償?shù)摹M耆梢妳^(qū)(針對下一次補(bǔ)償操作)必須滿足下面兩個條件:

          ·         被增量服務(wù)處理過并且更新已經(jīng)完全對本次補(bǔ)償服務(wù)可見

          ·         已經(jīng)被補(bǔ)償服務(wù)處理過

           

          則下一次補(bǔ)償操作就不會再關(guān)心Last(N)-TO之前的數(shù)據(jù)了。我們把上一次補(bǔ)償服務(wù)記為Last(C), 而此次的增量服務(wù)記為Last(C Last(N)).  則下一次補(bǔ)償服務(wù)的開始時間<=Last(N)-TO.因為大于這個時間的所有數(shù)據(jù)都是需要被補(bǔ)償?shù)摹Q個表達(dá)方式,此次的補(bǔ)償服務(wù)的開始時間是由上一次補(bǔ)償服務(wù)計算的得到的,為Last(C Last(N)) – TO.  Ts <= Last(C Last(N)) – TO

           

          同時我們需要注意的是,Last(N) – TO Last(N)每次補(bǔ)償服務(wù)是必須要檢測的,不然就會有遺漏,因為我們假設(shè)了不可見區(qū),前提條件就是每次都會檢測這個區(qū)域。所以結(jié)束時間: Last(N) – TO < Te < Last(N).

           

          開始時間是由上一次補(bǔ)償服務(wù)計算得到的,那這個值就需要保存下來。保存在文件中可以避免系統(tǒng)down掉后丟失。我們也會將這個時間值保存到timeTrace.propertiescompensation屬性上。

           

           

          算法

          補(bǔ)償服務(wù)的算法主要目的就是比較出遺漏的數(shù)據(jù)。為了比較有無遺漏,我們需要把db中的數(shù)據(jù)和增量服務(wù)已經(jīng)爬過的數(shù)據(jù)進(jìn)行比較才知道。我們會在內(nèi)存中存放增量服務(wù)已經(jīng)爬過的數(shù)據(jù)的oidupdateTime,在內(nèi)存中存放是為了提高性能。每次補(bǔ)償服務(wù)運(yùn)行時,也會把完全可見區(qū)從內(nèi)存中清除。

           

          ·         增量服務(wù)每次執(zhí)行后就會將爬出的數(shù)據(jù)的OIDupdateTime保存在內(nèi)存中,內(nèi)存中有一棵二叉排序樹維護(hù)OIDupdateTimepair。排序的keyOID。二叉排序樹可以用TreeSet實現(xiàn)。

          ·         補(bǔ)償服務(wù)運(yùn)行時,先創(chuàng)建兩個List一個用來存放需要update數(shù)據(jù)的oid,記為updateList,另一個用來存放add數(shù)據(jù)的oid,記為addList

          ·         接著從DB中取出TsTe之間所有數(shù)據(jù)的OIDupdateTime,也是根據(jù)OID排序的。

          ·         計算下一次補(bǔ)償服務(wù)的開始時間,Next(Ts) = Last(N) –TO;

          ·         現(xiàn)在就是要比較兩個有序集合。樹的訪問者應(yīng)該寫成:

          if(OID(C) == OID(N))

               if(updateTime(C) > updateTime (N))

               {

                            更新OID(N)updateTime;

                             updateList.add(OID(N));

                  }

                  else{

                 addList.add(OID(N))

                 }

                  //清除完全可見區(qū),下一次補(bǔ)償服務(wù)開始時間之前的數(shù)據(jù)

                     if(updateTime(N) < Next(Ts)) {

                     tressSetIterator.remove();

                    }

                 

          ·         updateListOID對應(yīng)的所有數(shù)據(jù)從db中獲取并update到索引中

          ·         addListOID對應(yīng)的所有的數(shù)據(jù)從db中獲取并add到索引中

          ·         Commit索引,并將Next(Ts)記錄到updateTrace文件中

           

          意外情況

          補(bǔ)償服務(wù)很久沒有被調(diào)度

          一般不會出現(xiàn),因為我們會將增量服務(wù)和補(bǔ)償服務(wù)的線程優(yōu)先級設(shè)為相同的。應(yīng)該會被分時處理。如果很久沒有不會被調(diào)度,正確性是可以保證的,因為開始時間都是記錄在文件中的,如果一直沒有跑,只是一下子補(bǔ)償?shù)臅r間段很長,并不會丟失補(bǔ)償?shù)臅r間段。但是不排除內(nèi)存溢出的風(fēng)險,因為存儲在內(nèi)存中的treeset在這種情況下會很大。在treeset很大時,我們可以檢測,如果超過一定的節(jié)點數(shù),就可以將treeset序列化到一個internal索引中,下次取出來時也是有序的。甚至可以分塊取出比較。

           

          Server突然shutdown

          Server shutdown突然shutdown,線程被interrupt掉,沒有執(zhí)行完,內(nèi)存中的樹也沒了。這時就需要每次啟動時,這個時間段內(nèi)的所有數(shù)據(jù)都會認(rèn)為是需要add到索引的,這樣就會出問題。所以需要提前檢測,每次系統(tǒng)啟動時,補(bǔ)償服務(wù)需要把這段時間內(nèi)的所有記錄的OIDupdateTimedb中獲取,直接和索引中的進(jìn)行比較,比較效率要低一些。但也不會出現(xiàn)數(shù)據(jù)丟失的情況。

           

           

          初始化值

          系統(tǒng)初始化時,補(bǔ)償服務(wù)初始化Ts(被掃描數(shù)據(jù)的起始時間)是,初始化索引的當(dāng)前時間-TO   而補(bǔ)償服務(wù)本身的開始時間是在增量服務(wù)開始之后。之后多少可以調(diào)。

           

          優(yōu)化

          優(yōu)化的重點放在了以下幾個方向。

          ·         DB壓力

          ·         補(bǔ)償延遲

          ·         消耗內(nèi)存的大小

          ·         比較次數(shù)

           

          以上選項之間有的都是矛盾的,比如說補(bǔ)償延遲要小,則補(bǔ)償服務(wù)的P(C)就要小,則查詢DB的次數(shù)就增加,對DB壓力就增大。

          所以針對不同的使用情況,比如DB資源,延遲的可接受程度,應(yīng)用服務(wù)器資源等,我們可能需要采用不同的策略,這就要我們的補(bǔ)償策略可調(diào)。

          為了可調(diào),我們不僅使一些參數(shù)可以配置,而且引入了分級補(bǔ)償服務(wù)的方案。在分級方案中,如果分n級,則n-1級的TO輸入值推薦和P(C)是相同的,但也是可調(diào)的。

           

          舉個例子:一個三級補(bǔ)償服務(wù),

          第一級:為了使補(bǔ)償?shù)难舆t最小,極端情況下我們可以采用和增量服務(wù)相同的周期假設(shè)為1分鐘,此時TO的輸入值也是周期值。此級的啟動時間也是初始化當(dāng)前時間記為Tinitial+P(N).

           

          第二級: 業(yè)務(wù)場景中絕大多數(shù)事務(wù)都是在3分鐘內(nèi)完成的,如果TO3分鐘,基本上絕大多數(shù)事務(wù)都可以及時的補(bǔ)償?shù)健4思壍膯訒r間是Tinitial+3

           

          第三級:也是最后一級,在App Server中配置的真正的TO10分鐘,為了保證正確性,TO的輸入值一定要是10分鐘,因為只需要保證正確性所以它的頻率也不需要太頻繁周期也設(shè)為10分鐘。從前文中可知Last(N) – TO < Te < Last(N). 此級沒有必要多實時,所以Te就取最小值=Last(N)-TO.

           

          我們將這個三級策略和一級策略進(jìn)行比較,我們假設(shè)一級策略的周期為2分鐘。假設(shè)整個時間段是10分鐘。

          比較項

          一級(2)

          三級(1, 3, 10)

          DB訪問次數(shù)

          5

          14

          延遲

          2

          <2

          內(nèi)存

          11分鐘數(shù)據(jù)的OID updateTime

          17

          訪問DB的數(shù)據(jù)量

          12×5=60

          10+6×10/3+2×10=50

          比較次數(shù)

          60

          50


          在這個分級策略中級數(shù)n, 每一級的P(c), TO, Te都是可調(diào)的,但需要注意最后一級的TO是不可調(diào)的必須等于真正的transaction timeout時間,Te的取值范圍是[Last(N)-TO, Last(N)]

           

          調(diào)優(yōu)的依據(jù)是我們會記錄每次補(bǔ)償操作的歷史記錄,比如每次補(bǔ)償成功的個數(shù),補(bǔ)償運(yùn)行的開始,結(jié)束時間等。

                        

          刪除檢測

          增量索引服務(wù)只是負(fù)責(zé)updateadd的檢測,它并不判定索引中document對應(yīng)的記錄在DB中是否已經(jīng)被刪除,索引中會積累很多在DB中已經(jīng)被清除的數(shù)據(jù)。這些document也要及時地從索引中刪除。所以會有一個定期的刪除檢測服務(wù),檢測出那些在索引中有,而在DB中已經(jīng)被物理刪除的記錄。

           

          刪除檢測服務(wù)的步驟:

          l           從索引中分批取出所有OID,根據(jù)OID排序

          l           用每個分批的最小值和最大值到DB中取出此OIDDB中存在(沒有被刪除)的所有OID,也根據(jù)OID排序

          Select OID from X where oid>12001 and oid<24100 order by oid;

          l           將索引中查處的OID      有序集合和DB中獲得的OID有序集合進(jìn)行對比,如果DB中沒有索引中有的就添加到deletedList.

          l           deletedList中的所有記錄對應(yīng)document從索引中刪除

           

          對于軟刪除,它們的狀態(tài)屬性active如果已經(jīng)被爬到索引中,直接從索引中選擇出那些active=0document刪除,如果沒有,可以將刪除檢測的sql語句改成

          Select OID from X where active = 0 and oid>12001 and oid<24100 order by oid;

          其他步驟同上面硬刪除的部分

           

          備份

          備份時需要注意不僅要備份最后一次commit之前的所有索引,而且需要備份timeTrace文件. 恢復(fù)后只需要從timeTrace的時間開始爬就可以了.

           

          注意點

          主表updateTime沒有更新

          有時候,業(yè)務(wù)邏輯更新了子對象,比如JobOrder對象包含了很多個Container對象,一個JobOrder對應(yīng)一個Lucene Document,當(dāng)Container對象更新時,它并沒有更新JobOrderupdateTime,只是更新了ContainerupdateTime。這也沒關(guān)系,我們再增量服務(wù)和補(bǔ)償策略中同時也會查出子表updateTime在當(dāng)前時間段的所有主表數(shù)據(jù)。

          container如果有刪除,就必須約定application必須要update主表的updateTime。否則用戶就會搜出他本不能訪問的被刪除的container



          posted on 2010-05-07 07:12 叱咤紅人 閱讀(2783) 評論(1)  編輯  收藏 所屬分類: Lucene

          評論

          # re: RDBMS的lucene爬蟲 2010-05-07 13:52 羅萊家紡
          阿那是表達(dá)式  回復(fù)  更多評論
            


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 腾冲县| 伊金霍洛旗| 绥化市| 长沙县| 通州市| 冕宁县| 右玉县| 诸暨市| 常德市| 西畴县| 志丹县| 龙井市| 儋州市| 西昌市| 高州市| 平阳县| 浦城县| 乌审旗| 财经| 甘肃省| 会东县| 开封县| 庆安县| 建水县| 莱州市| 交口县| 郴州市| 海兴县| 宜阳县| 拉萨市| 保康县| 靖边县| 黑龙江省| 方城县| 清涧县| 依兰县| 瑞安市| 南乐县| 鹤山市| 泸溪县| 桃江县|