這是一個簡單的數據生產導入的故事,原本故事情節應該是這樣的:數據整理-->測試驗證-->生產發佈-->生產驗證,然後就是各回各家,所以這本來應該是一個平淡的故事,然而實際卻變成瞭如下情節:數據整理-->測試驗證-->生產發佈-->生產驗證-->校驗失敗(預期數據未導入)-->問題排查-->解決問題- ...
這是一個簡單的數據生產導入的故事,原本故事情節應該是這樣的:數據整理-->測試驗證-->生產發佈-->生產驗證,然後就是各回各家,所以這本來應該是一個平淡的故事,然而實際卻變成瞭如下情節:數據整理-->測試驗證-->生產發佈-->生產驗證-->校驗失敗(預期數據未導入)-->問題排查-->解決問題-->生產發佈-->生產驗證-->校驗問題(大部分數據是正確的,少部分數據不正確)-->問題排查(當時未能排查出原因,但能判斷出異常與生產原有的幾條異常數據有關)-->異常數據刪除sql編寫-->測試校驗-->生產發佈-->生產校驗-->重新導入刪除部分數據(異常數據這次直接排除,沒包括在導入範圍)-->部分異常數據請示領導修正-->修正Sql準備-->測試校驗-->生產發佈-->修正數據對應數據導入-->生產校驗!
你以為到這裡就結束了?NO NO NO,故事怎麼可能就這麼結束,因為這批數據導入有對應的其它業務,還需要執行該部分業務,最終確認後才能各回各家,結果發現,坑爹的資料庫數據是修正了,但因為程式採用了Redis,異常數據還在Redis中,所以還要在Redis中刪除該部分異常數據,還好程式部分對此有處理,直接刪除沒導致程式功能異常,至此本次發佈才算結束,但此時也已經是凌晨0點了,這真是一個悲劇的故事……
首先需要介紹下本次導入的豬腳,一個預先寫好,且已經發佈至生產的存儲過程,另外該豬腳所在場景是MySql,其大致代碼精簡後如下
1 DROP PROCEDURE IF EXISTS `usp_SadEvent`; 2 DELIMITER $$ 3 CREATE PROCEDURE `usp_SadEvent` 4 ( 5 IN identityNo VARCHAR(20), 6 IN uName VARCHAR(15), 7 IN cAmount LONG 8 ) 9 label_at_start: 10 BEGIN 11 12 SELECT @uid := id FROM `user` 13 WHERE identity_no=identityNo AND NAME=uName; 14 15 IF @uid IS NULL THEN 16 select identityNo,uName,0 ret; 17 LEAVE label_at_start; 18 END IF; 19 update account set balance=balance+cAmount where uid=@uid; 20 select identityNo,uName,1 ret; 21 END label_at_start$$ 22 DELIMITER ;
首先就是what the fuck的執行失敗問題,調用該存儲過程結果居然都是返回ret=0失敗!!
還好當初寫存儲過程時,考慮到了結果查詢,比較容易就發現為什麼返回的uName是亂碼?碰上這種問題,直覺就是資料庫編碼有問題,一查果然如此
問題查出來了,怎麼修正呢,正常劇情當然是改資料庫預設編碼為utf8了,但改編碼後Mysql必須要重啟,生產環境是你想重啟就能重啟的嗎?好吧,還好mysql存儲過程支持指定編碼,修改存儲過程,在uName部分指定編碼集是utf8(補充雖然資料庫預設字元集是latin1,但實際裡面的表在創建時都預設指定utf8了,所以導致一直沒發現生產環境居然有預設編碼集問題)
IN uName VARCHAR(15) character set utf8,
改完後執行批量調用存儲過程的sql,大致如下
call usp_SadEvent('123131231313123132','張三',3000);#資料庫有 call usp_SadEvent('123454566778899999','李四',5000);#資料庫無李四,或資料庫里叫李斯
這裡特別標明第一條資料庫里是有對應數據,第二條在資料庫中是查不到用戶數據的,執行結果居然發現第二條“李四”的金額,被加到了“張三”身上,這又是什麼鬼!
其實問題就出在mysql的預設聲明參數上,只要在上面的調用語句下麵再加一句
call usp_SadEvent('123131231313123132','張三',3000);#資料庫有 call usp_SadEvent('123454566778899999','李四',5000);#資料庫無 select @uid;
執行結果很明顯的就告訴你@uid是有值的,而且值為張三的uid,好吧,沒想到Mysql中
SELECT @uid := id
這種隱式聲明參數的方式居然會在整個對話期間內都有效,所以還是老老實實改成如下顯式聲明才能測試正確
declare u_id long; select `id` into u_id FROM `user`
最終完整修改後的存儲過程應該如下
DROP PROCEDURE IF EXISTS `usp_SadEvent`; DELIMITER $$ CREATE PROCEDURE `usp_SadEvent` ( IN identityNo VARCHAR(20), IN uName VARCHAR(15) character set utf8, IN cAmount LONG ) label_at_start: BEGIN declare u_id long; select `id` into u_id FROM `user` WHERE identity_no=identityNo AND NAME=uName; IF u_id IS NULL THEN select identityNo,uName,0 ret; LEAVE label_at_start; END IF; update account set balance=balance+cAmount where uid=u_id; select identityNo,uName,1 ret; END label_at_start$$ DELIMITER ;
哎,事後描述問題似乎很簡單,實際排查這些問題還是挺坑的,哎……