猿创征文|Java 分布式之生成不重复 ID

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 也提供了相应的注解来实现,但我还没有去了解。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十⑧

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值