1. 从“context-mode”说起:一个被低估的工程概念
第一次看到“context-mode”这个词,很多人会以为是某个新出的框架或者库。其实不是。它更像是一种设计思路,一种在系统开发中处理“上下文”这件事的工程模式。我在过去几年做后端服务和前端状态管理时,反复遇到同一个问题:数据在多个模块之间传递时,到底该由谁持有、谁修改、谁销毁?这个问题不解决,代码就会变成一团乱麻。而“context-mode”正是用来回答这个问题的。
简单来说,context-mode 是一种将“上下文”作为一等公民来管理的模式。它强调把跨层级、跨模块共享的状态或环境信息,从具体的业务逻辑中抽离出来,用一个独立的、有明确生命周期的容器来承载。这个容器就是 context。而 mode 则指的是这个容器的运作方式——是全局单例,还是请求级隔离,还是组件树内共享。不同的 mode 决定了上下文的可见范围、并发安全性和资源开销。
它解决的核心问题是:避免隐式依赖和状态污染。在没有 context-mode 意识的项目里,我们经常看到全局变量满天飞,或者一个函数需要传七八个参数才能拿到它真正需要的那一个。更糟糕的是,当多个请求同时进来时,全局变量互相覆盖,bug 难以复现。context-mode 通过显式定义上下文的边界和传递规则,让这些问题在架构层面就被规避掉。
这篇文章适合谁看?如果你写过超过三个模块相互调用的项目,如果你被“这个变量到底是谁改的”折磨过,如果你正在设计一个需要支持多租户或多请求并发的系统,那么 context-mode 的讨论对你就有直接价值。我会从设计思路、核心细节、实操落地和问题排查四个层面展开,尽量把每个决策背后的“为什么”讲清楚。文中会涉及一些代码示例,以 Python 和 JavaScript 为主,但思路是跨语言的。
2. 内容整体设计与思路拆解
2.1 为什么需要独立的上下文容器
先看一个反面例子。假设你在写一个 Web 服务,每个请求进来都需要知道当前用户是谁、请求 ID 是什么、数据库连接用哪个。最省事的做法是搞几个全局变量:
current_user = None request_id = None db_conn = None然后在请求入口处赋值,在业务逻辑里直接读。单线程跑测试没问题,一上并发就完蛋。请求 A 把 current_user 设成 Alice,请求 B 紧接着设成 Bob,请求 A 后续的逻辑就读到了 Bob。这种 bug 在本地几乎不可能复现,到了生产环境才爆发,排查成本极高。
context-mode 的思路是:不要用全局变量,而是把上下文对象显式地传递下去。但这个“显式传递”如果靠函数参数一层层传,代码会变得极其臃肿。所以需要一种机制,既能保证隔离性,又不需要手动传参。不同语言给出了不同答案:Python 有 contextvars,JavaScript 有 AsyncLocalStorage,Go 有 context.Context。它们的共同点就是:上下文与当前执行流绑定,而不是与全局状态绑定。
选择这种方案的理由很直接:执行流是天然隔离的。每个请求、每个异步任务、每个线程都有自己的执行流,把上下文挂载到执行流上,就自动获得了隔离性。这比手动加锁或者用线程局部存储要优雅得多,也安全得多。
2.2 三种典型的 context-mode 及其适用场景
根据上下文的生命周期和可见范围,我把常见的 context-mode 分为三类。理解这三类的差异,是做出正确架构决策的前提。
第一种:请求级上下文(Request-Scoped Context)。这是 Web 开发中最常见的模式。每个请求进来时创建一个上下文对象,请求结束时销毁。上下文里通常放请求 ID、用户身份、租户信息、数据库事务句柄等。它的优势是隔离彻底,不同请求之间零干扰。缺点是创建和销毁有开销,不过对于绝大多数应用来说可以忽略不计。
第二种:会话级上下文(Session-Scoped Context)。适用于长连接场景,比如 WebSocket 或者 gRPC 流式调用。上下文在连接建立时创建,连接断开时销毁。一个会话内可能处理多个请求,这些请求共享一些状态,比如认证信息、订阅列表。这种模式需要额外注意内存泄漏问题,因为会话可能持续很长时间,上下文里积累的数据如果不清理,会越占越多。
第三种:应用级上下文(Application-Scoped Context)。这其实就是全局单例的变体,但比裸全局变量多了一层封装。它适合存放配置信息、连接池、缓存客户端这类真正全局唯一且不可变的数据。关键原则是:应用级上下文里只放只读或线程安全的数据。一旦你往里面塞了可变状态,就等于把全局变量的坑重新挖了一遍。
选择哪种 mode,取决于你的数据生命周期和隔离需求。我的一般建议是:默认用请求级,需要跨请求共享时升级到会话级,只有真正全局唯一的东西才放到应用级。这个优先级顺序能帮你避开大部分并发陷阱。
2.3 上下文传递的两种实现路径
确定了 mode 之后,下一个问题是:上下文怎么从入口传到深处?有两种主流路径。
路径一:显式参数传递。函数签名里加一个 context 参数,一层层往下传。这种做法的好处是依赖关系一目了然,看函数签名就知道它需要什么上下文。坏处是当调用链很深时,中间层函数被迫接收自己根本不用的 context,只是为了传给下一层。这被称为“参数钻探”(parameter drilling),是很多开发者反感显式传递的原因。
路径二:隐式执行流绑定。利用语言提供的机制,把上下文绑定到当前执行流上,函数内部直接读取,不需要参数。Python 的 contextvars、Node.js 的 AsyncLocalStorage 都属于这一类。好处是调用链干净,中间层无感知。坏处是依赖关系变得隐式,看函数签名看不出它依赖上下文,调试时可能需要额外手段来追踪。
我的实践经验是:混合使用。在模块边界处用显式传递,明确声明这个模块需要什么上下文;在模块内部用隐式读取,避免内部函数被 context 参数污染。这样既保证了模块间的契约清晰,又保持了内部实现的简洁。举个例子,Web 框架的中间件层显式接收 context 并做校验,然后把它绑定到执行流上,后续的业务逻辑直接读取即可。
3. 核心细节解析与实操要点
3.1 上下文对象的字段设计原则
上下文对象里该放什么、不该放什么,直接决定了这个模式是帮你还是害你。我踩过的坑总结下来,有三条原则值得遵守。
原则一:只放“环境信息”,不放“业务数据”。环境信息指的是描述当前执行环境的数据,比如请求 ID、用户 ID、语言偏好、时区。业务数据指的是具体业务逻辑产生的数据,比如订单详情、商品列表。把业务数据塞进上下文,会让上下文变成一个万能垃圾桶,最终谁都不敢清理它。业务数据应该通过函数返回值或专门的存储层传递。
原则二:字段尽量不可变。上下文创建后,字段值应该尽量不变。如果确实需要修改,比如用户身份在认证过程中从匿名变成已登录,应该用替换整个上下文对象的方式,而不是原地修改字段。这样做的好处是避免并发修改导致的状态不一致,也方便做快照和回滚。
原则三:控制字段数量。我见过一个上下文对象有三十多个字段,后来没人记得每个字段是干嘛的。建议把字段控制在十个以内,超出的部分考虑拆分成多个上下文,或者放到专门的配置对象里。字段命名要统一风格,要么全用驼峰,要么全用下划线,不要混用。
下面是一个请求级上下文的字段设计示例,用 Python 的 dataclass 实现:
from dataclasses import dataclass, field from typing import Optional import uuid @dataclass(frozen=True) class RequestContext: request_id: str = field(default_factory=lambda: str(uuid.uuid4())) user_id: Optional[str] = None tenant_id: Optional[str] = None locale: str = "zh-CN" trace_flags: int = 0注意这里用了frozen=True,保证实例不可变。需要修改时用dataclasses.replace生成新实例。字段数量控制在五个,每个都有明确用途。
3.2 上下文绑定与解绑的时机控制
隐式绑定上下文的机制,关键在于绑定和解绑的时机。绑早了,上下文还没准备好;绑晚了,前面的逻辑读不到;忘了解绑,上下文会泄漏到后续执行流中。
以 Python 的 contextvars 为例,标准做法是用ContextVar.set()和ContextVar.reset()配对使用。但手动配对容易出错,更好的方式是用上下文管理器:
from contextvars import ContextVar from contextlib import contextmanager _ctx_var: ContextVar[RequestContext] = ContextVar("request_context") @contextmanager def bind_context(ctx: RequestContext): token = _ctx_var.set(ctx) try: yield ctx finally: _ctx_var.reset(token)在请求入口处这样用:
async def handle_request(raw_request): ctx = RequestContext(user_id=extract_user(raw_request)) with bind_context(ctx): return await process(raw_request)finally块保证无论业务逻辑是否抛异常,上下文都会被重置。这一点非常重要。我见过因为异常导致上下文没重置,后续请求读到了前一个请求的用户身份,造成了严重的数据越权问题。所以绑定和解绑必须成对出现,且解绑要放在finally里。
在 Node.js 的 AsyncLocalStorage 中,对应的是als.run(ctx, callback),回调结束后自动清理,不需要手动 reset。这是 AsyncLocalStorage 比 contextvars 更省心的地方。但要注意,如果回调里启动了没有 await 的异步任务,那个任务可能不在上下文中运行。所以所有异步操作都要确保在run的回调内被 await。
3.3 并发场景下的隔离验证方法
上下文隔离是否真的生效,不能靠感觉,要有验证手段。我通常用两种方法来验证。
方法一:并发压力测试加断言。写一个测试脚本,同时发起多个请求,每个请求携带不同的用户 ID,然后在业务逻辑深处断言读到的用户 ID 与请求携带的一致。如果隔离有问题,断言会失败。这个测试要跑足够多的并发数,至少几百个,才能暴露偶发的串扰。
import asyncio async def test_isolation(): async def one_request(uid): ctx = RequestContext(user_id=uid) with bind_context(ctx): await asyncio.sleep(0.01) # 模拟业务处理 assert _ctx_var.get().user_id == uid, f"串扰: 期望 {uid}, 实际 {_ctx_var.get().user_id}" await asyncio.gather(*[one_request(f"user-{i}") for i in range(500)]) print("隔离验证通过")方法二:日志追踪。在上下文绑定后,把 request_id 注入到日志格式中。然后在日志系统里搜索同一个 request_id 是否出现在不同用户的请求中。如果出现,说明上下文串了。这个方法适合在预发环境做长时间观察,能发现压力测试覆盖不到的边界情况。
注意:验证隔离时一定要用异步或并发场景,单线程顺序执行永远测不出串扰问题。我早期就是只写了顺序测试,上线后才被并发问题教做人。
3.4 上下文与依赖注入的关系处理
很多人会把上下文和依赖注入(DI)混为一谈,觉得有了 DI 就不需要 context-mode。其实两者解决的是不同层面的问题。DI 解决的是“组件如何获取它依赖的服务”,context-mode 解决的是“执行流如何携带环境信息”。它们可以协作,但不能互相替代。
举个例子,数据库连接池适合用 DI 注入,因为它是应用级单例,所有请求共享。但当前请求使用哪个数据库事务,适合放在上下文里,因为它是请求级状态。如果把事务也做成 DI 单例,并发请求就会互相干扰。
我的做法是:DI 容器负责管理生命周期长的服务,上下文负责携带生命周期短的环境数据。在需要同时使用两者的地方,从 DI 容器拿到服务,从上下文拿到环境数据,组合使用。这样职责清晰,不会互相污染。
4. 实操过程与核心环节实现
4.1 从零搭建一个请求级上下文模块
下面以 Python FastAPI 为例,完整走一遍请求级上下文的搭建过程。选择 FastAPI 是因为它原生支持异步,能充分体现 contextvars 的价值。
第一步,定义上下文数据类。前面已经给过示例,这里补充一个细节:如果字段较多,可以用 Pydantic 的 BaseModel 替代 dataclass,获得自动校验能力。但要注意 Pydantic 默认是可变的,需要配置frozen=True。
第二步,创建 ContextVar 和绑定工具。除了前面给的bind_context上下文管理器,还需要一个便捷的读取函数:
def get_context() -> RequestContext: ctx = _ctx_var.get(None) if ctx is None: raise RuntimeError("上下文未绑定,请检查是否在请求处理流程中调用") return ctx这里选择抛异常而不是返回默认值,是为了尽早暴露问题。如果返回一个空上下文,业务逻辑可能拿着空 user_id 继续跑,产生难以追踪的错误。
第三步,编写中间件。FastAPI 的中间件在请求进入时执行,适合做上下文绑定:
from fastapi import FastAPI, Request app = FastAPI() @app.middleware("http") async def context_middleware(request: Request, call_next): ctx = RequestContext( user_id=request.headers.get("X-User-Id"), tenant_id=request.headers.get("X-Tenant-Id"), ) with bind_context(ctx): response = await call_next(request) response.headers["X-Request-Id"] = ctx.request_id return response注意with块包裹了call_next,保证整个请求处理过程都在上下文中。响应头里回写 request_id,方便客户端排查问题时提供追踪标识。
第四步,在业务逻辑中读取上下文。任何被中间件覆盖的处理函数,都可以直接调用get_context(),不需要在函数签名里加参数:
@app.get("/orders") async def list_orders(): ctx = get_context() orders = await fetch_orders(user_id=ctx.user_id, tenant_id=ctx.tenant_id) return {"request_id": ctx.request_id, "orders": orders}整个流程下来,业务函数签名保持干净,上下文通过执行流自动传递。这就是 context-mode 的核心价值体现。
4.2 上下文数据的序列化与跨进程传递
单进程内的上下文传递解决了,但真实系统往往是多进程甚至多服务的。上下文怎么跨进程传?答案是序列化后放在请求头或消息元数据里。
以 HTTP 为例,把上下文的关键字段编码到请求头中。常用的头包括X-Request-Id、X-User-Id、X-Tenant-Id,以及用于分布式追踪的traceparent。下游服务收到请求后,从头部解析出这些字段,重建上下文对象。
这里有个关键决策:哪些字段需要跨进程传递。不是所有字段都值得传。request_id 和 trace 信息必须传,用于全链路追踪。user_id 和 tenant_id 通常也要传,用于下游鉴权和数据隔离。但像 locale 这种只影响展示的字段,如果下游不涉及展示逻辑,可以不传,减少头部体积。
序列化格式建议用简单的键值对,不要传 JSON 字符串。因为 HTTP 头部对特殊字符有限制,JSON 里的引号和花括号需要转义,容易出错。每个字段一个独立的头,清晰且易于调试。
跨进程传递时还要注意信任边界。上游传来的 user_id 不能无条件信任,下游服务应该用自己的鉴权逻辑验证。上下文传递是为了方便追踪和辅助决策,不能替代安全校验。这一点我在实际项目中反复强调,因为见过太多因为信任上游头部而导致越权的案例。
4.3 上下文与日志系统的集成方案
上下文最大的实用价值之一,就是让日志具备追踪能力。没有上下文时,日志里只有零散的信息,排查问题像大海捞针。有了上下文,每条日志都自动带上 request_id,同一个请求的所有日志可以串起来看。
集成方式取决于日志库。以 Python 的 logging 为例,可以用 LogRecord 工厂或者 Filter 把上下文注入到每条日志记录中:
import logging class ContextFilter(logging.Filter): def filter(self, record): ctx = _ctx_var.get(None) if ctx: record.request_id = ctx.request_id record.user_id = ctx.user_id else: record.request_id = "-" record.user_id = "-" return True logger = logging.getLogger(__name__) logger.addFilter(ContextFilter()) formatter = logging.Formatter( "%(asctime)s [%(request_id)s] [%(user_id)s] %(levelname)s %(message)s" )这样配置后,每条日志都会自动带上 request_id 和 user_id。排查问题时,用 request_id 过滤日志,就能看到这个请求从入口到出口的完整轨迹。
提示:日志格式里 request_id 放在最前面,方便肉眼扫描。user_id 放在第二位,因为很多问题与特定用户相关。级别和消息放在后面,保持可读性。
如果用了结构化日志库(如 structlog),集成更简单,直接把上下文作为绑定字段传入即可。无论用哪种方式,核心原则是:上下文里的追踪字段必须出现在每条日志中,不能有遗漏。遗漏一条日志,追踪链就断了。
4.4 性能开销的实测与优化
引入上下文机制必然带来开销。绑定、读取、重置都有成本。我做过一组实测,在 Python 3.11 环境下,contextvars 的 set 和 get 操作大约在微秒级别,相比一次数据库查询的毫秒级开销,完全可以忽略。但如果上下文对象很大,或者绑定操作非常频繁,开销就会累积。
优化的方向有三个。第一,减小上下文对象体积。只放必要字段,大对象(如完整用户信息)只放 ID,需要时再查。第二,减少绑定次数。请求级上下文在入口绑定一次即可,不要在业务逻辑里反复绑定解绑。第三,避免在热路径上频繁读取。如果某个字段在一个循环里被读几千次,可以在循环外读一次存到局部变量。
我实测过一个极端场景:每秒处理一万个请求,每个请求绑定一次上下文,读取十次。总开销占 CPU 时间的比例不到百分之一。所以对于绝大多数应用,上下文机制的性能开销不需要担心。真正需要担心的是上下文对象设计不当导致的内存占用,那才是大头。
5. 常见问题与排查技巧实录
5.1 上下文丢失的典型场景与修复
上下文丢失是最常见的问题,表现为get_context()抛异常或者返回空。根据我的排查经验,主要有三个原因。
原因一:异步任务未在上下文内启动。在 Python 中,如果在一个请求处理函数里用asyncio.create_task启动后台任务,这个任务默认会复制当前上下文。但如果是在上下文绑定之前启动的,或者用了线程池执行器,上下文就不会自动传递。修复方法是显式传递上下文,或者在任务内部重新绑定。
原因二:中间件顺序错误。如果上下文中间件注册在鉴权中间件之后,鉴权逻辑就读不到上下文。中间件的执行顺序通常与注册顺序相反,所以上下文中间件应该最后注册,确保它最先执行。这个细节很容易搞错,我建议在中间件里加日志,确认执行顺序。
原因三:框架的上下文隔离机制冲突。某些框架自己实现了上下文管理,与语言原生的 contextvars 不兼容。比如在某些老版本的异步框架中,contextvars 的传播会被框架的调度器打断。这种情况下需要查阅框架文档,看是否有官方的上下文集成方案。
排查上下文丢失,我的标准流程是:先在入口处打印上下文是否绑定成功,再在业务逻辑处打印是否能读到,逐步缩小范围。如果入口成功、业务失败,问题就在中间件或框架调度;如果入口就失败,问题在绑定代码本身。
5.2 上下文串扰的排查思路
上下文串扰比丢失更隐蔽,因为它不报错,只是数据悄悄错了。典型表现是用户 A 看到了用户 B 的数据,或者请求 ID 对不上。
排查串扰,第一步是加断言。在上下文读取处,断言读到的 user_id 与预期一致。预期值可以从请求参数或数据库查询结果中获取。断言失败时打印完整上下文和调用栈,定位串扰发生的环节。
第二步是检查是否有全局可变状态。串扰往往不是上下文机制本身的问题,而是上下文里放了不该放的全局引用。比如上下文里存了一个可变字典,多个请求共享同一个字典实例,一个请求改了字典,另一个请求就读到了修改后的值。修复方法是确保上下文里的所有字段都是不可变类型,或者每次绑定都创建新实例。
第三步是检查异步任务的上下文传播。如果用了asyncio.gather并发执行多个子任务,每个子任务应该继承父任务的上下文。但如果子任务内部重新绑定了上下文,且没有正确重置,就可能污染父上下文。这种情况下,子任务应该用独立的上下文副本,不要修改共享的上下文对象。
5.3 上下文相关问题的速查表
| 问题现象 | 可能原因 | 排查方法 | 修复方案 |
|---|---|---|---|
| get_context 抛异常 | 上下文未绑定 | 检查中间件是否执行 | 调整中间件顺序,确保绑定在最外层 |
| 读到错误的 user_id | 上下文串扰 | 加断言,并发测试 | 确保字段不可变,检查全局引用 |
| 后台任务读不到上下文 | 异步任务未继承 | 在任务内打印上下文 | 显式传递或重新绑定上下文 |
| 内存持续增长 | 上下文未释放 | 监控上下文对象数量 | 检查会话级上下文的清理逻辑 |
| 日志缺少 request_id | 日志过滤器未生效 | 检查 Filter 是否添加 | 确认 Filter 添加在正确的 logger 上 |
| 跨服务追踪断链 | 头部未传递 | 检查请求头 | 在出口处注入追踪头部 |
这张表是我从多次排查中总结出来的,覆盖了八成以上的上下文相关问题。遇到新问题时,先对照这张表,能快速缩小排查范围。
5.4 几个容易忽视的实操心得
第一个心得:上下文对象的创建成本要控制。我见过在上下文构造函数里做数据库查询的,导致每个请求都要多一次查询。上下文构造应该是纯内存操作,不涉及 IO。需要查数据的话,在绑定上下文之前查好,把结果传进去。
第二个心得:上下文字段命名要带前缀。如果上下文字段和业务字段重名,日志里容易混淆。比如上下文里的id和业务里的id,看日志时分不清是哪个。建议上下文字段加前缀,如ctx_user_id、ctx_request_id,一目了然。
第三个心得:测试环境要模拟真实并发。单元测试通常顺序执行,测不出上下文问题。集成测试一定要用并发工具,比如 pytest-asyncio 的并发模式,或者用 locust 做压力测试。我现在的习惯是,任何涉及上下文的改动,都必须通过并发测试才能合并。
第四个心得:上下文变更要记日志。如果上下文在生命周期内被修改过(比如用户从匿名变为登录),要记录变更前后的值。这样排查问题时,能知道上下文在哪个环节被改了。这个日志量不大,但关键时刻能救命。
6. 上下文模式的扩展与演进方向
6.1 从请求级到任务级的上下文演进
请求级上下文解决了 Web 场景的问题,但在数据处理和后台任务场景中,执行单元不是 HTTP 请求,而是一个个任务。比如一个定时任务处理一批数据,每个数据项的处理应该有自己的上下文,而不是整个任务共享一个。
任务级上下文的设计要点是:每个任务单元创建独立上下文,任务单元之间隔离。实现上可以用任务 ID 作为上下文的追踪标识,在任务分发时创建上下文,在任务执行时绑定。如果任务有重试机制,每次重试应该创建新的上下文,但保留原始任务 ID 用于关联。
这种模式在批处理系统中特别有用。一个大批次拆成多个小任务并行执行,每个小任务有自己的上下文,日志里能看到每个小任务的执行轨迹,同时通过批次 ID 关联到整体。排查问题时,既能看整体进度,也能看单个任务的细节。
6.2 上下文与可观测性体系的融合
上下文天然适合作为可观测性体系的载体。追踪(Tracing)、指标(Metrics)、日志(Logging)三者的关联,靠的就是上下文里的追踪标识。把 trace_id、span_id、request_id 统一放在上下文里,三个系统就能通过同一个标识串联起来。
具体做法是:在请求入口生成 trace_id 和 span_id,放入上下文。日志系统从上下文读取这些标识,写入每条日志。追踪系统从上下文读取标识,创建 span 并关联父子关系。指标系统在打点时,把标识作为标签附加。这样,从一条日志能跳到对应的追踪链路,从追踪链路能跳到相关的指标图表,排查效率大幅提升。
我现在的项目里,上下文对象已经成了可观测性的中枢。任何新的可观测性需求,第一反应都是“能不能从上下文里拿数据”。这个思路让集成工作变得简单,因为数据源统一了。
6.3 上下文模式的边界与不适用场景
context-mode 不是银弹,有些场景不适合用。第一,纯计算型任务。如果一个函数只做数学计算,不涉及环境信息,硬塞上下文只会增加复杂度。第二,生命周期极短的操作。如果操作在微秒级完成,上下文绑定解绑的开销占比就不可忽视了。第三,强类型语言中的简单场景。在 Go 这类语言里,显式传递 context.Context 已经是惯例,隐式绑定反而违背语言习惯。
判断是否该用 context-mode,我的标准是:是否存在跨模块的环境信息传递需求,且这个信息与执行流绑定。如果两个条件都满足,用 context-mode 收益明显。如果只满足一个,就要权衡。如果都不满足,别用。
我见过为了用而用的案例,在一个只有三个函数的脚本里引入上下文机制,结果代码量翻倍,可读性下降。这是典型的过度设计。context-mode 的价值在于管理复杂度,如果本身没有复杂度,引入它反而制造复杂度。
6.4 我个人在实际操作中的体会
折腾了这么多项目,我对 context-mode 最深的体会是:它的核心不是技术,而是纪律。技术方案再优雅,如果团队不遵守使用规范,照样会出问题。比如约定好上下文只放环境信息,但总有人图方便把业务数据塞进去;约定好字段不可变,但总有人原地修改。这些违规操作积累起来,上下文就变成了新的全局变量。
所以我现在推行 context-mode 时,会配套做三件事。第一,写清楚使用规范,明确什么能放什么不能放。第二,在代码审查清单里加上上下文相关的检查项。第三,写自动化测试,用断言守住隔离性和不可变性。这三件事做到位,context-mode 才能真正发挥价值。
最后分享一个小技巧:在上下文对象里加一个created_at时间戳字段。排查问题时,通过这个时间戳能知道上下文是什么时候创建的,结合日志时间能算出上下文存活了多久。如果发现某个上下文存活时间异常长,很可能是泄漏了。这个字段成本极低,但排查价值很高,建议都加上。