1. 从“context-mode”说起一个被低估的工程概念第一次看到“context-mode”这个词很多人会下意识地把它归到某个具体框架的配置项里觉得无非是切换一下运行模式而已。但如果你真正在项目里跟上下文打过交道就会明白这四个字背后藏着一整套关于“状态管理”和“执行环境隔离”的工程哲学。我在过去几年里先后在几个不同规模的项目中处理过上下文相关的问题从最初级的全局变量污染到后来复杂的多租户上下文切换踩过的坑足够写一本小册子。这篇文章就把我对 context-mode 的理解、实践和踩坑经验完整地摊开来讲。先给不太熟悉的朋友一个直观的定义。所谓 context-mode本质上描述的是一套系统在运行过程中如何定义、切换和隔离“上下文”的机制。这里的“上下文”可以理解为程序执行时所需的一切环境信息的总和——包括但不限于当前用户身份、请求链路标识、配置参数、数据库连接、缓存句柄、日志追踪 ID 等等。而“mode”则意味着这套机制不是一成不变的它可以根据不同的场景切换成不同的行为模式。举个生活化的例子你早上出门上班穿的是通勤装背的是电脑包脑子里想的是今天要开的会晚上去健身房换运动服带毛巾和水壶脑子里想的是今天练哪个部位。同样是“你”这个人但所处的 context 完全不同行为模式也随之切换。程序里的 context-mode 解决的就是类似的问题。那为什么这个概念值得单独拿出来讲因为绝大多数线上事故的根源追到最后都能归结到上下文管理出了问题。我见过太多这样的案例A 用户的请求拿到了 B 用户的数据原因是上下文在异步调用中被污染了定时任务执行时读到了 Web 请求的上下文导致空指针崩溃多线程环境下上下文变量互相覆盖日志里全是错乱的追踪信息。这些问题在开发环境往往复现不了一到生产环境就间歇性发作排查起来极其痛苦。所以把 context-mode 这件事想清楚、做扎实是每一个后端工程师和架构师绕不过去的必修课。这篇文章适合谁看如果你正在设计一个需要处理多用户、多租户、多请求链路的系统或者你已经被上下文污染问题折磨过又或者你只是单纯想搞清楚这个概念到底在说什么那接下来的内容应该对你有用。我会从设计思路讲到具体实现从参数选择讲到问题排查尽量把每个决策背后的“为什么”都讲透。2. 上下文模式的核心设计思路与选型考量2.1 为什么需要显式的 context-mode 而不是隐式传递在早期的项目里我习惯把上下文信息通过函数参数一层层往下传。比如一个处理订单的函数需要用户 ID、租户 ID、追踪 ID那就把这些都塞进参数列表里。刚开始还好函数少、层级浅传起来不费劲。但随着业务膨胀一个请求要经过十几层调用参数列表越来越长每加一个上下文字段就要改几十个函数签名。更麻烦的是有些中间件、拦截器、异步任务根本没法通过参数传递你总不能把用户 ID 塞进消息队列的每一条消息里吧。这时候显式的 context-mode 就体现出价值了。它的核心思路是把上下文从“数据”变成“环境”。数据需要显式传递而环境是隐式可感知的。就像你不需要每次呼吸都告诉肺“现在吸入氧气”肺自然知道该干什么。程序里的上下文也应该这样——当前执行流天然知道自己处于什么上下文不需要每个函数都显式接收。但这里有个关键的设计抉择上下文到底应该绑定在什么维度上我见过三种主流方案各有优劣。第一种是线程绑定。上下文存在线程本地存储里同一个线程内随处可读。这是最直观的方案实现简单性能也好。但它的致命缺陷在于异步和线程池场景——你把任务丢给线程池新线程根本读不到原线程的上下文。我早期就栽过这个跟头一个异步导出功能导出的文件里全是空白的用户信息查了半天才发现是线程切换导致上下文丢失。第二种是执行流绑定。上下文跟着调用链走无论同步异步、线程怎么切换只要还在同一条执行流上上下文就能正确传递。这个方案更健壮但实现复杂度高需要框架层面的支持。像 Go 语言里的 context.Context 就是这种思路的典型代表通过显式传递 context 对象来保证执行流的连续性。第三种是请求绑定。上下文挂在请求对象上生命周期与请求一致。这在 Web 框架里很常见但一旦跳出请求处理的范围比如后台任务上下文就失效了。我的经验是没有银弹要根据系统的实际形态来选。如果你的系统以同步调用为主线程绑定够用且简单如果大量使用异步和协程执行流绑定是必须的如果是纯 Web 应用且不涉及后台任务请求绑定最省事。但现实往往是混合形态所以更务实的做法是设计一个可插拔的 context-mode 抽象层底层实现可以按场景切换。2.2 上下文隔离级别从共享到完全隔离的频谱选定了绑定维度下一个要决策的是隔离级别。这其实是一个频谱从完全共享到完全隔离中间有无数种可能。最左端是全局共享。所有执行流共用一份上下文改一处影响全局。这种模式只适合单用户、单任务的场景比如命令行工具。一旦涉及并发就是灾难。往右一点是按请求隔离。每个请求有独立的上下文副本请求之间互不干扰。这是 Web 应用最常见的模式也是大多数人理解的“上下文隔离”。但这里有个隐蔽的坑如果请求内部又发起了对下游服务的调用下游调用是否应该继承上游的上下文如果继承那下游的上下文就和上游耦合了如果不继承那追踪链路就断了。我一般建议采用“继承但可覆盖”的策略——下游默认继承上游的追踪 ID 和用户身份但可以覆盖自己的配置参数。再往右是按租户隔离。在多租户系统里不同租户的上下文必须严格隔离否则就是数据泄露。这种隔离不仅要体现在数据层面还要体现在配置、缓存、连接池等所有资源上。我见过一个 SaaS 系统因为缓存 key 没有加租户前缀导致 A 租户读到了 B 租户的缓存数据上线当天就被客户投诉了。最右端是完全隔离。每个执行单元有自己独立的上下文空间连内存都不共享。这通常用在安全要求极高的场景比如沙箱执行。但代价是资源开销大通信成本高。实际选型时我通常会用一张表来辅助决策隔离级别适用场景优点缺点全局共享单用户工具、脚本实现最简单并发下完全不可用按请求隔离常规 Web 应用隔离性好实现成熟异步场景需额外处理按租户隔离多租户 SaaS数据安全有保障资源开销较大完全隔离沙箱、安全敏感场景安全性最高性能和通信成本高这张表我在多个项目里反复用过基本能覆盖八成以上的决策场景。剩下的两成往往需要根据具体的性能要求和安全等级做微调。2.3 上下文传播跨进程、跨线程、跨异步的挑战上下文一旦离开当前执行单元传播就成了大问题。我把它拆成三种情况来看。跨线程传播是最常见的。线程池里的线程是复用的上一个任务残留的上下文如果不清理下一个任务就会读到脏数据。解决方案有两种一是每次任务提交时把上下文快照传进去任务开始时恢复结束时清理二是用可继承的线程本地存储让子线程自动继承父线程的上下文。前者更可控后者更方便但容易出意外。我一般推荐前者因为显式快照能让代码意图更清晰。跨异步传播更复杂。异步任务的执行时机不确定可能在原上下文已经销毁之后才执行。这时候如果还持有原上下文的引用就会读到已失效的数据。正确的做法是在异步任务创建时就把需要的上下文值拷贝出来而不是持有引用。这个坑我在做消息队列消费时踩过——消费者线程读到了生产者线程已经失效的上下文导致用户信息全是 null。跨进程传播涉及序列化和反序列化。上下文里的数据必须能被序列化否则传不过去。而且跨进程传播时要注意数据大小别把整个上下文对象都塞进 HTTP header 里那样既慢又容易超限。通常只传必要的标识字段比如追踪 ID、租户 ID其他信息由接收方自己查。3. 核心实现细节与实操要点拆解3.1 上下文对象的生命周期管理上下文对象的生命周期管理是 context-mode 实现中最容易出问题的地方。我把它分成四个阶段创建、绑定、使用、销毁。每个阶段都有讲究。创建阶段关键决策是“懒创建”还是“预创建”。懒创建是指第一次访问时才初始化上下文对象好处是省资源坏处是首次访问有延迟而且并发下需要加锁。预创建是指在执行流开始时就把上下文准备好好处是后续访问无延迟坏处是即使这次执行不需要上下文也会创建。我一般推荐预创建因为上下文创建的开销通常很小而并发下的懒创建加锁反而更贵。绑定阶段要把上下文对象和执行流关联起来。如果是线程绑定就是设置线程本地变量如果是执行流绑定就是把上下文对象挂到调用链上。这里要注意的是绑定的时机——必须在任何可能访问上下文的代码执行之前完成绑定。我见过一个 bug拦截器里先执行了一段日志代码日志代码里访问了上下文但此时上下文还没绑定结果日志里全是默认值。使用阶段要注意的是上下文的可变性。如果上下文对象是可变的那并发修改就会出问题。我的建议是上下文对象一旦创建就不可变需要修改时创建新的副本。这样虽然多了一些对象创建开销但换来了线程安全值得。销毁阶段最容易被忽视。很多人觉得执行流结束了上下文自然就被回收了不用管。但在使用线程池的场景下线程不销毁上下文就不会自动清理。必须显式地在任务结束时清理上下文否则下一个任务就会读到残留数据。我通常会在拦截器的 finally 块里做清理确保无论任务成功还是异常上下文都能被正确销毁。3.2 上下文数据的存取性能优化上下文数据的存取频率极高几乎每个函数调用都可能访问所以性能优化很有必要。我总结了几个实用的优化点。第一减少上下文对象的层级。有些实现把上下文设计成嵌套的 Map访问一个值要经过好几层查找。我建议把常用的字段直接作为上下文对象的属性用扁平结构存储。实测下来扁平结构的访问速度比嵌套 Map 快三到五倍。第二使用不可变对象和写时复制。前面提到上下文对象应该不可变但不可变意味着每次修改都要创建新对象。如果修改频繁对象创建开销会很大。解决方案是写时复制——只有在真正需要修改时才复制读操作直接读原对象。这样读多写少的场景下性能很好。第三缓存热点数据。有些上下文数据是从数据库或远程服务查来的每次访问都查一遍太慢。可以在上下文对象里加一层缓存第一次查完后存起来后续直接读缓存。但要注意缓存的失效策略别让过期数据一直留在上下文里。第四避免在上下文里存大对象。上下文应该只存标识和小配置大对象应该通过标识去外部存储查。我见过有人在上下文里存了整个用户对象包括头像二进制数据结果内存暴涨。上下文要轻这是原则。3.3 上下文与日志追踪的集成上下文和日志追踪是天然的一对。有了上下文日志里就能自动带上追踪 ID、用户 ID、租户 ID排查问题时能快速定位到具体请求。但集成时也有几个坑。第一个坑是日志框架的上下文感知。不是所有日志框架都能自动读取上下文有些需要手动把上下文值塞进日志的 MDC 里。我通常会在拦截器里做这个同步——上下文绑定时把关键字段同步到日志的 MDC上下文销毁时清理 MDC。这样日志里就能自动带上这些字段不用每个日志语句都手动传。第二个坑是异步日志的上下文丢失。如果日志是异步写的写日志的线程和业务线程不是同一个上下文就传不过去。解决方案是在日志事件里快照上下文值而不是在写日志时才去读上下文。这样即使异步写日志里带的也是正确的上下文。第三个坑是上下文值太多导致日志膨胀。如果把上下文里所有字段都塞进日志日志体积会暴涨。我一般只选三到五个关键字段进日志比如追踪 ID、用户 ID、租户 ID、请求路径。其他字段需要时再单独打。3.4 多租户场景下的上下文隔离实现多租户是 context-mode 最考验功力的场景。我做过一个 SaaS 系统租户数量上千每个租户有独立的配置、数据库 schema、缓存空间。上下文隔离没做好就是数据泄露。我的实现方案是这样的上下文对象里有一个 tenantId 字段所有数据访问层在操作前都必须先从这个字段取租户 ID然后路由到对应的数据源。为了防止遗漏我在数据访问层的基类里做了强制校验——如果 tenantId 为空直接抛异常不允许执行任何查询。这样虽然牺牲了一点灵活性但换来了安全性。缓存隔离更麻烦。不同租户的缓存 key 必须加租户前缀否则就会串。我见过一个系统缓存 key 是“user:123”结果两个租户都有 ID 为 123 的用户缓存互相覆盖。解决方案是在缓存 key 生成器里统一加租户前缀比如“tenant:456:user:123”。这个前缀生成逻辑要封装好不能让业务代码自己拼否则迟早有人忘。还有一个隐蔽的坑是连接池隔离。如果多个租户共用一个数据库连接池那连接上的会话变量比如当前 schema就可能被其他租户覆盖。解决方案是要么每个租户独立连接池要么在每次获取连接时重新设置会话变量。前者资源开销大后者性能开销大需要根据租户数量和请求量权衡。4. 完整实操流程与关键环节实现4.1 从零搭建一个上下文管理模块假设我们要在一个 Java Web 项目里实现 context-mode我会按下面的步骤来搭。第一步定义上下文对象。创建一个不可变的 Context 类包含需要的字段。用建造者模式来创建避免构造函数参数过多。public final class RequestContext { private final String traceId; private final String userId; private final String tenantId; private final MapString, String attributes; private RequestContext(Builder builder) { this.traceId builder.traceId; this.userId builder.userId; this.tenantId builder.tenantId; this.attributes Collections.unmodifiableMap( new HashMap(builder.attributes)); } // getter 方法省略 public static class Builder { private String traceId; private String userId; private String tenantId; private MapString, String attributes new HashMap(); public Builder traceId(String traceId) { this.traceId traceId; return this; } // 其他 builder 方法省略 public RequestContext build() { return new RequestContext(this); } } }第二步实现上下文持有器。用 ThreadLocal 来绑定上下文但要做成可继承的以便子线程能拿到父线程的上下文。public class ContextHolder { private static final InheritableThreadLocalRequestContext CONTEXT new InheritableThreadLocal(); public static void set(RequestContext context) { CONTEXT.set(context); } public static RequestContext get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }这里用 InheritableThreadLocal 而不是普通 ThreadLocal是为了让子线程能继承父线程的上下文。但要注意线程池里的线程是复用的继承只发生在创建时复用时不继承。所以线程池场景还需要额外处理。第三步实现拦截器。在请求进入时创建上下文并绑定请求结束时清理。public class ContextInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { RequestContext context new RequestContext.Builder() .traceId(generateTraceId()) .userId(extractUserId(request)) .tenantId(extractTenantId(request)) .build(); ContextHolder.set(context); MDC.put(traceId, context.getTraceId()); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ContextHolder.clear(); MDC.clear(); } }第四步处理异步场景。对于线程池任务需要在提交任务时快照上下文任务执行时恢复。public class ContextAwareTask implements Runnable { private final RequestContext context; private final Runnable delegate; public ContextAwareTask(Runnable delegate) { this.context ContextHolder.get(); this.delegate delegate; } Override public void run() { try { ContextHolder.set(context); delegate.run(); } finally { ContextHolder.clear(); } } }这套方案我在多个项目里用过基本能覆盖同步、异步、线程池等常见场景。关键是要保证上下文的创建和清理成对出现不能有遗漏。4.2 上下文切换的性能测试与参数调优搭好模块后一定要做性能测试。我通常关注三个指标上下文创建耗时、上下文访问耗时、上下文切换耗时。创建耗时方面一个包含五个字段的不可变上下文对象创建一次大约在几十纳秒级别对大多数应用来说可以忽略。但如果字段很多或者有复杂计算就要注意了。我见过一个上下文对象在创建时去查了数据库结果每个请求都多了一次查询性能直接崩了。上下文创建要轻这是铁律。访问耗时方面ThreadLocal 的 get 操作在纳秒级别非常快。但如果上下文对象内部有复杂的查找逻辑就会拖慢。我建议把最常用的字段直接作为对象属性避免嵌套查找。切换耗时方面主要开销在上下文的设置和清理。如果切换非常频繁比如每个小任务都切换一次累积起来也可观。优化方法是减少不必要的切换——如果一段代码不需要上下文就不要切换。调优参数方面我一般会调整线程池的 corePoolSize 和 maxPoolSize确保线程复用率足够高减少上下文创建次数。另外如果用了 InheritableThreadLocal要注意线程创建的开销尽量用线程池而不是每次新建线程。4.3 上下文数据的序列化与跨服务传递跨服务调用时上下文需要序列化到 HTTP header 或消息头里。我通常只传必要的标识字段比如 traceId、userId、tenantId其他字段由接收方自己查。序列化格式我推荐用简单的 key-value 对比如X-Trace-Id: abc123而不是把整个上下文对象序列化成 JSON。原因有三一是 header 有大小限制JSON 容易超二是 JSON 序列化反序列化有开销三是简单格式更易读排查问题时方便。接收方在收到请求后要从 header 里提取这些字段重建上下文。这里要注意的是接收方不应该完全信任传来的上下文特别是 userId 这种涉及权限的字段。我一般会在接收方做一次校验确保传来的 userId 和当前请求的认证信息一致防止伪造。对于消息队列场景上下文可以放在消息的 header 里。但要注意消息可能会被延迟消费上下文里的某些字段可能已经失效。所以消息消费方在重建上下文时要重新查询最新的数据而不是直接用消息里的旧数据。5. 常见问题与排查技巧实录5.1 上下文丢失的典型场景与定位方法上下文丢失是我遇到最多的问题表现是代码里读上下文时拿到 null 或默认值。常见场景有下面几个。场景一异步任务中丢失。主线程设置了上下文提交异步任务后异步线程读不到。原因是 ThreadLocal 不跨线程。解决方案是用 ContextAwareTask 包装或者用 InheritableThreadLocal但线程池场景下继承只在创建时发生复用时不继承。场景二线程池复用导致脏读。上一个任务设置了上下文但没清理下一个任务读到了上一个任务的上下文。解决方案是确保每个任务结束时都清理上下文用 try-finally 保证。场景三跨服务调用丢失。服务 A 设置了上下文调用服务 B 时没把上下文传过去服务 B 读不到。解决方案是在 HTTP 客户端或 RPC 框架里加拦截器自动把上下文序列化到 header 里。场景四拦截器顺序问题。上下文拦截器在认证拦截器之后执行导致认证时读不到上下文。解决方案是调整拦截器顺序上下文拦截器要最先执行。定位这类问题我的方法是在读取上下文的地方打日志打印当前线程名和上下文值。然后对比设置上下文的地方看是不是同一个线程。如果不是就是跨线程问题如果是同一个线程但值不对就是清理或覆盖问题。5.2 上下文污染与数据串号的排查思路上下文污染比丢失更隐蔽因为不报错只是数据不对。典型表现是 A 用户看到了 B 用户的数据。排查这类问题我通常从三个方向入手。第一检查上下文的设置点看是不是有地方覆盖了上下文。比如一个请求处理过程中某个内部调用又设置了一次上下文把原来的覆盖了。第二检查线程池的清理逻辑看是不是有任务没清理上下文。第三检查缓存和连接池看是不是有跨租户的共享。我遇到过一个典型案例一个定时任务和 Web 请求共用了一个线程池定时任务执行时把上下文设置成了系统用户结果 Web 请求复用到这个线程时读到了系统用户的上下文导致权限判断错误。解决方案是定时任务和 Web 请求用不同的线程池或者定时任务执行前先清理上下文。5.3 高频问题速查表问题现象可能原因排查方法解决方案上下文读到 null跨线程未传递打印线程名对比用 ContextAwareTask 包装上下文值错乱线程池未清理检查 finally 块确保任务结束清理上下文跨服务上下文丢失header 未传递抓包看 header加客户端拦截器上下文被覆盖多处设置上下文检查设置点统一上下文设置入口性能下降上下文创建过重压测创建耗时精简上下文对象日志无追踪 IDMDC 未同步检查拦截器上下文绑定时同步 MDC这张表是我从多次排查中总结出来的基本覆盖了八成以上的上下文问题。遇到新问题时我会先对照这张表看能不能匹配上匹配不上再深入分析。5.4 几个只有踩过坑才知道的实操心得第一个心得上下文清理要放在 finally 块里而且要用 remove 而不是 set(null)。set(null) 只是把值设为 nullThreadLocal 里的 Entry 还在时间长了会内存泄漏。remove 才是真正清理。第二个心得不要在线程池的线程里用 InheritableThreadLocal 做上下文传递。因为线程池的线程是复用的继承只发生在创建时复用时不继承。我早期用这个方案测试环境没问题生产环境偶尔出问题查了很久才发现是线程复用导致的。第三个心得上下文字段要少而精。我见过一个上下文对象有二十多个字段每次创建都要初始化一遍性能很差。后来精简到五个核心字段其他字段按需从外部查性能提升明显。第四个心得跨服务传递上下文时要加签名防篡改。特别是 userId 这种涉及权限的字段如果被恶意篡改就是越权漏洞。我一般会在网关层对上下文 header 做签名校验确保没被篡改。第五个心得上下文切换要有日志。每次设置和清理上下文时打一条 debug 日志记录线程名和关键字段。平时可以关掉排查问题时打开能快速定位上下文问题。6. 上下文模式的扩展应用与进阶思路6.1 在微服务架构中的上下文治理微服务架构下上下文治理的复杂度呈指数级上升。一个请求可能经过十几个服务每个服务都有自己的上下文。如果上下文传递不规范追踪链路就断了。我的做法是在网关层统一生成追踪 ID然后通过 HTTP header 透传到所有下游服务。每个服务在接收请求时从 header 里提取追踪 ID重建上下文。服务间调用时再把追踪 ID 传下去。这样整条链路都能用同一个追踪 ID 串起来。但这里有个问题如果某个服务是异步消费消息的消息里没有追踪 ID 怎么办我的方案是在消息生产时把追踪 ID 放进消息 header消费时提取。如果消息是第三方系统发的没有追踪 ID那就生成一个新的并在日志里标记这是新链路的起点。另外微服务下的上下文还要考虑版本兼容。如果上下文格式变了新旧服务之间可能传不过去。我一般会在上下文里加一个版本号接收方根据版本号决定怎么解析。这样升级时可以先升级接收方再升级发送方实现平滑过渡。6.2 上下文与权限系统的结合上下文和权限系统是天然的结合点。用户身份、角色、权限列表都可以放在上下文里这样权限校验时不用每次都查数据库。但这里有个坑权限数据可能会变。如果用户权限被修改了但上下文里还是旧数据就会导致权限判断错误。解决方案是给上下文里的权限数据加一个过期时间超过时间就重新查。或者用事件驱动的方式权限变更时主动刷新上下文。我一般会把权限校验分成两层第一层从上下文里读权限快速判断第二层如果上下文里没有或者过期了再查数据库。这样兼顾了性能和准确性。6.3 上下文模式的未来演进方向从趋势来看上下文模式正在从“显式管理”向“隐式感知”演进。早期的上下文需要手动设置和传递现在越来越多的框架开始自动处理上下文开发者只需要声明“我需要什么上下文”框架自动注入。另一个方向是上下文的标准化。现在每个框架都有自己的上下文实现互不兼容。未来可能会出现统一的上下文标准就像 HTTP header 一样跨框架、跨语言都能识别。这样微服务之间的上下文传递就更简单了。还有一个方向是上下文与可观测性的深度结合。上下文里不仅存追踪 ID还存性能指标、异常信息直接对接监控系统。这样排查问题时从上下文就能看到整个请求的健康状况不用再去翻日志。这些方向我都在关注也在一些小项目里做了尝试。比如用注解来自动注入上下文减少手动代码用 OpenTelemetry 的标准来传递上下文实现跨语言兼容。效果还不错但离大规模生产还有距离。6.4 一个真实项目的上下文改造记录最后分享一个我主导的上下文改造项目。那是一个老系统上下文用全局变量存并发下经常串号。改造分三步走。第一步引入 ThreadLocal 替换全局变量。这一步改动最小但解决了大部分串号问题。改造后压测串号率从千分之一下降到零。第二步加拦截器统一管理上下文生命周期。之前上下文的设置和清理散落在各处容易遗漏。加拦截器后创建和清理都集中在一处代码清爽了很多。第三步处理异步和线程池场景。用 ContextAwareTask 包装所有异步任务确保上下文正确传递。这一步最麻烦因为要改的地方多但改完后异步场景的上下文问题彻底解决了。整个改造历时两周改动了几十个文件。上线后观察了一个月上下文相关的问题从每周几次降到零。这个投入是值得的。改造过程中最大的体会是上下文管理不是加个 ThreadLocal 就完事了它是一套完整的机制涉及创建、绑定、传递、清理各个环节。任何一个环节没做好都会出问题。所以做上下文改造时要有全局视角不能只盯着一个点。