如何设计一个订单号生成服务?

如何设计要给订单号生成服务

一、数据量的大小

在设计订单号的生成服务时候,我们首先要考虑的就是数据量的大小,从而根据数据量的大小来设计出多少位数的ID,以免出现位数少,造成存储不下的问题,也避免了位置大从而影响索引的检索效率问题。

二、有意义的ID

为了方便排查问题,和易于理解和记忆,需要充分的考虑到订单号的格式和组成方式,例如使用时间戳、随机数、用户ID等信息来构造订单号

三、高可用

订单号生成服务需要保证高可用,利用多节点部署、负载均衡、健康检查等技术来提高可靠性和稳定性。作为其他服务的基础服务,必须做到高可用,否则就会阻塞其他的服务。

四、高性能

在TOB的场景下,接口的性能是至关重要的,而生成ID的服务是其他服务的基础服务,如果当前服务性能比较低,就会影响其他服务的性能效率的问题,所以可以采用内存缓存、异步处理等技术来优化性能。

五、唯一性

订单号必须保证唯一性,否则会出现订单冲突和数据不一致等问题。可以使用一些常见的唯一性生成算法,例如UUID、Snowflake、数据库生成、Redis等。

雪花算法(Snowflake)雪由Twitter研发的的一种分布式ID生成算法,它可以生成全局唯一且递增的ID。它的核心思想是将一个64位的ID划分成多个部分,每个部分都有不同的含义,包括时间戳、数据中心标识、机器标识和序列号等。

具体来说,雪花算法生成的ID由以下几个部分组成:

  1. 符号位(1bit):预留的符号位,始终为0,占用1位。
  2. 时间戳(41bit):精确到毫秒级别,41位的时间戳可以容纳的毫秒数是2的41次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000,算下来可以使用69年。
  3. 数据中心标识(5bit):可以用来区分不同的数据中心。
  4. 机器标识(5bit):可以用来区分不同的机器。
  5. 序列号(12bit):可以生成4096个不同的序列号。
alt

雪花算法的优势

  • 首先,时间戳位于ID的最高位,保证新生成的ID比旧的ID大,在不同的毫秒内,时间戳肯定不一样。

  • 其次,引入数据中心标识和机器标识,这两个标识位都是可以手动配置的,帮助业务来保证不同的数据中心和机器能生成不同的ID。

  • 还有就是,引入序列号,用来解决同一毫秒内多次生成ID的问题,每次生成ID时序列号都会自增,因此不同的ID在序列号上有区别。

所以,基于时间戳+数据中心标识+机器标识+序列号,就保证了在不同进程中主键的不重复,在相同进程中主键的有序性。

  • 高性能高可用:生成时不依赖于数据库,完全在内存中生成

  • 高吞吐:每秒钟能生成数百万的自增 ID

  • ID 自增:在单个进程中,生成的ID是自增的,可以用作数据库主键做范围查询。但是需要注意的是,在集群中是没办法保证一定顺序递增的。

雪花算法的劣势

  • 某个节点出现故障了,就需要改其对应机器ID或者数据中心的Id,也就需要重新部署系统。

  • 还有就是如果某个机器部署的时候出现了一摸一样的机器ID和数据中心的ID那么就有可能出现重复的ID,这样就会导致系统的错误和异常。

  • 依赖于系统的时间一致性,如果系统的时间被回拨,或者不一致,可能就会造成ID的重复

时间回拨是指系统在运行过程中,可能由于网络时间校准或者人工设置,导致系统时间主动或被动地跳回到过去的某个时间 解决方案:一旦发生这种情况,简单粗暴的做法是抛异常,发现时钟回调了,就直接抛异常出来。另外还有一种做法就是发现时钟变小了,就拒绝ID生成请求,等到时钟恢复到上一次的ID生成时间点后,再开始生成新的ID。

  • 需要Zookeeper来协调个节点的ID生成,但是ZK的部署其实是挺大的成本的,并且Zookeeper本身也可能是系统的瓶颈。

六、包含分库分表的业务字段

