用 ThreadLocal + Deque 打造一个“线程专属的调用栈” —— Spring Insight 的上下文管理术

用 ThreadLocal + Deque 打造一个"线程专属的调用栈" ------ Spring Insight 的上下文管理术

作者:苏渡苇

项目地址github.com/iweidujiang...(感谢 Star !)

一、引言

在开发分布式追踪系统(比如 Zipkin、Jaeger,或者我正在做的 Spring Insight)时,有一个核心问题必须解决:

如何在一个请求的整个生命周期中,准确地记录它经过了哪些方法、服务、数据库?

而要回答这个问题,关键在于------追踪上下文(Trace Context) 的管理。

今天,我们就来聊聊 Spring Insight 中一段看似简单、实则精巧的代码(王婆卖瓜了属于是,哈哈):

java 复制代码
private static final ThreadLocal<Deque<TraceSpan>> SPAN_STACK =
    new NamedThreadLocal<>("Spring Insight Trace Context") {
        @Override
        protected Deque<TraceSpan> initialValue() {
            return new ArrayDeque<>();
        }
    };

别小看这短短几行,它背后藏着 Java 并发编程和链路追踪设计的双重智慧。

为了说明我为什么这么写,我们先深入认识一下它的两个"主角":ThreadLocalDeque

二、ThreadLocal:每个线程的"私人保险箱"

ThreadLocal 是什么?

ThreadLocal 是 Java 提供的一个类,用于为每个线程维护一份独立的变量副本

你可以把它想象成每个线程都有一个"专属抽屉",彼此互不干扰。

java 复制代码
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello from Thread-" + Thread.currentThread().getName());
System.out.println(threadLocal.get()); // 每个线程看到的值都不同

典型使用场景

1. 用户会话/安全上下文(Security Context)

Spring Security 中,SecurityContextHolder 默认就使用 ThreadLocal 存储当前用户的认证信息:

java 复制代码
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 这个 auth 对象是当前线程独有的,不会被其他请求污染

这样,在 Controller、Service、DAO 层都能随时获取当前用户,而无需层层传递参数。

2. 事务管理(Transaction Context)

Spring 的声明式事务(@Transactional)依赖 ThreadLocal 来绑定当前线程的数据库连接和事务状态。

同一个线程内多次调用 DAO 方法,能复用同一个 Connection,保证事务一致性。

3. 日志追踪(MDC - Mapped Diagnostic Context)

SLF4J + Logback 中的 MDC 就是基于 ThreadLocal 实现的。

你可以在请求入口处设置 traceId:

java 复制代码
MDC.put("traceId", UUID.randomUUID().toString());
logger.info("Processing request..."); // 日志自动带上 traceId

这样,即使高并发下多个请求混在一起打日志,也能通过 traceId 追踪完整链路。

4. 多租户(Tenant ID)隔离

SaaS 系统中,常通过 ThreadLocal 在请求开始时解析租户 ID,并在线程内全局可用:

java 复制代码
TenantContext.setTenantId(tenantId);
// 后续所有 DB 查询自动加上 tenant_id = ? 条件

注意事项

  • 内存泄漏风险 :线程池中的线程长期存活,如果 ThreadLocal 不手动 remove(),其引用的对象无法被 GC。
  • 异步场景失效CompletableFuture@Async 等会切换线程,导致 ThreadLocal 上下文丢失(需用 TransmittableThreadLocal 解决)。

三、Deque:不只是队列,更是高效的"栈"

Deque 是什么?

Deque(Double-ended Queue)是 Java 集合框架中的双端队列接口,支持从头部和尾部高效插入/删除元素。

虽然名字叫"队列",但它完全可以当作栈(Stack)普通队列(Queue) 使用。

java 复制代码
Deque<String> deque = new ArrayDeque<>();
deque.push("A"); // 栈操作:压入
deque.push("B");
System.out.println(deque.pop()); // 输出 "B"(LIFO)

✅ 为什么用 Deque 而不是 Stack?

Java 自带的 Stack 类:

  • 继承自 Vector,所有方法都是 synchronized,性能差;
  • 设计老旧,不符合现代集合规范。

ArrayDeque

  • 非同步,性能高;
  • 底层是循环数组,内存连续,缓存友好;
  • 官方文档明确推荐:"This class is likely to be faster than Stack when used as a stack"。

典型使用场景

1. 方法调用栈模拟

编译器、解释器常用栈来管理函数调用。每进入一个函数,压入栈帧;返回时弹出。

2. 括号匹配、表达式求值

