GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GroovyScriptCacheContext 的脚本编译缓存没有跨进程失效机制,多实例部署下修改脚本后其他实例会永久返回旧脚本。 · Issue #214 · codingapi/springboot-framework

GroovyScriptCacheContext 的脚本编译缓存没有跨进程失效机制,多实例部署下修改脚本后其他实例会永久返回旧脚本。 #214

New issue
Copy link
New issue
Copy link
Closed
Closed
GroovyScriptCacheContext 的脚本编译缓存没有跨进程失效机制,多实例部署下修改脚本后其他实例会永久返回旧脚本。#214
Copy link

Description

@xiaosheng88
xiaosheng88
opened on Nov 9, 2026
Issue body actions

环境:com.codingapi.springboot:springboot-starter-script:17.3.1,Spring Boot 3.x、JDK 17。部署形态为同一套代码运行两个独立进程(admin 8080 与 mobile 8081),两个进程连接同一个数据库,脚本正文存在 t_groovy_script 表里。

现象:在 admin 实例(进程 A)的页面上编辑并保存 Groovy 校验脚本后,进程 A 再次执行该脚本,得到的是新逻辑的结果;同一个 scriptKey 在进程 B(mobile)执行时,返回的却始终是改动之前的判定结果,等待多久都不会恢复,只有重启进程 B 才正常。数据库中该 key 的脚本正文已经是新版,说明落库没问题,问题出在进程内缓存。

复现方式:进程 A 通过 GroovyScriptCacheContext.getInstance().getGroovyScript(key) 拿到脚本后 setScript 新正文、compile(true)、save(),此时 A 的缓存同步更新、库里正文也已更新;进程 B 之前已经 getGroovyScript 过同一个 key,此后继续 invoke,拿到的永远是旧的编译结果,不会因为库里正文变化而失效。这与业务代码的写法无关,与项目里 ScriptGroovyFactoryContext.save 的“先 remove(key) 再 compile(true)、save()”完全一致,而 remove 只清当前 JVM。

根因:GroovyScriptCacheContext 是按 key 懒加载的进程内静态缓存,首次 getGroovyScript 之后编译结果常驻内存,没有 TTL、没有版本校验、也不会回查存储,只有显式 remove 或 clear 才会失效。GroovyScript 的 save() 只负责写存储,不通知其他进程;remove(key) 也只作用于调用方 JVM。于是缓存失效只发生在写入的那个进程,其他节点永久处于脏读状态,而且没有任何途径能感知脚本已经被改过。

影响面:任何多实例部署都会遇到这个问题,包括水平扩容、PC 与 H5 双端、多副本灰度,只要脚本的读取方和写入方不是同一个进程就一定会发生;表现是脚本修改后不同节点行为不一致,且只能靠重启非写入节点恢复。

