點贊模塊中合并數(shù)據(jù)部分的實現(xiàn)

昨天我們談了一下如何設(shè)計點贊模塊,最后給出了優(yōu)化這個模塊的方案.

我們提到,可以通過合并收到的數(shù)據(jù),來進行優(yōu)化.而如何合并數(shù)據(jù),就成了一個尤為重要的問題.

針對這個問題,我們也給出了幾種解決方案,一種是重寫Tomcat,通過Tomcat進行攔截并定時處理,但是這種方案,實現(xiàn)起來難度有些大,第二種是通過服務(wù)器在處理請求時,是為其新建一個線程來處理的原理,來實現(xiàn),這種方案相對較簡單一些,第三種是通過消息隊列來實現(xiàn),這種方案挺復(fù)雜,但是難度不高.

今天我們就采用第二種方案來實現(xiàn).實現(xiàn)起來也是一波三折.且聽我慢慢道來.

我們打算這樣實現(xiàn):

  • 創(chuàng)建一個緩沖區(qū),讓所有線程共享,然后將一分鐘內(nèi)收到的數(shù)據(jù),全部保存到這個變量中,并給前臺返回一個成功的標記.
  • 過了一分鐘后,當收到請求時,先將緩沖區(qū)中的數(shù)據(jù),全部保存到數(shù)據(jù)庫中,然后將這個緩沖區(qū)清空,并再次寫入下個一分鐘之內(nèi)收到的數(shù)據(jù).

過程就是這么簡單.但是我們需要注意一些問題:

  • 如果在我們將數(shù)據(jù)保存到數(shù)據(jù)庫之后,清空緩沖區(qū)之前的這個時間段中,還有線程在向這個緩沖區(qū)寫入數(shù)據(jù),那么這些新寫入的數(shù)據(jù)沒有被保存就會被清除.所以我們需要先等待將之前收到的請求寫入到緩沖區(qū)結(jié)束后,阻塞后來收到的請求,再將數(shù)據(jù)保存到數(shù)據(jù)庫中,并清空緩沖區(qū).

  • 如果有多個線程執(zhí)行將數(shù)據(jù)保存到數(shù)據(jù)庫,并清空緩沖區(qū)的操作怎么辦?

我們先來看第一版的源代碼:

在第一版中,我們使用ReentrantLock來實現(xiàn)線程之間的同步.

我們先判斷此請求到來的時間與上次保存數(shù)據(jù)到數(shù)據(jù)庫的時間差,是否大于一分鐘.也就是判斷是否過去了一分鐘.如果是這樣,就獲取到ReentrantLock,并在有鎖的期間,將數(shù)據(jù)保存到數(shù)據(jù)庫中,然后重置保存數(shù)據(jù)到數(shù)據(jù)庫的時間點,并清空保存一分鐘之內(nèi)收到的全部數(shù)據(jù)的緩沖區(qū).lastAggegate這個變量是AtomicLong類型的,這是一個線程安全的Long類型.likeData這個緩沖區(qū),是ConcurrentHashMap類型的,這是一個線程安全的HashMap.

我們在做完上面的工作之后,釋放掉ReentrantLock.

下面的那塊代碼中,我們判斷是否已經(jīng)有線程占有ReentrantLock了,如果是,我們就一直循環(huán),等待它釋放.實際上就是起到了阻塞一分鐘之后收到的請求寫數(shù)據(jù)的作用.然后執(zhí)行后面的步驟.在后面,我們將收到的數(shù)據(jù),保存或者更新到likeData這個緩沖區(qū)中.

這里我們拿兩個同時到來的請求A和B,來分析一下上面的代碼:

第一種情況,先假設(shè)A和B都是在一分鐘之內(nèi)到來的,則會直接執(zhí)行下面的代碼塊.因為此時ReentrantLock并沒有被任意一個線程占有,所以這兩個線程A和B,會并發(fā)更新likeData這個緩存區(qū).這個沒有什么問題.

