1. 从“上下文模式”说起:一个被低估的工程概念
第一次看到“context-mode”这个词,很多人会下意识觉得它是个抽象到没法落地的概念。我刚开始接触时也这么想——上下文嘛,不就是个变量作用域的事儿?但真正在几个中大型项目里踩过坑之后,我才意识到,context-mode 本质上是一套关于“信息在什么范围内可见、可改、可传递”的工程约定。它决定了你的代码是越写越清晰,还是越写越像一团乱麻。
这篇文章适合谁看?如果你写过超过三千行的项目,经历过“改一个函数结果五个地方报错”的绝望,或者接手过别人留下的“祖传代码”而完全理不清数据流向,那 context-mode 这套思路就是为你准备的。它不绑定任何特定语言或框架,Python、JavaScript、Go、Java 都能用,核心是一种设计意识。
我先把结论摆出来:context-mode 要解决的核心问题是“上下文污染”和“上下文丢失”这一对矛盾。污染是指不该看到某份数据的地方看到了它,导致耦合;丢失是指该拿到某份数据的地方拿不到,只能靠层层透传或者全局变量硬塞。这两件事看起来方向相反,但根源是同一个——没有明确约定上下文的边界和传递规则。
接下来的内容,我会从设计思路、核心细节、实操落地、问题排查四个维度,把 context-mode 这套东西彻底拆开讲。每个部分都会配上我实际项目里的代码片段和踩坑记录,你可以直接抄作业,也可以根据自己项目的情况做调整。
2. 内容整体设计与思路拆解
2.1 为什么需要显式管理上下文
大部分项目一开始都是“能跑就行”的状态。函数需要什么参数就传什么参数,看起来挺清晰。但项目一大,问题就来了:A 函数需要用户信息,B 函数也需要,C 函数间接需要。于是你开始把用户信息一层层往下传,传了七八层之后,中间那些根本不关心用户信息的函数也被迫在签名里挂上这个参数。这就是典型的上下文透传灾难。
另一种做法是搞个全局变量或者单例,谁需要谁去取。这确实省了传参的麻烦,但代价是依赖关系完全隐形。你读一个函数的代码,根本不知道它依赖了哪些外部状态,测试也没法隔离,改一处全局状态可能引发连锁反应。
context-mode 的思路是在这两者之间找一个平衡点:上下文应该被显式创建、显式传递、显式销毁,但在传递过程中可以“穿透”那些不关心它的中间层。听起来有点矛盾?其实不矛盾。关键在于把“上下文的载体”和“上下文的消费者”分开。载体负责在调用链中流动,消费者只在真正需要的时候才去读取。
我用一个生活化的类比来解释。想象一个快递包裹从仓库送到你家。包裹就是上下文,快递员是中间层。快递员不需要知道包裹里是什么,他只需要把包裹送到正确的地址。你作为收件人,才是真正的消费者。如果每个中转站都要拆开包裹检查一遍,那效率就崩了。context-mode 要做的就是让包裹高效流动,同时保证只有该拆包裹的人才能拆。
2.2 三种常见的上下文管理模式对比
在实际工程中,我见过三种主流的上下文管理方式,各有适用场景。下面这张表是我根据多个项目的实际体验整理的对比:
| 模式 | 核心做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 显式传参 | 所有依赖通过函数参数传递 | 依赖清晰、易测试 | 深层调用链参数爆炸 | 小型项目、纯函数库 |
| 全局/单例 | 上下文放在全局可访问处 | 使用方便、无需传参 | 隐式依赖、难测试、并发问题 | 脚本、原型验证 |
| context-mode | 上下文对象显式创建,沿调用链传递,按需读取 | 依赖可追踪、测试友好、支持并发隔离 | 需要团队约定和纪律 | 中大型项目、并发场景 |
我重点说说为什么最终选择了 context-mode 这条路。显式传参在项目超过一定规模后基本不可维护,我经历过一个函数签名有十二个参数的噩梦,后来加了两个参数直接导致三十多个调用点要改。全局单例的问题更隐蔽,平时写起来爽,一旦出现并发请求互相污染数据,排查起来能让人崩溃。
context-mode 的核心优势在于它把上下文的生命周期和请求的生命周期绑定在一起。每个请求进来创建一个独立的上下文对象,这个对象沿着调用链传递,请求结束就销毁。这样既避免了全局污染,又不需要在每个函数签名里显式声明所有依赖。中间层函数可以选择“透传”上下文而不读取它,只有真正需要的层才去读取。
2.3 设计时的三个关键决策点
在动手实现之前,有三个决策点需要想清楚,它们直接决定了后续代码的风格和维护成本。
第一个决策:上下文是可变还是不可变。可变上下文用起来方便,任何一层都可以往里塞东西。但可变意味着状态可能在你不注意的时候被改掉,调试时很难定位是谁改的。不可变上下文每次修改都返回新对象,安全但写起来啰嗦。我的建议是:核心字段不可变,扩展字段用显式的 setter 方法并加日志。这样既保证了关键数据的安全,又保留了灵活性。
第二个决策:上下文传递是显式还是隐式。显式传递就是每个函数签名里都带上 context 参数,Go 语言的标准库就是这么做的。隐式传递靠线程本地存储或者异步上下文机制,比如 Python 的 contextvars 或者 JavaScript 的 AsyncLocalStorage。显式的好处是一目了然,坏处是签名冗长。隐式的好处是干净,坏处是容易让人忘记上下文的存在。我个人的偏好是核心链路显式传递,框架层用隐式机制兜底。
第三个决策:上下文的粒度怎么定。粒度太粗,一个上下文塞了所有东西,等于没隔离。粒度太细,每个功能一个上下文,传递起来又是一场灾难。我的经验是按请求边界划分上下文,一个请求一个上下文,里面包含这个请求处理过程中需要的所有共享状态。如果某个子系统需要额外的上下文,就在主上下文里挂一个子上下文对象,而不是新建一个独立的传递链。
3. 核心细节解析与实操要点
3.1 上下文对象的字段设计原则
上下文对象里到底该放什么?这个问题我踩过不少坑。最开始我把所有能想到的东西都往里塞,结果上下文变成了一个“万能袋子”,谁都可以往里扔东西,最后没人知道里面到底有什么。后来我总结了几条原则,分享给你。
原则一:只放跨层共享的数据。如果一份数据只在单个函数内部使用,那它就不应该进上下文。比如一个临时的循环变量、一个中间计算结果,这些放在局部变量里就好。上下文里应该放的是那些“多个层都需要访问”的数据,比如当前用户身份、请求追踪 ID、数据库事务对象、配置信息等。
原则二:字段命名要带语义前缀。我见过太多上下文里放一个data字段或者info字段,然后所有人往里塞东西。正确的做法是用语义化的命名,比如auth.user_id、trace.request_id、db.transaction。这样一眼就能看出这个字段属于哪个领域,也方便后续做权限控制——比如某些层只能读auth.*的字段,不能读db.*的字段。
原则三:区分“请求级”和“应用级”数据。请求级数据每个请求都不一样,比如用户 ID、追踪 ID。应用级数据所有请求共享,比如配置、连接池。这两类数据不应该混在同一个上下文对象里。我的做法是应用级数据放在一个独立的AppContext里,请求级数据放在RequestContext里,RequestContext持有AppContext的引用。这样既避免了重复创建,又保证了隔离。
下面是一个典型的上下文对象结构示例,用 Python 来演示:
from dataclasses import dataclass, field from typing import Optional, Dict, Any import uuid @dataclass(frozen=True) class AppContext: """应用级上下文,全局唯一,所有请求共享""" config: Dict[str, Any] db_pool: Any cache_client: Any @dataclass class RequestContext: """请求级上下文,每个请求独立创建""" app: AppContext request_id: str = field(default_factory=lambda: str(uuid.uuid4())) user_id: Optional[str] = None trace_span: Optional[Any] = None _extensions: Dict[str, Any] = field(default_factory=dict) def set_extension(self, key: str, value: Any) -> None: """显式设置扩展字段,方便追踪""" self._extensions[key] = value def get_extension(self, key: str, default: Any = None) -> Any: return self._extensions.get(key, default)注意这里AppContext用了frozen=True,表示不可变。RequestContext可变,但扩展字段通过显式方法访问,方便加日志和权限检查。
3.2 上下文传递的三种实现方式
上下文怎么从一层传到下一层,这是实操中最核心的问题。我分别说说三种方式的实现细节和适用场景。
方式一:函数参数显式传递。这是最直接的方式,每个需要上下文的函数都在签名里加上ctx参数。Go 语言的标准库就是这种风格,比如func DoSomething(ctx context.Context, arg string) error。这种方式的优点是依赖关系一目了然,测试的时候直接传一个 mock 上下文就行。缺点是签名会变长,而且中间层函数即使不需要上下文也得带着它。
方式二:线程本地存储。在同步编程模型里,可以用线程本地存储来隐式传递上下文。Python 的threading.local、Java 的ThreadLocal都是这类机制。每个线程有自己的上下文副本,函数内部直接读取,不需要参数传递。这种方式在 Web 框架里很常见,比如 Flask 的g对象和request对象就是基于线程本地存储实现的。但它的致命缺点是在异步编程模型下会失效,因为异步任务可能在不同线程间切换。
方式三:异步上下文变量。这是异步编程时代的解决方案。Python 的contextvars、JavaScript 的AsyncLocalStorage、Kotlin 的CoroutineContext都属于这一类。它们保证上下文在异步调用链中正确传递,即使任务在不同线程间调度也不会串。这是目前我认为最优雅的隐式传递方案,但需要语言和运行时支持。
我的实际项目里通常是混合使用:框架入口层用异步上下文变量自动注入,业务逻辑层通过函数参数显式传递。这样既保证了异步安全,又让业务代码的依赖关系保持清晰。下面是一个 Python 的示例:
import contextvars from functools import wraps # 定义上下文变量 _current_request_ctx: contextvars.ContextVar[RequestContext] = contextvars.ContextVar( "current_request_ctx" ) def get_current_context() -> RequestContext: """获取当前请求上下文,未设置时抛出异常""" ctx = _current_request_ctx.get(None) if ctx is None: raise RuntimeError("RequestContext not initialized") return ctx def with_request_context(func): """装饰器:自动注入请求上下文""" @wraps(func) async def wrapper(*args, **kwargs): ctx = RequestContext(app=get_app_context()) token = _current_request_ctx.set(ctx) try: return await func(*args, **kwargs) finally: _current_request_ctx.reset(token) return wrapper注意:使用
contextvars时一定要用try/finally确保上下文被重置,否则在长驻进程里可能造成内存泄漏或者上下文串扰。
3.3 上下文生命周期的管理要点
上下文的创建和销毁时机非常关键,搞错了会引发各种诡异问题。我总结了几条实操要点。
创建时机:请求进入时创建。在 Web 服务里,通常是在中间件或者请求处理器的入口处创建RequestContext。不要在更早的地方创建,比如应用启动时就创建一个全局的,那样所有请求会共享同一个上下文,完全失去隔离性。也不要在太晚的地方创建,比如业务逻辑内部,那样上层就拿不到上下文了。
销毁时机:请求结束时销毁。用try/finally保证即使发生异常也能正确清理。清理工作包括:关闭数据库事务、释放锁、清理临时文件、重置上下文变量。我见过一个项目因为忘记在异常路径下回滚事务,导致数据库连接池被占满,整个服务挂掉。这种问题排查起来非常痛苦,因为日志里看不出明显错误。
跨请求传递:需要显式复制。有时候一个请求会触发后台任务,后台任务需要访问原请求的上下文信息。这时候不能直接把上下文对象传过去,因为原请求结束后上下文就销毁了。正确的做法是复制需要的字段到一个新的上下文对象,然后传给后台任务。复制的时候要注意深拷贝和浅拷贝的问题,如果字段里有可变对象,浅拷贝可能导致两个上下文互相影响。
并发安全:每个协程独立上下文。在异步编程里,多个协程可能并发执行。如果它们共享同一个上下文对象,就会出现数据竞争。contextvars机制天然解决了这个问题,因为每个协程有自己的上下文副本。但如果你用的是显式传递,就要注意不要在多个协程间共享同一个可变上下文对象。
4. 实操过程与核心环节实现
4.1 从零搭建一个上下文管理模块
光说理论没意思,我带你从零搭一个可用的上下文管理模块。以 Python 异步 Web 服务为例,完整走一遍流程。
第一步:定义上下文数据结构。前面已经给过示例,这里补充一个关键细节——上下文里应该包含一个deadline字段,用来控制请求的超时。很多线上事故都是因为某个下游调用卡住,导致整个请求线程被占满。有了 deadline,每一层在发起下游调用前都可以检查剩余时间,快超时了就提前返回,避免雪崩。
import time from dataclasses import dataclass, field @dataclass class RequestContext: app: AppContext request_id: str user_id: Optional[str] = None deadline: float = 0.0 # Unix 时间戳,0 表示无限制 def remaining_time(self) -> float: """返回剩余可用时间(秒),无限制时返回 inf""" if self.deadline <= 0: return float('inf') return max(0.0, self.deadline - time.time()) def is_expired(self) -> bool: return self.remaining_time() <= 0第二步:实现上下文注入中间件。在 Web 框架的中间件里创建上下文,并设置到contextvars中。同时从请求头里提取追踪 ID 和用户身份信息,填充到上下文里。
async def context_middleware(request, call_next): # 从请求头提取信息 request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) user_id = extract_user_id(request) # 创建上下文,设置 30 秒超时 ctx = RequestContext( app=get_app_context(), request_id=request_id, user_id=user_id, deadline=time.time() + 30.0 ) token = _current_request_ctx.set(ctx) try: response = await call_next(request) response.headers["X-Request-ID"] = request_id return response finally: _current_request_ctx.reset(token)第三步:在业务逻辑中读取上下文。业务函数通过get_current_context()获取上下文,读取需要的字段。注意不要在业务函数里修改上下文的request_id和user_id这类核心字段,它们应该是只读的。
async def get_user_orders(product_id: str): ctx = get_current_context() # 检查超时 if ctx.is_expired(): raise TimeoutError("Request deadline exceeded") # 使用上下文中的数据库连接 async with ctx.app.db_pool.acquire() as conn: orders = await conn.fetch( "SELECT * FROM orders WHERE user_id = $1 AND product_id = $2", ctx.user_id, product_id ) return orders第四步:处理后台任务。如果需要在请求结束后执行后台任务,必须复制上下文。下面是一个示例:
async def schedule_background_task(ctx: RequestContext, task_func, *args): """复制上下文并调度后台任务""" # 只复制需要的字段,不直接传原上下文 bg_ctx = RequestContext( app=ctx.app, request_id=ctx.request_id, user_id=ctx.user_id, deadline=0.0 # 后台任务不继承请求超时 ) asyncio.create_task(task_func(bg_ctx, *args))4.2 参数选择与性能考量
上下文管理模块本身会带来一定的性能开销,需要合理选择参数来平衡功能和性能。
上下文对象的创建开销。每次请求创建一个RequestContext对象,如果字段很多,创建开销不可忽略。我的实测数据是:一个包含 10 个字段的 dataclass 对象,创建耗时大约 1-2 微秒。对于 QPS 在几千级别的服务来说,这个开销完全可以接受。但如果 QPS 上万,就要考虑用__slots__或者更轻量的结构来减少内存分配。
contextvars 的读写开销。contextvars的set和get操作比普通变量访问慢,因为涉及到上下文栈的查找。实测下来,get操作大约 0.1-0.2 微秒,set操作大约 0.5-1 微秒。在请求入口处 set 一次,业务逻辑里 get 多次,总体开销很小。但要注意不要在循环里频繁 set,那样开销会累积。
上下文复制的开销。跨请求复制上下文时,如果字段里有大对象,复制开销会很大。我的建议是只复制必要的标量字段,大对象通过引用共享。比如数据库连接池、缓存客户端这些,直接传引用就好,不需要深拷贝。
下面这张表是我在不同场景下的参数选择建议:
| 场景 | 上下文创建频率 | 推荐方案 | 预估开销 |
|---|---|---|---|
| 高 QPS Web 服务 | 每请求一次 | 轻量 dataclass + contextvars | < 5 微秒/请求 |
| 后台任务处理 | 每任务一次 | 显式传递 + 字段复制 | < 10 微秒/任务 |
| 批处理作业 | 每批次一次 | 显式传递 + 共享上下文 | 可忽略 |
| 长连接服务 | 每连接一次 | 连接级上下文 + 请求级子上下文 | < 3 微秒/请求 |
4.3 与现有框架的集成实践
实际项目里很少从零造轮子,更多是把 context-mode 的思路集成到现有框架里。我分别说说几种常见框架的集成方式。
FastAPI 集成。FastAPI 有依赖注入系统,可以用Depends来注入上下文。但依赖注入是函数参数级别的,对于深层调用链不太方便。我的做法是结合中间件和contextvars:中间件负责创建上下文,业务函数通过get_current_context()读取。这样既利用了 FastAPI 的中间件机制,又避免了参数透传。
Django 集成。Django 有中间件机制,可以在process_request里创建上下文,在process_response里销毁。Django 的threading.local在同步视图下工作良好,但如果用 ASGI 异步视图,就要换成contextvars。Django 3.0 之后对异步的支持越来越好,建议直接用contextvars。
Celery 集成。Celery 任务默认不在请求上下文里执行,需要手动传递。我的做法是在任务签名里加一个ctx_data参数,只传必要的标量字段,任务内部重建上下文。不要试图把整个上下文对象序列化传给 Celery,那样既慢又容易出问题。
gRPC 集成。gRPC 有 metadata 机制,可以把上下文的关键字段放在 metadata 里跨服务传递。服务端从 metadata 里提取字段,重建上下文。注意 metadata 有大小限制,不要放太大的数据。
5. 常见问题与排查技巧实录
5.1 上下文丢失的典型场景
上下文丢失是我遇到最多的问题,表现是业务代码里get_current_context()抛异常说上下文未初始化。下面整理了几种典型场景和排查方法。
场景一:异步任务未继承上下文。用asyncio.create_task()创建的任务默认会复制当前上下文,但如果你在创建任务之前就重置了上下文,任务里就拿不到。排查方法是检查任务创建的位置,确保在上下文有效期内创建。解决方法是显式传递上下文,或者在任务内部重新设置。
场景二:线程池任务未传递上下文。用ThreadPoolExecutor提交任务时,上下文不会自动传递,因为线程池的线程是复用的。解决方法是用contextvars.copy_context()复制当前上下文,然后在线程池任务里运行。
import contextvars from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=4) def submit_with_context(func, *args): ctx = contextvars.copy_context() return executor.submit(ctx.run, func, *args)场景三:中间件顺序错误。如果上下文中间件在认证中间件之后执行,那认证中间件里就拿不到上下文。排查方法是检查中间件的注册顺序,确保上下文中间件在最前面。解决方法是调整注册顺序,或者把上下文创建逻辑提前。
场景四:异常被吞掉导致 finally 未执行。如果代码里有except: pass这种吞异常的写法,可能导致finally块里的上下文重置逻辑没执行。排查方法是搜索代码里的裸except,改成至少记录日志。解决方法是统一异常处理规范,禁止裸except。
5.2 上下文污染的排查思路
上下文污染比丢失更隐蔽,表现是不同请求的数据串了,或者某个请求看到了不该看到的数据。下面是我总结的排查清单。
| 症状 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 用户 A 看到用户 B 的数据 | 上下文对象被复用 | 检查上下文创建位置 | 每请求创建新上下文 |
| 修改上下文影响其他请求 | 可变对象被共享 | 检查字段是否深拷贝 | 核心字段不可变 |
| 并发请求数据错乱 | 线程本地存储串了 | 检查是否用 contextvars | 改用 contextvars |
| 后台任务看到过期数据 | 上下文未复制 | 检查任务创建逻辑 | 显式复制字段 |
| 内存持续增长 | 上下文未销毁 | 检查 finally 块 | 确保 reset 被调用 |
我重点说说“用户 A 看到用户 B 的数据”这个症状。有一次线上出现这个问题,排查了半天发现是上下文对象被缓存在了一个全局字典里,key 用的是用户 ID,但某个边界情况下 key 算错了,导致两个用户命中了同一个缓存条目。这种问题的教训是:上下文对象绝对不要缓存,每个请求必须创建新的。
5.3 性能问题的定位与优化
上下文管理模块本身一般不会成为性能瓶颈,但在高并发场景下,一些细节可能被放大。下面是我遇到过的几个性能问题和优化方法。
问题一:上下文对象过大。如果上下文里塞了几十个字段,每次创建和复制的开销就会累积。优化方法是精简字段,把不常用的字段移到扩展字典里,按需创建。实测把一个 30 字段的上下文精简到 10 个核心字段加扩展字典,创建开销降低了约 60%。
问题二:频繁的上下文复制。跨请求传递时如果频繁复制上下文,开销会很大。优化方法是只复制变化的字段,不变的字段通过引用共享。比如app字段在所有请求间共享,复制时直接传引用就好。
问题三:contextvars 的 set 操作过多。每次 set 都会在上下文栈里压入新值,get 的时候要从栈顶往下找。如果在循环里频繁 set,栈会变得很深,get 变慢。优化方法是在循环外 set 一次,循环内只 get。如果确实需要在循环内修改,考虑用局部变量代替。
问题四:日志里打印整个上下文。有些项目为了方便调试,在每层日志里都打印整个上下文对象。上下文大的时候,序列化开销很可观。优化方法是只打印关键字段,比如request_id和user_id,其他字段按需打印。
实操心得:我习惯在上下文里加一个
__repr__方法,只输出关键字段,这样即使不小心打印了整个上下文,开销也可控。
6. 进阶话题:上下文模式的扩展应用
6.1 多租户场景下的上下文隔离
多租户系统里,上下文隔离的要求更高。每个租户的数据必须严格隔离,不能有任何串扰。我的做法是在上下文里加一个tenant_id字段,所有数据访问层都要检查这个字段,确保只访问当前租户的数据。
具体实现上,可以在数据库连接层面做隔离:每个租户一个独立的 schema,连接时根据tenant_id切换 schema。或者在查询层面做隔离:所有查询自动加上WHERE tenant_id = ?条件。前者隔离更彻底,后者实现更简单。我倾向于前者,因为后者一旦有人写查询时忘了加条件,就会造成数据泄露。
上下文里还要加一个tenant_config字段,存放租户级别的配置,比如功能开关、限流阈值等。这样业务代码可以根据租户配置做差异化处理,而不需要硬编码。
6.2 分布式追踪中的上下文传递
在微服务架构里,上下文需要跨服务传递。通常的做法是把关键字段放在请求头里,比如X-Request-ID、X-User-ID、X-Trace-ID。服务端从请求头里提取这些字段,重建上下文。
跨服务传递时要注意几个问题。第一,字段要精简,请求头有大小限制,不要放太多数据。第二,要防篡改,用户 ID 这类敏感字段要签名或者用内部令牌替代。第三,要处理缺失情况,如果请求头里没有某个字段,要有合理的默认值或者降级逻辑。
我通常会在上下文里加一个trace子对象,包含trace_id、span_id、parent_span_id等字段。每次跨服务调用时,生成新的span_id,把当前span_id作为parent_span_id传下去。这样就能在追踪系统里还原完整的调用链。
6.3 上下文与依赖注入的结合
依赖注入和上下文管理看起来是两件事,但其实可以结合。依赖注入容器可以管理应用级上下文,请求级上下文则由框架中间件管理。业务代码通过依赖注入获取应用级依赖,通过上下文获取请求级数据。
这种结合的好处是职责清晰:应用级依赖的生命周期由容器管理,请求级数据的生命周期由中间件管理。两者互不干扰,又能在业务代码里方便地访问。
实现上,可以在依赖注入容器里注册一个ContextProvider,业务代码通过它获取当前请求上下文。ContextProvider内部用contextvars读取当前上下文,如果不存在就抛异常。这样既保持了依赖注入的风格,又利用了上下文管理的隔离性。
7. 我踩过的坑和给你的建议
说了这么多,最后分享几个我实际踩过的坑,希望能帮你少走弯路。
第一个坑:上下文里放了可变对象,结果被意外修改。有一次我在上下文里放了一个列表,某个中间件往列表里 append 了一个元素,结果下游所有消费者都看到了这个元素。排查了很久才发现是中间件的问题。教训是:上下文里的可变对象要么用不可变替代(比如 tuple 代替 list),要么加严格的访问控制。
第二个坑:忘记在异常路径下清理上下文。有一次线上服务内存持续增长,排查发现是某个异常路径下没有重置contextvars,导致上下文对象一直挂在上下文栈里。教训是:所有 set 操作必须配对 reset,且放在 finally 块里。
第三个坑:上下文字段命名冲突。两个团队各自往上下文里加了同名字段,含义却不同,导致数据错乱。教训是:上下文字段要有命名空间前缀,比如auth_、db_、trace_,避免冲突。
第四个坑:过度依赖上下文导致测试困难。业务代码里到处get_current_context(),单元测试时不得不构造完整的上下文。教训是:核心业务逻辑尽量通过参数接收依赖,上下文只在框架层和边界层使用。这样业务逻辑可以独立测试,不依赖上下文环境。
如果你正在考虑引入 context-mode,我的建议是先从一个小模块试点,比如先在一个 API 端点里用起来,观察一段时间。确认没问题后再逐步推广到其他模块。不要一上来就全项目改造,那样风险太大。另外,团队约定比技术方案更重要,一定要把上下文的创建、传递、销毁规则写进开发规范,定期 review 代码里的上下文使用情况。技术方案再好,如果团队不遵守约定,一样会乱。