最近開發中遇到的一個MySQL主從延遲的坑,記錄並總結,避免再次犯同樣的錯誤。 ...
最近開發中遇到的一個MySQL主從延遲的坑,記錄並總結,避免再次犯同樣的錯誤。
情景
一個活動信息需要審批,審批之後才能生效。因為之後活動要編輯,編輯後也可能觸發審批,審批中展示的是編輯前的活動內容,考慮到欄位比較多,也要保存審批活動的內容,因此設計採用了一張臨時表,審批中的活動寫進審批表(activity_tmp),審批通過之後才把真正的活動內容寫進活動表(activity)。表的簡要設計如下,這裡將活動內容欄位合併為content展示:
activity_tmp()
id
status // 審批狀態
content // 審批階段提交的活動內容
activity
id
content // 審批通過後真正展示的活動內容
遇到的問題
當時是有編輯觸發審批的情況,發現審批通過之後活動內容是空的,於是開始追查問題的原因。這裡說一句,當程式出問題的時候,95%都是代碼的問題,先不要去懷疑環境出問題。好好的查日誌,然後看看你的代碼吧。
追查問題回溯
1、查activity_tmp表,發現當時提交審批的活動內容是正常的,而且狀態也更新為審批通過了,懷疑是寫入activity表失敗
2、查activity表,發現審批後的內容確實沒有寫入,懷疑是代碼問題
3、查看代碼,代碼邏輯沒看出問題,懷疑資料庫操作失敗,查看日誌
4、日誌顯示,有一句insert語句的活動內容為空,活動內容來自上一個mysql執行的是select語句,把該select語句拿出來放到線上的備庫查詢,發現活動內容是存在的。運行時查詢為空,執行完畢後查詢時內容存在,初步懷疑是主從延遲問題。
5、報錯只是部分失敗,確定是主從延遲的問題。
當時的問題代碼
$intStatus = $arrInput[‘status’];
$this->objActTmp->updateInfoByAId($intActId, $intStatus);
// 更新後,馬上查
$arrActContent = $this->objActTmp->getActByStatus($intStatus);
這就是主從延遲出現的地方,update後,馬上get,這是主從複製架構上開發的一個大忌。
解決方案
這類問題的解決方案有兩種:
- 如果第二步獲取的數據不需要第一步更新的status欄位,那就先讀,然後再更新
- 如果第二步獲取的數據需要依賴第一步的status欄位,那就在讀出來的時候先判斷是否為空,如果是空的,報錯,下一次重試。
總結
其實之前也聽到過這樣的例子,但是由於沒有親身經歷,所以只保留了一種理論上的記憶,實際上印象不深,經歷了這麼一次踩坑後,印象特別深刻,現在看到別人寫這樣的代碼也能馬上發現並指出。還是自己親身去踩坑印象最深。
日誌很重要,詳細的日誌更重要。日誌要記錄有用的信息,方便追查問題的時候去追溯問題的本質原因。我覺得日誌就應該儘量做成飛機中的黑匣子,幫助我們保存“事故“發生時的所有相關信息。
之後的學習里,主從延遲的機制和原理也值得去研究一番。