算法题经典场景:用栈判断 ({[]}) 是否合法,或计算 3 + (2 * 5)

3. 撤销(Undo)操作

图形编辑器、文本编辑器的"撤销"功能,本质就是把操作记录压入栈,撤销时弹出并反向执行。

4. 深度优先搜索(DFS)

递归容易栈溢出?可以用显式栈(Deque)实现非递归 DFS。

5. 链路追踪中的 Span 嵌套

这正是 Spring Insight 的用法!每个方法调用对应一个 TraceSpan,用栈来维护父子关系:

scss 复制代码
push(rootSpan)
  → push(serviceSpan)
    → push(repoSpan)
    ← pop(repoSpan)
  ← pop(serviceSpan)
← pop(rootSpan)

四、组合技:ThreadLocal + Deque = 线程专属调用栈

现在,把两者结合起来:

java 复制代码
ThreadLocal<Deque<TraceSpan>> SPAN_STACK

这句话的意思是:

每个线程都拥有一个独立的 Span 栈,用来记录当前请求的调用链路。

这解决了什么问题?

假设一个 HTTP 请求进来,执行流程如下:

复制代码
Controller → Service → Repository → DB

我们希望为每一步生成一个 TraceSpan,并形成父子关系:

SPAN_STACK 正是实现这种嵌套结构的关键:

  1. 进入 Controller :创建根 Span,push 到栈;
  2. 进入 Service :以栈顶 Span 为父,创建子 Span,再 push
  3. 退出 Repositorypop() 出当前 Span,结束计时,上报;
  4. 最终回到 Controllerpop() 根 Span,完成整条链路。

💡 关键点 :因为栈是 ThreadLocal 的,所以即使有 1000 个并发请求,每个线程的调用栈也完全独立,不会串错!

示意图

五、在 Spring Insight 中的实际应用(思路预览)

虽然 Spring Insight 项目还在开发中,但这段代码已经奠定了上下文管理的核心骨架

  • TraceContext.startSpan("operation"):内部调用 SPAN_STACK.get().push(newSpan)
  • TraceContext.endSpan()pop() 并结束计时
  • TraceContext.currentSpan()peek() 获取当前 Span,用于传递 traceId/spanId
  • 请求结束时调用 TraceContext.clear():防止线程池复用导致上下文污染

这种设计天然支持异步吗?

暂时不支持!但未来可通过 TransmittableThreadLocal(阿里开源)扩展,实现跨线程上下文传递。

六、总结:小结构,大作用

组件 作用 真实世界类比
ThreadLocal 线程隔离的上下文存储 每个服务员有自己的点菜单
Deque(作栈用) 管理嵌套调用的生命周期 函数调用的"回溯路径"
SPAN_STACK 二者结合,构建线程安全的追踪栈 每个请求的"行车记录仪"

这就像给每个线程配了一个"行车记录仪",全程记录它干了啥、花了多久、有没有出错。

而这一切,都始于那几行看似平淡的初始化代码。

🌟 最后:欢迎关注 Spring Insight!

如果你对链路追踪、APM、Java Agent 感兴趣,欢迎关注我的开源项目:

🔗 GitHub 地址github.com/iweidujiang...

目前项目包含:

  • Agent 模块:自动埋点、Span 采集
  • Collector 模块:接收并存储链路数据
  • Storage 模块:基于 MyBatis-Plus 的持久化
  • 后续将加入 Web UI、拓扑图、告警等能力!

你的 Star 是对我最大的鼓励!

相关推荐
玄〤2 小时前
黑马点评中 VoucherOrderServiceImpl 实现类中的一人一单实现解析(单机部署)
java·数据库·redis·笔记·后端·mybatis·springboot
bugcome_com2 小时前
API 域名部署指南:从单域名到混合架构的完整实战解析
架构
J_liaty2 小时前
Spring Boot拦截器与过滤器深度解析
java·spring boot·后端·interceptor·filter
短剑重铸之日2 小时前
《7天学会Redis》Day2 - 深入Redis数据结构与底层实现
数据结构·数据库·redis·后端
码事漫谈3 小时前
从C++到C#的转型完全指南
后端
码事漫谈3 小时前
TCP心跳机制:看不见的“生命线”
后端
亲爱的非洲野猪3 小时前
Java锁机制八股文
java·开发语言
rgeshfgreh3 小时前
C++字符串处理:STL string终极指南
java·jvm·算法
Zoey的笔记本3 小时前
「支持ISO27001的GTD协作平台」数据生命周期管理方案与加密通信协议
java·前端·数据库