第二種情況,假設(shè)A和B都是一分鐘之后到來的,先假設(shè)A先判斷是否過去一分鐘,并判斷為true,然后在下面獲得鎖,重置時間點,這時,B在判斷是否過去了一分鐘,判斷為false,然后B執(zhí)行下面的代碼塊,而因為ReentrantLock已經(jīng)被A獲取,所以它只能在while循環(huán)中一直等待.當A清空likeData這個緩沖區(qū)并釋放ReentrantLock之后,B才得以和A同時并行的將數(shù)據(jù)寫入到likeData這個緩沖區(qū)中.如果單看這兩個線程,這個過程也沒有什么問題.結(jié)果也是正確的.可是,在A清空likeData這個緩沖區(qū)的時候,在高并發(fā)的情況下,很可能有一分鐘之內(nèi)到來的請求C,正在向緩沖區(qū)中寫入數(shù)據(jù)!!這樣,請求C的數(shù)據(jù),就被悄無聲息的刪除了.

第三種情況,假設(shè)A和B都是一分鐘之后到來的,它倆又同時判斷是否過去了一分鐘,并同時判斷為true,然后它們同時請求鎖,A或B其中一個獲得了ReentrantLock,然后執(zhí)行后面的流程.這個沒有什么好解釋的.同樣,它也有第二種情況中我們說的那種特點,數(shù)據(jù)很可能被悄無聲息的刪除了.同時,它還有一種特點,就是在高并發(fā)情況下,假設(shè)有一百個線程是串行著請求鎖,即第一個線程釋放了鎖,第二個線程才請求鎖,等第二個線程釋放了鎖之后,第三個線程才請求鎖,依次類推.我們可以看到,這樣不僅會使意外清除數(shù)據(jù)的情況更加嚴重,還會有性能問題.我們需要執(zhí)行獲得一百次鎖,執(zhí)行一百次鎖內(nèi)需要執(zhí)行的操作.

第四種情況,假設(shè)A是一分鐘之內(nèi)到來的,B是一分鐘之后到來的,就可能出現(xiàn)我們上面第二種情況中,所說的那種意外.

這幾種情況里面,意外清空數(shù)據(jù)的情況最難處理,因為我們無法做到讓鎖內(nèi)需要執(zhí)行的一系列操作,讓其等到一分鐘之內(nèi)到來的請求都把數(shù)據(jù)寫入到likeData這個緩沖區(qū)之內(nèi),再執(zhí)行.

而多個線程同時判斷是否過去一分鐘,并判斷為true這種情況,我們可以通過將判斷及執(zhí)行所內(nèi)的操作放到**synchronized **塊中,來解決.

其實意外清空數(shù)據(jù)的這種情況,我們可以通過ReadWriteLock或者StampedLock來解決.我們之前介紹過這兩種鎖.

這兩種鎖為何能夠解決意外清空數(shù)據(jù)的情況呢?

各位應(yīng)該都知道,ReadWriteLock的規(guī)則,即:

  • 如果沒有線程持有寫鎖,那么可以有任意多個線程同時持有讀鎖,來讀數(shù)據(jù),因為讀操作肯定是線程安全的.

  • 寫鎖最多只能被一個線程同時占有.

  • 不能同時占有讀鎖和寫鎖.如果線程A占有讀鎖,而線程B請求寫鎖,那么B必須等待A先釋放讀鎖,才能占有寫鎖.同樣,如果線程A占有寫鎖,而線程B請求讀鎖,那么B必須等待A先釋放寫鎖,才能占有讀鎖.

這個規(guī)則是否跟我們這里的需求很相似呢?

于是,我們寫出了第二版的代碼:

這里我們將判斷是否已經(jīng)過去了一分鐘,以及需要執(zhí)行的對應(yīng)的操作,都放到了synchronized同步代碼塊中,這樣,當執(zhí)行這個代碼塊的時候,因為是串行操作,所以同一時間只能有一個線程能夠獲得寫鎖,并執(zhí)行相應(yīng)的操作.

而可以并行執(zhí)行的將數(shù)據(jù)寫入緩沖區(qū)的操作,我們給其加一個讀鎖,讓其并行執(zhí)行.

