当前位置: 首页 > 科技观察

一次重复单号引发的事故让我苦不堪言!

时间:2023-03-14 11:22:36 科技观察

我们在网上发生了意外。这个事故的表现是这样的:系统有两个相同的订单号,但是订单内容不一样,系统根据订单号查询时一直报错,也不能正常回调,而且不止一次,所以这次系统升级必须要解决。之前处理的同事也修改过几次,但是效果还是不好,总会出现单号重复的问题,所以趁着这个问题,好好看看自己写的代码同事。这里简单展示一下当时的代码:/***OD订单号生成*订单号生成规则:OD+yyMMddHHmmssSSS+5位(商户ID3位+随机数2位)22位*/publicstaticStringgetYYMMDDHHNumber(StringmerchId){StringBufferorderNo=newStringBuffer(newSimpleDateFormat("yyMMddHHmmssSSS").format(newDate()));if(StringUtils.isNotBlank(merchId)){if(merchId.length()>3){orderNo.append(merchId.substring(0,3)}/**生成指定数字的随机数**/publicstaticStringgetRandomByLength(intsize){if(size>8||size<1){return"";}Randomne=newRandom();StringBufferendNumStr=newStringBuffer("1");StringBufferstaNumStr=newStringBuffer("9");for(inti=1;iorderNos=Collections.synchronizedList(newArrayList());IntStream.range(0,100).parallel().forEach(i->{orderNos.add(getYYMMDDHHNumber(merchId));});ListfilterOrderNos=orderNos.stream().distinct().collect(Collectors.toList());System.out.println("生成订单号:"+orderNos.size());System.out.println("过滤重复后的订单数:"+filterOrderNos.size());System.out.println("重复订单数:"+(orderNos.size()-filterOrderNos.size()));}果然测试结果如下:生成的订单数:100之后的订单数过滤重复:87重复订单数:13当时我惊呆了🤯里面居然有13个重复的!!!赶紧让同事不要发版,我接了这个活儿!这炎热的山羽,想要得到一个明确的解决方案是不可能的。和同事讨论了大概6+分钟的业务场景,决定做如下修改:去掉传入的商户ID(据同事说,商户ID也传入,防止重复下单,但是事实证明没有用)只保留三位毫秒(减少长度,保证应用切换不存在重复的可能)使用线程安全的计数器递增数字(三位最低保证并发数为800不重复,我在代码中给了4位)把日期换成java8日期类来格式化(线程安全和代码简洁的考虑,可以点这里阅读详情)经过以上思考,我最终的代码是:/**订单号生成(新)**/privatestaticfinalAtomicIntegerSEQ=newAtomicInteger(1000);privatestaticfinalDateTimeFormatterDF_FMT_PREFIX=DateTimeFormatter.ofPattern("yyMMddHHmmssSS");privatestaticZoneIdZONE_ID=ZoneId.of("亚洲/上海");publicstaticStringgenerateOrderNo(){LocalDateTimedataTime=LocalDateTime.now(Val0SE_ID)9ue(){SEQ00)AndSet(10;}returndataTime.format(DF_FMT_PREFIX)+SEQ.getAndIncrement();}当然,代码不能这么随便写完。现在我们必须测试主要功能:publicstaticvoidmain(String[]args){ListorderNos=Collections.synchronizedList(newArrayList());IntStream.range(0,8000).parallel().forEach(i->{orderNos.add(generateOrderNo());});ListfilterOrderNos=orderNos.stream().distinct().collect(Collectors.toList());System.out.println("数量生成的订单:“+orderNos.size());System.out.println("过滤后的重复订单数:"+filterOrderNos.size());System.out.println("重复订单数:"+(orderNos.size()-filterOrderNos.size()));}/**测试结果:生成订单数:8000过滤重复后订单数:8000重复订单数:0**/太棒了,成功了一次,可以直接上线了向上。.但是回过头来看上面的代码,虽然最大程度的解决了并发单号重复的问题,但是我们的系统架构还是存在一个潜在的隐患:如果当前应用有多个实例(集群),是没有重复?可能的?针对这个问题,势必需要一个行之有效的解决方案,所以此时我想到:如何区分多实例应用订单号?以下是我的大致思路:使用UUID(第一次生成订单号时初始化一个)使用redis记录一个增加的ID使用数据库表维护一个增加的ID应用所在的网络IP应用所在的端口号使用第三方算法(Snowflake算法等)使用进程ID(某种程度上可行的方案)我这里想到了,我们的应用运行在docker中,应用端口在各个docker容器也是一样,但是网络不存在重复IP的问题,也有重复进程的可能。UUID方法以前就受过苦。扯远了,redis或者DB也是比较好的办法,但是独立性差。..同时,还有一个因素也很重要,就是所有与订单号生成相关的应用都在同一台主机上(linux物理服务器),所以我目前的系统架构选择了IP方式.下面是我的代码:importorg.apache.commons.lang3.RandomUtils;importjava.net.InetAddress;importjava.time.LocalDateTime;importjava.time.ZoneId;importjava.time.format.DateTimeFormatter;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;importjava.util.concurrent.atomic.AtomicInteger;importjava.util.stream.Collectors;importjava.util.stream.IntStream;publicclassOrderGen2Test{/**订单号生成**/privatestaticZoneIdZONE_ID=ZoneId.of("亚洲/上海");privatestaticfinalAtomicIntegerSEQ=newAtomicInteger(1000);privatestaticfinalDateTimeFormatterDF_FMT_PREFIX=DateTimeFormatter.ofPattern("yyMMddHHmmssSS");publicstaticStringgenerateOrderNo(){LocalDateTimedataTime=LocalDateTime.now(ZONE_ID);if(SEQ.intValue()>99){SEQ.getAndSet(1000);}returndataTime.format(DF_FMT_PREFIX)+getLocalIpSuffix()+SEQ.getAndIncrement();}privatevolatilestaticStringIP_SUFFIX=null;privatestaticStringgetLocalIpSuffix(){if(null!=IP_SUFFIX){returnIP_SUFFIX;}try{synchronized(OrderGen2Test.class){if(null!=IP_SUFFIX){returnIP_SUFFIX;}NoAddressAddress=NoAddress.getLocalHost();//172.17.0.4172.17.0.199,StringhostAddress=address.getHostAddress();.length()>4){StringipSuffix=hostAddress.trim().split("\\.")[3];if(ipSuffix.length()==2){IP_SUFFIX=ipSuffix;returnIP_SUFFIX;}ipSuffix="0"+ipSuffix;IP_SUFFIX=ipSuffix.substring(ipSuffix.length()-2);returnIP_SUFFIX;}IP_SUFFIX=RandomUtils.nextInt(10,20)+"";returnIP_SUFFIX;}}catch(Exceptions){System.out.println("配置的IP地址:"+e.getMessage());IP_SUFFIX=RandomUtils.nextInt(10.20)+"";returnIP_SUFFIX;}}publicstaticvoidmain(String[]args){ListorderNos=Collections.synchronizedList(newArrayList());集成流。范围(0.8000)。平行线()forEach(i->{orderNos.add(generateOrderNo());});ListfilterOrderNos=orderNos.stream().distinct().collect(Collectors.toList());System.out.println("样品订单:"+orderNos.get(22));System.out.println("生成订单数:"+orderNos.size());System.out.println("过滤重复邮单数量:"+filterOrderNos.size());System.out.println("重复订单数:"+(orderNos.size()-filterOrderNos.size()));}}/**订单样本:20082115575546011022生成订单数:8000重复订单数过滤后:8000重复订单数:0**/最后,代码说明和一些建议不需要在generateOrderNo()方法中加锁,因为AtomicInteger使用了CAS自旋锁(保证可见性和原子性,请具体自己了解)getLocalIpSuffix()方法不需要在不为null的逻辑上加同步锁(双向校验锁,整体是安全的单例模式)我自己实现的方法不是唯一的解决问题的方法。问题的具体解决方案取决于当前的系统架构。任何测试都是必要的。我同事之前尝试过几次解决这个问题后没有测试自己。不测试会损害开发性的专业性!好了,本文到此结束。想看之前同事的牛逼系列干货,可以关注公众号Java技术栈阅读