尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

effect 持久化模块 Redis Lua 脚本加载失败重试机制:`SCRIPT LOAD` 瞬态错误不再被无限缓存

effect 持久化模块 Redis Lua 脚本加载失败重试机制:`SCRIPT LOAD` 瞬态错误不再被无限缓存 effect 持久化模块 Redis Lua 脚本加载失败重试机制SCRIPT LOAD瞬态错误不再被无限缓存【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code本文讲解 effecteffect-smol 开源仓库持久化persistence模块中一项关键修复Redis Lua 脚本通过SCRIPT LOAD加载失败时失败结果不再被永久缓存而是自动重试同时EVALSHA遇到NOSCRIPT时会重新加载脚本后再次执行。你将掌握 effect 如何用Cache的失败零 TTL 策略与 NOSCRIPT 兜底保证 Redis 持久化存储、队列与限流在脚本加载抖动场景下依然可靠并了解对应源码与测试的实现细节。修复背景一次SCRIPT LOAD失败为何会变成永久故障effect 的持久化模块位于 packages/effect/src/unstable/persistence大量依赖 Redis Lua 脚本实现原子操作。核心入口是 Redis.ts 中定义的Redis服务其文档明确说明Lua scripts are loaded throughSCRIPT LOAD, cached, and then invoked withEVALSHA.即先通过SCRIPT LOAD把 Lua 源码上传给 Redis得到一个 SHA1 摘要后续调用EVALSHA直接用摘要执行省去每次传输源码的开销。问题在于SCRIPT LOAD是一次网络往返可能因瞬时网络抖动、连接重连、服务端繁忙等原因失败。如果这种失败被当作脚本加载结果缓存起来那么后续所有EVALSHA调用都会命中缓存中的失败结果整个持久化功能持续报错——这正是本变更集.changeset/pre/retry-redis-script-load.md要修复的缺陷。变更集描述原文Fix Redis script evaluation so transientSCRIPT LOADfailures are retried instead of being cached indefinitely.该修复对应effect: patch级别的补丁在 CHANGELOG.md 中亦有收录。修复核心失败零 TTL成功永久缓存先看修复前的问题根源。Redis.make用Cache.makeWith构建脚本缓存const scriptCache yield* Cache.makeWith( (script: Scriptany) options.sendstring(SCRIPT, LOAD, script.lua), { capacity: Number.POSITIVE_INFINITY, timeToLive: (exit) Exit.isSuccess(exit) ? Duration.infinity : Duration.zero } )关键在timeToLive这一行Redis.ts加载成功Exit.isSuccess(exit)为真TTL 为Duration.infinitySHA1 摘要被无限期缓存这是期望行为——脚本内容不变无需重复上传加载失败失败出口TTL 为Duration.zero意味着该缓存条目立即过期、不驻留缓存下次调用会重新发起SCRIPT LOAD。这是Cache.makeWith的典型用法。查看 Cache.ts 的实现makeWith的timeToLive回调接收退出结果与 key允许根据成功/失败状态定制缓存条目生命周期这正是修复所依赖的能力。而在缓存条目层面Cache.ts 中处理 TTL 时对Duration.isZero(ttl)的条目会直接从哈希表中移除——即失败结果不落地、立刻可重试。执行路径EVALSHA与 NOSCRIPT 兜底重载脚本缓存放行后真正的调用发生在eval内部const evalSha (sha: string) options.sendConfig[result]( EVALSHA, sha, script.numberOfKeys(...params).toString(), ...script.params(...params).map((param) String(param)) ) return Cache.get(scriptCache, script).pipe( Effect.flatMap(evalSha), Effect.catchIf( (error) String(error.cause).includes(NOSCRIPT), () Cache.refresh(scriptCache, script).pipe(Effect.flatMap(evalSha)) ) )见 Redis.ts执行顺序与容错逻辑Cache.get(scriptCache, script)取脚本 SHA1未命中或条目已过期时自动触发SCRIPT LOAD回源Effect.flatMap(evalSha)用 SHA1 执行EVALSHAEffect.catchIf捕获错误若错误 cause 中包含NOSCRIPTRedis 返回NOSCRIPT No matching script通常意味着服务端脚本缓存被SCRIPT FLUSH清空、重启或故障转移则调用Cache.refresh(scriptCache, script)强制重新加载脚本随后再次EVALSHA。这与SCRIPT LOAD失败重试形成互补的两道防线加载阶段SCRIPT LOAD失败失败零 TTL自动重试执行阶段EVALSHA返回 NOSCRIPT显式refresh重载脚本后重试一次。测试验证两条路径都有单测背书该修复并非纸上谈兵Redis.test.ts 中两个用例直接验证了上述行为。用例一SCRIPT LOAD失败后重试测试通过一个可注入的send函数模拟第一次SCRIPT LOAD失败、第二次成功断言命令序列为SCRIPT → SCRIPT → EVALSHAif (command SCRIPT) { scriptLoadAttempts 1 if (scriptLoadAttempts 1) { return Effect.fail(new Redis.RedisError({ cause: new Error(ERR transient script load failure) })) } return Effect.succeed(sha as any) }Redis.test.ts关键断言assert.strictEqual(first._tag, Failure) // 第一次调用失败透传 SCRIPT LOAD 失败 assert.strictEqual(second, ok) // 第二次调用自动重载后成功 assert.deepStrictEqual(commands.map(([command]) command), [SCRIPT, SCRIPT, EVALSHA])这说明失败结果没有被缓存——第二次evalScript(key)重新触发了SCRIPT LOAD并成功拿到 SHA1随后EVALSHA正常执行。用例二EVALSHA返回 NOSCRIPT 后重载第二个用例模拟第一次EVALSHA返回NOSCRIPT断言命令序列为SCRIPT → EVALSHA → SCRIPT → EVALSHA即首次加载后执行失败检测到 NOSCRIPT 后重新SCRIPT LOAD再执行一次并成功if (command EVALSHA evalShaAttempts 1) { return Effect.fail(new Redis.RedisError({ cause: new Error(NOSCRIPT No matching script) })) }Redis.test.ts实战影响哪些功能依赖这套重试机制Redis服务是 effect 持久化模块的地基脚本加载的可靠性直接影响以下组件组件文件依赖的 Lua 脚本持久化 KV 存储Persistence.tssetManyRedis批量 SET PEXPIRE 原子写入持久化队列PersistedQueue.tsofferRedis、resetQueueRedis、requeueRedis、completeRedis、failedRedis、takeRedis、expireAllRedisRedis 限流器RateLimiter.tsfixedWindowScript、tokenBucketScript、adaptiveConsumeScript、adaptiveFeedbackScript例如 PersistedQueue.ts 的offerRedis脚本把SADD幂等去重与RPUSH入队合并为一次原子操作local result redis.call(SADD, key_ids, id) if result 1 then redis.call(RPUSH, key_queue, payload) end而 RateLimiter.ts 的fixedWindowScript在单个脚本内完成读取当前令牌数、按需初始化、计算下一过期时间、写入新状态并返回[currentTokens, nextPttl]供上层决策。这类脚本一旦因瞬时加载失败被错误缓存队列入队、限流计数等全部业务都会连锁报错本次修复让这些组件在脚本加载抖动时能够自愈。这些脚本统一通过Redis.script(...)构建类型化描述符Redis.ts声明 Lua 源码、参数映射、key 数量与结果类型withReturnType可细化返回类型结合Redis.eval的缓存与重试机制形成定义一次、随处安全执行的完整链路。小结本变更集虽小却修复了一个隐蔽而危险的故障模式瞬态SCRIPT LOAD失败若被无限缓存会让依赖 Lua 脚本的 Redis 持久化功能从偶发抖动恶化为持续不可用。修复思路可以提炼为可复用的模式对幂等、可重放的资源加载如脚本上传失败结果应设置零 TTL让下一次调用自然重试对运行期可能失效的缓存对象如服务端脚本被清空应在特定错误码NOSCRIPT出现时强制refresh重载用可控的注入式send函数编写测试精确断言命令序列验证缓存与重试的完整路径。相关实现与测试可直接在仓库中查阅Redis.ts、Redis.test.ts、Cache.ts以及使用该机制的上层模块 Persistence.ts、PersistedQueue.ts、RateLimiter.ts。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表