文章目录
- 为什么需要分布式全局唯一Id
- Id生成规则部分硬性要求
- Id生成系统的可用性要求
- 为什么不用UUID
- 生成分布式雪花Id
- POM
- 代码示例
- API
- 生成18位雪花Id
- 生成13位雪花Id
为什么需要分布式全局唯一Id
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。
如在美团点评的金融、支付、餐饮、酒店;猫眼电影等产品的系统中数据日渐增长,对数据分库分表后需要有一个唯一Id来标识一条数据或消息;特别一点的如订单、骑手、优惠券也都需要有唯一Id坐标时。
此时一个能够生成全局唯一Id的系统是非常必要的。
Id生成规则部分硬性要求
- 全局唯一
- 趋势递增:在Mysql的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据结构来存储索引数据,在主键的选择上-面我们应该尽量使用有序的主键来保证写入性能;
- 单调递增:尽量保证下一个Id一定大于上一个Id,例如事务版本号、IM增量消息、排序等特殊需求;
- 信息安全:如果Id是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量.所以在一些应用场景下,需要Id无规则不规则,让竞争对手不好猜;
- 含时间戳:这样就能够在开发中快速了解这个分布式Id的生成时间。
Id生成系统的可用性要求
- 高可用:发一个获取分布式Id的请求,服务器就要保证99.999%的情况下给我创建一个唯一分布式Id;
- 低延迟:发一个获取分布式Id的请求,服务器要快,极速;
- 高QPS:例如并发一口气10万个创建分布式Id请求同时杀过来,服务器要顶得住且一下子成功创建10万个分布式Id。
为什么不用UUID
在《阿里巴巴 Java 开发手册》第五章 MySQL 规定第九条中,强制规定了单表的主键 id 必须为无符号的 bigint 类型,且是自增的。MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。
生成分布式雪花Id
POM
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.6</version></dependency>
代码示例
@Testpublic void test(){System.out.println(IdUtil.getSnowflakeNextId());}
API
package cn.hutool.core.util;import cn.hutool.core.exceptions.UtilException;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.ObjectId;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.lang.id.NanoId;
import cn.hutool.core.net.NetUtil;public class IdUtil {public IdUtil() {}public static String randomUUID() {return UUID.randomUUID().toString();}public static String simpleUUID() {return UUID.randomUUID().toString(true);}public static String fastUUID() {return UUID.fastUUID().toString();}public static String fastSimpleUUID() {return UUID.fastUUID().toString(true);}public static String objectId() {return ObjectId.next();}/** @deprecated */@Deprecatedpublic static Snowflake createSnowflake(long workerId, long datacenterId) {return new Snowflake(workerId, datacenterId);}public static Snowflake getSnowflake(long workerId, long datacenterId) {return (Snowflake)Singleton.get(Snowflake.class, new Object[]{workerId, datacenterId});}public static Snowflake getSnowflake(long workerId) {return (Snowflake)Singleton.get(Snowflake.class, new Object[]{workerId});}public static Snowflake getSnowflake() {return (Snowflake)Singleton.get(Snowflake.class, new Object[0]);}public static long getDataCenterId(long maxDatacenterId) {Assert.isTrue(maxDatacenterId > 0L, "maxDatacenterId must be > 0", new Object[0]);if (maxDatacenterId == Long.MAX_VALUE) {--maxDatacenterId;}long id = 1L;byte[] mac = null;try {mac = NetUtil.getLocalHardwareAddress();} catch (UtilException var6) {}if (null != mac) {id = (255L & (long)mac[mac.length - 2] | 65280L & (long)mac[mac.length - 1] << 8) >> 6;id %= maxDatacenterId + 1L;}return id;}public static long getWorkerId(long datacenterId, long maxWorkerId) {StringBuilder mpid = new StringBuilder();mpid.append(datacenterId);try {mpid.append(RuntimeUtil.getPid());} catch (UtilException var6) {}return (long)(mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);}public static String nanoId() {return NanoId.randomNanoId();}public static String nanoId(int size) {return NanoId.randomNanoId(size);}public static long getSnowflakeNextId() {return getSnowflake().nextId();}public static String getSnowflakeNextIdStr() {return getSnowflake().nextIdStr();}
}
生成18位雪花Id
public class SnowFlake {// 起始的时间戳,这个时间戳可以是你的系统初始时间,一般取当前时间戳private final static long START_TIMESTAMP = 1672502400000L; // 2023-01-01 00:00:00// 每一部分占用的位数,可以根据自己的需求进行调整,这里是按照默认的占位数进行分配private final static long SEQUENCE_BIT = 12; // 序列号占用的位数private final static long MACHINE_BIT = 5; // 机器标识占用的位数private final static long DATA_CENTER_BIT = 5; // 数据中心占用的位数// 每一部分的最大值,可以根据占用的位数进行计算得到private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);// 每一部分向左的位移,计算出来的值是为了后面生成 ID 做准备private final static long MACHINE_LEFT = SEQUENCE_BIT;private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;private long dataCenterId; // 数据中心 IDprivate long machineId; // 机器 IDprivate long sequence = 0L; // 序列号private long lastTimeStamp = -1L; // 上一次时间戳/*** <h2>构造方法</h2>** @param dataCenterId 数据中心 ID* @param machineId 机器 ID*/public SnowFlake(long dataCenterId, long machineId) {if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {throw new IllegalArgumentException("数据中心标识不能大于等于 " + MAX_DATA_CENTER_NUM + " 或小于 0");}if (machineId > MAX_MACHINE_NUM || machineId < 0) {throw new IllegalArgumentException("机器标识不能大于等于 " + MAX_MACHINE_NUM + " 或小于 0");}this.dataCenterId = dataCenterId;this.machineId = machineId;}/*** <h2>雪花算法核心方法</h2>* 通过调用 nextId() 方法,让当前这台机器上的 snowflake 算法程序生成一个全局唯一的 id*/public synchronized long nextId() {// 获取系统当前时间戳long currentTimeStamp = getSystemCurrentTimeMillis();if (currentTimeStamp < lastTimeStamp) {throw new RuntimeException("时钟向后移动,拒绝生成雪花算法ID");}if (currentTimeStamp == lastTimeStamp) {// 当前毫秒内,序列号自增sequence = (sequence + 1) & MAX_SEQUENCE;// 序列号超出范围,需要等待下一毫秒if (sequence == 0L) {// 获取下一毫秒currentTimeStamp = getNextMill(lastTimeStamp);}} else {// 不同毫秒内,序列号置为 0sequence = 0L;}lastTimeStamp = currentTimeStamp;// 使用位运算生成最终的 IDreturn (currentTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT| dataCenterId << DATA_CENTER_LEFT| machineId << MACHINE_LEFT| sequence;}/*** <h2>获取系统当前时间戳</h2>** @return 当前时间(毫秒)*/private long getSystemCurrentTimeMillis() {return System.currentTimeMillis();}/*** <h2>获取下一毫秒</h2>* 当某一毫秒的时间,产生的 id 数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生 ID** @param lastTimestamp 上次生成 ID 的时间截* @return 当前时间戳*/private long getNextMill(long lastTimestamp) {long timeMillis = getSystemCurrentTimeMillis();while (timeMillis <= lastTimestamp) {timeMillis = getSystemCurrentTimeMillis();}return timeMillis;}public static void main(String[] args) {SnowFlake worker1 = new SnowFlake(1, 1);System.out.println(worker1.nextId());}
}
生成13位雪花Id
package com.ais.common.web.utils;public class IdUtil {/*** 开始时间截 (本次时间戳为:Thu Nov 04 2010 09:42:54 GMT+0800 (中国标准时间)----1288834974657L---1656543015264587776--19 )*/private final long startTime = 1683803335498L;/*** 机器id所占的位数*/private final long workerIdBits = 3L;/*** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)*/private final long maxWorkerId = -1L ^ (-1L << workerIdBits);/*** 序列在id中占的位数*/private final long sequenceBits = 5L;/*** 机器ID向左移12位*/private final long workerIdShift = sequenceBits;/*** 时间截向左移22位(10+12)*/private final long timestampLeftShift = sequenceBits + workerIdBits;/*** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)*/private final long sequenceMask = -1L ^ (-1L << sequenceBits);/*** 工作机器ID(0~1024)*/private long workerId;/*** 毫秒内序列(0~4095)*/private long sequence = 0L;/*** 上次生成ID的时间截*/private long lastTimestamp = -1L;private final static IdUtil idWorker = new IdUtil(1);//==============================Constructors=====================================/*** 构造函数** @param workerId 工作ID (0~1024)*/public IdUtil(long workerId) {if (workerId > maxWorkerId || workerId < 0) {throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));}this.workerId = workerId;}// ==============================Methods==========================================/*** 获得下一个ID (该方法是线程安全的)** @return SnowflakeId*/public synchronized long nextId() {long timestamp = timeGen();//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常if (timestamp < lastTimestamp) {throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));}//如果是同一时间生成的,则进行毫秒内序列if (lastTimestamp == timestamp) {sequence = (sequence + 1) & sequenceMask;//毫秒内序列溢出if (sequence == 0) {//阻塞到下一个毫秒,获得新的时间戳timestamp = tilNextMillis(lastTimestamp);}}//时间戳改变,毫秒内序列重置else {sequence = 0L;}//上次生成ID的时间截lastTimestamp = timestamp;//移位并通过或运算拼到一起组成64位的IDreturn ((timestamp - startTime) << timestampLeftShift) | (workerId << workerIdShift) | sequence;}/*** 阻塞到下一个毫秒,直到获得新的时间戳** @param lastTimestamp 上次生成ID的时间截* @return 当前时间戳*/protected long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 返回以毫秒为单位的当前时间** @return 当前时间(毫秒)*/protected long timeGen() {return System.currentTimeMillis();}public static long getSnowflakeNextId() {return idWorker.nextId();}/*** 测试*/public static void main(String[] args) {System.out.println(IdUtil.getSnowflakeNextId());}
}