1. 简介

1.1 出现原因

在互联网的业务系统中,涉及到各种各样的ID,如在支付系统中就会有支付ID、退款ID等。那一般生成ID都有哪些解决方案呢?特别是在复杂的分布式系统业务场景中,我们应该采用哪种适合自己的解决方案是十分重要的。另外,如果使用数据库分片技术,将一个数据库进行拆分,通过数据库中间件连接。如果数据库中该表选用ID自增策略,则可能产生重复的ID,此时应该使用分布式ID生成策略来生成ID。

1.2 分布式ID的特性

  • 唯一性:必须保证ID是全局性唯一的,基本要求
  • 有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
  • 高性能性:高可用低延时,ID生成响应要快,否则反倒会成为业务瓶颈
  • 高可用性:确保任何时候都能正确的生成ID。
  • 带时间:ID里面包含时间,一眼扫过去就知道哪天的交易。

2. 分布式ID的生成方案

2.1 UUID

UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡MAC地址)等数据计算生成的。UUID可以被任何人独立创建,并按需发布。

2.1.1 测试

/**
 * @author King
 * @version 1.0
 * @description TODO
 * @date 2023/8/23 11:16
 */
public class UUID {
    public static void main(String[] args) {
        String uuid = randomUUID().toString().replace("-", "");
        System.out.println(uuid);
    }
}

2.1.2 测试结果

java自增单号生成_java自增单号生成

2.1.3 优点

  • 生成足够简单,本地生成无网络消耗,具有唯一性
  • 性能好,没有高可用风险

2.1.4 缺点

  • 无序不可读,查询效率低
  • 长度过长,存储以及查询对MySQL的性能消耗较大,MySQL官方明确建议主键要尽量越短越好,作为数据库主键 UUID 的无序性会导致数据位置频繁变动,严重影响MYSQL性能。

2.2 基于Redis生成id

利用redis的 incr命令实现ID的原子性自增。

2.2.1 测试及结果

java自增单号生成_笔记_02

2.2.2 优点

  • 不依赖于数据库,灵活方便,且性能优于数据库
  • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

2.2.3 缺点

  • 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作量比较大。
  • 考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台 Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。各个 Redis 生成的 ID 为:
No1:1, 6, 11, 16, 21
No2:2, 7, 12, 17, 22
No3:3, 8, 13, 18, 23
No4:4, 9, 14, 19, 24
No5:5, 10, 15, 20, 25

随便负载到哪个机确定好,未来很难做修改。步长和初始值一定需要事先确定。使用 Redis 集群也可以解决单点故障的问题。

  • 另外,比较适合使用 Redis 来生成每天从0开始的流水号。比如订单号 = 日期 + 当日自增长号。可以每天在 Redis 中生成一个 Key ,使用 INCR 进行累加。

2.2.4 注意

用redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
  • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

2.3 基于数据库自增的ID

使用数据库的id自增策略,如 MySQL 的 auto_increment。

2.3.1 使用

创建一张单独的表用来生成id,id设为 NOT NULL auto_increment

CREATE TABLE test (
    id bigint(24) unsigned NOT NULL auto_increment, 
    value char(5) NOT NULL default ''
	) ENGINE=MyISAM;

当我们需要一个ID的时候,向表中插入一条记录返回主键ID

insert into test(value) VALUES ('1');

2.3.2 优点

  • 简单
  • 数据库生成的ID绝对有序

2.3.3 缺点

  • DB单点存在宕机风险
  • 高并发数据库容易宕机
  • 有性能瓶颈

2.4 基于雪花算法(Snowflake)模式(推荐)

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0

java自增单号生成_学习_03

2.4.1 使用

mybatis-plus

在实体类中的id上加入如下配置,指定类型为id_worker

@TableId(value = "id",type = IdType.ID_WORKER)
private Long id;

在application.yml文件中配置数据中心id和机器id

mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: 
  global-config:
    datacenter-id: 1 # datacenter-id:数据中心id(取值范围:0-31) 
    workerId: 1 # workerId:机器id(取值范围:0-31)

2.4.2 优点

  • 高效,每秒可生产数十万id
  • 所有id是根据时间递增的,无法根据其计算出业务量,避免被爬虫遍历数据。
  • 在Java中生成的id是long类型的有序整数,相较uuid作为数据库的主键,更易用于索引搜索和排序,对数据库(MySQL B-Tree)友好

2.4.3 缺点

  • id的时间戳部分只能表示69年,不过一般一个系统也很难超过这个限制。
    递增的,无法根据其计算出业务量,避免被爬虫遍历数据。
  • 在Java中生成的id是long类型的有序整数,相较uuid作为数据库的主键,更易用于索引搜索和排序,对数据库(MySQL B-Tree)友好

2.4.3 缺点

  • id的时间戳部分只能表示69年,不过一般一个系统也很难超过这个限制。
  • 雪花算法依赖系统的时间一致性,如果系统时间被回调,可能会出现id重复导致主键冲突的情况。另外分布式系统中,各节点的时间并不能保证完全同步,所以有可能出现分布式系统中不能出现完全递增的情况。