如何在高併發環境下設計出無鎖的數據庫操做(Java版本)

 

一個在線2k的遊戲,每秒鐘併發都嚇死人。傳統的hibernate直接插庫基本上是不可行的。我就一步步推導出一個無鎖的數據庫操做。java

 

1. 併發中如何無鎖。node

一個很簡單的思路,把併發轉化成爲單線程。Java的Disruptor就是一個很好的例子。若是用java的concurrentCollection類去作,原理就是啓動一個線程,跑一個Queue,併發的時候,任務壓入Queue,線程輪訓讀取這個Queue,而後一個個順序執行。 數據庫

在這個設計模式下,任何併發都會變成了單線程操做,並且速度很是快。如今的node.js, 或者比較普通的ARPG服務端都是這個設計,「大循環」架構。設計模式

這樣,咱們原來的系統就有了2個環境:併發環境 + 」大循環「環境緩存

併發環境就是咱們傳統的有鎖環境,性能低下。架構

」大循環「環境是咱們使用Disruptor開闢出來的單線程無鎖環境,性能強大。併發

 

2. 」大循環「環境 中如何提高處理性能。異步

一旦併發轉成單線程,那麼其中一個線程一旦出現性能問題,必然整個處理都會放慢。因此在單線程中的任何操做絕對不能涉及到IO處理。那數據庫操做怎麼辦?性能

增長緩存。這個思路很簡單,直接從內存讀取,必然會快。至於寫、更新操做,採用相似的思路,把操做提交給一個Queue,而後單獨跑一個Thread去一個個獲取插庫。這樣保證了「大循環」中不涉及到IO操做。spa

 

問題再次出現:

若是咱們的遊戲只有個大循環還容易解決,由於裏面提供了完美的同步無鎖。

可是實際上的遊戲環境是併發和「大循環」並存的,即上文的2種環境。那麼不管咱們怎麼設計,必然會發如今緩存這塊上要出現鎖。

 

3. 併發與「大循環」如何共處,消除鎖?

咱們知道若是在「大循環」中要避免鎖操做,那麼就用「異步」,把操做交給線程處理。結合這2個特色,我稍微改下數據庫架構。

本來的緩存層,必然會存在着鎖,例如:

public TableCache

{
  private HashMap<String, Object> caches = new ConcurrentHashMap<String, Object>();
}

這個結構是必然的了,保證了在併發的環境下可以準確的操做緩存。可是」大循環「卻不能直接操做這個緩存進行修改,因此必須啓動一個線程去更新緩存,例如:

private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

EXECUTOR.execute(new LatencyProcessor(logs));

class LatencyProcessor implements Runnable

{

  public void run()
  { 

    // 這裏能夠任意的去修改內存數據。採用了異步。
  }
}

OK,看起來很漂亮。可是又有個問題出現了。在高速存取的過程當中,很是有可能緩存尚未被更新,就被其餘請求再次獲取,獲得了舊的數據。

 

4. 如何保證併發環境下緩存數據的惟一正確?

咱們知道,若是隻有讀操做,沒有寫操做,那麼這個行爲是不須要加鎖的。

我使用這個技巧,在緩存的上層,再加一層緩存,成爲」一級緩存「,原來的就天然成爲」二級緩存「。有點像CPU了對不?

一級緩存只能被」大循環「修改,可是能夠被併發、」大循環「同時獲取,因此是不須要鎖的。

當發生數據庫變更,分2種狀況:

1)併發環境下的數據庫變更,咱們是容許有鎖的存在,因此直接操做二級緩存,沒有問題。

2)」大循環「環境下數據庫變更,首先咱們把變更數據存儲在一級緩存,而後交給異步修正二級緩存,修正後刪除一級緩存。

這樣,不管在哪一個環境下讀取數據,首先判斷一級緩存,沒有再判斷二級緩存。

這個架構就保證了內存數據的絕對準確。

並且重要的是:咱們有了一個高效的無鎖空間,去實現咱們任意的業務邏輯。

 

最後,還有一些小技巧提高性能。

1. 既然咱們的數據庫操做已經被異步處理,那麼某個時間,須要插庫的數據可能不少,經過對錶、主鍵、操做類型的排序,咱們能夠刪除一些無效操做。例如:

a)同一個表同一個主鍵的屢次UPdate,取最後一次。

b)同一個表同一個主鍵,只要出現Delete,前面全部操做無效。

2. 既然咱們要對操做排序,必然會存在一個根據時間排序,如何保證無鎖呢?使用

private final static AtomicLong _seq = new AtomicLong(0);

便可保證無鎖又全局惟一自增,做爲時間序列。

相關文章
相關標籤/搜索