在專案開發當中,經常有這樣一種場景,對資料庫進行新增、修改、刪除操作的應用直接連線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;
}
}複製程式碼
備註:
-
注意要設定佇列的最大容量,如果佇列中的資料數量超過最大容量,可以根據自己的業務情況,刪除隊頭或者不再加入資料。
-
這個方案在應用重啟的資料,本地快取會被清理,造成資料丟失。
-
必須有一個開關,控制是否接收訊息。因為一旦生產者傳送的併發量太大,會引起其他問題,這個時候,可以通過開關控制不接收訊息,以便達到降級的效果。畢竟我們只是重新整理快取而已,大不了不刷。
使用MQ
如果MQ有如下的特性的話,也可以嘗試使用:
當資料未從master同步過來時,可以把訊息的狀態設定為later,讓訊息傳送者每隔一段時間再次傳送,例如2s後、5s後1分鐘後,這樣不斷的傳送,直到一個小時後,停止傳送。複製程式碼
這樣的話,應用就無需使用本地快取了,直接利用MQ。同時當應用重啟的時候,訊息也不會丟失。