摘要:文本已收錄至我的倉庫,歡迎記一次在工作中愚蠢的操作,本文關鍵字線程安全我怎么天天在寫啊一交代背景我這邊有一個系統,提供一個接口去發送各種信息比如短信郵件微信等等渠道。小王用了一陣子也沒說有什么問題,于是這個需求就交付了。
前言
只有光頭才能變強。文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y
記一次在工作中愚蠢的操作,本文關鍵字:線程安全
(我怎么天天在寫Bug啊)
一、交代背景我這邊有一個系統,提供一個RPC接口去發送各種信息(比如短信、郵件、微信)等等渠道。我這邊的系統架構是這樣的:
概括:service系統提供一個RPC接口,別人調用我提供的接口,我在service系統中對這個消息進行判斷、拼接等等業務邏輯,最后會將這個消息放到消息隊列里邊。sender系統會消費消息隊列里邊的數據,然后發送消息
例子:小王調用我們的RPC接口,想要發送郵件。我對郵件的參數進行判斷和拼裝成一個我這邊定義好的Task,將這個Task丟到消息隊列里邊。sender系統消費這個Task,調用java.mail的API完成發送郵件的功能。
小王調用我們這個RPC接口,只要service系統把這個task丟到消息隊列里邊去,我們就返回response給小王。
只要這個task放到了消息隊列里邊,我們就返回success。所以有的時候,小王會問:“我這明明返回是success啊,怎么我的郵件沒發出去呢” ------(異步)
每發送一封郵件,我們都會將這封郵件的信息入庫(保存在MySQL中),在MySQL中我們可以得知這封郵件的發送時間,發送狀態等等。
而小王的這些郵件又十分在意是否成功發送出去了,如果發送失敗了他那邊需要重發。于是,他監聽我們DB的binlog,根據binlog的信息來判斷是否需要重發。
由于種種的原因,小王希望調用我們RPC接口的時候就能拿到一個唯一的標識好讓他去判斷這封郵件是成功還是失敗
顯然,入庫的Email ID是不可能的(因為他調我們RPC接口,我們將Task放到消息隊列就返回了。此時sender系統還沒消費呢)
于是,我們這邊打算在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對象
將Task對象放到消息隊列里邊
要明確的是:等到整一個調用鏈結束(將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,卻是別的Task的內容。是不是出大問題啦(錯亂消費?數據全亂了?),驚慌失措
然后,他那邊繼續補充:
之后發現郵件是發送成功的,但是他拿到部分的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");
醒悟:
現在我有50個線程,每個線程在處理數據的時候都會有一個默認的sendResponse對象,這個對象是用枚舉來標識Response.SUCCESS。所以,這50個線程都共享著這個sendResponse對象
50個線程共享著這個sendResponse對象,每個線程都可以修改sendResponse里邊的msgId屬性,這就自然是線程不安全的。
所以小王能拿到其他Task的msgId(小王的線程設置完msgId之后,還沒返回,三歪的線程又更改了一次msgId,導致小王拿到三歪的msgId了)
總結:
終于知道為啥當初前同事在代碼上保留了msgId屬性,但是沒有使用這個屬性。
使用枚舉就不應該帶 有狀態的屬性(能修改、可變的屬性)
最后樂于輸出干貨的Java技術公眾號:Java3y。公眾號內有200多篇原創技術文章、海量視頻資源、精美腦圖,關注即可獲取!
覺得我的文章寫得不錯,點贊!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75826.html
摘要:文本已收錄至我的倉庫,歡迎記錄一次在寫代碼時愚蠢的操作,本文涉及到的知識點不可變性一交代背景我這邊有一個系統,提供一個接口去發送短信。 前言 只有光頭才能變強。文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y 記錄一次在寫代碼時愚蠢的操作,本文涉及到的知識點:String不可變性 一、交代背景 我這邊有一個系統,...
從事 Android 開發工作要滿 5 年了,雖然明白自己技術很一般,但是也總是期望能夠有機會進入更好的平臺發展。這不,因為機緣巧合有了一次 Booking 的面試邀請(是在 hackerrank 上),然后開始臨時抱佛腳 (leetcode 走起),最終選擇了一個周末去完成線上測試,結果我完全沒預料到。本以為會被某道題的邏輯繞昏,結果哪知道被標準輸入這個東西卡得死死的,現在就記錄一下這次非常糟...
摘要:最近面試一家有直播業務的公司,明顯感覺到對多線程的理解有一些要求。第一輪面試大概就面了分鐘左右,一輪下來口干舌燥。下面對面試題做了下簡單分類,分享給大家。 最近面試一家有直播業務的公司,明顯感覺到對多線程的理解有一些要求。第一輪面試大概就面了 70 分鐘左右,一輪下來口干舌燥。 下面對面試題做了下簡單分類,分享給大家。 多線程 有什么方法可以監控線程的狀態 synchronized ...
摘要:把內存分成兩種,一種叫做棧內存,一種叫做堆內存在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配。堆內存用于存放由創建的對象和數組。 一次慘痛的阿里技術面 就在昨天,有幸接到了阿里的面試通知,本來我以為自己的簡歷應該不會的到面試的機會了,然而機會卻這么來了,我卻沒有做好準備,被面試官大大一通血虐。因此,我想寫點東西紀念一下這次的經歷,也當一次教訓了。其實面試官大大...
閱讀 1249·2023-04-26 02:38
閱讀 928·2023-04-25 20:13
閱讀 3588·2021-11-19 11:31
閱讀 2396·2019-08-30 15:55
閱讀 2717·2019-08-30 14:11
閱讀 3157·2019-08-30 13:45
閱讀 1371·2019-08-29 18:41
閱讀 1147·2019-08-29 16:18