MyBatis-Plus 使用雪花算法生成分布式 ID

前言

本文将介绍 MyBatis-Plus 如何使用雪花算法(Snowflake)生成分布式全局唯一 ID,并解决 ID 冲突问题。

版本说明

组件版本
MyBatis-Plus3.3.1

雪花算法的介绍

概述

Snowflake 算法来源于 Twitter,使用 Scala 语言实现,利用 Thrift 框架实现 RPC 接口调用,最初的项目起因是数据库从 MySQL 迁移到 Cassandra,而 Cassandra 没有现成可用的 ID 生成机制,就催生了该算法。Snowflake 的特性如下:

  • Snowflake 能够按照时间有序生成 ID
  • 经测试 Snowflake 每秒能够产生 26 万个自增可排序 ID
  • 分布式系统内不会产生 ID 碰撞(由 dataCenterIdworkerId 做区分),并且生成效率较高
  • Snowflake 算法生成 ID 的结果是一个 64 bit 大小的整数,刚好为一个 Long 型,转换成字符串后长度最多是 19

结构

Snowflake 算法的特性是有序、全局唯一、高性能、低延迟(响应时间在 2ms 以内),可在分布式环境(多集群,跨机房)下使用,因此使用 Snowflake 算法得到的 ID 是分段组成的:

  • 第 1 位不使用,因为二进制里第一个位如果是 1,那么都是负数,但往往生成的 ID 都是正数,所以第一个位统一都是 0
  • 与指定日期(1970-01-01 00:00:00)的时间差(毫秒级),41 位,够用 69 年
  • 数据中心 ID + 工作机器 ID,一共 10 位,包括 5 位 dataCenterId 和 5 位 workerId,最多支持 32 个数据中心(机房),每个数据中心 32 台机器,一共是 1024 台机器
  • 序列号,12 位,每台机器每毫秒内最多产生 4096 个序列号

snowflake-1

  • 1bit:符号位,固定是 0,表示全部 ID 都是正整数
  • 41bit:时间戳(毫秒数时间差),从指定的日期算起,够用 69 年,用 Long 类型表示的时间戳是从 1970-01-01 00:00:00 开始算起的
  • 10bit:数据中心 ID + 工作机器 ID,有异地部署、多数据中心(机房)的也可以配置,需要线下规划好各地数据中心(机房),各机器实例 ID 的编号
  • 12bit:序列号,一共 12 位,前面所有位都相同的话,每台机器每毫秒内最多产生 4096 个序列号

优缺点

优点:

  • 可以根据自身业务特性分配 bit 位,非常灵活
  • 毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的
  • 不依赖数据库等三方系统,以服务的方式部署,稳定性更高,生成 ID 的效率也是非常高,低延迟

缺点:

  • 强依赖机器时钟,如果机器的时钟回拨了,会导致生成重复的 ID
  • 若生成环境中使用了容器化技术(比如 Docker、K8s),实例的个数随时有变化,那么 Snowflake 需要一定的改造才能更好地应用到生产环境中
  • 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步(如时钟回拨),有时候可能会出现不是全局递增的情况(此缺点可认为无所谓,一般分布式 ID 只是要求趋势递增,并不会严格要求递增,90% 的业务需求都只需要趋势递增)

适用场景:

  • 分布式应用环境的数据主键生成

雪花算法的使用

  • 当使用 @TableId(type = IdType.ASSIGN_ID) 时,MyBatis-Plus 会自动使用其内置的雪花算法(com.baomidou.mybatisplus.core.toolkit.Sequence)生成主键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@TableName("t_order")
