图片来自Pexels本次事故的表现是这样的:系统中出现了两个相同的订单号,但是订单内容不一样,系统根据订单号查询时一直报错,而且不能正常回调,而且不止一次,所以这次系统升级必须要解决。之前处理的同事也修改过几次,但是效果还是不好:总会有重复单号的问题,于是趁着这个问题,好好看看同事写的代码.这里简单展示一下当时的代码:/***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生成订单数:100过滤重复后订单数:87重复订单数:13我当时惊呆了,100个并发竟然有13个重复!!!赶紧让同事不要发版,我接了这个活儿!你得到这个热山玉了吗?一个明确的解决方案是不够的。我花了大约6分钟与同事讨论业务场景。最后决定做如下改动:去掉传入商户ID(据同事说传入商户ID也是为了防止重复下单,结果发现没有用)只保留三位毫秒(减少长度,保证不存在应用切换)重复的可能性)使用线程安全的计数器递增数字(三位数最小保证800并发不重复,我在代码中给了4位数)替换日期转换withjava8dateclasstoformat(threadsafetyandcodesimplicityconsiderations)经过以上思考,我最终的代码是:yyMMddHHmmssSS");privatestaticZoneIdZONE_ID=ZoneId.of("亚洲/上海");publicstaticStringgenerateOrderNo(){LocalDateTimedataTime=LocalDateTime.now(ZONE_ID);if(SEQ.intValue()>9990){SEQ.getAndSet(1000);}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应用所在的端口号使用第三方算法(SnowflakeAlgorithm等)使用进程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()方法不需要在不为空的逻辑上加同步锁(双向校验锁,整体是安全的单例模式)实现方式不解决问题的唯一途径。问题的具体解决方案取决于当前的系统架构。任何测试都是必要的。这个问题之前我的同事尝试解决了几次之后都没有自己测试,不测试会损害开发专业!作者:funnyZpC编辑:陶佳龙来源:cnblogs.com/funnyzpc/p/13541713.html