Unlock MyBatis Performance: Mastering First and Second Level Caches

This article explains how MyBatis first‑level (session) and second‑level (cross‑session) caches work, the conditions required for them to function, common pitfalls when integrating with SpringBoot, and step‑by‑step instructions to enable and safely use second‑level caching in real projects.

Programmer DD
Programmer DD
Programmer DD
Unlock MyBatis Performance: Mastering First and Second Level Caches

Overview of MyBatis Caching

MyBatis provides a two‑level cache architecture—first‑level cache (session scoped) and second‑level cache (shared across sessions) — to improve query performance.

First‑Level Cache

The first‑level cache is enabled by default and works only within the same SqlSession. It is cleared when the session ends.

Typical use case: when an order table and a member table have a one‑to‑many relationship, repeated queries for the same member_id can be served from the first‑level cache.

Conditions for the first‑level cache to take effect:

Same session

Same mapper (namespace)

Same statement (method)

Identical SQL and parameters

No session.clearCache() call between queries

No insert/update/delete executed between queries

Why First‑Level Cache May Not Work with SpringBoot

When MyBatis is integrated with SpringBoot, each SQL execution creates a new SqlSession by default, so the session‑scoped cache never reuses data.

Solution: annotate the service method with @Transactional so that Spring reuses the same SqlSession within the transaction.

@Transactional
public void someMethod() {
    // queries here share the same SqlSession and first‑level cache
}

How First‑Level Cache Is Managed Internally

During a query, SqlSessionUtils.getSqlSession tries to obtain a session from the transaction manager; if none exists, it creates a new DefaultSqlSession. The session is stored in a ThreadLocal via TransactionSynchronizationManager.bindResource, allowing subsequent queries in the same thread to reuse it.

ThreadLocal session storage
ThreadLocal session storage

Second‑Level Cache

Second‑level cache is disabled by default and must be enabled manually. It is suitable for static data such as dictionary, menu, or permission tables that are read‑heavy and rarely updated.

Enabling steps:

Set mybatis-plus.configuration.cache-enabled: true in application.yml.

Add @CacheNamespace on the mapper interface.

Make the entity class implement Serializable.

mybatis-plus:
  configuration:
    cache-enabled: true
@Mapper
@CacheNamespace
public interface ItemMapper {
    // mapper methods
}
CacheNamespace annotation
CacheNamespace annotation

Conditions for Second‑Level Cache to Work

Cache is populated only after the session is committed or closed.

Same mapper (namespace) and same statement.

Identical SQL and parameters.

If readWrite=true (default), the cached object must implement Serializable.

Cache Eviction Rules

Cache is cleared only after a modifying session (insert/update/delete) is committed.

Any update operation clears the entire namespace cache.

How Second‑Level Cache Is Filled

When a query finishes, SqlSession.commit() triggers TransactionalCache.flushPendingEntries, which writes the result into the second‑level cache managed by TransactionalCacheManager. Subsequent queries retrieve data from PerpetualCache without hitting the database.

TransactionalCache flow
TransactionalCache flow

Why MyBatis Disables Second‑Level Cache by Default

It is not recommended because the cache is scoped by namespace; an update in one mapper does not invalidate caches of other mappers, leading to stale data when tables are joined across namespaces.

Example: ItemMapper updates an item name, but XxxMapper still returns the old name from its second‑level cache because the namespaces are independent.

@Mapper
@CacheNamespace
public interface XxxMapper {
    @Select("select i.id itemId, i.name itemName, p.amount, p.unit_price unitPrice " +
            "from item i join payment p on i.id = p.item_id where i.id = #{id}")
    List<PaymentVO> getPaymentVO(Long id);
}

@Test
void test() {
    List<PaymentVO> first = xxxMapper.getPaymentVO(1L);
    Item item = itemMapper.selectById(1L);
    item.setName("new name");
    itemMapper.updateById(item);
    List<PaymentVO> second = xxxMapper.getPaymentVO(1L); // returns cached old name
}

This demonstrates the hidden risk of second‑level caching when tables are related across different mappers.

Conclusion

First‑level cache is simple, session‑scoped, and safe; second‑level cache can improve performance for static data but must be used with caution due to potential data inconsistency across namespaces.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavacacheBackend DevelopmentMyBatisspringbootFirst-Level CacheSecond-Level Cache
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.