traceId:SkyWalking的traceId生成策略

废话不多说,咱们直接上代码讲解

复制代码
/**
 * 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 类中的几个关键方法的代码块的解释:

  1. private IDContext(long lastTimestamp, short threadSeq) { ... }: 构造函数,用于初始化 IDContext 实例的初始状态。
    ○ this.lastTimestamp = lastTimestamp;: 将传入的 lastTimestamp 参数赋值给实例变量 lastTimestamp,表示上一次记录的时间戳。
    ○ this.threadSeq = threadSeq;: 将传入的 threadSeq 参数赋值给实例变量 threadSeq,表示线程内序列。
  2. private long nextSeq() { ... }: 生成下一个序列,由时间戳和线程内序列组成。
    ○ return timestamp() * 10000 + nextThreadSeq();: 调用 timestamp() 方法获取当前时间戳,乘以 10000,然后加上调用 nextThreadSeq() 方法获取的下一个线程内序列。
  3. 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;: 返回当前时间戳。
  4. private short nextThreadSeq() { ... }: 获取下一个线程内序列,注意在达到最大值后会重新从0开始。
    ○ if (threadSeq == MAX_SEQ) { ... }: 如果线程内序列达到最大值,重置为0。
    ■ threadSeq = 0;: 重置线程内序列。
    ○ return threadSeq++;: 返回当前线程内序列值,并将其递增。

在正常情况下,timestamp() 方法确实返回最新的时间戳,因为在正常情况下,当前时间戳大于等于上一次记录的时间戳。

但是,当系统检测到时间回移时,它会调整时间戳,避免出现由于时间回移导致的不连续性。lastShiftValue 在这里的作用是记录时间回移的数量,而不是直接返回经过调整的时间戳。

为什么要这样设计呢?考虑以下情况:

  1. 时间回移的可能性: 操作系统或运维可能会引起时间回移,导致当前时间小于上一次记录的时间。在这种情况下,返回 lastShiftValue 允许应用程序意识到发生了时间回移,而不是简单地返回一个调整后的时间戳。
  2. 连续性和递增性: 返回 lastShiftValue 可以确保时间戳的连续性和递增性。在时间回移的情况下,返回 lastShiftValue 提供了一种方式来追踪这种变化。这有助于保持生成的 ID 在整个系统中的一致性,避免由于时间回移引起的问题。
    总体来说,这个设计是为了在系统检测到时间回移时提供一种反映这一情况的机制,并确保生成的时间序列仍然是有序和递增的。
相关推荐
专注API从业者7 分钟前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
摇滚侠24 分钟前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql
keep one's resolveY1 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
天空属于哈夫克31 小时前
企业微信API常见的错误和解决方案
java·数据库·企业微信
摇滚侠2 小时前
VMvare 虚拟机 Oracle19c 安装步骤,远程连接 Oracle19c,百度网盘安装包
java·oracle
梁萌2 小时前
idea报错找不到XX包的解决方法
java·intellij-idea·启动报错·缺少包
Agent产品评测局2 小时前
生产排期与MES/ERP系统打通,实操方法详解 —— 2026企业级智能体自动化选型与实战指南
java·运维·人工智能·ai·chatgpt·自动化
阿丰资源3 小时前
基于Spring Boot的电影城管理系统(直接运行)
java·spring boot·后端
呱牛do it3 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 8)
java
消失的旧时光-19434 小时前
Spring Boot 工程化进阶:统一返回 + 全局异常 + AOP 通用工具包
java·spring boot·后端·aop·自定义注解