共计 5160 个字符,预计需要花费 13 分钟才能阅读完成。
download:构建数据工程师能力模型,实战八大企业级我的项目内置文档资料
分库分表订单全局 ID
概述
分库分表后涉及到的另一个问题就是主键如何保障唯一且自增。以前单库单表的时候只需要利用数据库个性进行自增即可,现在因为是各自独立的库表,数据库之间的主键自增无奈进行交互,比如数据库 1 的订单明细表主键自增到了 1001,数据库 2 的订单明细表主键现在是 1000,如果现在往数据库 2 的订单明细表中插入一条数据,这个时候获取到的主键 ID 会是 1001,这样就会造成业务上的主键冲突。
全局 ID
为理解决订单明细表主键的重复问题。靠数据库的主键自增是无奈做到了。如何解决这个问题呢?可能给我的项目中引入一个全局唯一的 ID 服务,这个服务就是用来生成全局唯一 ID 的,每次生成的 ID 都不一样,可能保障主键的唯一性。
全局 ID 算法
全局 ID 需要保障如下的个性:
全局唯一:必须保障 ID 是全局性唯一的,基本申请
高性能:高可用低延时,ID 生成响应要块,否则反倒会成为业务瓶颈
高可用:100% 的可用性是骗人的,然而也要有限靠近于 100% 的可用性
好接入:要秉着拿来即用的设计原则,在零碎设计和实现上要尽可能的简略
趋势递增:最好趋势递增,这个申请就得看具体业务场景了,一般不严格申请
罕用的全局 ID 算法如下:
雪花算法 Snowflake
百度 uid-generator
美团 Leaf
滴滴 Tinyid
雪花算法
次要分为 4 个部分:
是 1 个 bit:0,这个是无意义的。
是 41 个 bit:示意的是工夫戳。
是 10 个 bit:示意的是机房 id,0000000000,因为我传进去的就是 0。
是 12 个 bit:示意的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 0000 0000。
1 bit,是无意义的:
因为二进制里第一个 bit 为如果是 1,那么都是正数,然而咱们生成的 id 都是负数,所以第一个 bit 对立都是 0。
41 bit:示意的是工夫戳,单位是毫秒。
41 bit 可能示意的数字多达 2^41 – 1,也就是可能标识 2 ^ 41 – 1 个毫秒值,换算成 > 年就是示意 69 年的工夫。
10 bit:记录工作机器 id,代表的是这个服务最多可能部署在 2^10 台机器上,也就是 1024 台机器。
然而 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意义就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可能代表 2 ^ 5 个机器(32 台机器),这里可能随便拆分,比如拿出 4 位标识业务号,其余 6 位作为机器号。可能随便组合。
12 bit:这个是用来记录同一个毫秒内产生的不同 id。
12 bit 可能代表的最大正整数是 2 ^ 12 – 1 = 4096,也就是说可能用这个 12 bit 代表的数字来分别同一个毫秒内的 4096 个不同的 id。也就是同一毫秒内同一台机器所生成的最大 ID 数量为 4096
简略来说,你的某个服务假设要生成一个全局唯一 id,那么就可能发送一个请求给部署了 SnowFlake 算法的零碎,由这个 SnowFlake 算法零碎来生成唯一 id。这个 SnowFlake 算法零碎首先必定是知道自己所在的机器号,(这里姑且讲 10bit 全副作为工作机器 ID)接着 SnowFlake 算法零碎接收到这个请求之后,首先就会用二进制位运算的形式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。接着用以后工夫戳(单位到毫秒)占用 41 个 bit,而后接着 10 个 bit 设置机器 id。最初再判断一下,以后这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最初的 12 个 bit。
/**
- 雪花算法解析 结构 snowflake 的结构如下(每部分用 - 离开):
- 0 – 0000000000 0000000000 0000000000 0000000000 0 – 00000 – 00000 – 000000000000
- 第一位为未使用,接下来的 41 位为毫秒级工夫(41 位的长度可能使用 69 年),而后是 5 位 datacenterId 和 5 位 workerId(10
- 位的长度最多反对部署 1024 个节点),最初 12 位是毫秒内的计数(12 位的计数次序号反对每个节点每毫秒产生 4096 个 ID 序号)
- <p>
- 一共加起来刚好 64 位,为一个 Long 型。(转换成字符串长度为 18)
*/
public class IdWorkerUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(IdWorkerUtils.class);
/**
* 工作机器 ID(0~31)
*/
private long workerId;
/**
* 数据中心 ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence;
/**
* 开始工夫截 (2015-01-01)
*/
private long twepoch = 1288834974657L;
/**
* 机器 id 所占的位数
*/
private long workerIdBits = 5L;
/**
* 数据标识 id 所占的位数
*/
private long datacenterIdBits = 5L;
/**
* 这个是二进制运算,就是 5 bit 最多只能有 31 个数字,也就是说机器 id 最多只能是 32 以内
*/
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 这个是二进制运算,就是 5 bit 最多只能有 31 个数字,机房 id 最多只能是 32 以内
*/
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在 id 中占的位数
*/
private long sequenceBits = 12L;
/**
* 机器 ID 向左移 12 位
*/
private long workerIdShift = sequenceBits;
/**
* 数据标识 id 向左移 17 位(12+5)
*/
private long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 工夫截向左移 22 位(5+5+12)
*/
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为 4095 (0b111111111111=0xfff=4095)
*/
private long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 上次生成 ID 的工夫截
*/
private long lastTimestamp = -1L;
public IdWorkerUtils(long workerId, long datacenterId, long sequence) {
// sanity check for workerId
// 这儿不就查看了一下,申请就是你传送进来的机房 id 和机器 id 不能超过 32,不能小于 0
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
LOGGER.info("worker starting. timestamp left shift {}, datacenter id bits {}, worker id bits {}, sequence bits {}, workerid {}",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
public long getWorkerId() {return workerId;}
public long getDatacenterId() {return datacenterId;}
public long getTimestamp() {return System.currentTimeMillis();
}
public synchronized long nextId() {
// 这儿就是获取以后工夫戳,单位是毫秒
long timestamp = timeGen();
if (timestamp < lastTimestamp) {LOGGER.error("clock is moving backwards. Rejecting requests until {}.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
// 这个意义是说一个毫秒内最多只能有 4096 个数字
// 无论你传送几进来,这个位运算保障始终就是在 4096 这个范畴内,避免你自己传送个 sequence 超过了 4096 这个范畴
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);
}
} else {sequence = 0;}
// 这儿记录一下最近一次生成 id 的工夫戳,单位是毫秒
lastTimestamp = timestamp;
// 这儿就是将工夫戳左移,放到 41 bit 那儿;// 将机房 id 左移放到 5 bit 那儿;// 将机器 id 左移放到 5 bit 那儿;将序号放最初 12 bit;// 最初拼接起来成一个 64 bit 的二进制数字,转换成 10 进制就是个 long 型
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;
}
private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();
while (timestamp <= lastTimestamp) {timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {return System.currentTimeMillis();
}
// --------------- 测试 ---------------
/*public static void main(String[] args) {IdWorkerUtils worker = new IdWorkerUtils(1, 1, 1);
for (int i = 0; i < 30; i++) {System.out.println(worker.nextId());
}
}*/
}
复制代码
时钟回拨
因为雪花算法中蕴含工夫戳,因此依赖零碎的工夫,如果零碎的工夫因为某一些原因回到了过来的某个工夫,比如现在的零碎工夫是 2022 年 8 月 7 日 09 时 01 分 10 秒,然而下次获获取到的工夫是 2022 年 8 月 7 日 09 时 00 分 10 秒,那么就可能导致生成的全局 ID 与过来的某一个 ID 重复,这就是时钟回拨问题。
为理解决时钟回拨问题可能把之前的零碎获取到哦啊的工夫戳缓存起来,每次获取工夫戳和上次的进行比较,如果本次获取的工夫小于上一次的工夫,就证实时钟回拨了,就可能取上次工夫戳 + 1 来解决。
总结
其实对于分布式 ID 的生成策略。无论是咱们上述提到的哪一种。无非需要具备以下两种个性。自增的、不重复的,而对于不重复且是自增的,那么很容易想到的是工夫,而雪花算法就是基于工夫戳。然而毫秒级的并发下如果间接拿来用,显然是不合理的。那么就要在这个工夫戳下面做一些文章。至于怎么能让这个货色保持唯一且自增。就要打开自己的脑洞了。可能看到雪花算法中是基于 synchronized 锁进行实现的。