1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        記一次訂單號重復的事故,快看看你的 uuid 在并發(fā)下還正確嗎?

        共 5520字,需瀏覽 12分鐘

         ·

        2020-09-05 23:53

        ??來源:? ?cnblogs.com/funnyzpc/p/13541713.html

        去年年底的時候,我們線上出了一次事故,這個事故的表象是這樣的: 系統(tǒng)出現(xiàn)了兩個一模一樣的訂單號,訂單的內容卻不是不一樣的,而且系統(tǒng)在按照 訂單號查詢的時候一直拋錯,也沒法正?;卣{,而且事情發(fā)生的不止一次,所以 這次系統(tǒng)升級一定要解決掉。

        經(jīng)手的同事之前也改過幾次,不過效果始終不好:總會出現(xiàn)訂單號重復的問題, 所以趁著這次問題我好好的理了一下我同事寫的代碼。

        這里簡要展示下當時的代碼:

        ????/**
        ????*?OD單號生成
        ????*?訂單號生成規(guī)則:OD + yyMMddHHmmssSSS + 5位數(shù)(商戶ID3位+隨機數(shù)2位) 22位
        ????*/

        ???public?static?String?getYYMMDDHHNumber(String?merchId){
        ??????????StringBuffer?orderNo?=?new?StringBuffer(new?SimpleDateFormat("yyMMddHHmmssSSS").format(new?Date()));
        ??????????if(StringUtils.isNotBlank(merchId)){
        ??????????????if(merchId.length()>3){
        ??????????????????orderNo.append(merchId.substring(0,3));
        ??????????????}else?{
        ??????????????????orderNo.append(merchId);
        ??????????????}
        ??????????}
        ??????????int?orderLength?=?orderNo.toString().length();
        ??????????String?randomNum?=?getRandomByLength(20-orderLength);
        ??????????orderNo.append(randomNum);
        ??????????return?orderNo.toString();
        ???}


        ??????/**?生成指定位數(shù)的隨機數(shù)?**/
        ??????public?static?String?getRandomByLength(int?size){
        ??????????if(size>8?||?size<1){
        ??????????????return?"";
        ??????????}
        ??????????Random?ne?=?new?Random();
        ??????????StringBuffer?endNumStr?=?new?StringBuffer("1");
        ??????????StringBuffer?staNumStr?=?new?StringBuffer("9");
        ??????????for(int?i=1;i??????????????endNumStr.append("0");
        ??????????????staNumStr.append("0");
        ??????????}
        ??????????int?randomNum?=?ne.nextInt(Integer.valueOf(staNumStr.toString()))+Integer.valueOf(endNumStr.toString());
        ??????????return?String.valueOf(randomNum);
        ??????}

        可以看到,這段代碼寫的其實不怎么好,代碼部分暫且不議,代碼中使訂單號不重復的主要因素點是隨機數(shù)和毫秒,可是這里的隨機數(shù)只有兩位 在高并發(fā)環(huán)境下極容易出現(xiàn)重復問題,同時毫秒這一選擇也不是很好,在多核CPU多線程下,一定時間內(極小的)這個毫秒可以說是固定不變的(測試驗證過),所 以這里我先以100個并發(fā)測試下這個訂單號生成,測試代碼如下:

        ????public?static?void?main(String[]?args)?{
        ????????final?String?merchId?=?"12334";
        ????????List?orderNos?=?Collections.synchronizedList(new?ArrayList());
        ????????IntStream.range(0,100).parallel().forEach(i->{
        ????????????orderNos.add(getYYMMDDHHNumber(merchId));
        ????????});

        ????????List?filterOrderNos?=?orderNos.stream().distinct().collect(Collectors.toList());

        ????????System.out.println("生成訂單數(shù):"+orderNos.size());
        ????????System.out.println("過濾重復后訂單數(shù):"+filterOrderNos.size());
        ????????System.out.println("重復訂單數(shù):"+(orderNos.size()-filterOrderNos.size()));
        ????}

        果然,測試的結果如下:

        生成訂單數(shù):100
        過濾重復后訂單數(shù):87
        重復訂單數(shù):13

        當時我就震驚?了,一百個并發(fā)里面竟然有13個重復的!??!,我趕緊讓同事先不要發(fā)版,這活兒我接了!

        對這一燙手的山竽拿到手里沒有一個清晰的解決方案可是不行的,我大概花了6+分鐘和同事商量了下業(yè)務場景,決定做如下更改:

        • 去掉商戶ID的傳入(按同事的說法,傳入商戶ID也是為了防止重復訂單的,事實證明并沒有叼用)
        • 毫秒僅保留三位(縮減長度同時保證應用切換不存在重復的可能)
        • 使用線程安全的計數(shù)器做數(shù)字遞增(三位數(shù)最低保證并發(fā)800不重復,代碼中我給了4位)
        • 更換日期轉換為java8的日期類以格式化(線程安全及代碼簡潔性考量)

        經(jīng)過以上思考后我的最終代碼是:

        ????/**?訂單號生成(NEW)?**/
        ????private?static?final?AtomicInteger?SEQ?=?new?AtomicInteger(1000);
        ????private?static?final?DateTimeFormatter?DF_FMT_PREFIX?=?DateTimeFormatter.ofPattern("yyMMddHHmmssSS");
        ????private?static?ZoneId?ZONE_ID?=?ZoneId.of("Asia/Shanghai");
        ????public?static?String?generateOrderNo(){
        ????????LocalDateTime?dataTime?=?LocalDateTime.now(ZONE_ID);
        ????????if(SEQ.intValue()>9990){
        ????????????SEQ.getAndSet(1000);
        ????????}
        ????????return??dataTime.format(DF_FMT_PREFIX)+SEQ.getAndIncrement();
        ????}

        當然代碼寫完成了可不能這么隨隨便便結束了,現(xiàn)在得走一個測試main函數(shù)看看:

        ????public?static?void?main(String[]?args)?{

        ????????List?orderNos?=?Collections.synchronizedList(new?ArrayList());
        ????????IntStream.range(0,8000).parallel().forEach(i->{
        ????????????orderNos.add(generateOrderNo());
        ????????});

        ????????List?filterOrderNos?=?orderNos.stream().distinct().collect(Collectors.toList());

        ????????System.out.println("生成訂單數(shù):"+orderNos.size());
        ????????System.out.println("過濾重復后訂單數(shù):"+filterOrderNos.size());
        ????????System.out.println("重復訂單數(shù):"+(orderNos.size()-filterOrderNos.size()));
        ????}

        ????/**
        ????????測試結果:
        ????????生成訂單數(shù):8000
        ????????過濾重復后訂單數(shù):8000
        ????????重復訂單數(shù):0
        ????**/

        真好,一次就成功了,可以直接上線了。。。

        然而,我回過頭來看以上代碼,雖然最大程度解決了并發(fā)單號重復的問題,不過對于我們的系統(tǒng)架構還是有一個潛在的隱患:如果當前 應用有多個實例(集群)難道就沒有重復的可能了?鑒于此問題就必然需要一個有效的解決方案,所以這時我就思考:多個實例應用訂單號如何區(qū)分開呢?以下為我思考的大致方向:

        • 使用UUID(在第一次生成訂單號時初始化一個)

        • 使用redis記錄一個增長ID

        • 使用數(shù)據(jù)庫表維護一個增長ID

        • 應用所在的網(wǎng)絡IP

        • 應用所在的端口號

        • 使用第三方算法(雪花算法等等)

        • 使用進程ID(某種程度下是一個可行的方案)

          在此我想了下,我們的應用是跑在docker里面,而且每個docker容器內的應用端口都一樣,不過網(wǎng)路IP不會存在重復的問題,至于進程也有存在重復的可能, 對于UUID的方式之前吃過虧,遠之吧,redis或DB也算是一種比較好的方式,不過獨立性較差。。。,同時還有一個因素也很重要,就是所有涉及到訂單號生成的 應用都是在同一臺宿主機(linux實體服務器)上, 所以就目前的系統(tǒng)架構我選用了IP的方式。一下是我的代碼:

        import?org.apache.commons.lang3.RandomUtils;

        import?java.net.InetAddress;
        import?java.time.LocalDateTime;
        import?java.time.ZoneId;
        import?java.time.format.DateTimeFormatter;
        import?java.util.ArrayList;
        import?java.util.Collections;
        import?java.util.List;
        import?java.util.concurrent.atomic.AtomicInteger;
        import?java.util.stream.Collectors;
        import?java.util.stream.IntStream;

        public?class?OrderGen2Test?{

        ????/**?訂單號生成?**/
        ????private?static?ZoneId?ZONE_ID?=?ZoneId.of("Asia/Shanghai");
        ????private?static?final?AtomicInteger?SEQ?=?new?AtomicInteger(1000);
        ????private?static?final?DateTimeFormatter?DF_FMT_PREFIX?=?DateTimeFormatter.ofPattern("yyMMddHHmmssSS");
        ????public?static?String?generateOrderNo(){
        ????????LocalDateTime?dataTime?=?LocalDateTime.now(ZONE_ID);
        ????????if(SEQ.intValue()>9990){
        ????????????SEQ.getAndSet(1000);
        ????????}
        ????????return??dataTime.format(DF_FMT_PREFIX)+?getLocalIpSuffix()+SEQ.getAndIncrement();
        ????}

        ????private?volatile?static?String?IP_SUFFIX?=?null;
        ????private?static?String?getLocalIpSuffix?(){
        ????????if(null?!=?IP_SUFFIX){
        ????????????return?IP_SUFFIX;
        ????????}
        ????????try?{
        ????????????synchronized?(OrderGen2Test.class){
        ????????????????if(null?!=?IP_SUFFIX){
        ????????????????????return?IP_SUFFIX;
        ????????????????}
        ????????????????InetAddress?addr?=?InetAddress.getLocalHost();
        ????????????????//??172.17.0.4??172.17.0.199?,
        ????????????????String?hostAddress?=?addr.getHostAddress();
        ????????????????if?(null?!=?hostAddress?&&?hostAddress.length()?>?4)?{
        ????????????????????String?ipSuffix?=?hostAddress.trim().split("\\.")[3];
        ????????????????????if?(ipSuffix.length()?==?2)?{
        ????????????????????????IP_SUFFIX?=?ipSuffix;
        ????????????????????????return?IP_SUFFIX;
        ????????????????????}
        ????????????????????ipSuffix?=?"0"?+?ipSuffix;
        ????????????????????IP_SUFFIX?=?ipSuffix.substring(ipSuffix.length()?-?2);
        ????????????????????return?IP_SUFFIX;
        ????????????????}
        ????????????????IP_SUFFIX?=?RandomUtils.nextInt(10,?20)?+?"";
        ????????????????return?IP_SUFFIX;
        ????????????}
        ????????}catch?(Exception?e){
        ????????????System.out.println("獲取IP失敗:"+e.getMessage());
        ????????????IP_SUFFIX?=??RandomUtils.nextInt(10,20)+"";
        ????????????return?IP_SUFFIX;
        ????????}
        ????}


        ????public?static?void?main(String[]?args)?{
        ????????List?orderNos?=?Collections.synchronizedList(new?ArrayList());
        ????????IntStream.range(0,8000).parallel().forEach(i->{
        ????????????orderNos.add(generateOrderNo());
        ????????});

        ????????List?filterOrderNos?=?orderNos.stream().distinct().collect(Collectors.toList());

        ????????System.out.println("訂單樣例:"+?orderNos.get(22));
        ????????System.out.println("生成訂單數(shù):"+orderNos.size());
        ????????System.out.println("過濾重復后訂單數(shù):"+filterOrderNos.size());
        ????????System.out.println("重復訂單數(shù):"+(orderNos.size()-filterOrderNos.size()));
        ????}
        }

        /**
        ??訂單樣例:20082115575546011022
        ??生成訂單數(shù):8000
        ??過濾重復后訂單數(shù):8000
        ??重復訂單數(shù):0
        **/

        [最后] 代碼說明及幾點建議

        • generateOrderNo()方法內不需要加鎖,因為AtomicInteger內使用的是CAS自旋轉鎖(保證可見性的同時也保證原子性,具體的請自行了解)
        • getLocalIpSuffix()方法內不需要對不為null的邏輯加同步鎖(雙向校驗鎖,整體是一種安全的單例模式)
        • 本人實現(xiàn)的方式并不是解決問題的唯一方式,具體解決問題需要視當前系統(tǒng)架構具體而論
        • 任何測試都是必要的,我同事在前幾次嘗試解決這個問題后都沒有自測,不測試有損開發(fā)專業(yè)性!



        推薦閱讀

        阿里精選:Java 代碼精簡之道

        Java8 中用法優(yōu)雅的 Stream,性能也""優(yōu)雅""嗎?

        ElasticSearch 索引 VS MySQL 索引

        還在手動部署SpringBoot應用?試試這個自動化插件!

        MySQL執(zhí)行計劃Explain詳解

        瀏覽 56
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            中文字幕淫乱视频 | 久久国产亚洲AV无码麻豆牛牛 | 日本逼逼 | 北条麻妃一区二区三区中文字幕 | 青青草成人在线观看 | 2025天天操夜夜操 | 狠狠干狠狠爱 | 国产偷窥精品 | 未满小箩利一区二区三区 | 国产夫妻性生活勉费视频 |