實時重新整理快取-處理mysql主從延遲的一些設計方案

Sam同學發表於2019-02-28

在專案開發當中,經常有這樣一種場景,對資料庫進行新增、修改、刪除操作的應用直接連線master庫,只對資料庫進行查詢的應用,會先建立一箇中央快取,例如redis或者memcache,如果快取沒有命中,那麼直接訪問slave庫。下文會介紹一下在重新整理中央快取時,如果發生主從延遲,應該如何處理。也即是,當應用System-A 把資料庫寫入master庫的時候,System-B應用在讀取slave庫的時候,master庫的資料還沒同步到slave庫,如果這個時候重新整理快取的話,會直接把舊的資料刷到快取裡的。

備註:

筆者在處理這個問題時,重新整理中央快取的機制是使用MQ訊息進行通知的。本文也是基於MQ這種技術背景下,想到的一些解決方案。複製程式碼

本地快取框架快取資料

我們可以根據資料的update_time 來判斷master庫的資料是否已經同步到slave庫。假設有一個update資料庫的操作,通過update_time得知,最新的master庫的資料還未同步到slave庫,那麼我們可以把這條資料的主鍵儲存到本地快取當中,例如使用LinkedBlockingQueue 這個佇列作為本地快取,將資料主鍵id儲存到佇列中,然後啟動一個job去掃描這個佇列,一旦發現佇列中有資料,則進行處理。在處理資料的過程中,如果發現該條資料還是未從master同步過來,那麼繼續把這條資料的主鍵放入佇列中,等待下一次的處理,一直到master庫的資料同步過來為止。如若由於資料庫原因或者資料原因或者程式碼問題等,導致資料一直處於入佇列/出佇列的死迴圈當中,那麼我們可以為資料設定一個出入佇列的次數,例如5次,超過五次的,則該條資料把它丟失掉。

下面列出一些虛擬碼:

佇列實現

public class DelayQueue {
    private static final Logger LOGGER = LoggerFactory.getLogger(DelayQueue.class);
    private static final int QUEUE_MAX_ELEMENT_COUNT = 20000;
    private  LinkedBlockingQueue<MessageElement> queue = new  LinkedBlockingQueue<MessageElement>(QUEUE_MAX_ELEMENT_COUNT);
    private static class SingletonHolder {
        private static final DelayQueue INSTANCE = new DelayQueue ();
    }

    private DelayQueue (){}

    public static final DelayQueue getInstance() {
        return SingletonHolder.INSTANCE;
    }


    /**
     *把元素插入佇列,如果此時佇列已滿,則丟棄掉
     */
    public void offer(MessageElement messageElement){
            boolean result = queue.offer(messageElement);
            //佇列滿了
            if (!result) {
                LOGGER.warn(dataBase masterSlaveDataDelayQueue full);
            }
    }

    /**
     * 把頭部的元素出棧
     */
    public MessageElement poll (){
        return queue.poll();
    }
}複製程式碼

掃描本地快取佇列的job

使用spring的定時任務註解:

    /**
     *該方法是單執行緒排程的,如果該執行緒未執行完,後續的排程將不會執行
     */

    @Scheduled(cron="0 0/5 * * * ?")
    public void handleQueueMessage(){
        while(true){
            String result = "true";//最好從配置檔案讀取,當值為false時,不接收訊息
            if (Constants.FALSE.equals(result)) {
                return;
            }
            MessageElement messageElement = DelayQueue .getInstance().poll();
            if (messageElement == null) {
                break;
            }
            LOGGER.info("receiveMessage from delay queue"+messageElement.toString());
            salesService.handleMessage(messageElement);
        }
    }複製程式碼

訊息體


public class MessageElement {

    private Long id;//資料主鍵
    private AtomicInteger count = new AtomicInteger();//控制出入佇列的最大次數

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }

    public AtomicInteger getCount() {
        return count;
    }
    public void setCount(AtomicInteger count) {
        this.count = count;
    }
}複製程式碼

備註:

  1. 注意要設定佇列的最大容量,如果佇列中的資料數量超過最大容量,可以根據自己的業務情況,刪除隊頭或者不再加入資料。

  2. 這個方案在應用重啟的資料,本地快取會被清理,造成資料丟失。

  3. 必須有一個開關,控制是否接收訊息。因為一旦生產者傳送的併發量太大,會引起其他問題,這個時候,可以通過開關控制不接收訊息,以便達到降級的效果。畢竟我們只是重新整理快取而已,大不了不刷。

    使用MQ

    如果MQ有如下的特性的話,也可以嘗試使用:

當資料未從master同步過來時,可以把訊息的狀態設定為later,讓訊息傳送者每隔一段時間再次傳送,例如2s後、5s後1分鐘後,這樣不斷的傳送,直到一個小時後,停止傳送。複製程式碼

這樣的話,應用就無需使用本地快取了,直接利用MQ。同時當應用重啟的時候,訊息也不會丟失。


原文連結


實時重新整理快取-處理mysql主從延遲的一些設計方案

相關文章