Activity

  1. xlorne commented on Nov 9, 2026

    @xlorne
    xlorne
    on Nov 9, 2026
    Contributor
    More actions

    处理方式

    1. 首先可以补充数据更新相关的事件,从而运行更新同步其他进程的数据能力。
    2. cache机制可以允许提供其他的实现策略,例如可以redis实现策略,通过共享redis的数据的方式解决数据共享问题。
  2. xlorne commented on Nov 10, 2026

    @xlorne
    xlorne
    on Nov 10, 2026
    Contributor
    More actions

    问题定位

    感谢反馈,问题已定位,确认属实。

    根因(以 springboot-starter-script 17.x 源码为准,类名为 GroovyScriptCacheContext,issue 中提到的 ScriptGroovyFactoryContext 实际对应 GroovyScriptController.save):

    1. GroovyScriptCacheContext 是进程内静态单例 LRU 缓存(LinkedHashMap accessOrder),getGroovyScript 命中缓存后直接返回,没有 TTL、没有版本校验、不会回查存储,只有显式 remove/clear 才失效。
    2. GroovyScript.save() 只调用 GroovyScriptRepositoryContext.save(this) 写存储并清理本进程临时数据,不携带任何跨进程通知;remove() 同理。
    3. GroovyScriptController.save 的流程是"取出缓存对象 → setScript → compile(true) → save()",整条链路的缓存失效只发生在写入方 JVM 内,其他实例感知不到脚本已变更,且编译缓存以脚本内容 SHA256 为 key(内容变了会重新编译),但对象缓存不失效就永远拿不到新内容,因此表现与 issue 描述一致。

    影响版本线:17.x(issue 环境 springboot-starter-script:17.3.1,Spring Boot 3.x / JDK 17)。8.x 的同名模块存在相同机制,如需同步修复请在下方确认。

    处理方案(对应上述两条方向):

    1. 新增脚本缓存变更事件机制:GroovyScript.save() / remove() 后发布 GroovyScriptCacheEvent(SAVE / REMOVE),由可替换的 GroovyScriptCacheSync SPI 负责广播;其他实例接收事件后本地失效对应缓存,下次读取自动回源仓储加载新版本。默认提供单进程实现(行为等价于现状),多实例部署时由使用方接入 Redis pub/sub 等实现广播。
    2. 缓存策略可插拔:将 GroovyScriptCacheContext 内部的 LRU 容器抽象为 GroovyScriptCache 接口 + 默认 LRU 实现,支持替换为 Redis 等共享存储实现(与现有 GroovyScriptRepositoryContext.setRepository 的注入方式一致)。

    交付方式:基于 17.3.x 创建特性分支 feature/issue-214-groovy-cache-sync-17x,测试先行,完成后推送该分支,是否合并由维护者决定。

  3. xlorne commented on Nov 10, 2026

    @xlorne
    xlorne
    on Nov 10, 2026
    Contributor
    More actions

    修复完成

    特性分支已推送:feature/issue-214-groovy-cache-sync-17x(基线 17.3.x,提交 0355e566)
    PR 创建入口(目标分支 17.3.x,是否合并由维护者决定):https://github.com/codingapi/springboot-framework/pull/new/feature/issue-214-groovy-cache-sync-17x

    最终方案(与定位回复的差异:按评审意见增加了框架事件桥接)

    1. 跨实例失效同步(SPI)

      • 新增 GroovyScriptCacheEvent(含 SAVE/REMOVE 类型、key、来源实例标识、更新时间)与 GroovyScriptCacheSync SPI;
      • GroovyScript.save() / remove() 写路径发布事件;默认实现 LocalGroovyScriptCacheSync 保持单进程行为(保存/删除后本进程缓存同步失效);
      • 多实例部署时使用方实现 GroovyScriptCacheSync 广播事件(如 Redis pub/sub),远端实例收到后调用 GroovyScriptCacheSyncContext.receive(event) 失效本地缓存(按来源实例标识过滤回环),下次 getGroovyScript 自动回源仓储加载新版本。
    2. 进程内事件桥接

      • GroovyScriptCacheEvent 实现 ISyncEvent,写路径同时经 EventPusher.push 进程内分发(无 Spring 上下文时为安全 no-op);
      • 业务代码实现 IHandler<GroovyScriptCacheEvent> 即可监听脚本保存/删除变更。
    3. 缓存策略可插拔

      • LRU 容器抽象为 GroovyScriptCache 接口(默认 DefaultGroovyScriptCache,行为与原实现一致);
      • GroovyScriptCacheContext.setCache(...) 可替换为 Redis 等共享缓存实现(注入方式与 GroovyScriptRepositoryContext.setRepository 一致);
      • GroovyScriptCacheContext.remove(key) 调整为纯本地失效(原实现会先回源再删除,净效果不变)。

    验证结果

    • 模块测试:springboot-starter-script 96/96 通过(新增 9 个同步机制单元测试 + 2 个框架事件桥接 Spring 集成测试),JDK 17.0.17;
    • 全量构建:./mvnw clean test -P travis 全部 5 个模块 456 个测试通过,BUILD SUCCESS。

    其他

    • 8.x 版本线存在相同机制,如需同步修复请确认,将按 8.2.x 基线另行处理(Ref #214)。
  4. xlorne
    added a commit that references this issue on Nov 10, 2026

    docs: 补充 Groovy 脚本缓存多实例部署文档与 Redis 同步实现示例 (17.x/springboot-starter-sc…

    e5a67f0
  5. xlorne commented on Nov 10, 2026

    @xlorne
    xlorne
    on Nov 10, 2026
    Contributor
    More actions

    补充说明:已在特性分支追加提交 e5a67f08,完善能力文档 docs/capabilities/springboot-starter-script/groovy-runtime.md:

    • 新增「多实例部署」章节,明确部署前提:① 注入共享仓储(默认仓储为进程内 HashMap,不提供跨实例共享)② 实现 GroovyScriptCacheSync 跨实例失效同步;
    • 提供 Redis pub/sub 失效同步完整实现示例(推荐方案:Redis 仅作失效通知通道,脚本正文存共享数据库,远端实例失效后自动回源);
    • 提供 Redis 共享缓存容器实现示例(可选方案,需处理 Class<?> 字段序列化);
    • 补充行为说明(回环过滤、SHA256 编译缓存自愈、临时脚本进程内语义)。

    另经全仓库排查,其余模块(security 的 JWT/Redis 两种认证模式、data-authorization 的 SQL 解析与权限规则、starter 核心的消息与注册表类)均不存在同类多实例脏读问题,本次仅需调整 script 模块。

  6. xlorne commented on Nov 10, 2026

    @xlorne
    xlorne
    on Nov 10, 2026
    Contributor
    More actions

    8.x 版本线修复完成

    特性分支已推送:feature/issue-214-groovy-cache-sync-8x(基线 8.2.x,提交 679874a6)
    PR 创建入口(目标分支 8.2.x,是否合并由维护者决定):https://github.com/codingapi/springboot-framework/pull/new/feature/issue-214-groovy-cache-sync-8x

    按仓库规范做了 JDK 8 代际适配(未直接 cherry-pick):匿名内部类显式泛型、new ArrayList<>() 代替 stream().toList(),提交信息使用 Ref #214(8.x 合入 8.2.x 不会自动关闭 issue)。代码与文档(多实例部署章节 + Redis 实现示例)与 17.x 保持一致。

    验证结果(JDK 1.8.0_462 / Corretto 8,字节码 major version 52)

    • springboot-starter:108/108 通过;springboot-starter-script:96/96 通过(含新增 9 个同步机制单元测试 + 2 个框架事件桥接集成测试)。

    ⚠️ 需要维护者关注的基线问题(与本修复无关)

    ./mvnw clean test -P travis 在 JDK 8 下无法完成全量构建:8.2.x 基线根 pom 声明 jsqlparser.version=5.0(字节码版本 55.0 = Java 11),导致 springboot-starter-data-authorization 在 JDK 8 下编译失败(无法访问 net.sf.jsqlparser.expression.Expression,类文件具有错误的版本 55.0, 应为 52.0)。该问题在本次改动之前即存在(本分支未触碰该模块与根 pom),建议另开 issue 评估降级到 JDK 8 兼容版本(如 4.x)及 API 兼容性。

  7. xlorne
    closed this as completedin 0355e56on Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.