@[TOC]
一、前言
至此,seata系列的内容包括:
- can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;
- Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
- Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
- 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)
- 【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】
- 【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server
- 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么
- 【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
- 【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信
- 【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务
本文接着来看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));
变量/常量解释:
- 常量
twepoch
表示我们的时间戳时间从2020-05-03
开始计算,即当前时间的时间戳需要减去twepoch
的值1588435200000L
;- 常量
workerIdBits
表示机器号workerId占10位;- 常量
timestampBits
表示时间戳timestamp占41位;- 常量
sequenceBits
表示序列化占12位;- 常量
maxWorkerId
表示机器号的最大值为1023;- long类型的变量
workerId
本身也是一个雪花算法,只是从头往后数,第2位开始,一共10位用来表示workerId,其余位全是0;- AtomicLong类型的变量
timestampAndSequence
,其本身也是一个雪花算法,头11位不使用,中间41位表示timestamp,最后12位表示sequence;- long类型的常量
timestampAndSequenceMask
,用于从一个完整的雪花ID(long类型)中摘出timestamp 和 sequence
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地址生成;
机器id由两值相加:
(byte值 & 0B11)
<< Byte.SIZE,即最大值为 0B11=3;然后左移8位为:1100000000
; 所以此处最大十进制值为768
;(byte值 & 0xFF)
;16进制的F对应二进制为 1111,所以最大十进制值FF
: 255;- 然后:768 + 255 = 1023,即机器id(
workerId
)最大不会超过1023
;0B对应二进制,0x对应十六进制
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;其中:
highest 1 bit
: 最高位的1bit始终是0
;next 10 bit
: workerId 10个bit表示机器号;next 41 bit
: timestamp 41个bit表示当前机器的时间戳(ms级别),每毫秒递增;lowest 12 bit
: sequence 12位的序号,如果一台机器在一毫秒内有很多线程要来生成id,12bit的sequence会自增;
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()方法逻辑:
- 解决sequence序列号被用尽问题;
- 累加序列号
sequence
,Seata中设计的sequence
是和timestamp
放在同一个变量里,累加之后再和mask
(后53位全是1
)做一个与运算,得到真正的时间戳伴随的序列;- 将workerId 和
时间戳伴随的序列
通过或运算组合成最终的UUID。
下面细看一下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生成算法的稳定性;
如果timestampAndSequence
中的当前时间戳大于等于
服务器真实的时间戳,会将线程 睡眠5ms;
博主看到这里时有两个问题:
- 为什么判断时间戳时是大于等于,而不是大于?
- 为什么就让线程睡眠了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全局事务的执行流程。