一個在線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);
便可保證無鎖又全局惟一自增,做爲時間序列。