只有光頭才能變強。java
文本已收錄至個人GitHub倉庫,歡迎Star:github.com/ZhongFuChen…git
記一次在工做中愚蠢的操做,本文關鍵字:線程安全github
(我怎麼每天在寫Bug啊)安全
我這邊有一個系統,提供一個RPC接口去發送各類信息(好比短信、郵件、微信)等等渠道。我這邊的系統架構是這樣的:微信
歸納:service系統提供一個RPC接口,別人調用我提供的接口,我在service系統中對這個消息進行判斷、拼接等等業務邏輯,最後會將這個消息放到消息隊列裏邊。sender系統會消費消息隊列裏邊的數據,而後發送消息架構
例子:小王調用咱們的RPC接口,想要發送郵件。我對郵件的參數進行判斷和拼裝成一個我這邊定義好的Task,將這個Task丟到消息隊列裏邊。sender系統消費這個Task,調用java.mail
的API完成發送郵件的功能。併發
小王調用咱們這個RPC接口,只要service系統把這個task丟到消息隊列裏邊去,咱們就返回response給小王。異步
每發送一封郵件,咱們都會將這封郵件的信息入庫(保存在MySQL中),在MySQL中咱們能夠得知這封郵件的發送時間,發送狀態等等。測試
而小王的這些郵件又十分在乎是否成功發送出去了,若是發送失敗了他那邊須要重發。因而,他監聽咱們DB的binlog,根據binlog的信息來判斷是否須要重發。ui
因爲種種的緣由,小王但願調用咱們RPC接口的時候就能拿到一個惟一的標識好讓他去判斷這封郵件是成功仍是失敗
因而,咱們這邊打算在service系統生成一個messageId,而後返回給他,將這個messageId綁定到Task裏邊,一直到入庫。
上面肯定好需求和思路以後,我這邊就去看返回給小王的response對象,一看,發現已經有msgId字段了
public class SendResponse {
// 錯誤碼
private int errCode;
// 錯誤信息
private String errInfo;
// messageId
private long msgId;
}
複製代碼
我搜了一下這個字段的信息ctrl + shift + f
,發現這msgId沒有被用到啊。一想,這恰好,我來用了。我看了一下用法,發現這邊不是直接使用SendResponse的,而是在外面包了一個枚舉類,代碼大概以下:
public enum Response {
SUCCESS(1, "success"),
PARAM_MISSING(2, "param is missing"),
INVALID_xxxx(3, "xxxx is invalid"),
INVALID_xxxx(4, "xxxx is invalid"),
private SendResponse sendResponse;
private Response(int errCode, String errInfo) {
sendResponse = new SendResponse();
sendResponse.setMsgId(0);
sendResponse.setErrCode(errCode);
sendResponse.setErrInfo(errInfo);
}
public SendResponse getSendResponse() {
return sendResponse;
}
}
複製代碼
有了枚舉使用起來就很簡單了,好比我發現小王某個參數傳進來有問題,我反手就是:
Response.PARAM_ERROR
複製代碼
service系統主要作了兩件事
要明確的是:等到整一個調用鏈結束(將Task對象放到消息隊列中),纔會將sendResponse對象返回出去。而又由於可能要判斷的地方有點多,因此咱們這邊是這樣設計了一個Map來存儲數據,這個Map貫穿整條鏈路:
// 首先將sendResponse默認設置爲success,也就是代碼以下:
map.put("sendResponse",Response.SUCCESS);
// 若是中途某個地方可能有問題了,那咱們將Map中sendResponse進行修改
map.put("sendResponse",Response.ERROR);
// 等整條鏈路完成,從Map拿出sendResponse返回
return map.get("sendResponse");
複製代碼
因而我要作的就是:在將SendResponse返回以前,我生成一個惟一的msgId,並插入到SendResponse對象裏邊就行了。
Response.getSendResponse().setMsgid(uuid);
複製代碼
這個需求完成得很是快,簡單測試了一下也沒毛病,就果斷上線了。小王用了一陣子也沒說有什麼問題,因而這個需求就交付了。
昨天,小王告訴我:「我這邊郵件發送失敗啦,有msgId,看下是什麼緣由形成的「
因而我就去撈線上的日誌,發現根據他給出的msgId,我這邊打出的日誌都不是發送郵件的(而是其餘Task的日誌)。我這就慌了,難道咱們這個系統出問題了?
而後,他那邊繼續補充:
以後發現郵件是發送成功的,可是他拿到部分的msgId是別的Task的,不是郵件的。因而只能先比對剩下的郵件是否有問題,再看看MsgId是什麼緣由。
現有的條件是:
因此,判斷系統是沒問題的,只是msgId在併發的過程當中出了問題(拿到其餘Task的msgId了)
因而我就去找緣由啦,在查代碼的時候發現前同事還在Service系統中的某個類留了一個註解@NotThreadSafe
。我就以爲確定是中途哪一個地方我沒注意到,致使小王拿到了其餘Task的msgId。
人肉Debug了一個午休的時間仍是沒找出來:每一個線程都獨有一份的操做對象,對象的屬性都沒有逸出(都在方法內部操做),跟着整塊鏈路一直傳遞,直至鏈路結束。
後來,一想,我應該只看msgId生成的地方就行了呀。才發現,項目裏邊用的是枚舉啊!
// 首先將sendResponse默認設置爲success,也就是代碼以下:
map.put("sendResponse",Response.SUCCESS);
// 若是中途某個地方可能有問題了,那咱們將Map中sendResponse進行修改
map.put("sendResponse",Response.ERROR);
// 把response的msgId的值設置爲當前Task綁定的值
map.get("sendResponse").setMsgid(uuid);
// 等整條鏈路完成,從Map拿出sendResponse返回
return map.get("sendResponse");
複製代碼
醒悟:
Response.SUCCESS
。因此,這50個線程都共享着這個sendResponse對象總結:
樂於輸出乾貨的Java技術公衆號:Java3y。公衆號內有200多篇原創技術文章、海量視頻資源、精美腦圖,關注便可獲取!
以爲個人文章寫得不錯,點贊!