订单系统到最后都可能会考虑分库分表,所以在最初设计订单号的时候,需要考虑将和分表有关的字段编码到订单号中,如买家ID等。

七、实战

1) Redis

通过 Redis 的 incr 命令即可实现对 id 原子顺序递增,例如:

127.0.0.1:6379> incr sequence_id_biz_type

为了提高可用性和并发,我们可以使用 Redis Cluster。

不过,我们也知道,即使 Redis 开启了持久化,不管是快照(snapshotting, RDB)、只追加文件(append-only file, AOF)还是 RDB 和 AOF 的混合持久化依 然存在着丢失数据的可能,那就意味着产生的 ID 存在着重复的概率

2) Leaf-segment 数据库生成

创建数据库

字段名称 数据类型 备注
biz_tag varchar(128) 业务标签
max_id bigint 使用过的最大ID
step int 步长(没次生成的ID个数)
description varchar 描述
update_time timestamp 更新时间

原 MySQL 方案每次获取 ID 都得读写一次数据库,造成数据库压力大。改为 批量获取,每次获取一个 segment(step 决定大小)号段的值。用完之后再去数据 库获取新的号段,可以大大的减轻数据库的压力。

各个业务不同的发号需求用 biz_tag 字段来区分,每个 biz-tag 的 ID 获取相互 隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复 杂的扩容操作,只需要对 biz_tag 分库分表就行。

重要字段说明:biz_tag 用来区分业务,max_id 表示该 biz_tag 目前所被分配 的 ID 号段的最大值,step 表示每次分配的号段长度。原来获取 ID 每次都需要写数据库,现在只需要把 step 设置得足够大,比如 1000。那么只有当 1000 个号被 消耗完了之后才会去重新读写一次数据库。读写数据库的频率从 1 减小到了 1/step。

优点

  • Leaf 服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID 号码是趋势递增的 8byte 的 64 位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 Leaf 仍能正常对外提供服务。
  • 可以自定义 max_id 的大小,非常方便业务从原有的 ID 方式上迁移过来。

缺点

  • ID 号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999 数据波动大,当号段使用完之后还是会在获取新号段时在更新数据库的 I/O 依然会存在着等待,tg999 数据会出现偶尔的尖刺。

为此,希望 DB 取号段的过程能够做到无阻塞,不需要在 DB 取号段的时候 阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。 而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系 统的 TP999 指标。

  • DB 宕机会造成整个系统不可用。

3)Leaf-snowflake方案

Leaf-segment 方案可以生成趋势递增的 ID,同时 ID 号是可计算的,不适用 于订单 ID 生成场景,比如竞对在两天中午 12 点分别下单,通过订单 id 号相减 就能大致计算出公司一天的订单量,这个是不能忍受的。面对这一问题,美团提 供了 Leaf-snowflake 方案。

Leaf-snowflake 方案完全沿用 snowflake 方案的 bit 位设计,即是“1+41+10+12” 的方式组装 ID 号。对于 workerID 的分配,当服务集群数量较小的情况下,完全 可以手动配置。Leaf 服务规模较大,动手配置成本太高。所以使用 Zookeeper 持 久顺序节点的特性自动对 snowflake 节点配置 wokerID。Leaf-snowflake 是按照下 面几个步骤启动的:

启动 Leaf-snowflake 服务,连接 Zookeeper,在 leaf_forever 父节点下检查自 己是否已经注册过(是否有该顺序子节点)。

如果有注册过直接取回自己的 workerID(zk 顺序节点生成的 int 类型 ID 号), 启动服务。

如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取 回顺序号当做自己的 workerID 号,启动服务。

4) UUID

public static void main(String[] args) {
    
    
        for (int i = 0; i < 5; i++) {
            String rawUUID = UUID.randomUUID().toString();
            System.out.println(rawUUID);
            //去除"-"
            String uuid = rawUUID.replaceAll("-""");
            System.out.println(uuid);
        }
    }

本文由 mdnice 多平台发布

猜你喜欢

转载自blog.csdn.net/weixin_46350527/article/details/132444411
今日推荐