MongoDB高并發(fā)下的upsert問題
一、問題現(xiàn)象
運行中心同事報告生產環(huán)境日志每天0點附近都會出現(xiàn)幾筆錯誤日志,日志內容截圖如下:
二、問題分析
系統(tǒng)采用MongoDB數(shù)據(jù)庫,該表建有groupName+limitKey組合唯一索引,所以分析下來第一感覺就是重復執(zhí)行insert報duplicate key錯誤了。
找到對應的業(yè)務代碼如下:
return mongoTemplate.findAndModify( new Query() .addCriteria(Criteria.where(“limitKey”).is(limitKey)) .addCriteria(Criteria.where(“groupName”).is(groupName)), new Update() .inc(“cnt”, delta) .set(“updateTime”, LocalDateTime.now()) .setOnInsert(“createTime”, LocalDateTime.now()) .setOnInsert(“expireTime”, keepAliveDays == 0 ? null : LocalDateTime.now().plusDays(keepAliveDays)), new FindAndModifyOptions() // 先查詢,如果沒有符合條件的,會執(zhí)行插入,插入的值是查詢值 更新值 .upsert(true) // 返回當前最新值 .returnNew(true), LimitKeyStatEntity.class);
執(zhí)行upsert操作,即首先查庫里是否已存在記錄,存在則執(zhí)行update更新操作,否則執(zhí)行insert插入操作。
代碼邏輯并未發(fā)現(xiàn)有啥問題,但是為啥就報insert沖突了呢?
經上網查詢發(fā)現(xiàn),MongoDB本身是有文檔級的寫入鎖的。也就是說,當一個進程開始修改一個文檔時,該文檔被鎖定,其他文檔不可以再對其進行寫入甚至讀取。這個寫入鎖的存在本身就是為了防止不同程序更新文檔時產生的寫入沖突。然而,update其實分為兩步。首先是搜索文檔位置,然后再執(zhí)行文檔更新。
因此MongoDB在高并發(fā)的情況下,可能會出現(xiàn)以下問題:
- 兩個線程t1,t2同時update一條記錄時
- t1讀,記錄不存在;t2讀,記錄不存在。
- t1寫,執(zhí)行insert,插入成功
- t2寫,執(zhí)行insert,由于唯一索引原因,重復插入失敗。
再對應查到這幾筆累計請求,確實系生產上某個大商戶每天0點開始頻繁發(fā)退貨,而且退貨間隔時間小到1ms導致的。
三、解決方案
mongoTemplate的findAndModify()方法目前暫無好的辦法來解決高并發(fā)下上述缺陷,只能從業(yè)務方案上來解決。
由于該應用場景為消費訂單MQ消息并執(zhí)行額度累計,因此在寫MongoDB異常導致業(yè)務上更新累計值失敗時,認為消費MQ消息失敗并拋出異常,利用RocketMQ消息失敗重試機制,重新消費并執(zhí)行額度累計。