基于Redis实现分布式全局唯一ID生成器的一种可靠方案

为什么需要全局唯一ID

传统的单体架构的时候,我们基本是单库然后业务单表的结构。每个业务表的ID一般我们都是从1增,通过AUTO_INCREMENT=1设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据。这种情况根据数据库的自增ID就会产生相同ID的情况,不能保证主键的唯一性。

分布式系统中生成全局id方法由很多,我们选择的时候也比较纠结。每种方式都有各自的使用场景,如果我们熟悉各种方式及优缺点,结合自身的业务,使用的时候才能更好的选择。下面先看一下几种常见的:UUID、数据库预取、雪花算法、Redis自增等等

PS:后续补充下,目前业界已经有比较多成熟的解决方案,比如:百度UidGenerato美团Leaf等,功能都非常强大,可以直接拿来适用或者借鉴!!

常见的ID生成策略

  • UUID

    UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID是由一组32位数的16进制数字所构成,所以UUID理论上的总数为 16^32=2^128,约等于 3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。

    优点:本地生成ID,不需要进行远程调用,时延低,性能高。

    缺点:

    • UUID过长,很多场景不适用,比如用UUID做数据库索引字段。
    • 没有排序,无法保证趋势递增
  • 数据库生成

    专门建一张表,用来生成id,每台服务器每次提前生成一批订单号放在内存中,每次使用的时候取一个,当库里没有了,再生成一批

    优点:

    • 简单,能够保证唯一性

    缺点:

    • 可用性难以保证,强依赖DB,当DB异常时整个系统不可用。虽然配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号
    • 不能保证插入时间和id一样完全一致递增,难以满足部分按时间排序需求
  • 雪花算法-Snowflake

    Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。而 Java中64bit的整数是Long类型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 来存储的。

    • 第1位占用1bit,其值始终是0,可看做是符号位不使用。
    • 第2位开始的41位是时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么雪花算法可用的时间年限是(1L<<41)/(1000L360024*365)=69 年的时间。
    • 中间的10-bit位可表示机器数,即2^10 = 1024台机器,但是一般情况下我们不会部署这么台机器。如果我们对IDC(互联网数据中心)有需求,还可以将 10-bit 分 5-bit 给 IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,具体的划分可以根据自身需求定义。
    • 最后12-bit位是自增序列,可表示2^12 = 4096个数。

    优点:

    • 时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序
    • 性能高,在一毫秒一个数据中心的一台机器上可产生4096个有序的不重复的ID
    • 可以根据自身业务需求灵活调整bit位划分,满足不同需求

    缺点

    • 依赖机器时钟,如果机器时钟回拨,会导致重复ID生成
    • 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不时全局递增的情况
  • 使用redis实现

    Redis实现分布式唯一ID主要是通过提供像 INCRINCRBY 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。

    优点:

    • 性能比较高
    • 生成的数据是绝对有序的,可以方便业务实现根据id排序
    • 可以根据自身业务需求灵活调整bit位划分,满足不同需求

    缺点

    • 依赖于redis,需要系统引进redis,增加了系统的复杂性
    • 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候会出现不时全局递增的情况

最终采用方案

因为业务上需要id按照插入时间严格递增,并且尽量保持连续,对于性能也需要有一定保证,刚好我们系统也需要Redis,最终确定Redis方案,另外,为了防止Redis宕机或者系统重启出现id重复,每达到一定步长,会异步将当前值记录到MySQL中。下边时id设置及记录表结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE id_sequnce  (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`key` varchar(20) NOT NULL COMMENT '序列唯一key',
`start_id` bigint(20) NULL DEFAULT NULL COMMENT 'id起始值,创建后从当前id生成',
`max_id` bigint(20) NULL DEFAULT NULL COMMENT '最大id,达到最大值后,从startid重新生成',
`step` bigint(20) NULL DEFAULT NULL COMMENT '步长',
`write_step` bigint(20) NULL DEFAULT 2000 COMMENT '记录步长,每间隔此长度,异步更新current_id',
`current_id` bigint(20) NULL DEFAULT NULL COMMENT '当前id,记录上次获取的id长度',
`is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0:未删除 1:删除',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `IDX_KEY`(`key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
sequenceDiagram
autonumber
服务器->>Redis: 使用INCRBY实现指定步长自增
服务器->>MySQL: 每间隔write_step,异步更新current_id
Redis->>MySQL: Redis宕机或服务重启,获取current_id+write_step,防止id重复

Redis使用Cluster集群方式搭建

MySQL部署使用MHA部署