這里我們可以看到,當一個線程A判斷已經(jīng)過去一分鐘,并要將數(shù)據(jù)寫入到數(shù)據(jù)庫時,需要先獲取寫鎖,而要占有寫鎖,必須沒有線程持有讀鎖.即之前的所有請求已經(jīng)將數(shù)據(jù)都寫入了likeData這個緩沖區(qū)之后,讀鎖都釋放了之后,線程A才能占有寫鎖并執(zhí)行相應(yīng)的操作.這就解決了數(shù)據(jù)被意外清除的問題.

同樣,因為要獲得讀鎖來將數(shù)據(jù)寫入到緩沖區(qū)時,必須先等待寫鎖的釋放,也就相當于阻塞了之后到來的請求的寫數(shù)據(jù)操作,防止在獲得寫鎖并執(zhí)行操作的這段時間中,到來的請求意外的向緩沖區(qū)中寫入數(shù)據(jù)并最終被清空.

用一百個線程,各發(fā)送了九次請求,沒有發(fā)現(xiàn)問題.請求中的全部數(shù)據(jù),可以被正確的保存到數(shù)據(jù)庫中.

在上面我們使用循環(huán)來獲取讀鎖和寫鎖,其實還有更好的寫法,就是使用上面提到過的StampedLock.因為ReadWriteLocktryLock()方法是立即返回的,所以我們需要通過while循環(huán),不斷地測試是否能夠獲得鎖.即使可以為其設(shè)置超時時間,也是極不方便的.如果設(shè)置的過小,我們無法保證在這個超時時間之內(nèi),會獲得鎖,如果設(shè)置的過大,又浪費時間,降低了效率.而StampedLock中的獲得鎖的方法,是會阻塞當前線程的.也就是說,如果獲取不到鎖,就會阻塞當前線程,一直到獲取到.這種方式其實更好一些,減少了上面因為while循環(huán)中CPU空轉(zhuǎn)造成的資源浪費.

還有一種更簡單的方案,就是將全部的操作,都放在synchronized代碼塊中.這個應(yīng)該也很好理解.這里不再詳細敘述這種方案.

但是,使用這種方案,有一個致命缺陷,就是性能的問題.我們向緩沖區(qū)寫數(shù)據(jù)的操作是可以并行的,如果全都放在synchronized里面,就只能是串行的,那全部的請求都得一個個的串行處理,對性能是極大的消耗.

而我們使用讀寫鎖的方案,只是將判斷以及寫數(shù)據(jù)庫的操作放入到synchronized塊中,雖然是串行,但是相當輕量級.大多數(shù)情況下,實際上只有判斷這條語句是需要并行執(zhí)行的,匯編指令也就是三條.

上面的讀寫鎖的代碼中,synchronizedexpression中,我們用的是一個用final修飾的,Integer類型的變量.它是不可變的.synchronized會取得** expression中的對象的monitor,將其當做互斥條件,一個對象只有一個monitor與之對應(yīng),如果我們拿一個不是final的可變的對象來做expression,那么很可能并沒有被正確的同步,得到的結(jié)果也是不正確的.當我用likeData這個ConcurrentHashMap對象時,以及lastAggegate**這個變量時,會出現(xiàn)錯誤的結(jié)果.

上面我們的緩沖區(qū)用的是ConcurrentHashMap這個容器,這個容器在讀多寫少時,性能很好,而我們現(xiàn)在是寫多讀少,跟它相反,雖然還沒有遇到什么性能問題,但是這里應(yīng)該選擇一個合適的適合寫多讀少的線程安全的Map.不知道有沒有.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,918評論 0 11
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,901評論 11 349
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,896評論 18 399
  • 關(guān)于在北京大學(xué)舉辦“諾貝爾獎與文化軟實力”論壇的通知 北京大學(xué)校友屠呦呦女士榮摘2015年度諾貝爾生理學(xué)或醫(yī)學(xué)獎桂...
    知識分子閱讀 440評論 0 0
  • 2016.8.16#是云開霧散的太陽,武志紅的《成為自己》講到所有的孩子都是肆無忌憚的伸出觸角,強壯的媽媽接納著他...
    愛花的小巫閱讀 624評論 0 0

友情鏈接更多精彩內(nèi)容