高并發下的九死一生,一個不小心就掉入萬丈深淵
引言
了解LZ的猿友應該都知道,LZ最近弄了一個hbase(不理解hbase的猿友可以把hbase當做與oracle,mysql,sqlserver等一樣的數據庫,并不影響閱讀本文)的大數據平臺,或許現在叫平臺還有點名不副實,不過它很快就會發展到這個規模,LZ一直堅信著。在建立這個平臺的過程中,LZ遇到過各種千奇百怪的問題,在這里LZ就分享一個非常簡單,但卻很奇葩的問題。
問題來源
問題的來源特別簡單,LZ為了迎合模塊化開發的思想,做了很多獨立的模塊,這些模塊以jar包的形式協同工作,類似于spring當中的spring-core,spring-beans,spring-context等等。
在LZ的一個common包中,有這樣的一個工具類,代碼如下。(備注:LZ為了簡單,去掉了很多跟本文無關的代碼,但不影響閱讀,因為這個類就是一些靜態的工具類方法,主要用于處理日期)
public class DateUtil {
private DateUtil(){}
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String date) throws ParseException {
return DATE_FORMAT.parse(date);
}
}</code></pre>
這個類特別簡單,相信有不少猿友都會覺得這個類沒有多大問題。因為這段代碼太簡單了,當你的項目報錯的時候,你很難想到這段代碼就是錯誤的根源。很顯然,LZ就在hbase的應用中使用了這個工具類,結果就導致了一個奇葩問題。
大致描述一下這個工具類使用的場景。LZ的hbase應用接收了來自于其它系統大量的日志信息,并會將這些日志信息存儲在hbase當中,其實就是一個簡單的日志保存功能。如果單純從功能上來講,就是一個簡單的curd(增刪改查)操作中的c(增)操作。唯一不同的是,由于存儲的是來自很多系統的系統日志,webservice日志,mq日志,url訪問日志,因此并發量會有點高,至少比LZ平時做的企業應用要高太多太多了。
這個工具類就是在解析日志信息中的日期字符串(比如日志的發生時間)時報的錯,具體的錯誤信息如下。(備注:以下是真實的報錯信息,顯示的錯誤位置與上面的代碼不符,不過各位猿友完全可以認為就是上面的方法報的錯,因為事實上parseTimestamp這個方法就和上面方法的代碼是一樣的。)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
at java.lang.Long.parseLong(Long.java:431)
at java.lang.Long.parseLong(Long.java:468)
at java.text.DigitList.getLong(DigitList.java:177)
at java.text.DecimalFormat.parse(DecimalFormat.java:1297)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.xxxxxxx.core.common.util.DateUtil.parseTimestamp(DateUtil.java:95)
at com.xxxxxxx.core.common.util.DateUtil.parse(DateUtil.java:84)
at com.xxxxxxx.hbase.generator.LogRowKeyGenerator.generate(LogRowKeyGenerator.java:21)
... 22 more
問題分析
看到這個錯誤,大部分老道一點的程序猿一眼就能定位問題,肯定是傳過來的日期格式不對,所以導致在解析的時候出錯了。
LZ自認為還算老道吧(小小的自夸一下),自然也很快的意識到了問題的根源。于是最簡單的方式,調試一下代碼,看傳過來的日志信息到底是什么樣子。
LZ在catch塊里加入了斷點,當報出這個錯誤的時候,會進入調試(只能在catch塊里捕捉,因為這個異常是時而出現的,而且毫無規律)。但是結果很意外,LZ仔細且認真的看了傳送過來的日志信息,日期格式卻明明是正確的。這時候LZ就傻眼了,格式明明是正確的,解析怎么可能報錯呢?
LZ不相信這種奇怪的問題,于是LZ采用最簡單的辦法,希望印證心中所想,將代碼改成如下的樣子。
public class DateUtil {
private DateUtil(){}
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String date) throws ParseException {
System.out.println("date:" + date);
return DATE_FORMAT.parse(date);
}
}</code></pre>
這是最簡單粗暴的調試方式,也是LZ初入程序猿這個職業時經常用的辦法。可惜,結果依然不如人意,當偶爾出現異常時,打印出來的日志格式依舊是正確的。事實上,各個系統使用的客戶端也是LZ開發的,也不應該出現日期格式錯誤的問題。
這到底怎么回事呢?事實就是,日期格式是正確的,但就是解析失敗!
水落石出
LZ在想不明白一個問題的時候,習慣出來抽根煙,透透風。不過不得不說,這個辦法真的好使,LZ一瞬間靈感就襲腦了。
這么奇葩的問題,也只有高并發可以解釋了!
于是二話不說,扔掉煙頭,LZ就回到電腦前打開了SimpleDateFormat這個類的源碼。果然,在這個類的注釋里,有這么一段話。
* Date formats are not synchronized.
- It is recommended to create separate format instances for each thread.
- If multiple threads access a format concurrently, it must be synchronized
externally.</code></pre>
這段話的意思很簡單,翻譯過來就是:日期格式化的類是非同步的,建議為每一個線程創建獨立的格式化實例。如果多個線程并發訪問同一個格式化實例,就必須在外部添加同步機制。
由于LZ錯誤的將SimpleDateFormat的單個實例放置于高并發的環境下,并且沒有任何同步機制,于是就導致了這個奇葩的問題。接下來,LZ便快速的將代碼改成了類似于如下的形式。
public class DateUtil {
private DateUtil(){}
public static Date parse(String date) throws ParseException {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(date);
}
}</code></pre>
果然,那個奇怪的異常再也不出現了,事情到此已經水落石出了。最后,LZ奉上一段示例代碼,猿友們運行這個程序,就會出現解析失敗的異常,但是很明顯,我們的日期格式是正確的。
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DateUtil {
private DateUtil(){}
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(500);
for (int i = 0; i < 500; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
try {
DATE_FORMAT.parse("2014-01-01 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
}
}
});
}
Thread.sleep(3000000);
}
}</code></pre>
小結
高并發所引發的問題往往很難解決,因為它無法穩定的重現。比如本文中的問題,如果不是在高并發的情況下,可能你的程序運行半年甚至更久,都不一定能出現幾次解析失敗的異常。就算是偶爾出現,你也可能會以為是日期格式錯誤,從而忽略掉它,殊不知事實并非如此。
同樣的功能,不同的人寫出來的代碼質量確實是有很大差距的。就算是本文中這么簡單的一個日期工具類,一不小心都可能造成意料之外的錯誤。幸好JDK的代碼寫的足夠規范,大部分類的線程安全性都寫的很清楚,這才讓LZ找到了問題根源。
相信當下有不少猿友認為自己做的項目或是寫的代碼沒有什么技術含量,以至于每日渾渾噩噩,激情匱乏。但是本文就告訴你這樣一個道理, 不是因為項目讓你不能發光,而是因為你才讓項目不能發光 。
來自:http://www.importnew.com/23010.html