<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 5520字,需瀏覽 12分鐘

           ·

          2020-09-05 23:53

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

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

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

          這里簡要展示下當(dāng)時(shí)的代碼:

          ????/**
          ????*?OD單號生成
          ????*?訂單號生成規(guī)則:OD + yyMMddHHmmssSSS + 5位數(shù)(商戶ID3位+隨機(jī)數(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ù)的隨機(jī)數(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í)不怎么好,代碼部分暫且不議,代碼中使訂單號不重復(fù)的主要因素點(diǎn)是隨機(jī)數(shù)和毫秒,可是這里的隨機(jī)數(shù)只有兩位 在高并發(fā)環(huán)境下極容易出現(xiàn)重復(fù)問題,同時(shí)毫秒這一選擇也不是很好,在多核CPU多線程下,一定時(shí)間內(nèi)(極小的)這個(gè)毫秒可以說是固定不變的(測試驗(yàn)證過),所 以這里我先以100個(gè)并發(fā)測試下這個(gè)訂單號生成,測試代碼如下:

          ????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("過濾重復(fù)后訂單數(shù):"+filterOrderNos.size());
          ????????System.out.println("重復(fù)訂單數(shù):"+(orderNos.size()-filterOrderNos.size()));
          ????}

          果然,測試的結(jié)果如下:

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

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

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

          • 去掉商戶ID的傳入(按同事的說法,傳入商戶ID也是為了防止重復(fù)訂單的,事實(shí)證明并沒有叼用)
          • 毫秒僅保留三位(縮減長度同時(shí)保證應(yīng)用切換不存在重復(fù)的可能)
          • 使用線程安全的計(jì)數(shù)器做數(shù)字遞增(三位數(shù)最低保證并發(fā)800不重復(fù),代碼中我給了4位)
          • 更換日期轉(zhuǎn)換為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();
          ????}

          當(dāng)然代碼寫完成了可不能這么隨隨便便結(jié)束了,現(xiàn)在得走一個(gè)測試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("過濾重復(fù)后訂單數(shù):"+filterOrderNos.size());
          ????????System.out.println("重復(fù)訂單數(shù):"+(orderNos.size()-filterOrderNos.size()));
          ????}

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

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

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

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

          • 使用redis記錄一個(gè)增長ID

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

          • 應(yīng)用所在的網(wǎng)絡(luò)IP

          • 應(yīng)用所在的端口號

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

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

            在此我想了下,我們的應(yīng)用是跑在docker里面,而且每個(gè)docker容器內(nèi)的應(yīng)用端口都一樣,不過網(wǎng)路IP不會存在重復(fù)的問題,至于進(jìn)程也有存在重復(fù)的可能, 對于UUID的方式之前吃過虧,遠(yuǎn)之吧,redis或DB也算是一種比較好的方式,不過獨(dú)立性較差。。。,同時(shí)還有一個(gè)因素也很重要,就是所有涉及到訂單號生成的 應(yīng)用都是在同一臺宿主機(jī)(linux實(shí)體服務(wù)器)上, 所以就目前的系統(tǒng)架構(gòu)我選用了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("過濾重復(fù)后訂單數(shù):"+filterOrderNos.size());
          ????????System.out.println("重復(fù)訂單數(shù):"+(orderNos.size()-filterOrderNos.size()));
          ????}
          }

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

          [最后] 代碼說明及幾點(diǎn)建議

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



          推薦閱讀

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

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

          ElasticSearch 索引 VS MySQL 索引

          還在手動部署SpringBoot應(yīng)用?試試這個(gè)自動化插件!

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

          瀏覽 56
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲免费视频欧洲免费视频 | 99国精产品在一区二区豆花线 | 久久久久久精 | 亚洲精品一二三四区 | 高潮视频在线观看免费 |