我用 ThreadLocal 存当前登录用户,本以为线程私有绝不会串,结果某个用户偶尔会看到另一个用户的数据,因为在线程池里线程是复用的、用完没清的 ThreadLocal 残留给了下一个请求的深度复盘

我用 ThreadLocal 存当前登录用户(请求进来时 set、后续各层从 ThreadLocal 取,避免层层传参),想当然以为 ThreadLocal 线程私有、每个请求一个线程各存各的绝不会串。结果线上出了最可怕的 bug:偶发地某个用户页面上显示出了另一个用户的数据(订单、隐私),用户身份串了、是严重越权。排查才定位到线程池:Web 服务器用线程池处理请求、线程是复用的(处理完一个请求会被拿去处理下一个),而我请求处理完没 remove() ThreadLocal,于是线程上残留着上一个请求 set 的 User;线程池复用这个线程处理另一个用户的请求时,若新请求某分支没 set 就 get,就读到了上一个用户残留的 User、数据就串了。复盘才搞懂:ThreadLocal 的值绑定在线程上、不是绑定在请求上;线程不是用完就销毁而是被线程池复用,所以值会一直残留直到被覆盖或 remove;线程私有只防线程间串、不防同一线程复用时前后任务串。这篇复盘从故障现场讲到 ThreadLocal 与线程复用、线程私有的真义、内存泄漏,再到用完必 remove(拦截器 afterCompletion/try-finally)、get 防 null、跨线程用 TransmittableThreadLocal、封装自动清理的完整正解,以及其他复用载体上残留没清理的坑,和复用的载体会继承上一个使用者的残留、用完不清会串扰污染、清理恢复原状是复用的隐含契约的认知。

我用 ThreadLocal 存当前登录用户,本以为线程私有绝不会串,结果某个用户偶尔会看到另一个用户的数据,因为在线程池里线程是复用的、用完没清的 ThreadLocal 残留给了下一个请求:一次没清理复用载体上的残留、ThreadLocal 串数据的深度复盘

那个"偶发的、用户看到别人数据"的严重 bug,源于我对 ThreadLocal 在线程池里行为的误解。我用 ThreadLocal<User> 存"当前登录用户"(请求进来时 set,后续各层从 ThreadLocal 取,避免层层传参)。我想当然地以为:ThreadLocal 是"线程私有"的,每个请求一个线程、各存各的,绝不会串。结果线上出了最可怕的那种 bug:偶发地,某个用户的页面上,显示出了另一个用户的数据(甚至是别人的订单、隐私信息)——用户身份串了,这是严重的越权/数据泄露。排查了好久,才定位到:问题出在 线程池——Web 服务器用线程池处理请求,线程是复用(处理完一个请求,这个线程会被拿去处理下一个请求);而我请求处理完没有 remove() ThreadLocal,于是这个线程上残留着上一个请求 set 进去的 User;当线程池复用这个线程处理另一个用户的请求,如果新请求某个分支没 set 就直接 get(或在 set 之前 get),就读到了上一个用户残留的 User——数据就这么串了。复盘这件事,我才彻底搞懂,后背发凉:问题出在我以为"ThreadLocal 线程私有就绝对安全",却忽略了"在线程池里,线程是复用"这个关键前提。ThreadLocal 的值是绑定在"线程"上的,不是绑定在"请求/任务"上;线程不是用完就销毁,而是被线程池复用——所以一个线程上的 ThreadLocal 值,会一直存在,直到被覆盖或 remove;用完没 remove(),残留的值就跟着复用的线程,传给了下一个使用这个线程的请求——这既造成数据串扰(越权),残留的大对象也造成内存泄漏。根本原因是:ThreadLocal 的值绑定在线程上(非请求),线程池里线程被复用、值会残留;我用完没 remove(),残留值被复用同一线程的下一个请求读到,造成数据串扰/越权和内存泄漏。问题的根,是没清理复用载体(线程)上的残留——ThreadLocal 用完没 remove,在线程复用的线程池里残留给了下一个请求;根源是以为线程私有就安全、忽略了线程会被复用。这篇就把这次"ThreadLocal 串数据"的坑,从头到尾复盘一遍。

故障现场:线程复用,残留的用户串给了下一个请求

问题在于 ThreadLocal 用完没清、残留在复用的线程上:

