問題描述
場景是這樣的,我們的支付系統在一筆支付完成後,需要發出通知給到商戶。支付完成的消息通過消息隊列發送給通知的服務。通知服務的有一部分處理邏輯是這樣的:
notifyPersist.saveNotifyRecord(notifyRecord);
notifyRecord = rpNotifyService.getNotifyByMerchantNoAndMerchantOrderNoAndNotifyType(notifyRecord.getMerchantNo(), notifyRecord.getMerchantOrderNo(), notifyRecord.getNotifyType());
notifyQueue.addElementToList(notifyRecord);
三行代碼。我解釋下,通知服務收到消息解析成 notifyRecord 對象,然後存入數據庫,然後馬上取出添加到任務隊列。另外又一個獨立的線程去處理這個任務隊列。
項目上線後,客戶反饋偶爾會出現收不到通知的情況。
問題排查
經過日誌跟蹤,我發現是在上述代碼的第二行,查詢記錄的時候數據庫返回null,也就是沒有查詢到記錄。導致任務隊列沒有該筆支付的通知任務。
一開始覺得非常不解,因爲通過日誌我們發現第一行代碼是執行成功的,既然插入成功了,沒理由查詢不到啊??而且我去數據庫看確定是有這條記錄的。莫非是見鬼了!!
我先說下
慢慢靜下心來思考,結合這個現象是偶發性的,我想到有沒有可能是因爲讀寫分離延時造成的。我先說下我們的存儲架構:
centos 6.5 64位操作系統。mycat1.6版本,mysql 5.6.21
數據庫服務器有兩臺,一臺主,一臺從,利用mycat配置了主從複製和讀寫分離。寫操作在主機上,讀操作在從機上。如下圖所示:
有沒有可能是主庫上插入成功後,從庫還沒有來得急同步完成,應用就馬上查詢,所以查不到。爲了驗證我的想法,我把代碼改成下面這樣發佈到生產先看看效果:
notifyPersist.saveNotifyRecord(notifyRecord);
Thread.sleep(300);
notifyRecord = rpNotifyService.getNotifyByMerchantNoAndMerchantOrderNoAndNotifyType(notifyRecord.getMerchantNo(), notifyRecord.getMerchantOrderNo(), notifyRecord.getNotifyType());
notifyQueue.addElementToList(notifyRecord);
思路也很簡單粗暴,就是插入之後不馬上讀,而是等一會讓從庫同步完再讀。發佈後,跑了一個段時間,沒有反饋異常。證明我懷疑的沒錯,問題確實出現在mycat讀寫分離延時上。
解決方案
當然,上面定位問題的sleep也勉強算是一個解決方案。只不過感覺比較low,原理很好理解。在插入數據和查詢數據中間加一個sleep()方法,相當於等一會再讀。如果應用對時效要求不高,
此方法也不失唯一種快速有效的方案。
找到了問題的根源我就去mycat的官網和相關論壇尋找解決方案。
功夫不負有心人,後來我查到相關資料,mycat可以通過註解的方式指定某些 sql 語句強制走主庫,如下所示:
<select id="listBy" parameterType="java.util.Map" resultMap="BaseResultMap">
/**mycat:db_type=master*/select * from rp_notify_record
<where>
<include refid="condition_sql" />
</where>
<![CDATA[ order by create_time desc]]>
</select>
注意到 /** */裏面的註解,通過指定 db_type=master 保證後面的 sql 語句走主庫。這樣就不存在延時導致查詢不到的問題了。