Java 分布式之生成不重复 ID
- 在分布式系统中,如何保证多个机器生成的 ID 不重复呢 ?
数据库设置 ID 自增
-
MySql 数据库支持主键 id 自增。
-
Oracle 数据库不支持主键 id 自增,但可以用。
-
具体的操作可以看文档上一篇文章:配置数据库实现 ID 自增
-
优点:方便。快捷。
-
缺点:只适合数据库设置。
生成 UUID
-
生成的 ID 是:一组
32 位数的 16 进制数字
加4 个 '-'
所构成的字符串
。 -
使用 Java 自带 API 来创建。
-
全球唯一(重复率极低)。
-
32 个十六进制数占 128 bit,而一个’-‘符号在 UTF-8 编码中占 8 bit,一个’-'符号在 UTF-16 编码中占 16 bit。
-
优点
:简单,代码方便,生成ID性能非常好,基本不会有性能问题。 -
缺点
:没有排序,无法保证趋势递增。 -
UUID 往往是使用字符串存储,查询的效率比较低。
-
存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
-
传输数据量大。
-
不可读。
-
结构
:xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx。- 用 x 表示的字符没有特殊含义
- M 表示的十六进制数是用来表示 UUID 版本的,可以取值 1,2,3,4,5;当前规范有 5 个版本
- N 表示的十六进制数是用来表示 UUID 变体(variant),其二进制表示中有两位固定了:10xx;所以可以取值 8,9,a,b
public class GenerateId {
public static String uuId() {
UUID uuid = UUID.randomUUID();
return uuid.toString();
}
}
雪花算法生成 ID
-
生成的 ID 是:
64 bit 的纯数字串
。 -
自行创建方法来创建。
-
69 年内不重复。
-
64 bit。
-
优点
:毫秒数在高位,自增序列在低位,整个ID都是趋势递增的(根据自身业务特性分配bit位,非常灵活)。 -
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
-
缺点
:强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。 -
结构
:- 第一部分:1 bit,无意义,固定为 0。二进制中最高位是符号位,1 表示负数,0 表示正数。用来表示 ID 都是正整数,所以固定为 0。
- 第二部分:41 个 bit,表示时间戳,精确到毫秒,可以使用 69 年。时间戳带有自增属性。
- 第三部分:10 个 bit,表示 10 位的机器标识,最多支持 1024 个节点。此部分也可拆分成 5 位 datacenterId 和 5 位 workerId。datacenterId 表示机房 ID,workerId 表示机器 ID。
- 第四部分:12 个 bit,表示序列化,即一些列的自增 ID,可以支持同一节点同一毫秒生成最多 4096 个 ID 序号。
public class SnowFlake {
/**
* 起始的时间戳(可设置当前时间之前的邻近时间)
* <p>
* 1662266227517L(2022/09/04 12:37:07)
*/
private final static long START_STAMP = 1662266227517L;
/**
* 序列号占用的位数(第四部分)
*/
private final static long SEQUENCE_BIT = 12;
/**
* 机器标识占用的位数(第三部分之一)
*/
private final static long MACHINE_BIT = 5;
/**
* 数据中心占用的位数(第三部分之二)
*/
private final static long DATA_CENTER_BIT = 5;
/**
* 第三部分的最大值(31D)
*/
private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
/**
* 第四部分的最大值(4095D)
*/
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
/**
* 第三部分需要向左移位的位移(12)及(12 + 5)
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
/**
* 第二部分需要向左移位的位移(12 + 5 + 5)
*/
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
/**
* 数据中心 ID(0~31),5 bit
*/
private final long dataCenterId;
/**
* 工作机器 ID(0~31),5 bit
*/
private final long machineId;
/**
* 相同毫秒内需要自增的序列号(0~4095),12 bit
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastStamp = -1L;
/**
* 初始化 类,填入 数据中心 ID、工作机器 ID
*
* @param dataCenterId 数据中心 ID
* @param machineId 工作机器 ID
*/
public SnowFlake(long dataCenterId, long machineId) {
if(dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_NUM) {
throw new IllegalArgumentException("dataCenterId can't be " +
"greater than MAX_DATA_CENTER_NUM " +
"or less than 0");
}
if(machineId < 0 || machineId > MAX_MACHINE_NUM) {
throw new IllegalArgumentException("machineId can't be " +
"greater than MAX_MACHINE_NUM or less than 0");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
/**
* 生成 下一个 ID
* <p>
* 添加了 同步锁
*
* @return ID
*/
public synchronized long nextId() {
long currStamp = getCurrentTimestamp();
if(currStamp < lastStamp) {
throw new RuntimeException("Clock moved backwards.Refusing to generate id.");
}
// 确定好 时间戳 和 序列号
if(currStamp == lastStamp) {
// 相同服务器在相同毫秒内创建 ID 时,序列号自增
// 相同服务器:表示 第三部分(数据中心 ID、工作机器 ID)相同
// 相同毫秒:表示 第二部分(时间戳)相同
// 所以需要用 第四部分 来区分 ID,设置为 自增
sequence = (sequence + 1) & MAX_SEQUENCE;
if(sequence == 0L) {
// 序列号达到最大了,就阻塞到下一个毫秒,获得新的时间戳
currStamp = getNextTimestamp();
}
} else {
// 不同的毫秒时,表示是 某个毫秒的第一个 ID
// 所以 序列号置为零
sequence = 0L;
}
// 更新 上一次生成 ID 的时间戳
lastStamp = currStamp;
// 通过 移位、和或运算符 将四个部分拼接到一起,组成 64 bit的 ID
// 就是把 第一、二、三部分的有效数值移动 n 位,然后用 '或' 运算进行拼接
// 或运算(|)是用于二进制数值间运算的,相同位均为 0,结果位才为 0,否则为 1。
return (currStamp - START_STAMP) << TIMESTAMP_LEFT
| dataCenterId << DATA_CENTER_LEFT | machineId << MACHINE_LEFT
| sequence;
}
/**
* 获取 下一个 ID 的时间戳
* <p>
* 下一个 ID 的时间戳一定要 大于等于 上一个 ID 的时间戳
*
* @return 可用的时间戳
*/
private long getNextTimestamp() {
long stamp = getCurrentTimestamp();
while(stamp <= lastStamp) {
stamp = getCurrentTimestamp();
}
return stamp;
}
/**
* 获取 当前时间戳
*
* @return 当前时间戳
*/
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
}
其他
- MyBatisPlus 和 JPA 也提供了相应的注解来实现,但我还没有去了解。