@Data
public class Order {

/**
* 不依赖数据库自增主键,插入数据时由 MyBatis-Plus 自动生成主键
* 默认使用雪花算法(Snowflake)生成 64 位的长整型 ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;

private String orderNo;

private Long userId;

private BigDecimal amount;

}
  • 在 MyBatis-Plus 中,Snowflake 算法生成的 ID 是一个 64 位长整型(| 1 bit 符号位 | 41 bit 时间戳 | 5 bit 数据中心 ID | 5 bit 工作机器 ID | 12 bit 序列号 |),其默认的 Snowflake 参数为:
参数默认值说明
dataCenterId1数据中心 ID(0 ~ 31)
workerId1工作机器 ID(0 ~ 31)

ID 冲突问题

  • 在 MyBatis-Plus 中,Snowflake 参数的默认值在分布式部署时可能会出现 ID 冲突,因此通常需要自定义 Snowflake 参数(包括 dataCenterIdworkerId),避免 ID 冲突的发生

ID 冲突解决方案

在 MyBatis-Plus 中,Snowflake 参数的默认值在分布式部署时可能会出现 ID 冲突,因此通常需要自定义 Snowflake 参数(包括 dataCenterIdworkerId),避免 ID 冲突的发生。

硬编码 Snowflake 参数

  • 通过定义一个 Bean 来覆盖 MyBatis-Plus 默认的 IdentifierGenerator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Sequence;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SnowflakeConfig {

@Bean
public IdentifierGenerator idGenerator() {
return new IdentifierGenerator() {

// 硬编码 Snowflake 参数,workerId 为 2,dataCenterId 为 3
private final Sequence sequence = new Sequence(2, 3);

@Override
public Number nextId(Object entity) {
return sequence.nextId();
}

};
}
}

通过配置文件 Snowflake 参数

  • application.yml 配置文件中,添加以下配置内容
1
2
3
snowflake:
workerId: 2
dataCenterId: 3
  • 通过定义一个 Bean 来覆盖 MyBatis-Plus 默认的 IdentifierGenerator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Sequence;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "snowflake")
public class SnowflakeConfig {

private long workerId;
private long dataCenterId;

@Bean
public IdentifierGenerator idGenerator() {
return new IdentifierGenerator() {

// 自定义 Snowflake 参数
private final Sequence sequence = new Sequence(workerId, dataCenterId);

@Override
public Number nextId(Object entity) {
return sequence.nextId();
}

};
}

}

动态计算得到 Snowflake 参数

  • 通过定义一个 Bean 来覆盖 MyBatis-Plus 默认的 IdentifierGenerator,Snowflake 参数都是动态计算得到的,适用于 Docker / Kubernetes 环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Sequence;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.net.InetAddress;
import java.net.NetworkInterface;

@Configuration
public class SnowflakeConfig {

@Bean
public IdentifierGenerator identifierGenerator() {
return new IdentifierGenerator() {

// 动态获取 Snowflake 参数
private final Sequence sequence = new Sequence(getWorkerId(), getDatacenterId());

@Override
public Number nextId(Object entity) {
return sequence.nextId();
}

};
}

/**
* 获取工作机器 ID(0~31)
*/
private long getWorkerId() {
try {
// 根据 IP 或容器主机名计算工作机器 ID(0 ~ 31)
String hostAddress = InetAddress.getLocalHost().getHostAddress();
return (hostAddress.hashCode() & 31);
} catch (Exception e) {
// 异常情况返回默认值 0
return 0;
}
}

/**
* 获取数据中心 ID(0~31)
*/
private long getDatacenterId() {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
NetworkInterface ni = NetworkInterface.getByInetAddress(inetAddress);

if (ni == null) {
// 返回默认值 0
return 0;
}

byte[] mac = ni.getHardwareAddress();
if (mac == null || mac.length == 0) {
// 返回默认值 0
return 0;
}

// 根据 MAC 地址计算数据中心ID(0 ~ 31)
return (mac[mac.length - 1] & 31);
} catch (Exception e) {
// 异常情况返回默认值 0
return 0;
}
}

}

从 Redis 获取 Snowflake 参数

  • 从 Redis 或者数据库中获取 Snowflake 参数(大公司常用),确保在分布式环境(Docker、Kubernetes、集群)下数据中心 ID(dataCenterId)和工作机器 ID(workerId)都不会重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Sequence;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class SnowflakeConfig {

private final StringRedisTemplate redisTemplate;

public SnowflakeConfig(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

@Bean
public IdentifierGenerator identifierGenerator() {
return new IdentifierGenerator() {

// 从 Redis 动态获取 Snowflake 参数
private final Sequence sequence = new Sequence(getWorkerIdFromRedis(), getDatacenterIdFromRedis());

@Override
public Number nextId(Object entity) {
return sequence.nextId();
}

};
}

/**
* 使用 Redis 分配工作机器 ID
*/
private long getWorkerIdFromRedis() {
try {
Long value = redisTemplate.opsForValue().increment("snowflake:workerId");
if (value == null) {
// 返回默认值 0
return 0;
}
// 限制工作机器 ID 为 0~31
return value % 32;
} catch (Exception e) {
// 异常情况返回默认值 0
return 0;
}
}

/**
* 使用 Redis 分配数据中心 ID
*/
private long getDatacenterIdFromRedis() {
try {
Long value = redisTemplate.opsForValue().increment("snowflake:dataCenterId");
if (value == null) {
// 返回默认值 0
return 0;
}
// 限制数据中心 ID 为 0~31
return value % 32;
} catch (Exception e) {
// 异常情况返回默认值 0
return 0;
}
}

}

参考资料