Understanding MyBatis First‑Level and Second‑Level Caches and Their Integration with Spring Boot
This article explains how MyBatis implements first‑level (session) and second‑level (cross‑session) caches, the conditions required for each to work, why the first‑level cache often fails in Spring Boot without transactions, how to enable and use the second‑level cache safely, and the hidden risks of stale data when different mappers share related tables.
MyBatis provides a two‑level caching architecture—first‑level cache scoped to a SqlSession and second‑level cache that can be shared across sessions—to improve query performance.
The first‑level cache is enabled by default and works only within the same SqlSession. It becomes effective only when the following conditions are met: the same session, the same mapper (namespace), the same statement (method), identical SQL and parameters, no explicit session.clearCache() call, and no insert/update/delete statements executed between queries.
When MyBatis is integrated with Spring Boot, a new SqlSession is created for each SQL execution, so the first‑level cache appears ineffective. Wrapping the method with @Transactional forces the same SqlSession to be reused within the transaction, allowing the first‑level cache to function as expected.
Internally, mapper methods eventually call SqlSessionUtils.getSqlSession , which tries to obtain a SqlSession from the transaction manager; if none exists, it creates a DefaultSqlSession . The obtained session is then registered via TransactionSynchronizationManager.bindResource , storing a SessionHolder in a ThreadLocal variable for the current thread.
Second‑level cache is disabled by default. To enable it, set cache-enabled: true in the MyBatis configuration (e.g., mybatis-plus.configuration.cache-enabled: true ), annotate the mapper interface with @CacheNamespace , and ensure the entity class implements Serializable (required when readWrite=true , which is the default).
Second‑level cache becomes effective only after the SqlSession is committed or closed. The same conditions as the first‑level cache apply (same mapper, same statement, same SQL and parameters). Additionally, the cached objects must be serializable when readWrite=true .
Cache clearing occurs only after a transaction commit. Any insert, update, or delete operation clears the entire cache for the affected namespace; XML‑configured @CacheNamespace updates cannot selectively clear entries.
When a query finishes, BaseExecutor.queryFromDatabase stores the result in a local PerpetualCache . Upon commit, TransactionalCache.flushPendingEntries transfers the data to the second‑level cache. Subsequent queries invoke MybatisCachingExecutor.query , which checks TransactionalCacheManager for a matching key (constructed from mapper, method, SQL, and parameters) and returns the cached result without hitting the database.
Example: an @RestController with a @GetMapping("/{id}") method calls itemMapper.selectById(id) . When the same request is sent twice in different sessions, the second request retrieves the data from the second‑level cache, as shown by the logs.
Enabling second‑level cache requires three steps: (1) set cache-enabled: true in the YAML configuration, (2) add @CacheNamespace to the mapper interface, and (3) make the entity class implement Serializable . The article includes screenshots of the configuration and annotations.
A hidden danger arises when different mappers share related tables. Because second‑level cache is scoped by namespace, an update performed by one mapper does not invalidate the cache of another mapper, leading to stale data. The article provides a test case where XxxMapper.getPaymentVO returns an outdated itemName after ItemMapper updates the same row.
The test code demonstrates two calls to xxxMapper.getPaymentVO before and after updating the Item table without a surrounding transaction. The first call caches the result; the subsequent update clears only ItemMapper 's namespace cache, leaving XxxMapper 's cache untouched, so the second call still returns the old value.
In conclusion, while second‑level cache can improve performance for static reference data (e.g., dictionaries, menus), it introduces significant hidden risks when tables are related across namespaces. Therefore, careful consideration is needed before enabling it, and many developers choose to keep it disabled.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.