一、Otel的traceId规范
如图,Otel生成的traceId分为高低位,各占16位,每一位都是十六进制的字符.即traceId大小为16字节,128位.下一章节会详细介绍高低位都是如何生成的.
二、ThreadLocalRandom的生成规则
2.1 mix64混淆算法
public String generateTraceId() {
//安卓JVM or 默认JVM
Random random = (Random)randomSupplier.get();
long idHi = random.nextLong();
long idLo;
do {
idLo = random.nextLong();
} while(idLo == 0L);
//1.long -> 十六进制字符
//2.toString
return TraceId.fromLongs(idHi, idLo);
}
生成高位和低位调用的方法是同一个mix64算法,这里采用的randomSupplier是ThreadLocalRandom,我们直接看内部是如何实现的
public long nextLong() {
return mix64(nextSeed());
}
需要拿到一个随机种子再传入mix64方法,我们先看这个混淆算法是怎么实现的
private static long mix64(long z) {
z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
z = (z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L;
return z ^ (z >>> 33);
}
首先可以看到实现很简单,只有位移,异或和大质数乘法.最终的目的是混淆输入数据,确保输出的均匀性和无偏性(指输出的伪随机数的均匀分布特性不受seed的影响).常用于分布式系统下高性能,高质量的伪随机数生成.
感兴趣的可查看SplitMix64 伪随机数生成算法,此处不做展开.
2.2 种子的生成
混淆算法有了,我们再来看种子是如何生成的.就不再一层一层往下追了,直接把所有和seed相关的操作摘出来放在一起学习:
//根据系统时间 和 JVM时间 初始化seeder
private static final AtomicLong seeder =
new AtomicLong(mix64(System.currentTimeMillis()) ^ mix64(System.nanoTime()));
//用于seed递增的大质数,说明了种子是线性递增的
private static final long GAMMA = 0x9e3779b97f4a7c15L;
// at end of <clinit> to survive static initialization circularity
static {
String sec = VM.getSavedProperty("java.util.secureRandomSeed");
if (Boolean.parseBoolean(sec)) {
byte[] seedBytes = java.security.SecureRandom.getSeed(8);
long s = (long)seedBytes[0] & 0xffL;
for (int i = 1; i < 8; ++i)
s = (s << 8) | ((long)seedBytes[i] & 0xffL);
seeder.set(s);
}
}
//使用Unsafe类直接操作内存
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + GAMMA);
return r;
}
ThreadLocalRandom中根据系统时间 和 JVM启动后的相对时间初始化一个seeder,以及一个大质数作为seed递增常量,这也说明了seed的生成实际上就是一个简单的线性加法.
这里有一类特殊情况,如果系统属性设置了secureRandomSeed且为true,会使用安全性更高的SecureRandom来生成seeder的初始值
nextSeed()通过Unsafe类直接操作当前线程内存,取出SEED的值加上递增常量再写回内存,并把新生成的seed返回给混淆算法
细心观察这份代码的人应该可以发现,nextSeed()中会取当前线程内存中的SEED,那第一次取会发生什么呢,初始化相关的逻辑在哪?接下来的代码就展示了这一部分:
//ThreadLocalRandom没有同名的构造函数,而是提供了名为current的静态方法来返回实例
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
U.putLong(t, SEED, seed);
U.putInt(t, PROBE, probe);
}
PROBE的初始值就是0,此时调用localInit()方法,对seeder线性增长再混淆后的结果就是seed的初始值
seeder和seed的区别就是每个线程的seed初始值都来自于seeder,线程内的seed根据这个初始值递增.
三、JDK自带的UUID
我在自己的电脑上简单测试对比了Otel和UUID的性能,各自生成10000次并统计时间,交换测试的位置前后差距都在几十倍以上,当然即便差距很大生成10000次UUID的总耗时不过也就一百来毫秒.给出我的测试代码,可以尝试在本地运行查看结果(前提是有Otel的依赖)
package org.example;
import io.opentelemetry.api.internal.OtelEncodingUtils;
import io.opentelemetry.api.internal.TemporaryBuffers;
import io.opentelemetry.api.trace.TraceId;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
public class Main {
private static final int cnt = 10000;
public static void main(String[] args) {
//testUUID();
testTraceId();
testUUID();
}
static void testUUID(){
long start = System.currentTimeMillis();
String traceId = " ";
for(int i=0;i<cnt;i++){
traceId = UUID.randomUUID().toString();
}
System.out.println("UUID生成时长: " + traceId);
System.out.println(System.currentTimeMillis()-start);
}
static void testTraceId(){
long start = System.currentTimeMillis();
for(int i=0;i<cnt;i++){
String traceId = generateTraceId();
}
System.out.println("traceId生成时长: ");
System.out.println(System.currentTimeMillis()-start);
}
static String generateTraceId(){
ThreadLocalRandom random = ThreadLocalRandom.current();
long idHi = random.nextLong();
long idLo;
do {
idLo = random.nextLong();
} while(idLo == 0L);
return fromLongs(idHi, idLo);
}
public static String fromLongs(long traceIdLongHighPart, long traceIdLongLowPart) {
if (traceIdLongHighPart == 0L && traceIdLongLowPart == 0L) {
return " ";
} else {
char[] chars = TemporaryBuffers.chars(32);
OtelEncodingUtils.longToBase16String(traceIdLongHighPart, chars, 0);
OtelEncodingUtils.longToBase16String(traceIdLongLowPart, chars, 16);
return new String(chars, 0, 32);
}
}
}
traceId大致的生成逻辑已经在上一章梳理过了,UUID之所以慢是因为内部采用的随机数生成器是SecureRandom,它的好处是尽管性能不如ThreadLocalRandom,却能提供更好的安全性.具体怎么选择取决于你的使用场景.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。