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

血的教训,我因为订单号重复的事故差点被开除

时间:2023-03-22 17:14:07 科技观察

转载本文请联系Java极客技术公众号。1.简介曾经有一个项目,我们在网上出了事故。事故的表现大致是这样的:系统中出现了两份相同的订单号,但订单内容不同,而且该事件不止一次发生。被老板发现后,扣了一个月的业绩!经过排查,出现了这个问题。主要有两个原因:数据库订单表中,订单号没有唯一键约束。生成订单号时,使用随机数。结果,一些订单号重复了。我对这个问题做了一些研究,想和大家分享一些收获!本文主要以电子商务的订单编码规则为例进行探讨。其他类型服务号的设计思路其实也是类似的。废话不多说,干货!订单命名的几个规则总结:不再赘述:这点相信大家都明白,而且一定是全球唯一的。如果使用序列号,其他人很容易从订单号推断出公司的整体运作情况。禁用随机码:很多人在分析生成单号的时候,第一个想到的肯定是不重复唯一性,第二个可能就是安全性。如果要同时满足前两个,很容易想到使用随机码,随机码在一定程度上更安全??,更不重复,但是可读性差,有可能的重复。防止并发:针对系统的并发业务场景(如秒杀),需要满足并发场景下订单号生成速度快、不重复的需求。控制位数:订单号的位数尽量在10-18位之间。如果太短,如果交易量太大,就很难防止重复。如果太长,可读性差,没有意义。2.解题实践上面说了订单号生成的规则,那么这样的规则怎么实现比较好呢?下面总结了几种常见的处理方式,我们一一分析!2.1.方案一:UUIDUUID是UniversallyTheUniqueIdentifier的缩写,译为UniversalUniqueIdentificationCode,顾名思义,UUID是一种用来记录唯一??标识符的数据。它根据开放软件基金会(OSF)指定的标准计算,使用以太网卡地址(MAC)、以秒为单位的纳米时间、芯片ID代码和许多可能的数字。一般来说,UUID码由以下三部分组成:当前日期时间时钟序列全球唯一的IEEE机器识别码(有网卡则从网卡获取,无网络则通过其他方式获取card)UUID的标准形式包含32个16位基数,用连字符分为五段,例如:00000191-adc6-4314-8799-5c3d737aa7de。以java为例,可以通过以下方式生成:Stringuuid=UUID.randomUUID().toString();虽然这个解决方案实施起来简单方便;但是数据库查询效率很差,而且内容很长,在实际项目场景开发中,一般用来记录用户的移动设备ID等硬件信息!所以不建议使用uuid来生成订单号!2.2、方案二:数据库自增所谓数据库自增,就是在数据库中设置某列为自增列,并为该列设置一个初始值。不需要在代码级别进行任何特殊处理。以Mysql的用户表的ID列为例,可以通过以下方式在建表时生成。创建表`tb_user`(`id`bigint(20)unsignedNOTNULLAUTO_INCREMENT,`name`varchar(20)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_0900_ai_ci;,在单体服务下没有问题,但是在大流量的分布式服务环境下,并发性能很低。以后量大的时候,需要分库分表mysql。此时订单号会重复,不建议使用!2.3、方案三:Snowflake算法Snowflake(中文简称:Snowflake算法)是Twitter内部的一种ID生成算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的ID号。其内部结构如下:可以清楚的看到Snowflake由4部分组成:第一部分:bit值,是未使用的符号位;第二部分:由一个41位的时间戳(毫秒)组成,它的值是当前时间相对于某个时间的偏移量。第三部分:工作机id,由服务节点id和数据中心id组成。第四部分:表示每台工作机每毫秒产生的流水号ID,同一毫秒生产内最多可产生4095个ID。由于Java中64位整数是long类型,所以Java中SnowFlake算法生成的id存储在long中。SnowFlake算法可以保证:所有生成的id都会按照时间趋势递增,整个分布式系统中不会产生重复的id(因为有服务节点id和数据中心id区分)。需要注意的是:在分布式环境中,5位datacenter和worker表示最多可以部署31个数据中心,每个数据中心最多可以部署31个节点。41位的二进制长度最多可以表示2^41-1毫秒,也就是69年,所以雪花算法最多可以正常使用69年。为了最大限度地利用该算法,在使用时应指定一个开始时间。否则会出现重复!在高并发环境下,Snowflake算法可以生成一个全局唯一的订单号,但是长度达到了21位,所以不推荐,但是可以用来生成主键ID,完全没问题!2.4.方案三:如果分布式组件想要在分布式环境下生成一个唯一的订单号,我们可以使用分布式组件来帮助我们生成一个全局唯一的订单号。比如我们可以使用redis分布式缓存组件中的incr命令来帮我们生成一个全局自增序列号!实现逻辑如下://基于某个key实现自增Stringres=jedis.get(key);if(StringUtils.isBlank(res)){jedisClient.set(key,INIT_ID);//设置自增长的初始值,INIT_ID为初始值jedisClient.expire(key,seconds);//设置过期时间,seconds为过期多少秒}longorderId=jedis.incr(key);//生成+1订单号(如果存在)设计一个订单号规则!在设计规则之前,我们先看看网上几大厂商的订单号格式。京东订单号格式:157444499苏宁易购订单号格式:2000839647凡客诚品订单号格式:213052230059银泰订单号格式:10030522161715小米订单号格式:1111218032345170先分析一下凡客诚品订单号的生成规则和银泰网。Fancl和银泰的订单号都包含0522,因为这两个订单都是2013年5月22日下单的。基本猜测,Fancl的订单规则是:商号+年份后2位+月份+日期+订单数字;泰网订单号规则:年第三位+业务代码+年末1位+月+日+订单数;京东商城和苏宁易购订单号不显示规则。最后我们分析一下小米订单号1111218032345170,可以分解为1——111218——03234——5170四个部分。第一部分,1个购买,2个退货。第二部分表示2011年12月18日下的订单,省略前两位。第三部分时间戳对应的是00:53:54,换算成秒是03234秒。最后一部分表示同一秒内下的第5170个订单,也就是说小米认为一秒内不会超过10000个订单。总结一下,小米的下单规则是:业务码+年末2位+月+日+秒+订单号,固定长度为16。这个订单号规则可以保证100个不重复年!同样,借鉴小米的订单号规则,我们也可以生成相同的订单号。实现过程如下://获取当前时间DatecurrentTime=newDate();//将当前时间格式化为[年末2位+月+日]StringoriginDateStr=newSimpleDateFormat("yyMMdd").format(currentTime);//计算当前时间经过的秒数))/1000;//得到[年末2位+月+日+秒],如果秒的长度不足,则添加0StringyyMMddSecond=originDateStr+StringUtils.leftPad(String.valueOf(differSecond),5,'0');//获取[业务代码]+[年末2位+月+日+秒]作为自增键;StringprefixOrder=sourceType+""+yyMMddSecond;//通过key函数使用redis自增,实现单秒自增;不同的key,从0开始自增,设置60秒过期LongincrId=redisUtils.saveINCR(prefixComplaint,60);//生成订单号StringorderNo=prefixOrder+StringUtils.leftPad(String.valueOf(incrId),4,'0');这个订单号在大流量环境下可以保证全局唯一,生成速度非常快,支持高并发环境,还支持时间排序!3.总结通过上面的实例演示,我们可以做一个详细的总结!综上所述,在大流量环境下,我们可以利用redis的incr功能来实现序号自增特性,同时配合订单的设计规则那么,为了保证高并发环境下顺序的唯一性!四、参考1、如何正确设计订单号???2.并发下唯一的订单号生成规则