上一篇:深夜看了张一鸣的微博,让我越想越后怕

去年年底的时候,我们线上出了一次事故。

图片来自 Pexels

这个事故的表象是这样的:系统出现了两个一模一样的订单号,订单的内容却不是不一样的,而且系统在按照订单号查询的时候一直抛错,也没法正常回调,而且事情发生的不止一次,所以 这次系统升级一定要解决掉。

经手的同事之前也改过几次,不过效果始终不好:总会出现订单号重复的问题, 所以趁着这次问题我好好的理了一下我同事写的代码。

这里简要展示下当时的代码:

      /*** OD单号生成* 订单号生成规则:OD + yyMMddHHmmssSSS + 5位数(商户ID3位+随机数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();}/** 生成指定位数的随机数 **/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<size;i++){endNumStr.append("0");staNumStr.append("0");}int randomNum = ne.nextInt(Integer.valueOf(staNumStr.toString()))+Integer.valueOf(endNumStr.toString());return String.valueOf(randomNum);}

可以看到,这段代码写的其实不怎么好,代码部分暂且不议,代码中使订单号不重复的主要因素点是随机数和毫秒,可是这里的随机数只有两位。

在高并发环境下极容易出现重复问题,同时毫秒这一选择也不是很好,在多核 CPU 多线程下,一定时间内(极小的)这个毫秒可以说是固定不变的(测试验证过)。

所以这里我先以 100 个并发测试下这个订单号生成,关注微信订阅号码匠笔记,回复架构获取一些列的架构知识。

