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 测试结果
2.1.3 优点
- 生成足够简单,本地生成无网络消耗,具有唯一性
- 性能好,没有高可用风险
2.1.4 缺点
- 无序不可读,查询效率低
- 长度过长,存储以及查询对MySQL的性能消耗较大,MySQL官方明确建议主键要尽量越短越好,作为数据库主键 UUID 的无序性会导致数据位置频繁变动,严重影响MYSQL性能。
2.2 基于Redis生成id
利用redis的 incr命令实现ID的原子性自增。
2.2.1 测试及结果
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
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重复导致主键冲突的情况。另外分布式系统中,各节点的时间并不能保证完全同步,所以有可能出现分布式系统中不能出现完全递增的情况。