Unlock MyBatis Performance: Master First‑Level and Second‑Level Caches in SpringBoot

This article explains how MyBatis implements first‑level (session) and second‑level (namespace) caches, details their activation conditions, common pitfalls—especially when integrated with SpringBoot—and provides step‑by‑step configuration, code examples, and best‑practice recommendations to ensure cache effectiveness while avoiding hidden hazards.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Unlock MyBatis Performance: Master First‑Level and Second‑Level Caches in SpringBoot

MyBatis Caching Overview

To improve query performance, MyBatis provides a two‑level caching architecture: a first‑level (session) cache and a second‑level (namespace) cache.

First‑Level Cache

Application scenario : When a query returns duplicate foreign‑key values (e.g., many orders share the same member_id), the same SQL executed within a single SqlSession can be served from the first‑level cache, avoiding repeated database hits.

Effective conditions :

Same SqlSession (session scope)

Same mapper namespace

Same statement (method) within the mapper

Identical SQL and parameters

No call to session.clearCache() between queries

No insert/update/delete executed between the queries

Why it may not work with SpringBoot : MyBatis creates a new SqlSession for each SQL execution by default. Consequently, the first‑level cache never sees the same session and appears ineffective.

When a transaction is active, SpringBoot reuses the same SqlSession, allowing the cache to work. Adding @Transactional to the service method enables the cache:

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

During the first query, MyBatis obtains a SqlSession from SqlSessionUtils. If a transaction is present, the same session is stored in TransactionSynchronizationManager (ThreadLocal). Subsequent queries in the same transaction retrieve the cached session, making the first‑level cache effective.

Second‑Level Cache

Application scenario : Static reference data such as dictionaries, menus, or permission tables are read frequently but change rarely. A second‑level cache, shared across sessions, can serve these hot‑spot queries.

Enabling steps :

Set cache-enabled: true in the MyBatis configuration (e.g., mybatis-plus.configuration in application.yml).

Add @CacheNamespace to the mapper interface.

Make the entity class implement java.io.Serializable (required when readWrite=true).

Effective conditions :

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

Same mapper namespace and same statement.

Identical SQL and parameters.

If readWrite=true, the cached objects must be Serializable.

Cache eviction conditions :

Any insert, update, or delete in the same namespace clears the entire namespace cache.

Only after a modifying transaction commits does the eviction occur.

When a query finishes, MyBatis calls SqlSession.commit(). This triggers TransactionalCache.flushPendingEntries(), which writes the result into the second‑level cache:

During a subsequent query, MybatisCachingExecutor.query() looks up the cache via TransactionalCacheManager. If a matching key (built from mapper, method, SQL, and parameters) is found, the result is returned without hitting the database:

Why MyBatis disables second‑level cache by default : It is generally discouraged because the cache is scoped to a mapper namespace. Updates in a different mapper do not invalidate the cached data, which can lead to stale results.

Hidden hazard example : Two mappers, ItemMapper and XxxMapper, share the same namespace cache. Updating an Item via ItemMapper does not clear the cache used by XxxMapper, so a subsequent query through XxxMapper may return outdated itemName values.

@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("java并发编程");
    itemMapper.updateById(item);
    List<PaymentVO> second = xxxMapper.getPaymentVO(1L);
    // second still returns the old itemName because XxxMapper's cache was not cleared
}

Therefore, while second‑level cache can improve performance for static data, its namespace‑level granularity and automatic eviction rules make it prone to inconsistencies; use it cautiously.

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.

JavaPerformancecacheMyBatisspringboot
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.