// 我的写法: ThreadLocal存当前用户, 但用完没remove
public class UserContext {
    private static final ThreadLocal CURRENT = new ThreadLocal<>();
    public static void set(User u) { CURRENT.set(u); }
    public static User get() { return CURRENT.get(); }   // ✗ 可能拿到上个请求残留的User!
    // 缺一个 remove()!
}

// 拦截器: 请求进来set用户
public boolean preHandle(HttpServletRequest req, ...) {
    User u = parseUser(req);
    UserContext.set(u);
    return true;
    // ✗ 请求结束没有 UserContext.remove() —— 用户残留在这个线程上!
}

/*
数据串了是怎么发生的(线程池复用 + 没清理):
  1. 请求A进来, 由线程池的 thread-5 处理, set了 用户A 到ThreadLocal;
  2. 请求A处理完, 但【没remove】 → thread-5 上残留着 用户A;
  3. thread-5 被线程池【复用】, 拿去处理 请求B(另一个用户);
  4. 若请求B 某个分支【没set就get】(如未登录接口、set之前的逻辑、set失败), 就读到了 thread-5 上
     残留的 用户A → 请求B 看到了 用户A 的数据! → 数据串扰、越权、隐私泄露!
  5. 而且: 残留的User大对象一直挂在复用的线程上 → 内存泄漏(线程池线程长期存活)。

为什么 ThreadLocal "线程私有"不等于"安全":
  - ThreadLocal的值绑定在【线程】上, 不是绑定在【请求/任务】上;
  - 在线程池里, 线程是【复用】的(不是用完销毁): 处理完A的线程会去处理B;
  - 所以线程上的ThreadLocal值, 会从"上一个任务"残留到"下一个任务"——除非你remove它;
  - "线程私有"防的是"不同线程之间串", 但防不了"同一线程被复用时, 前后任务之间串"!

ThreadLocal内存泄漏的额外原因:
  - ThreadLocalMap的key是ThreadLocal的弱引用, 但value是强引用;
  - 若ThreadLocal对象被回收但value没remove, value会一直挂在线程上(线程池线程长寿)→ 泄漏。

★ 核心: ThreadLocal的值绑在线程上、不绑在请求上; 线程池里线程复用, 值会残留给下一个任务;
  用完【必须 remove()】(用try-finally保证), 否则数据串扰(越权)+内存泄漏。"线程私有"≠"任务私有"。

看着"用户 B 看到了用户 A 的数据"这种最不该发生的越权,我又惊出一身冷汗又恍然:"我一直觉得 ThreadLocal 线程私有,一个请求一个线程,各管各的、绝不会串……谁知道线程池里线程是复用的!我用完不清,残留的用户就跟着这个线程,串给了下一个请求。'线程私有'和'请求私有'根本是两回事。"这个坑最危险的地方在于:偶发、难复现(只有"线程恰好被复用 + 新请求恰好没 set 就 get"时才串);它平时大量正常(每个请求都 set 了就不会读到残留),给你"没问题"的错觉;可一旦串了,后果是最严重的安全问题(越权、隐私泄露)下面就来拆解,ThreadLocal 该怎么正确用。

第一件事:搞懂 ThreadLocal 与线程复用

我顺着这次事故,把 ThreadLocal 和线程池复用的关系彻底理清了。

ThreadLocal 为什么在线程池里会串数据? 怎么正确用?

【核心: ThreadLocal值绑在线程上(非请求)、线程池里线程复用、值会残留给下一个任务; 用完必须remove()
   (try-finally), 否则数据串扰(越权)+内存泄漏; "线程私有"≠"请求/任务私有", 复用的载体用完要清干净】

1. ThreadLocal 是什么:
   - 给每个线程一份独立的变量副本, 实现"线程私有"的数据存储(避免传参、避免共享变量加锁);
   - 常用于: 存当前用户、traceId、数据库连接、事务上下文等"请求级"信息。

2. 为什么会串(线程复用 + 没清理):
   - ThreadLocal的值绑定在【线程】对象上, 不绑定在"请求/任务"上;
   - 线程池里线程是【复用】的: 处理完任务A的线程, 会被拿去处理任务B(线程不销毁);
   - 所以线程上的ThreadLocal值, 会从A残留到B——除非你remove;
   - 若B没set就get(或set前get), 就读到A的残留值 → 串数据、越权。

3. "线程私有"的真正含义(别误解):
   - 它保证的是: 不同线程看到的是各自的副本(线程间不串);
   - 它【不保证】: 同一个线程被复用时, 前后任务的数据隔离——这要靠你remove。

4. 内存泄漏:
   - ThreadLocalMap: key是ThreadLocal的弱引用, value是强引用;
   - ThreadLocal对象被回收后, value还强引用着、挂在长寿的线程池线程上 → 泄漏;
   - 用完remove能同时解决"串数据"和"内存泄漏"。

5. 正确用法:
   ① 用完必须 remove(): 在请求结束(拦截器afterCompletion/finally)清理;
      try { UserContext.set(u); ...处理... } finally { UserContext.remove(); }
   ② 别假设get一定有值: get前判断null, 或确保每个请求入口都set;
   ③ 在线程池/异步场景特别小心: 任务被不同线程执行、线程被复用, ThreadLocal更易串;
   ④ 跨线程传递上下文用专门机制(TransmittableThreadLocal等), 别裸用ThreadLocal跨线程。

6. 本质: 在"复用的载体"上留下的状态, 不会随你的任务结束而消失, 会被下一个使用者看到
   - 线程是复用的载体, ThreadLocal是留在上面的状态;
   - 用完不清理, 残留就会"串"给下一个使用者(数据串扰)、或一直占着(泄漏);
   - 凡复用的东西(线程、连接、对象池、缓冲区), 用完都要"恢复干净", 别留残留。

一句话: ThreadLocal值绑在线程上(非请求)、线程池复用线程、值会残留串给下一个任务并泄漏; 用完必须
   remove()(try-finally); "线程私有"≠"任务私有"; 凡复用的载体, 用完要清理残留、恢复干净状态。

这套认知,是整个坑的根。ThreadLocal 是什么:给每个线程一份独立副本,常存当前用户/traceId/连接等请求级信息为什么会串:值绑在线程上(非请求)、线程池里线程复用(不销毁)、值从 A 残留到 B,B 没 set 就 get 就读到 A 的残留"线程私有"的真义:它保证不同线程间不串,但不保证同一线程被复用时前后任务隔离——那要靠你 remove内存泄漏:ThreadLocalMap 的 value 是强引用,不 remove 会挂在长寿的线程池线程上泄漏正确用法:用完必 remove()(try-finally)、别假设 get 有值、线程池/异步场景特别小心、跨线程用专门机制本质:在"复用的载体"上留下的状态不会随任务结束消失、会被下一个使用者看到;凡复用的东西用完都要恢复干净一句话:ThreadLocal 值绑在线程上(非请求)、线程池复用线程、值会残留串给下一个任务并泄漏;用完必须 remove()(try-finally);"线程私有"≠"任务私有";凡复用的载体,用完要清理残留、恢复干净状态。

第二件事:正解——用完 remove,在请求边界清理

知道了线程复用会残留,正解就清楚了:用完必 remove,在请求结束的边界统一清理。

// 正解1: 提供 remove(), 并在请求结束时清理(本次缺的)
public class UserContext {
    private static final ThreadLocal CURRENT = new ThreadLocal<>();
    public static void set(User u) { CURRENT.set(u); }
    public static User get() { return CURRENT.get(); }
    public static void remove() { CURRENT.remove(); }    // ✓ 必须提供并调用
}

// 正解2: 在拦截器的 afterCompletion(请求彻底结束)里 remove —— 保证一定清理
public class UserInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest req, ...) {
        UserContext.set(parseUser(req));
        return true;
    }
    public void afterCompletion(HttpServletRequest req, ..., Exception ex) {
        UserContext.remove();          // ✓ 无论请求成功失败, 都清理, 不残留给下个请求
    }
}

// 正解3: 或用 try-finally 包住作用域, 保证 remove
public Result handle(Request req) {
    UserContext.set(req.getUser());
    try {
        return doBusiness();
    } finally {
        UserContext.remove();          // ✓ 用完即清, 异常也清(注意finally别return, 同605)
    }
}

// 正解4: get 前别假设一定有值(防御)
User u = UserContext.get();
if (u == null) {
    throw new UnauthorizedException("未登录");   // 而非默默用到残留值; 至少能暴露问题
}

// 正解5: 跨线程/异步传递上下文, 别裸用ThreadLocal
//   - 提交到线程池的任务、CompletableFuture、@Async, 子线程拿不到父线程的ThreadLocal;
//   - 需要传递用 阿里 TransmittableThreadLocal(TTL) 等专门方案, 或显式把上下文作参数传进任务。

// 反例(别这样):
// 只 set 不 remove                      // ✗ 线程池复用时残留, 串数据+泄漏(本次)
// 在异步任务里直接 ThreadLocal.get()    // ✗ 子线程拿不到/拿到错的, 用TTL或传参

// 核心: ThreadLocal用完必remove()(拦截器afterCompletion/try-finally), 在请求边界清理;
//   get前防null; 跨线程传上下文用TransmittableThreadLocal或显式传参, 别裸用。

这套正解的关键,是把"用完清理"作为铁律,在请求的边界统一 remove,让复用的线程每次都从干净状态开始提供并调用 remove():这是本次缺的——只 set 不 remove 必残留。在拦截器 afterCompletion 清理:请求彻底结束时 remove,无论成功失败都清,不残留给下个请求。或 try-finally 包住作用域:用完即清、异常也清。get 前防 null:别默默用到残留值,至少暴露问题。跨线程传上下文用专门方案:子线程拿不到父线程 ThreadLocal,用 TransmittableThreadLocal 或显式传参,别裸用。

第三件事:其他几个"复用载体上残留没清理"的坑

顺着这次 ThreadLocal,我把"在复用的载体上留下状态没清理"的几类坑也一并理了:

几类"复用载体上残留没清理"的坑:

坑1: ThreadLocal用完没remove(本篇)——线程复用残留串数据/泄漏; 正解: 用完remove。

坑2: 连接池的连接上残留会话状态——上个请求改了连接的会话变量/未提交事务, 复用时影响下个;
   正解: 归还连接前重置会话状态/回滚未完事务(连接池一般会, 但自定义状态要自己清)。

坑3: 对象池/缓冲区复用没清空——从对象池借的对象残留上次的数据, 直接用导致脏数据;
   正解: 借出/归还时重置对象到初始状态。

坑4: 单例/静态变量被请求改了——静态可变状态被一个请求改了, 影响其他请求(本就该避免可变静态);
   正解: 别用可变的单例/静态存请求级状态。

坑5: 复用的Bean(Spring默认单例)存了请求级字段——单例Bean里放可变实例字段存当前请求数据, 并发串;
   正解: 单例Bean别存请求级可变状态, 用方法参数/ThreadLocal(且remove)/request作用域bean。

坑6: 缓冲区/数组复用没清残留长度——复用byte[]读数据, 上次的残留字节没清, 读到脏数据;
   正解: 按实际长度处理, 或复用前清零。

共同的根: 为了性能, 我们复用很多"载体"(线程、连接、对象、缓冲区、单例); 而【复用】意味着
   "上一个使用者留下的状态, 会被下一个使用者看到"——除非用完【清理/重置】; 凡复用的载体, 都遵循
   "用完归还时, 必须恢复到干净的初始状态"的纪律, 否则前一个的残留会串扰/污染后一个。

这些坑看似不同,根却是同一个:为了性能,我们复用很多"载体"(线程、连接、对象、缓冲区、单例);而复用意味着"上一个使用者留下的状态,会被下一个使用者看到"——除非用完清理/重置凡复用的载体,都要遵循"用完归还时,必须恢复到干净的初始状态"的纪律。认清这个根("复用的载体用完要清理残留、恢复原状"),才不会让前一个的残留污染后一个。

第四件事:ThreadLocal 风险 / 正确清理——两张对照表

我把 ThreadLocal 在不同场景的风险、以及正确用法要点,整理成对照表,贴在了团队的 Java 规范里:

场景 风险 对策
线程池复用 + 没 remove 数据串扰、越权、泄漏 用完 remove(必做)
get 前没 set 读到上次残留值 每入口 set + get 防 null
异步/子线程 get 拿不到父线程的值 TransmittableThreadLocal/传参
存大对象不清 内存泄漏(线程长寿) 用完 remove
异常路径没清理 残留 finally / afterCompletion 清
用法要点 怎么做
清理时机 请求结束(afterCompletion/finally)
清理方式 ThreadLocal.remove()
取值防御 get 后判 null
跨线程 TransmittableThreadLocal 或传参
别用 只 set 不 remove;异步裸用

这两张表的核心,第一张是ThreadLocal 在"线程复用 + 没 remove"时会数据串扰(越权)和泄漏,这是它最危险的场景;第二张是用法铁律就一条——"用完 remove,在请求边界清理"。记住一条:用 ThreadLocal,set 和 remove 必须成对;尤其在线程池/Web 环境,没 remove 就是埋了串数据的雷。

第五件事:关于 ThreadLocal 的几组容易想当然的认知

这次事故也让我厘清了几组关于 ThreadLocal 的、容易想当然的概念:

直觉以为 实际上
ThreadLocal 线程私有,绝不会串 线程池复用线程,值会残留串给下个任务
一个请求一个线程,各管各的 线程是复用的,不是一请求一新线程
"线程私有"=="请求私有" 不等;线程私有不保证复用时任务隔离
用完不 remove 没关系 残留串数据(越权)+内存泄漏
get 一定拿到本请求 set 的值 没 set 时拿到上次残留值
子线程能拿到父线程的 ThreadLocal 拿不到;要用 TTL 或传参
ThreadLocal 不会内存泄漏 value 强引用挂在长寿线程上会泄漏

这张表里,我栽的是第一行和第三行:把"ThreadLocal 线程私有"理解成了"绝对安全、请求私有",没意识到线程池里线程是复用的、"线程私有"防不住复用时前后任务串厘清这些,核心是一个意识:ThreadLocal 的"私有"是绑在"线程"上的,而在线程池里"线程"是被反复复用的载体——所以它的值会从一个任务残留到下一个任务;"线程私有"绝不等于"任务私有",要靠你在用完时 remove 来保证每个任务从干净状态开始。

第六件事:用 ThreadLocal / 复用资源时,我现在的自检习惯

现在每当我用 ThreadLocal、或使用任何"会被复用的载体",我都会先按这张图问自己:

这张图的精髓,是"复用的载体用完必清/重置、在边界清理、取用前防残留、跨线程用专门方案"先问载体会不会复用(会就用完清)、清理放在边界(afterCompletion/finally)、取用前防残留跨线程用 TTL这套习惯,让我从"ThreadLocal 私有就放心"变成了"复用载体用完必清"——核心始终是:ThreadLocal 值绑在线程上(非请求)、线程池复用线程、值会残留串给下一个任务并泄漏;用完必须 remove();凡复用的载体,用完要清理残留、恢复干净状态。

我立下的几条规矩

这场"ThreadLocal 串数据、用户看到别人信息"的事故,换来了我用 Java 时,刻进骨子里的几条铁律:

  1. ThreadLocal 的值绑定在"线程"上,不绑定在"请求/任务"上。
  2. 线程池里线程是复用的(不是一请求一新线程);ThreadLocal 值会从上个任务残留到下个任务。
  3. "线程私有"只防线程间串,不防同一线程复用时前后任务串——那要靠 remove。
  4. ThreadLocal 用完必须 remove(),在请求边界(拦截器 afterCompletion / try-finally)统一清理。
  5. 不 remove 会导致:数据串扰(越权、隐私泄露)+ 内存泄漏(value 挂在长寿线程上)。
  6. get 前别假设一定有值,判 null,别默默用到上次的残留值。
  7. 跨线程/异步传上下文用 TransmittableThreadLocal 或显式传参,别裸用 ThreadLocal;凡复用载体用完都要恢复干净。

附:一个自动清理 ThreadLocal 的封装

借这次的坑,我把 ThreadLocal 的"set + 用完自动 remove"封装成一个工具,用 try-with-resources 自动清理,从根上杜绝忘了 remove。

// 把 set + 自动remove 封装成 AutoCloseable, 用 try-with-resources 保证清理
public final class ScopedUser implements AutoCloseable {
    private static final ThreadLocal CURRENT = new ThreadLocal<>();

    public static User get() {
        User u = CURRENT.get();
        if (u == null) throw new IllegalStateException("当前无用户上下文(未set或已清理)");
        return u;
    }

    // 进入作用域时set, close时(作用域结束)自动remove
    public static ScopedUser bind(User u) {
        CURRENT.set(u);
        return new ScopedUser();
    }

    @Override public void close() {
        CURRENT.remove();          // ✓ try-with-resources 结束时自动调用, 保证清理
    }
}

// 用法: try-with-resources, 离开作用域自动remove, 想忘都忘不了
public Result handle(Request req) {
    try (ScopedUser ignored = ScopedUser.bind(req.getUser())) {
        return doBusiness();        // 业务里 ScopedUser.get() 取当前用户
    }                               // ← 这里自动 remove(), 即使doBusiness抛异常也清
}

// 原则: 把"必须成对、容易漏掉后半段"的操作(set/remove、lock/unlock、open/close),
//   封装成 try-with-resources / try-finally 的形式, 让"清理"由语言机制自动保证,
//   而不是依赖开发者每次记得手写 remove —— 把容易忘的纪律, 变成想忘都忘不了的结构。

这个封装的价值,在于它把"ThreadLocal 用完要 remove"这条容易忘的纪律,变成了 try-with-resources 自动保证的结构:只要用 try (ScopedUser.bind(u)) 的形式,离开作用域就自动 remove,想忘都忘不了。它和前面 605(try-with-resources)、612(never 穷尽检查)一脉相承:对于"必须成对、容易漏掉后半段"的操作(set/remove、lock/unlock、open/close),把它封装成由语言机制自动兜底的结构,远比依赖人每次记得手写可靠。

写在最后

回头看,这场由"ThreadLocal 用完没清、残留串数据"引发的越权事故,真正教给我的,远不止"ThreadLocal 用完 remove"这一个技巧。它让我对"当我们为了效率复用一个'载体'(线程、容器、空间、资源)时, 上一个使用者在它上面留下的痕迹, 如果不主动清除, 就会被下一个使用者继承; 而'复用带来的高效'和'残留带来的污染', 是同一枚硬币的两面——享受复用, 就必须承担'用完清理、恢复原状'的责任",有了一次刻骨的体会。我栽跟头,是因为我只想着"复用线程的高效", 却忘了"复用"的另一面: 这个被复用的线程, 带着我上次留下的东西——我以为每个请求拿到的是一张"白纸"(干净的线程), 可它其实是一张"上一个请求写过、我没擦干净的纸";我把"线程私有"误当成了"每次都是全新的", 却没意识到: "私有'说的是'不和别的线程共享', 而'复用'说的是'这个线程本身是反复使用的、带着历史的'; 我没在用完后把这张纸擦干净, 下一个用它的人, 就读到了我留下的字这让我领悟到一个关于"复用、残留与归还"的深刻认知:"复用"是提升效率的普遍手段(复用线程、连接、对象、内存、空间、工具), 但它有一个常被忽略的前提: 每个使用者用完后, 必须把这个共享的载体'恢复到干净的、可供下一人使用的状态'——否则, 一个使用者留下的残留(状态、数据、副作用), 就会跨越'使用的边界', 污染、误导、甚至危害下一个使用者;这就是为什么"用完归还时清理/重置'是一切'资源复用'的隐含契约: 借了书要还回原处、用了公共厨房要打扫干净、ThreadLocal用了要remove、对象池借了要重置——"把它恢复原状'不是可选的礼貌, 而是'复用'能安全成立的必要条件;谁享受了"不用每次重新创建'的便利, 谁就欠下了"用完要清理干净'的债这给了我一种使用"复用资源"时的自觉:每当我使用一个"会被反复复用的载体"(线程、连接、对象池、缓冲区、共享空间)时,要清醒地意识到"我不是它的唯一使用者, 我用完它还要交给下一个人; 我在它上面留下的任何状态, 不清掉就会跟着它传下去"——把"用完彻底清理、恢复到干净的初始状态"当成使用复用资源的必尽义务(用 try-finally/边界清理保证), 而非可有可无;"认清复用的载体会继承前者的残留、用完必须清理恢复原状",是安全享受一切复用、避免残留污染的关键认清复用的载体会继承上一个使用者的残留、用完不清会串扰污染下一个、清理恢复原状是复用的隐含契约——这,是我用一次 ThreadLocal 串数据的事故,换来的、关于 Java、也关于如何对待一切复用资源的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次用 ThreadLocal 时,把那个 remove() 稳稳地写进 finally 或拦截器的 afterCompletion 里,那我对着那个"用户看到别人数据"的越权 bug 惊出的那身冷汗,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的 HTTP 请求明明已经超时返回了,可它在后台启动的 goroutine 还在埋头跑、下游调用也没停,goroutine 越积越多内存一路涨,因为我没把 context 的取消信号传下去也没人监听它的深度复盘

2026-6-3 3:05:38

技术教程

我的分页接口每次都查一下总共有多少条数据好显示总页数,小表时秒回,数据涨到几千万后这个 COUNT 比查数据本身还慢、直接拖垮了接口的深度复盘

2026-6-3 3:16:52

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索