废话不多说,咱们直接上代码讲解
/*** SkyWalking的traceId生成策略* traceId 是用于唯一标识一个跟踪操作(trace)的标识符*/
public class SkyWalkingTraceIdGenerator {// 生成一个唯一的进程ID,使用UUID去除横杠private static final String PROCESS_ID = UUID.randomUUID().toString().replaceAll("-", "");/** 使用ThreadLocal保持每个线程的IDContext* 在每个线程首次访问 THREAD_ID_SEQUENCE 时,会为该线程创建一个新的 IDContext 实例* 并且这个实例会一直附着在这个线程上,供线程在后续的操作中使用* 这是为了确保每个线程都有自己独立的 IDContext 实例,以避免线程之间的干扰* 创建一个 ThreadLocal 对象,并通过提供的 Supplier 实例在每个线程首次访问该 ThreadLocal 对象时获取初始值* 在这里,初始值就是由()-> new IDContext(System.currentTimeMillis(),(short) 0) 提供的,即每个线程都会获取一个新的IDContext实例*/private static final ThreadLocal<IDContext> THREAD_ID_SEQUENCE = ThreadLocal.withInitial(//(short) 0 表示线程内序列的起始值,即 IDContext 类中的 threadSeq 字段的初始值。在这里,它将线程内序列的起始值设置为0() -> new IDContext(System.currentTimeMillis(), (short) 0));private SkyWalkingTraceIdGenerator() {}/*** 生成一个新的ID,由三个部分组成。* 第一个部分表示应用程序实例ID。* 第二个部分表示线程ID。* 第三个部分也有两个部分,1)时间戳(以毫秒为单位)2)线程内的序列,在0(包括)和9999(包括)之间。** @return 用于表示跟踪或段的唯一ID*/public static String generate() {return Joiner.on(".").join(PROCESS_ID,String.valueOf(Thread.currentThread().getId()),String.valueOf(THREAD_ID_SEQUENCE.get().nextSeq()));}// IDContext类用于保持线程特定的上下文,其中包含了跟踪 ID 生成所需的信息private static class IDContext {private static final int MAX_SEQ = 10_000;//记录上一次的时间戳private long lastTimestamp;//记录线程内的序列private short threadSeq;// 记录上一次时间回移的时间戳,仅用于考虑由运维或操作系统引起的时间回移。private long lastShiftTimestamp;//记录上一次时间回移的值private int lastShiftValue;private IDContext(long lastTimestamp, short threadSeq) {this.lastTimestamp = lastTimestamp;this.threadSeq = threadSeq;}private long nextSeq() {return timestamp() * 10000 + nextThreadSeq();}private long timestamp() {long currentTimeMillis = System.currentTimeMillis();if (currentTimeMillis < lastTimestamp) {if (lastShiftTimestamp != currentTimeMillis) {lastShiftValue++;lastShiftTimestamp = currentTimeMillis;}return lastShiftValue;} else {lastTimestamp = currentTimeMillis;return lastTimestamp;}}private short nextThreadSeq() {if (threadSeq == MAX_SEQ) {threadSeq = 0;}return threadSeq++;}}
}
这是 IDContext 类中的几个关键方法的代码块的解释:
- private IDContext(long lastTimestamp, short threadSeq) { … }: 构造函数,用于初始化 IDContext 实例的初始状态。
○ this.lastTimestamp = lastTimestamp;: 将传入的 lastTimestamp 参数赋值给实例变量 lastTimestamp,表示上一次记录的时间戳。
○ this.threadSeq = threadSeq;: 将传入的 threadSeq 参数赋值给实例变量 threadSeq,表示线程内序列。 - private long nextSeq() { … }: 生成下一个序列,由时间戳和线程内序列组成。
○ return timestamp() * 10000 + nextThreadSeq();: 调用 timestamp() 方法获取当前时间戳,乘以 10000,然后加上调用 nextThreadSeq() 方法获取的下一个线程内序列。 - private long timestamp() { … }: 获取当前时间戳,考虑了时间回移的情况。
○ long currentTimeMillis = System.currentTimeMillis();: 获取当前时间戳。
○ if (currentTimeMillis < lastTimestamp) { … }: 如果当前时间小于上一次记录的时间戳,表示发生了时间回移。
■ if (lastShiftTimestamp != currentTimeMillis) { … }: 如果上一次时间回移的时间戳不等于当前时间戳,表示这是第一次发现时间回移。
● lastShiftValue++;: 时间回移值加1,用于在 timestamp() 方法中计算回移后的时间。
● lastShiftTimestamp = currentTimeMillis;: 记录时间回移的时间戳。
■ return lastShiftValue;: 返回回移后的时间。
○ else { … }: 如果当前时间大于等于上一次记录的时间戳,表示正常情况。
■ lastTimestamp = currentTimeMillis;: 更新上一次记录的时间戳。
■ return lastTimestamp;: 返回当前时间戳。 - private short nextThreadSeq() { … }: 获取下一个线程内序列,注意在达到最大值后会重新从0开始。
○ if (threadSeq == MAX_SEQ) { … }: 如果线程内序列达到最大值,重置为0。
■ threadSeq = 0;: 重置线程内序列。
○ return threadSeq++;: 返回当前线程内序列值,并将其递增。
在正常情况下,timestamp() 方法确实返回最新的时间戳,因为在正常情况下,当前时间戳大于等于上一次记录的时间戳。
但是,当系统检测到时间回移时,它会调整时间戳,避免出现由于时间回移导致的不连续性。lastShiftValue 在这里的作用是记录时间回移的数量,而不是直接返回经过调整的时间戳。
为什么要这样设计呢?考虑以下情况:
- 时间回移的可能性: 操作系统或运维可能会引起时间回移,导致当前时间小于上一次记录的时间。在这种情况下,返回 lastShiftValue 允许应用程序意识到发生了时间回移,而不是简单地返回一个调整后的时间戳。
- 连续性和递增性: 返回 lastShiftValue 可以确保时间戳的连续性和递增性。在时间回移的情况下,返回 lastShiftValue 提供了一种方式来追踪这种变化。这有助于保持生成的 ID 在整个系统中的一致性,避免由于时间回移引起的问题。
总体来说,这个设计是为了在系统检测到时间回移时提供一种反映这一情况的机制,并确保生成的时间序列仍然是有序和递增的。