测试代码如下:

    public static void main(String[] args) {final String merchId = "12334";List<String> orderNos = Collections.synchronizedList(new ArrayList<String>());IntStream.range(0,100).parallel().forEach(i->{orderNos.add(getYYMMDDHHNumber(merchId));});List<String> filterOrderNos = 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 的日期类以格式化(线程安全及代码简洁性考量)

经过以上思考后我的最终代码是:

    /** 订单号生成(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();}

当然代码写完成了可不能这么随随便便结束了,现在得走一个测试 main 函数看看:

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

  • 应用所在的端口号

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

  • 使用进程 ID(某种程度下是一个可行的方案)

在此我想了下,我们的应用是跑在 Docker 里面,而且每个 Docker 容器内的应用端口都一样,不过网路 IP 不会存在重复的问题,至于进程也有存在重复的可能,对于 UUID 的方式之前吃过亏。

总之吧,Redis 或 DB 也算是一种比较好的方式,不过独立性较差。。。

同时还有一个因素也很重要,就是所有涉及到订单号生成的应用都是在同一台宿主机(Linux 实体服务器)上, 所以就目前的系统架构我选用了 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<String> orderNos = Collections.synchronizedList(new ArrayList<String>());IntStream.range(0,8000).parallel().forEach(i->{orderNos.add(generateOrderNo());});List<String> filterOrderNos = 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 的逻辑加同步锁(双向校验锁,整体是一种安全的单例模式)

  • 本人实现的方式并不是解决问题的唯一方式,具体解决问题需要视当前系统架构具体而论

  • 任何测试都是必要的,我同事在前几次尝试解决这个问题后都没有自测,不测试有损开发专业性!

作者:funnyZpC

出处:cnblogs.com/funnyzpc/p/13541713.html

感谢您的阅读,也欢迎您发表关于这篇文章的任何建议,关注我,技术不迷茫!小编到你上高速。

· END ·

最后,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。

正文结束

推荐阅读 ↓↓↓

1.不认命,从10年流水线工人,到谷歌上班的程序媛,一位湖南妹子的励志故事

2.如何才能成为优秀的架构师?

3.从零开始搭建创业公司后台技术栈

4.程序员一般可以从什么平台接私活?

5.37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

6.IntelliJ IDEA 2019.3 首个最新访问版本发布,新特性抢先看

7.漫画:程序员相亲图鉴,笑屎我了~

8.15张图看懂瞎忙和高效的区别!

一个人学习、工作很迷茫?

点击「阅读原文」加入我们的小圈子!

1次订单事故,扣了我3个月绩效!相关推荐

  1. 1标志图片_这四种情况将不再扣分罚款!11月起,全国高速统一限速标志

    如果你经常跑高速, 那么你可能经常会有这样的经历: 本来开车开得好好的, 忽然遇到限速标志, 赶紧进行紧急刹车. 又或者限速标志在不醒目的地方, 完全没有发现, 等到下了高速才知道自己因为超速被扣分了 ...

  2. 美团延长旅行订单免费取消保障政策至2月29日

    2月1日消息,根据"美团旅行"微信号发布的消息,受疫情影响,消费者变更或取消原有旅行订单的需求不断增加.即日起,美团联合平台商家延长旅行订单应急服务保障政策,尽力帮助用户减少损失. ...

  3. 力扣每日打卡8月24日打卡(459. 重复的子字符串,简单)

    a b c d a b c d a b c d 0 1 2 3 4 5 6 7 8 9 10 11 当循环到d时,i为3时,0与4对比,1与5对比,2与6对比,3与7对比 之后4与8对比,5与9对比, ...

  4. HTTP/3发布了,我们来谈谈HTTP/3

    经过了多年的努力,在 6 月 6 号,IETF (互联网工程任务小组) 正式发布了 HTTP/3 的 RFC, 这是超文本传输协议(HTTP)的第三个主要版本,完整的 RFC 超过了 20000 字, ...

  5. (四)秒杀扣库存、订单创建、支付、读各种信息

    扣库存方案 下单减库存? 并发请求 创建订单(恶意下单&不会超卖) 扣库存 支付 支付减库存? 并发请求 创建订单(订单超卖) 支付 (订单支付不了) 扣库存 预扣库存? 并发请求 扣库存 创 ...

  6. 这样设计订单系统,同事直呼 666!

    来源:人人都是产品经理 | http://dwz.win/Wkh 本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考 ...

  7. 订单系统:从0到1设计思路

    https://baijiahao.baidu.com/s?id=1611220684816408868&wfr=spider&for=pc 概述 本文主要讲述了在传统电商企业中,订单 ...

  8. Spring Cloud【Finchley】实战-02订单微服务

    文章目录 Spring Cloud[Finchley]专栏 概述 数据模型-订单微服务 API 业务逻辑分析 搭建订单微服务 依赖及配置文件 pom.xml application.yml 将微服务注 ...

  9. 如何避免订单重复支付?

    咖友提问: 如何设计避免订单出现重复支付的逻辑? 问题补充:假设有这么一种情况: 订单已下单成功并且正处于支付页面,用户调起支付网关进行支付.支付成功了一次,但是由于某种情况导致未接收到银行返回的[支 ...

  10. 如何设计订单系统?不妨看看这篇文章

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 来源:r6d.cn/uEJQ 本文主要讲述了在传统电商企业中,订单系统应承 ...

最新文章

  1. Ubuntu 系统 下 AndroidStudio 工具 使用ctrl+z 回退上一步的方法
  2. Windows 10版星巴克应用现身官网
  3. mongodb type it for more
  4. python的程序结构有哪几种_python异常处理结构有哪几种形式
  5. 根据内容来产生一个二维码
  6. Hyperledger Fabric 核心模块(6)Fabric-ca-server
  7. java super关键字简述
  8. 使用java实现数字滤波器
  9. 【架构】Heartbeat高可用服务(2)
  10. 外星人台式电脑_2020年双11:高端电竞游戏笔记本电脑本推荐:联想、惠普、雷神、ROG、外星人等品牌游戏本挑选指南...
  11. linux7重启network,mmp的 rhel7 network重启一直失败,求原因!!!
  12. 数据结构课程设计(基于AVL树的身份证管理系统)
  13. 三款Linux文件传输工具简单介绍
  14. 3397. 【GDOI2014模拟】雨天的尾巴
  15. C语言中task的用法,c – 在std :: packaged_task中使用成员函数
  16. 计算机中常用源码,什么是源码
  17. Android Room,编译成功,倒是 build APK 失败提示: 无法访问:ActivityCompatApi23
  18. 拼图游戏Canvas版
  19. 两化融合资质认证流程
  20. 关于免费的seo网站推广方法有哪些?

热门文章

  1. 世界为何对区块链狂热?是因为一个“财富密码”
  2. 初识Hadoop入门介绍
  3. BZOJ1108 [POI2007]天然气管道Gaz
  4. Probably at least one of the constraints in the following list is one you don't want.
  5. 常去的国外网站一览表
  6. java基础七--网络编程(1)
  7. wpf 自动完成comboxBox
  8. android开发——图文并茂
  9. 你理解这些Cisco NAT分类和原理吗
  10. 超详细mac新手教程-Mac界面篇