# Cache invalidation on metadata change When a PM saves a change in BACK — adds a column to a form, updates a permission, registers a new module — the framework drops the cached interpretation of the old metadata. **The cache-clear is synchronous in the BACK process via Spring's `@CacheEvict`**, NOT a JMS fan-out. A separate JMS path with similarly-named classes exists for a different purpose (base-data merge); the two are easy to confuse and this page calls them out explicitly. ## The actual cache-invalidation path (synchronous, in-process) ``` PM saves in BACK │ ▼ BACK controller (e.g. /business/addUpdateDelBusinessData) calls BusinessBaseServiceImpl.addBusinessData / updateBusinessData / deleteBusinessData │ ▼ Save service calls businessCleanRedisData.delCleanRedisData(...) (e.g., BusinessBaseServiceImpl.java:1122, 1224, 1375, 1441, 1597, 1677) │ ▼ BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(, ...) dispatches to one of the named cleaners on CleanRedisServiceImpl │ ▼ CleanRedisServiceImpl.cleanRedisByTableNameGdsModle() (or similar) fires @CacheEvict against a fixed list of named cache regions │ ▼ Spring CacheManager evicts the named entries │ ▼ Next /business/getModelBysId call re-reads from DB and re-populates the cache. ``` The cleaner methods are in `xlyBusinessService/src/main/java/com/xly/service/impl/CleanRedisServiceImpl.java`. A representative one — invoked when `gdsmodule` rows change — evicts 17 cache regions in a single call: ``` @CacheEvict(value = { "getGdsmoduleTree", "getGdsmoduleList", "getModuleTreePro", "getSysjurisdictionTreePro", "getsDisplayTypeAll", "businessBaseServiceGetMenuList", "getBuMenu", "getMenu", "getsAuthsId", "businessCommonServicegetModulelistAll", "gdsmoduleById", "getSaveProName", "businessParameterGetParameter", "getPrcName", "getKpiModelByUser", "getUserByFromId", "getUserByActionId", "getModuleTreeProAll" }, allEntries = true) public void cleanRedisByTableNameGdsModle() { … } ``` Other table-named cleaners on the same class evict the regions relevant to `gdsconfigformmaster`, `gdsconfigformslave`, `gdsconfigtbmaster`, `gdsformconst`, `gdsjurisdiction`, `gdsconfigcharmaster`, login-info, billnosetting, kpimaster, `SysSystemSettings`, etc. ## What the JMS `CHANGE_GDS_MODULE` queue actually does (NOT cache-bust) The framework has a JMS queue `P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE` and a consumer thread `ConsumerChangeGdsModuleThread`, both of which sound like they should be doing cache invalidation — but they don't. `ConsumerChangeGdsModuleThread.run()` resolves a `changeGdsModuleService` bean (`ChangeGdsModuleServiceImpl`) and calls `changeTableData(sGdsModuleId, sJobId)`, which invokes the stored procedure `PRO_ERPMERGEBASEGDSMODULE` (via `proDao.proErpMergeBaseGdsModule`, mapped in `ProMapper.xml`). That proc consolidates per-tenant `gdsmodule` rows into a flattened "base" lookup table — a base-data merge job, not a cache evict. A `grep` of `xlyErpJmsConsumer/` for `@CacheEvict` or `cleanRedis*` returns zero hits — the consumer side clears nothing in Redis. The same goes for the other 23 `ERP_JMS_ACTIVEMQ_*` queues in [`P2pQueue.java`](../../api-reference/messaging.md): each one drives a domain-specific base-data merge or fan-out work item, not cache invalidation. ## Cross-node cache coherence — Redis-backed, confirmed `@EnableCaching` is on `EntryApplicationBoot.java:22` and `ApiApplicationBoot.java:24`. **No custom `CacheManager` bean is declared anywhere in the in-scope source** (no `RedisCacheManager`, no `@Bean CacheManager`-returning method, no `implements CacheManager`, no `spring.cache.*` property in any `application*.yml`). With `spring-boot-starter-data-redis` present and no other cache provider on the classpath (no Caffeine, EhCache, Hazelcast, JCache; the `shiro-ehcache` jar in `xlyFlow` is for Shiro's own session cache, not Spring Cache), Spring Boot 2.2.5 auto-configures **`RedisCacheManager`**. **Empirically verified** against the live dev Redis at `118.178.19.35:16379` (database 0): 233 of 267 keys use Spring Cache's default `::` separator. Sample key shape matching the `@Cacheable` SpEL spec from `BusinessGdsconfigformsServiceImpl.java:209-211`: ``` businessGdsconfigformsServiceGetFormconstData::{sLanguage=sChinese, sModelsId=…, sSubsidiaryId=1111111111, sUserId=…, sBrandsId=1111111111} gdsmoduleById::gdsmoduleById___ ``` So `@CacheEvict` on any node clears the shared Redis store and the next read on every node sees the eviction. Cross-node coherence works through Redis, not through JMS. ## When you change metadata directly via SQL Inserts/updates done through MyBatis or BACK trigger `businessCleanRedisData.delCleanRedisData*`. Raw `UPDATE gdsmodule SET …` against the DB does **not** trigger any cleaner. The cache will serve stale metadata until either: 1. The cache TTL expires (check the cache config for the actual TTL). 2. A bounce of the application servers (one node at a time if the cache is local; once if shared). 3. A manual call to one of the `BusinessCleanRedisDataImpl.delCleanRedisDataByTableName(, …)` methods is invoked from inside the application (e.g., via a maintenance endpoint). Note this clears whatever the local `CacheManager` is bound to; if that turns out to be in-memory, the cleanup must run on every node. ## Common bug: the cache is the bug When something looks like "I changed it but the page still shows the old value", check (in this order): 1. Did the change actually commit? (Confirm with `SELECT` against the DB.) 2. Did the change go through a path that invokes `BusinessCleanRedisData`? (Direct DB writes or controllers that bypass `BusinessBaseServiceImpl` won't.) 3. Is the cache shared across nodes (Redis-backed) or local (`ConcurrentMapCacheManager`)? Confirm by inspecting the active `CacheManager` bean on a running node. 4. If the cache is local, did every node get the eviction call? The five-key composite returned by [`getModelBysId` in Slice 1](../../slices/01-hello-world.md) re-runs from the cache; understanding which layer is stale is the key to the bug.