Workers 的全局变量能当缓存或计数器用吗?

我在本地给 Workers 加了个顶层 let counter = 0,每个请求自增,顺序请求得到 1、2、3。再发三个并发请求,计数一起涨到 6。看起来,全局变量就是个现成的计数器。

能当缓存,不能当计数器,更不能当 session。全局变量只是“同一个实例内可丢失的状态”,实例随时可能被驱逐,也不保证你的下一个请求落在同一个实例上。

原因在于 Workers 不是一个常驻进程,而是平台调度的 V8 isolate。理解了这一点,下面几个问题就都顺了。如果你还没看过整体模型,先看 Workers 和传统服务器有什么不同。

同一个实例里,全局变量会共享吗?

会。我在本地跑过:顶层 let counter = 0 在 handler 里自增,顺序请求得到 1、2、3;三个并发请求(各 await 300ms)看到同一个 bootId,计数一起涨到 6。它们在同一个 isolate 里交错执行。

为什么说这份共享状态不可靠?

官方文档说,isolate 不一定长寿,可能因资源压力、疑似逃逸沙箱或触及资源限制被驱逐;请求也不保证落到同一实例,所以建议不要依赖或修改全局状态。

具体来说:

  • 实例被驱逐,全局变量就丢了。
  • 可能有多个实例,各自的全局变量互不一致。
  • 本地并发请求能共享变量,不能推断线上同一用户总命中同一实例,官方明确不保证。

所以把模块顶层变量当 session 或限流计数器,会在跨实例和驱逐时悄悄出错。

并发请求会互相踩状态吗?

会。一个实例在单线程事件循环上并发处理多个请求,并发不是串行:两次 await 之间,别的请求可能改掉你的状态。这和 Node 单进程的并发问题一样,只是在 Workers 上更容易被忽视,因为你很少意识到自己在一个共享的实例里。

为什么模块顶层不能做 I/O?

我在本地的模块顶层调用 crypto.randomUUID(),wrangler dev 直接启动失败,报 Disallowed operation called within global scope。这个报错说明全局作用域不允许异步 I/O、设置定时器和生成随机数,要放进 handler。

正确做法是惰性初始化:

let bootId: string | undefined;

export default {
  async fetch(request: Request): Promise<Response> {
    bootId ??= crypto.randomUUID(); // 在 handler 内,允许
    return Response.json({ bootId });
  },
} satisfies ExportedHandler;

这里的 bootId 是典型的“实例级、可丢失”状态:只用来观察,不参与正确性。

需要真正的状态,该放哪?

把状态交给存储产品或 Durable Objects。选型见 存储选型决策树,需要串行与强一致的场景看 Durable Objects 模型。

全局变量共享和模块顶层 I/O 报错在本地 wrangler dev(workerd,wrangler 4.151.3)上跑过;isolate 被驱逐、请求不保证落到同一实例这些说法来自官方文档,我没有在线上复现。本地是单进程模拟,不代表线上的调度与驱逐表现。行为以官方文档为准,会变化。