cache-invalidation.md 5.9 KB

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(<sTable>, ...)
    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: each one drives a domain-specific base-data merge or fan-out work item, not cache invalidation.

The cross-node coherence question (open)

@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). Spring Boot 2.2.5 will then auto-pick a CacheManager based on what's on the classpath; the most likely outcome with spring-boot-starter-data-redis present is that Spring auto-configures Redis-backed caching, which would make @CacheEvict clear the shared Redis store and therefore fan out across nodes implicitly. This has not been confirmed by live inspection of a running node — it's the natural reading of the config but worth verifying when a deployment is available. If the auto-picked manager is in fact ConcurrentMapCacheManager (in-memory, per-JVM), the multi-node coherence story is broken under this code path and would need a separate fix.

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(<table>, …) 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 re-runs from the cache; understanding which layer is stale is the key to the bug.