1. 前言
最近項目有個需求:一個內部派單系統,同事反饋:來一個活兒系統就立馬派給我,我的工作永無結束之日。程序員小哥們能不能在系統控制每日派單上限,每日派單超過這個上限就別分給我了,我想早點下班偶遇我未來的女朋友。嚶嚶嚶…
“年輕人,勇敢的去追求愛情吧,剩下的交給我”
望着同事心滿意足下班的背影,我冷笑,“哼,年輕,絲毫不懂工作的樂趣。”
2. 開心的改造代碼
咱先梳理下邏輯:
- 1.一天24小時隨時都有可能來新訂單。原本邏輯系統每分鐘掃描未派出去的訂單,並根據某些規則下發給用戶。
- 2.不同訂單會派給不同角色下的用戶。
戴上我山寨的earPods Pro 閃瞎眼Max,“愛情買賣”調到最大聲,音樂一放,我最閃亮,翹着二郎腿就開幹, 在最終派單的代碼中添加如下邏輯:
//是否強制分配
//是否有配置上限,有則判斷:超上限不分配。沒有配置則能直接通過
if (!force && dispatchOverLimitPerDay(orderType, userId)) {
log.warn("經辦人:{},本日已超出類型:{}的分配上限,系統不分配orderId:{}", userId, orderType, orderId);
return;
}
//派單邏輯
...
orderService.save(entity);
方法內查詢用戶當天已經派了多少單,當天還剩餘可派多少:
//當天開始時間和結束時間
Date date = new Date();
Date beginOfDate = DateUtil.getBeginOfDate(date);
Date endOfDate = DateUtil.getEndOfDate(date);
//當天某一角色下用戶,當天已分配的數量
List<DispatchOrderNumLimitResDTO> dayDispatchedInfo = orderService.queryDispatchedOrderNumGroupByUserId(orderType, beginOfDate, endOfDate);
// 查找某一角色下用戶配置的每日上限,無則默認無上限
List<DispatchCaseNumLimitInfo> dayLimitInfo = findLimitConfig();
//待分配的用戶當天是否還有剩餘
int remainNum = getRemainNum(userId,dayDispatchedInfo,dayLimitInfo);
...
...
return remainNum > 0;
本地測試通過,測試驗收通過,生產發版上線,需求標記爲已解決。一套流程走下來,如德芙般絲滑。合上電腦,揹包,打卡下班一氣呵成。
3. 天吶,系統貌似出現了問題
先介紹下項目:
- springboot + mybatis-plus + dubbo
- 分佈式定時任務採用quartz
- 預發佈1臺服務器,生產3臺服務器
- MySQL 5.6.42
第二天上班收到測試反饋:系統超出了配置的用戶派單上限,原本配置每日分配上限爲12,實際分配了18。如下:
遇到bug不要慌,甩鍋已經是基本操作了。看數據庫的確是凌晨00:00系統自動派單的,看了系統日誌,同事昨晚最遲操作系統是在20:54。排除人爲因素,那就是系統層面的原因了。
4. 一個鍵盤一包煙,一個BUG找一天
接下來就是一天的排查,有以下幾個懷疑的點:
- 每日上限數配置配置有誤/被修改過
- 系統查詢條件有問題
- 系統的定時任務由於併發導致查詢出髒數據,也就導致系統超出當天的分配上限
4.1 每日上限數配置配置有誤/被修改過
這個純粹是想甩鍋,去nacos查看配置確認:pre和prod上的nacos配置都是12。問了幾個同事,他們都表示沒有修改過nacos。
況且,上線當天(6/10 18:30上線,6/11早上發現問題)系統分配是剛好12條上限,且有剩餘其他訂單等待11號分配。此路不通。
疑點:上線當天正常,第二天卻超上限分配,差別是時間。
4.2 系統查詢條件有問題
查詢邏輯爲:查詢一天的開始~結束時間段之內,每個人派了多少單。
SQL語句如下:
select order_id,user_id,count(1) as dispatchedNum
from wait_order
where
is_deleted = false and orderType = #{orderType}
and create_time >= #{beginOfDate}
and create_time <= #{endOfDate}
order by userId
是不是獲取開始時間和結束時間有問題?
我在本地跑main方法輸出這兩個時間,發現也是正常的:
00:00:00 ~ 23:59:59,時間獲取是正常的。我手動替換掉#{beginOfDate}等佔位參數去生產環境跑此條SQL,能把當天分配的所有訂單都查詢出來(12條正常的 + 6條額外分配的)。那我判定SQL是沒問題的。
4.3 系統的定時任務由於併發導致查詢出髒數據
這個就能很好的解釋分配超限問題了。由於同事小M寫的分佈式定時任務有問題,導致同時有倆線程A,B同時執行分配。
- 假設A線程在某一刻查詢出某人當天分配了6條,準備分配剩餘6條。
- B線程此時也查詢出6條,並且把剩餘6條給分配了。(B:“我就是比你快” A:“老哥,男人不能太快”)
- A繼續分配6條訂單,這樣就導致了超限分配
科學~ 合理~
跑去同時小M位置上,反手甩給他一口鋥光瓦亮的鍋
你以爲這就結束了?
小M反手就是程序員靈魂提問:“請先看下文檔,請仔細看文檔,請熟讀並背誦文檔”。
okok我錯了,定時任務沒有問題。(其實項目的定時任務一直沒出過問題)
此外,還懷疑了數據庫主從沒同步的問題,但都否決了。
第一天排查無果,加上了幾行log日誌後下班回家。
5. 柳暗花明
第二天一早看數據庫,超限派單問題依然存在——某賬戶設置一天派單8件,但實際派單12件。日誌打印如下:
2020-06-12 00:00:00.175 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[]
2020-06-12 00:00:00.386 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=1, remainNum=7)]
2020-06-12 00:00:00.437 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=1, remainNum=7)]
2020-06-12 00:00:00.487 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=1, remainNum=7)]
2020-06-12 00:00:00.632 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=1, remainNum=7)]
2020-06-12 00:00:00.681 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=2, remainNum=6)]
2020-06-12 00:00:00.732 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=3, remainNum=5)]
2020-06-12 00:00:00.781 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=4, remainNum=4)]
2020-06-12 00:00:00.831 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=5, remainNum=3)]
2020-06-12 00:00:00.882 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=6, remainNum=2)]
2020-06-12 00:00:00.931 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=7, remainNum=1)]
2020-06-12 00:00:00.978 [pool-124-thread-1] INFO - 每日分配上限剩餘信息:[XXXDTO(userId='測試1', dispatchedNum=8, remainNum=0)]
2020-06-12 00:00:00.978 [pool-124-thread-1] WARN - 經辦人:測試1,本日已超出類型:3的分配上限,系統不分配orderId:XXX
我從MySQL數據中抓住了元兇,當天插入數據如下:
疑問:
- 第3,4,5,6次派單dispatchedNum(已派單)數量仍爲1?
- 系統實際多派了4單,從數據上看,有4單插入的時間均爲00:00:00。我回頭看昨天的派單,發現11號數據中,有6單插入時間也爲00:00:00,數量恰好是多派的單!我們再看一下前一天的數據:
我回頭看看工具類源碼:
public static Date getBeginOfDate(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(11, 0);
calendar.set(12, 0);
calendar.set(13, 0);
return calendar.getTime();
}
public static Date getEndOfDate(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(11, 23);
calendar.set(12, 59);
calendar.set(13, 59);
return calendar.getTime();
}
問題定位!
getBeginOfDate方法,只把時,分,秒設置爲0,未把毫秒也設置爲0。同理,getEndOfDate也未把毫秒設置爲999,導致我的SQL查詢語句實際爲帶了毫秒查詢:
至此,看官們可能還有最後一個疑問:爲什麼中途會有條記錄爲00:00:01的數據呢?你聽我給你 狡辯 解釋。
定時任務調用service.save(entity)方法,有可能打在不同的svc上,而這三個svc系統時間可能有毫秒時間的差別,所以~~~
6. 解決BUG
//getBeginOfDate方法
calendar.set(Calendar.MILLISECOND,0);
//getEndOfDate方法
calendar.set(Calendar.MILLISECOND,999);
問題出在了工具類上,我的心情:完~
(開頭的同事仍在努力尋找愛情ing~)