这边先来看一个场景:
在实际开发过程中,业务中最常见的一个服务就是生成业务单号,比如电商订单编号、入库单号、服务单号等等。那么如果让你创建一个订单号,你会考虑那些问题?有什么好的设计思路嘛?
需求分析考虑一下如果让你设计一个服务单号,需要满足那些基本需求呢?、
全局唯一性: 订单号需要在在整个系统独一无二,避免重复安全性: 订单号不能暴漏太多信息,比如流水信息、用户信息,那么自增这种方案就不能考虑了禁用随机码: 随机码虽然可以满足前两个条件,但是随机码没有更多信息,比如相对顺序、日期信息,同时伴有一定概率会重复满足并发需求: 像在特定场合下(如秒杀)需要做到并发场景下的订单号的生成控制位数: 订单号的位数尽量在 10 位 ~ 18 位之间。太短的情况下,如果交易量过大,很难做到防止重复,太长可读性差、意义也不大。有业务含义: 订单号尽可能包含业务信息,比如订单时间、业务类型等再此基础上如何设计一个优秀的订单号服务呢?这个时候最好梳理一下功能点,需要满足那些要求:
设计目标:满足高并发需求可拓展性高满足峰值压力支持分布式架构检索效率,如果订单号存储在数据库中,检索效率需要考虑设计方案接下来会阐述一些常见的方案,再谈论一下这种解决方案的优缺点。
方案一:数据库自增ID所谓数据库自增,意思是在数据库中给某个列设置为自增列,并且给该列设置一个初始值,代码层面无需任何特殊处理,以 Mysql 的用户表 ID 列为例,可以通过如下方式在创建表的时候生产。
CREATE TABLE `tb_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;优点:实现简单保证唯一性缺点:数据库存在性能瓶颈,特别是高并发情况下分布式数据库,在多个实例情况下难以保证全局唯一、安全性低,自增ID容易暴露信息无业务含义适用场景:如果是单体服务下,并且是初始业务情况下可以使用,但需要对数据库进行分库分表会出现重复ID。不建议直接使用这种方式,上限较低。
方案二:UUIDUUID 是Universally Unique Indentifier的缩写,翻译为通用唯一识别码,顾名思义 UUID 是一个用于记录唯一标识一条的数据,其按照开放软件基金会(OSF)指定的标准进行计算,用到了以太网卡地址(MAC)、纳秒级时间、芯片 ID 码和许多可能的数字。
总的来说,UUID 码由以下三部分组成:
当前日期和时间时钟序列全局唯一的 IEEE 机器识别码(如果有网卡从网卡获得,没有网卡则通过其他方式获得)UUID 的标准形式包含 32 个 16 进制数字,以连字号分为五段,示例:00000191-adc6-4314-8799-5c3d737aa7de。
以java为例,通过以下方式即可生成:
String uuid = UUID.randomUUID().toString();优点:全局唯一性简单易用安全性: UUID的随机性使得它不容易被猜测或预测,增加了数据的安全性无需集中管理:由于UUID是本地生成的,不需要依赖于中心化的ID生成服务,减少了单点故障的风险。可扩展性:UUID的生成过程是分布式的,可以在多个节点上并行生成,适合大规模分布式系统。缺点:长度较长可读性差: 无业务含义,难以理解索引效率低: UUID的随机性会导致插入时的索引分裂和碎片化,从而降低写入性能,查询效率也低排序问题: UUID不具备自然的时间顺序,因此不适合用于需要按时间顺序进行排序的场景适用场景:如果是在分布式系统中,对订单号有着特别高的要求,并且不需要长期持久化存储以及不需要频繁查询那么就可以推荐使用。
分布式文件存储系统,该系统需要处理大量的文件上传、下载和管理操作。这些文件可能来自不同的用户和设备,并且需要在多个服务器之间进行分布存储和访问。为了确保每个文件都有一个唯一的标识符,并且能够在全球范围内保持唯一性,UUID是一个非常适合的选择。
方案三: 雪花算法Snowflake(中文简称:雪花算法) 是 Twitter 内部的一个 ID 生算法,可以通过一些简单的规则保证在大规模分布式情况下生成唯一的 ID 号码。Snowflake把 64-bit分别划分成多段,分开来标识机器ID、时间等。其核心思想是:使用 41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号,最后还有一个符号位,永远是0。SnowFlake 的结构图如下所示:
SnowFlake可以很清晰的看出,Snowflake 由 4个部分组成:
第一部分:bit 值,为未使用的符号位第二部分:由 41 位的时间戳(毫秒)构成,它的取值是当前时间相对于某一时间的偏移第三部分:表示工作机器 id,由服务节点 id 和数据中心 id 组合而成第四部分:表示每个工作机器每毫秒生成的序列号 ID,同一毫秒内最多可生成生产 4095 个 ID。由于在 Java 中 64bit 的整数是 long 类型,因此在 Java 中 SnowFlake 算法生成的 id 就是 long 来存储的。
SnowFlake 算法可以保证:
1.所有生成的 id 按时间趋势递增2.整个分布式系统内不会产生重复id(因为有服务节点 id 和数据中心 id 来做区分)需要注意的是:
在分布式环境中,5 个 bit 位的 datacenter 和 worker 表示最多能部署 31 个数据中心,每个数据中心最多可部署 31 台节点。41 位的二进制长度最多能表示2^41 -1毫秒即 69 年,所以雪花算法最多能正常使用 69 年,为了能最大限度的使用该算法,在使用的时候,应该为其指定一个开始时间,不然会发生重复!优点高并发下的唯一性:由于算法设计考虑到了时间戳、机器标识等因素,因此即使是在大规模分布式系统中也能保证生成的ID是唯一的。趋势递增:生成的ID通常是按照时间顺序递增的,这有利于数据库索引性能优化。信息量丰富:ID中包含了时间戳、工作节点ID等信息,使得每个ID都携带了额外的信息价值。不依赖数据库:相比于传统基于数据库自增ID的方式,雪花算法不需要访问数据库即可生成ID,减少了数据库的压力。高效:计算效率高,适合快速生成大量ID的需求。缺点时钟回拨问题:如果服务器时钟出现回拨,则可能产生重复ID的问题。不过,可以通过等待时钟追赶上来或者拒绝服务来解决这个问题。有限的空间:虽然64位足够大,但理论上还是存在上限,对于某些极端情况可能不够用。可读性差:生成的ID对人类来说不易于阅读和理解。安全性较低:因为包含时间戳,所以可能泄露一些关于数据创建时间的信息,这在某些安全敏感的应用场景下可能是不利的。适用场景分布式系统:特别适用于需要跨多个服务器或数据中心工作的应用程序,如电商网站、社交平台等。高并发应用:当系统需要处理大量的并发请求并要求快速响应时,比如在线游戏、实时消息传递服务等。需要唯一标识符的服务:任何需要生成全局唯一标识符的服务都可以采用此方法,例如订单管理系统、用户账号注册等。大数据分析:由于ID带有时间信息,有助于进行时间序列数据分析。方案四:借助Redis,分布式组件要想在分布式环境下生成一个唯一的订单编号,我们可以通过分布式组件的方式,来帮忙我们生成全局唯一的订单号,例如我们可以采用 redis 分布式缓存组件中的incr命令,来帮我们生成一个全局自增长的序列号。
实现某个Key实现自增的代码如下:
// 基于某个key实现自增长
String res = jedis.get(key);
if (StringUtils.isBlank(res)) {
// 设置初始值,INIT_ID 是初始值
jedisClient.set(key, INIT_ID);
// 设置过期时间,seconds 是多少秒过期
jedisClient.expire(key, seconds);
}
//存在就生成+1的订单号
long orderId = jedis.incr(key);这种方式生成的自增长序列号,非常的快,可以很好的满足大流量环境下的编号要求唯一的特性!
案例分析:在互联网几个大厂的订单号分析一下:
京东商城订单号格式:157444499
苏宁易购订单号格式:2000839647
凡客诚品订单号格式:213052230059
小米订单号格式:1111218032345170
凡客诚品和银泰网订单号都含有 0522,这是因为这 2 张订单都是2013年5月22号下的订单。
基本猜测一下,凡客的订单规则是:业务编码+年的后2位+月+日+订单数;泰网的订单号规则:年的第三位数+业务编码+年的后1位+月+日+订单数;而京东商城和苏宁易购的订单号看不出规则。
再来分析一下小米订单编号:
1211218032345170(16位)
//拆解成为四个部分
1——211218—03234—5170第一部分,1 表示购买,2 表示退货。第二部分,表示 2021 年 12 月 18 日下的单,前面两位省掉了。第三部分,时间戳对应00:53:54,换算成秒是03234秒。最后一部分,表示在同一秒内下的第 5170 单,也就是说,小米认为,在一秒内不会超过一万个订单。总结通过上面的示例演示,下面针对这几种情况做一个分析与总结。尽可能的选择一种合理的方式。
实现方案优势劣势数据库自增代码层面无需任何特殊处理;利用MySQL特点实现数据递增并发性能差;MySQL负担重UUID实现简单、方便;重复性低可读性低;过于冗长;数据库查询效率低雪花算法基于内存、速度快;性能高;不会产生额外的网络开销;数据依次成递增依赖于服务器时间,如变动服务器时间则存在重复的情况Redis基于内存、速度库;使用简单;可分布数据、扩展性强需要独立搭建一套服务、增加了维护成本;跨应用调用、存在网络开销总体上来说,优先选择使用Redis进行分布式的处理方式,如果在没有Redis的情况下,那就优先选用雪花算法。
如果想要设计一个订单号,需要保留业务类型、时间信息等。
比如通过2位数字表示业务类型,如交易订单、支付单、结算单等都是不同的业务类型,可以有不同的编号。 中间的18-20位用一个唯一的ID来表示,可以用雪花算法,也可以用Leaf,总之就是他需要保证唯一性。 最后4位,基于基因法,将分表后的结果获取到,把他也编码到订单号中。