MyBatis First/Second Level Cache

MyBatis first-level cache is per SqlSession: repeated identical queries in the same session hit the cache and do not hit the database. Second-level cache is per Mapper namespace and can be shared across SqlSessions; it is process-local by default but can be wired to Redis. This article explains scope, invalidation, and when to use each, with examples and a comparison table.

Overview

  • First-level cache: Scope is the SqlSession (one session). For the same SQL and parameters, the second call returns the cached result without hitting the DB. The cache is cleared when the SqlSession is closed or when update/insert/delete runs in that namespace. Enabled by default; no config.
  • Second-level cache: Scope is the Mapper namespace. Shared across SqlSessions. Enable with <cache> (or <cache-ref>) on the mapper. It is process-local; in a distributed setup each instance has its own cache, and reads can be stale. Use for read-heavy or weak-consistency cases; for high write or strong consistency, prefer application-level cache (e.g. Redis).
  • Choice: First-level only affects repeated queries within a single request. Second-level can help on a single machine with read-heavy, weakly consistent data; for distributed or high write, usually disable it and use an external cache.

Example

Example 1: First-level cache — repeated query in same SqlSession

Java
try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    User u1 = mapper.selectById(1L);
    User u2 = mapper.selectById(1L);  // No SQL; returns same reference as u1
}
  • The second selectById hits the first-level cache. u1 and u2 are the same object; changes to u1 affect u2. After mapper.update(...), the cache for that namespace is cleared.

Example 2: First-level cache not shared — different SqlSessions

Java
try (SqlSession s1 = factory.openSession()) {
    s1.getMapper(UserMapper.class).selectById(1L);
}
try (SqlSession s2 = factory.openSession()) {
    s2.getMapper(UserMapper.class).selectById(1L);  // Executes SQL; different session
}
  • Each request typically uses a new SqlSession, so first-level cache does not cross requests. It only reduces repeated queries within the same request.

Example 3: Enabling second-level cache (XML)

XML
<mapper namespace="com.example.UserMapper">
  <cache eviction="LRU" size="1024" readOnly="true"/>
  <select id="selectById" resultType="User" useCache="true">
    SELECT * FROM user WHERE id = #{id}
  </select>
</mapper>
  • <cache> turns on second-level cache for that namespace. Configure eviction (e.g. LRU), size, and readOnly. Selects default to useCache="true"; insert/update/delete clear the namespace cache.

Example 4: Disabling cache for a specific query

XML
<select id="selectByIdForUpdate" resultType="User" useCache="false" flushCache="true">
  SELECT * FROM user WHERE id = #{id} FOR UPDATE
</select>
  • Use when you need fresh data (e.g. pessimistic locking).
CacheScopeInvalidated whenTypical use
First-levelSqlSessioninsert/update/delete in same session, or session closeRepeated identical queries in one request
Second-levelMapper namespaceWrite in same namespace, or TTL/evictionSingle machine, read-heavy, weak consistency
  • Second-level stores serialized results (or object references when readOnly). In distributed deployments, each node has its own cache and updates only clear the local cache. Usually disable second-level and use Redis or similar for shared cache.

Core Mechanism / Behavior

  • First-level: SqlSession holds a PerpetualCache (Map). Key includes statementId, rowBounds, SQL, and params. Writes clear the cache for the affected namespace.
  • Second-level: <cache> creates a Cache for the namespace. Reads check second-level first; on miss, query DB and put result in cache. CUD operations in that namespace clear the cache.
  • Transaction: First-level is effective within one transaction (one SqlSession). Second-level is updated on session commit/close (configurable), so transaction boundaries matter.

Key Rules

  • First-level cache is always on. Writes in the same SqlSession clear it. To force a fresh read, use SqlSession.clearCache() or a new SqlSession.
  • Enable second-level only for single-machine, read-heavy, weak-consistency scenarios. For distributed, high write, or strong consistency, disable it and use application-level cache (e.g. Cache-Aside + Redis).
  • Entities used in second-level cache should implement Serializable if required by the cache implementation. readOnly="true" avoids deserialization but requires immutability or thread safety if the same instance is shared.

What's Next

See Cache-Aside and Caching Pitfalls for application-level caching. MyBatis cache is ORM-level; it can complement or replace second-level cache.