Application-Level Caching in Persistent Frameworks
Relational database interactions frequent become the primary bottleneck in high-throughput applications. To mitigate excessive I/O operations and network latency, modern persistence frameworks integrate in-memory caching layers. MyBatis implements a dual-tier caching architecture designed to balance query performance with data accuracy. This guide examines the operational mechanics of both cache levels, providing concrete implementations and architectural considerations.
Level 1 Cache: Session-Bound Storage
Core Mechanics
Level 1 cacching operates exclusively within the boundaries of a single SqlSession. The framework activates it automatically for every session instance, and it cannot be disabled. The cache lifecycle strictly parallels the session lifecycle: query results are populated upon the first execution and persist until the session terminates, is manually cleared, or undergoes data modification.
When identical SQL statements and parameters are processed within the same session context, MyBatis bypasses the database driver and returns the stored result directly. This eliminates redundant round-trips. However, any INSERT, UPDATE, or DELETE execution triggers an automatic cache eviction. This safeguard prevents applications from consuming stale data that diverges from the actual database state.
Implementation Demonstration
Consider a Maven-based project focusing on an inventory domain. The following directory layout illustrates the setup:
mybatis-cache-lab/
├── src/main/java/com/techdemo/
│ ├── model/Item.java
│ ├── dao/ItemMapper.java
│ └── runner/LevelOneCacheRunner.java
└── src/main/resources/
├── mappings/ItemMapper.xml
└── persistence-config.xml
The domain entity requires standard accessors. A constructor log will help trace object instantiation during execution:
package com.techdemo.model;
public class Item {
private Long identifier;
private String productName;
private Double unitCost;
public Item() {
System.out.println("[TRACE] New Item instance initialized.");
}
// Getters and Setters omitted for brevity
}
The mapper interface defines standard retrieval and modification operations:
package com.techdemo.dao;
import com.techdemo.model.Item;
public interface ItemMapper {
Item fetchById(Long id);
void modifyCost(Item target);
}
The XML configuration maps directly to the interface methods:
<mapper namespace="com.techdemo.dao.ItemMapper">
<select id="fetchById" resultType="com.techdemo.model.Item">
SELECT id AS identifier, name AS productName, price AS unitCost
FROM stock_warehouse WHERE id = #{id}
</select>
<update id="modifyCost">
UPDATE stock_warehouse SET price = #{unitCost} WHERE id = #{identifier}
</update>
</mapper>
The execution class demonstrates cache behavior across sequential queries and DML triggers:
package com.techdemo.runner;
import com.techdemo.dao.ItemMapper;
import com.techdemo.model.Item;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
public class LevelOneCacheRunner {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("persistence-config.xml"));
try (SqlSession session = factory.openSession()) {
ItemMapper repository = session.getMapper(ItemMapper.class);
System.out.println("=== First Retrieval ===");
Item recordA = repository.fetchById(501L);
System.out.println("=== Second Retrieval ===");
Item recordB = repository.fetchById(501L);
System.out.println("Reference match: " + (recordA == recordB));
System.out.println("=== Executing DML ===");
recordA.setUnitCost(45.00);
repository.modifyCost(recordA);
session.commit();
System.out.println("=== Post-Update Retrieval ===");
Item recordC = repository.fetchById(501L);
System.out.println("Reference match after mutation: " + (recordA == recordC));
}
}
}
Execution Analysis: The initial call instantiates a fresh object. The subsequent call reuses the exact same memory reference, confirming session-level caching. Executing the update operation invalidates the local pool, forcing a fresh database fetch for the final query and resulting in a different object reference.
Level 2 Cache: Namespace-Wide Distribution
Architectural Overview
Level 2 caching extends beyond individual sessions, operating at the SqlSessionFactory or Mapper namespace level. It remains disabled by default and requires explicit configuration. Data transfers from a session’s Level 1 cache to the Level 2 cache only when the session commits or closes. Subsequent sessions querying the same namespace will inspect this shared pool before establishing a database connection.
Activation demands three strict conditions: enabling the global cache flag in the core configuration, declaring a <cache /> element within the target mapper XML, and ensuring all cached entities implement java.io.Serializable. Serialization is mandatory because the framework must safely detach objects from their originating session to prevent cross-session state pollution.
Configuration & Verification
To enable the global cache, update the main configuration file:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
Next, append the cache declaration to the mapper definition:
<mapper namespace="com.techdemo.dao.ItemMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<!-- SQL statements remain unchanged -->
</mapper>
The domain entity must now support serialization:
public class Item implements java.io.Serializable { ... }
A dedicated runner class isolates the Level 2 cache verification by utilizing separate session instances:
package com.techdemo.runner;
import com.techdemo.dao.ItemMapper;
import com.techdemo.model.Item;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
public class LevelTwoCacheRunner {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("persistence-config.xml"));
Item initialFetch;
try (SqlSession s1 = factory.openSession()) {
initialFetch = s1.getMapper(ItemMapper.class).fetchById(501L);
} // Session closure flushes data to shared L2 pool
Item subsequentFetch;
try (SqlSession s2 = factory.openSession()) {
subsequentFetch = s2.getMapper(ItemMapper.class).fetchById(501L);
}
System.out.println("Content equality: " + initialFetch.equals(subsequentFetch));
System.out.println("Identity match: " + (initialFetch == subsequentFetch));
}
}
Key Observation: While the data content matches perfectly, the identity check returns false. This occurs because the framework deserializes a new instance from the shared cache, intentionally decoupling the returned object from the original reference to maintain session isolation and prevent unintended side effects.
Operational Guidelines & System Design Trade-offs
Selecting the appropriate cache level depends entirely on data volatility and access patterns:
- Level 1 excels in short-lived, sequential operations within a single transaction. It guarantees strict consistency by invalidating on any modification, making it ideal for complex, multi-step business logic where immediate accuracy is mandatory.
- Level 2 targets high-read, low-write datasets such as configuration dictionaries, product categories, or reference tables. It drastically reduces database load across concurrent requests but introduces eventual consistency risks that require careful application logic.
In distributed environments, native MyBatis caching often falls short due to the absence of cross-node synchronization and advanced expiration policies. Enterprise architectures typically delegate caching responsibilities to external systems like Redis or Ehcache by implementing custom cache managers via the type attribute in the mapper configuration. This approach centralizes cache management, enables TTL configurations, and mitigates cache stampede scenarios while keeping the persistence layer lightweight. Understanding the fundamental compromise between response velocity and data freshness allows developers to architect systems that leverage caching strategically rather than indiscriminately.