cache-invalidation.md 3.26 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 — every running node has to drop its cached interpretation of the old metadata. xly does this through JMS, not by polling.

The path

PM saves in BACK
    │
    ▼
BACK controller writes the changed gds_* row
    │
    ▼
Controller publishes a JMS "module changed" message
    │
    ▼
Every node's xlyErpJmsConsumer receives it
    │
    ▼
ConsumerChangeGdsModuleThread.run() clears the relevant Redis keys
    │
    ▼
Next /business/getModelBysId call on any node re-reads the table
    and re-populates the cache with the new value

The handler is in xlyErpJmsConsumer/src/main/java/com/xly/xlyerpjmsconsumer/thread/ConsumerChangeGdsModuleThread.java.

Why JMS, not poll-and-bust

xly often runs across multiple nodes (xlyEntry, xlyApi, xlyInterface each on their own JVM, sometimes scaled horizontally). Polling for "has the metadata changed?" would either be slow (the change isn't visible until the next poll) or chatty (constant heartbeats). JMS fans out the invalidation to every node within milliseconds.

xly uses both ActiveMQ and RocketMQ in the codebase, but the metadata-change path documented here is the ActiveMQ / JMS one: xlyErpJmsConsumer listens on P2pQueue.ERP_JMS_ACTIVEMQ_CHANGE_GDS_MODULE with @JmsListener, and ConsumerChangeGdsModuleThread handles the cache-bust work. RocketMQServiceImpl exists for other integration flows.

Which keys get cleared

The Redis cache holds:

  • Module metadata by sId.
  • Form metadata by sId.
  • Field-list slaves keyed by form sId.
  • Per-tenant overlay merges (a derived cache).
  • Permission rules per (module, role).

The consumer thread receives the changed row's IDs and clears each cache key family that could plausibly include it. Over-invalidating is the safe option here — the cost of an extra DB read on the next request is far smaller than the cost of serving stale metadata.

When you change metadata directly via SQL

Inserts/updates done through MyBatis or BACK trigger the JMS event. Inserts/updates done by an engineer running raw UPDATE gdsmodule SET ... against the production DB do not trigger it. 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.
  3. A manual JMS message is sent (see BusinessCleanRedisDataImpl in xlyBusinessService).

The third option is the supported workaround; option (2) is the brute-force fallback.

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. Is the JMS broker reachable from the BACK node? (If not, the invalidation event silently isn't published.)
  3. Are all consumer nodes running? (If a node is paused, it'll serve stale metadata until restarted.)
  4. Did the change happen via raw SQL? (Then no JMS event was published — manually trigger it.)

The "five-table read" of Slice 1 re-runs from the cache; understanding which layer is stale is the key to the bug.