Understanding MyBatis Caching: First-Level and Second-Level Cache Mechanisms

Introduction to Caching

Caching is a common feature in ORM frameworks, designed to enhance query performance and reduce database load by storing frequently accessed data in memory.

Cache Architecture

In the MyBatis source code, classes related to caching are located in the cache package. The core of this system is the Cache interface, with the default implementation being PerpetualCache, which is a basic cache backed by a HashMap.

MyBatis implements its caching functionality using the Decorator Pattern. This pattern allows adding new functionality to an object dynamically without altering its structure, offering a more flexible alternative to inheritance.

The cache hierarchy can be broadly categorized into three types: basic caches, eviction algorithm caches, and decorator caches.

First-Level Cache (Local Cache)

The first-level cache, also known as the local cache, operates at the session level. MyBatis enables this cache by default, requiring no additional configuration. It can be effectively disabled by setting the localCacheScope to STATEMENT, which limits caching to the individual statement execution.

The cache object is maintained within the Executor. Specifically, the BaseExecutor (the parent class of SimpleExecutor, ReuseExecutor, and BatchExecutor) holds a PerpetualCache instance in its constructor.

// Simplified BaseExecutor constructor
protected BaseExecutor(Configuration config, Transaction transaction) {
    this.transaction = transaction;
    this.sessionCache = new PerpetualCache("SessionCache");
    this.outputParamCache = new PerpetualCache("OutputParamCache");
    this.configuration = config;
}

Within a single session, executing the same SQL statement multiple times will retrieve results from the in-memory cache, avoiding a database query. However, in different sessions, the same query will hit the database, bypassing the first-level cache.

The caching logic is found in the query() method of BaseExecutor. Before querying the database, it attempts to retrieve data from the cache using a CacheKey.

// Query logic in BaseExecutor
List<E> results = (List<E>) sessionCache.getObject(cacheKey);
if (results != null) {
    // Handle cached output parameters
} else {
    // If not in cache, query from the database
    results = queryFromDatabase(mappedStatement, parameter, boundSql, cacheKey);
    // Store the result in the first-level cache
    sessionCache.putObject(cacheKey, results);
}
return results;

Write operations, such as update or delete, trigger a cache clear to insure data consistency.

// Cache clearing on update
public int update(MappedStatement ms, Object parameter) throws SQLException {
    clearLocalCache(); // This clears the first-level cache
    return doUpdate(ms, parameter);
}

The primary limitation of the first-level cache is its scope. It is bound to a single session, meaning data is not shared across sessions. This can lead to stale data issues in multi-session or distributed environments, where one session updates data that another session might still have cached.

Second-Level Cache

The second-level cache is introduced to solve the problem of data not being shared across sessions. It operates outside the SqlSession and is managed by a CachingExecutor decorator.

When the second-level cache is enabled, MyBatis wraps the standard executor (e.g., SimpleExecutor) with a CachingExecutor. For a query, the CachingExecutor first checks the second-level cache. If the data is not found, it delegates the request to the underlying executor (which then checks the first-level cache) and finally stores the result in the second-level cache.

Enabling the Second-Level Cache

Enabling the second-level cache requires two steps:

  1. Enable it globally in mybatis-config.xml (it's enabled by default).
<!-- Enable global second-level cache (default is true) -->
<setting name="cacheEnabled" value="true"/>

  1. Configure it for a specific mapper in the Mapper.xml file using the <cache> tag.
<!-- Cache configuration for a specific mapper -->
<cache 
    type="org.apache.ibatis.cache.impl.PerpetualCache"
    size="1024"
    eviction="LRU"
    flushInterval="120000"
    readOnly="false"/>

The <cache> tag supports several attribute:

  • type: The cache implementation class.
  • size: The maximum number of objects the cache can hold.
  • eviction: The eviction policy (e.g., LRU, FIFO).
  • flushInterval: The time in milliseconds before the cache is automatically refreshed.
  • readOnly: If true, the cache returns the same instance to all callers; objects must be serializable if false.

If cacheEnabled is true but a mapper's <cache> tag is missing, the CachingExecutor is still created, but since no cache object exists for that mapper, the second-level cache is not utilized.

To disable the second-level cache for a specific query, you can set useCache="false" on the <select> statement.

Transactional Behavior and Cache Flushing

The second-level cache is not written to until the transaction is committed. This is managed by the TransactionalCache class, which holds pending entries and only writes them to the underlying cacche upon a commit() call.

// Simplified commit logic in TransactionalCache
public void commit() {
    // Write pending entries to the delegate cache
    for (Map.Entry<Object, Object> entry : pendingEntries.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    // Reset state for the next transaction
    pendingEntries.clear();
}

Write operations (insert, update, delete) automatically clear the relevant cache entries. This is controlled by the flushCache attribute on the SQL mapping tags, which defaults to true for write operations.

Use Cases and Advanced Configuration

The second-level cache is most effective in read-heavy applications, such as querying historical data or order histories, where write operations are infrequent.

By default, each mapper has its own isolated cache. To share a cache across different mappers (e.g., for related tables), you can use the <cache-ref> tag to reference a cache configuration from another namespace.

<!-- Reference a cache from another mapper -->
<cache-ref namespace="com.example.dao.UserMapper"/>

For more advanced use cases, you can integrate third-party caching solutions like Redis. This involves implementing the Cache interface and providing the implementation class in the <cache> tag's type attribute.

<!-- Using a Redis-based cache -->
<cache type="org.mybatis.caches.redis.RedisCache" 
       eviction="FIFO" 
       flushInterval="60000" 
       size="512" 
       readOnly="true"/>

You would also need to add the corresponding dependency to your project's pom.xml.

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

Tags: MyBatis Caching ORM java sql

Posted on Thu, 04 Jun 2026 18:41:26 +0000 by jini01