文章目录
一、前言
至此,seata系列的内容包括:
本文接着来看Seata的全局事务ID(transactionId
)和分支事务ID(branchId
)是如何生成的?
二、分布式ID初始化
在Seata Server启动的时候( 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么【云原生】)会初始化UUID生成器UUIDGenerator
;
1、UUIDGenerator
public class UUIDGenerator {
private static volatile IdWorker idWorker;
/**
* generate UUID using snowflake algorithm
* @return UUID
*/
public static long generateUUID() {
// DCL + volatile ,实现并发场景下保证idWorker的单例特性
if (idWorker == null) {
synchronized (UUIDGenerator.class) {
if (idWorker == null) {
init(null);
}
}
}
// 每次通过雪花算法实现的nextId()获取一个新的UUID
return idWorker.nextId();
}
/**
* 初始化IDWorker
*/
public static void init(Long serverNode) {
idWorker = new IdWorker(serverNode);
}
}
UUIDGenerator
会委托给其组合的IdWorker
根据雪花算法生成分布式ID,生成的雪花Id由0、10位的workerId、41位的时间戳、12位的sequence序列号组成。
2、IdWorker
IdWorker中有8个重要的成员变量/常量:
/**
* Start time cut (2020-05-03)
*/
private final long twepoch = 1588435200000L;
/**
* The number of bits occupied by workerId
*/
private final int workerIdBits = 10;
/**
* The number of bits occupied by timestamp
*/
private final int timestampBits = 41;
/**
* The number of bits occupied by sequence
*/
private final int sequenceBits = 12;
/**
* Maximum supported machine id, the result is 1023
*/
private final int maxWorkerId = ~(-1 << workerIdBits);
/**
* business meaning: machine ID (0 ~ 1023)
* actual layout in memory:
* highest 1 bit: 0
* middle 10 bit: workerId
* lowest 53 bit: all 0
*/
private long workerId;
/**
* 又是一个雪花算法(64位,8字节)
* timestamp and sequence mix in one Long
* highest 11 bit: not used
* middle 41 bit: timestamp
* lowest 12 bit: sequence
*/
private AtomicLong timestampAndSequence;
/**
* 从一个long数组类型中抽取出一个时间戳伴随序列号,偏向一个辅助性质
* mask that help to extract timestamp and sequence from a long
*/
private final long timestampAndSequenceMask = ~(-1L << (timestampBits + sequenceBits));
变量/常量解释:
IdWorker构造器中会分别初始化TimestampAndSequence、WorkerId。
public IdWorker(Long workerId) {
// 初始化时间戳sequence
initTimestampAndSequence();
// 初始化workerId
initWorkerId(workerId);
}
1) 初始化时间戳和序列号
initTimestampAndSequence()方法负责初始化timestamp
和sequence
;
private void initTimestampAndSequence() {
// 拿到当前时间戳 - (2020-05-03 时间戳)的数值,即当前时间相对2020-05-03的时间戳
long timestamp = getNewestTimestamp();
// 把时间戳左移12位,后12位留给sequence使用
long timestampWithSequence = timestamp << sequenceBits;
// 把混合sequence(默认为0)的时间戳赋值给timestampAndSequence
this.timestampAndSequence = new AtomicLong(timestampWithSequence);
}
// 获取当前时间戳
private long getNewestTimestamp() {
//当前时间的时间戳减去2020-05-03的时间戳
return System.currentTimeMillis() - twepoch;
}
2)初始化机器ID
initWorkerId(Long workerId)方法负责初始化workId,默认不会传过来workerId,如果传过来则使用传过来的workerId,并校验其不能大于1023,然后将其左移53位;
private void initWorkerId(Long workerId) {
if (workerId == null) {
// workid为null时,自动生成一个workerId
workerId = generateWorkerId();
}
// workerId最大只能是1023,因为其只占10bit
if (workerId > maxWorkerId || workerId < 0) {
String message = String.format("worker Id can't be greater than %d or less than 0", maxWorkerId);
throw new IllegalArgumentException(message);
}
this.workerId = workerId << (timestampBits + sequenceBits);
}
1> 如果没传则基于MAC地址生成;
2> 如果基于MAC地址生成workerId出现异常,则也1023为基数生成一个随机的workerId;
最后同样,校验workerId不能大于1023,然后将其左移53位,用于拼接出分布式ID。
三、分布式ID获取
上面我们了解到在Seata Server启动时会初始化UUID生成器UUIDGenerator
的成员IDWorker
,以用于生成分布式ID;在后续TM开启全局事务、或RM创建分支事务加入到全局事务时,都会调用UUIDGenerator
#generateUUID()
方法生成分布式事务ID(全局事务ID transactionId
、分支事务ID branchId
);
1、生成UUID的入口
public static long generateUUID() {
// DCL + volatile ,实现并发场景下保证idWorker的单例特性
if (idWorker == null) {
synchronized (UUIDGenerator.class) {
if (idWorker == null) {
init(null);
}
}
}
// 每次通过雪花算法实现的nextId()获取一个新的UUID
return idWorker.nextId();
}
idWorker变量被volatile
关键字所修饰,确保其在多线程环境下的可见性,再结合DCL(Double Check Lock,双重检查锁)确保idWorker的单例性。
每次要获取新的一个UUID时,会通过IdWorker#nextId()
方法实现;
2、如何生成一个UUID
IdWorker#nextId()
方法负责生成一个UUID;其中:
public long nextId() {
// 解决sequence序列号被用尽问题!
waitIfNecessary();
// 自增时间戳的sequence,等于对一个毫秒内的sequence做累加操作,或 timestamp + 1、sequence置0
long next = timestampAndSequence.incrementAndGet();
// 把最新时间戳(包括序列号)和mask做一个与运算,得到真正的时间戳伴随的序列
long timestampWithSequence = next & timestampAndSequenceMask;
// 最后和workerId做或运算,得到最终的UUID;
return workerId | timestampWithSequence;
}
nextId()方法逻辑:
下面细看一下waitIfNecessary()
是如何解决序列号被用尽的问题;
1)如何解决序列号被用尽的问题
waitIfNecessary()
会解决序列号被用尽的问题;
private void waitIfNecessary() {
// 获取当前时间戳 以及相应的sequence序列号
long currentWithSequence = timestampAndSequence.get();
// 通过位运算拿到当前的时间戳
long current = currentWithSequence >>> sequenceBits;
// 获取当前真实的时间戳
long newest = getNewestTimestamp();
// 如果`timestampAndSequence`中的当前时间戳 大于等于 真实的时间戳,说明当前机器时间之前的sequence序号 / 某个毫秒内的序列号 已经被耗尽了;
if (current >= newest) {
try {
// 如果获取UUID的QPS过高,导致时间戳对应的sequence序号被耗尽了
// 线程休眠5毫秒
Thread.sleep(5);
} catch (InterruptedException ignore) {
// don't care
}
}
}
如果有大量的线程并发获取UUID、获取UUID的QPS过高,可能会导致从初始化IdWorker时间戳开始 到 当前时间戳的序列号全部用完了(也可以理解为某一个毫秒内的sequence耗尽
);但是时间戳却累加了、进到下一个毫秒(或下几毫秒);然而当前实际时间却还没有到下一毫秒。如果恰巧此时重启了seata server,再初始化IdWorker时的时间戳就有可能会出现重复,进而导致UUID重复。
所以Seata为了尽可能的保证UUID生成算法的稳定性;
博主看到这里时有两个问题:
- 为什么判断时间戳时是大于等于,而不是大于?
- 为什么就让线程睡眠了5ms?
为什么判断时间戳时是大于等于,而不是大于?
如果是大于(current > newest
),而不是大于等于(current >= newest
);
考虑一种极端的场景,UUID的时间戳已经累加到了当前时间,此时Seata Server立马关机重启(假设这个过程耗时不到1ms
),就会出现重复的UUID。不过这种场景仅存在于理论上;现实应该不会,所以我认为 大于(current > newest
) 是没有问题的。
为什么就让线程睡眠了5ms?
这里就睡眠5ms,可能只是把所有的流量都往后均摊,因为往往高峰期时间也比较短;并且一个毫秒会有4096
个序列号,而从seata Server启动开始也不会立刻就是高峰期,所以到当前时间之前 也会有很多的时间戳给UUID使用;不过这个简单粗暴的阻塞线程确实会浪费一些系统资源。
为什么是睡眠5ms,而不是3ms、2ms,可能是出于压测的结论,也可能作者也没想那么多。
2)时钟回拨问题的解决
UUIDGenerator
(或者说IdWorker
)通过借用未来时间来解决sequence天然存在的并发限制,如果timestampAndSequence
中的当前时间戳大约 服务器的当前时间,仅仅会睡眠5ms,起一个缓冲的作用;但timestampAndSequence
仍会继续递增,使用未来的时间。
Seata Server服务不重启基本没有问题,当接入Seata Server的服务们QPS比较高时,重启Seata Server就可能会出现新生成的UUID和历史UUID重复问题。
四、总结和后续
本文聊了Seata中分布式ID是使用雪花算法生成的,对一个64位的UUID,其最高位恒为0,10个bit表示机器号,41个bit表示当前机器的时间戳(ms级别),12位的序号。
seata又对毫秒内序列号用尽、时钟回拨做了特殊处理。
下一篇文章我们将聊Seata全局事务的执行流程。