Understanding SpringBoot Two‑Level Caching: MyBatis vs Application‑Level Cache
The article explains how layered caching in Java back‑ends—combining MyBatis first‑ and second‑level caches with a service‑layer Caffeine + Redis cache—affects cache granularity, consistency, distribution, and performance, and provides concrete configuration examples, code snippets, and best‑practice guidelines.
Overall Cache Layer Architecture
In high‑performance Java services, caching is divided into two layers: DAO‑level cache (MyBatis first‑level and second‑level caches) and Service‑level cache (Caffeine local cache + Redis distributed cache). The two layers differ in scope, eviction, granularity, consistency, and distribution support, and must not be mixed.
Cache Request Flow
When a query is issued, the priority order is: Application‑level cache (Caffeine → Redis) → MyBatis first‑level cache → MyBatis second‑level cache → Database. If an upper layer hits, the request stops before reaching lower layers.
MyBatis First‑Level Cache (Session Cache)
Enabled by default and cannot be disabled.
Stored in SqlSession as a thread‑local cache.
Identical SQL within the same session is served from cache without hitting the DB.
Cache is cleared automatically when the session is closed, committed, or when DML operations occur.
Problem: Updates inside a transaction are not visible until commit, leading to stale reads.
Solutions:
Manually call sqlSession.clearCache().
Set flushCache="true" on mapper statements.
Route all business queries through the application‑level cache to avoid MyBatis cache pitfalls.
MyBatis Second‑Level Cache (Namespace Cache)
It is a global cache shared across SqlSession instances for the same mapper namespace.
Enabling Conditions
Global switch cache-enabled: true in application.yml.
Explicit <cache/> element in the mapper XML or annotation.
Result objects must implement Serializable.
Data is stored in the second‑level cache only after a commit or session close.
Configuration Example
mybatis-plus:
configuration:
cache-enabled: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImplMapper XML example:
<mapper namespace="com.cache.mapper.UserMapper">
<cache eviction="LRU" flushInterval="3600000" size="2048" readOnly="false" blocking="false"/>
</mapper>Four Main Parameters
eviction : eviction policy, default LRU.
flushInterval : automatic refresh interval in milliseconds.
size : maximum number of cached objects.
readOnly : false for read‑write (requires serialization), true for read‑only.
Execution Flow
Check if namespace cache contains the key.
If present, return cached result without SQL.
If absent, query DB and store result in first‑level cache.
On commit or session close, promote data to second‑level cache.
Any DML on the same namespace clears the entire namespace cache.
Pros and Cons
Zero code intrusion; works via configuration.
Reduces duplicate SQL and DB load.
Effective for simple single‑table lookups.
Cons: very coarse granularity (whole table), cannot clear individual entries, causes stale data in JOIN queries, does not support distributed consistency, and TTL cannot be fine‑tuned.
Integrating MyBatis with Redis for Distributed Second‑Level Cache
To overcome the lack of cross‑instance sharing, replace the default cache implementation with org.mybatis.caches.redis.RedisCache.
Dependency:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>Mapper configuration example:
<cache type="org.mybatis.caches.redis.RedisCache">
<property name="expireTime" value="3600"/>
</cache>After integration, multi‑instance cache sharing is achieved, but the same coarse‑grained namespace limitation and JOIN issues remain.
Application‑Level Two‑Level Cache (Caffeine + Redis)
This is the recommended enterprise‑grade cache, placed in the Service layer.
Design
L1 Caffeine : in‑process, microsecond latency, handles hot traffic, reduces Redis pressure.
L2 Redis : distributed, persistent fallback, guarantees consistency across instances.
Full Query Process
Check Caffeine; if hit, return.
If miss, query Redis.
If Redis hits, write back to Caffeine and return.
If Redis miss, query the database.
Write DB result to both Redis and Caffeine.
Configuration Snippets
Spring Boot starters:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.benmanes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>Cache manager configuration (simplified):
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class DoubleCacheConfig {
@Bean("caffeineCacheManager")
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.maximumSize(20000)
.removalListener((key, value, cause) ->
System.out.printf("Local cache evicted key=%s, cause=%s%n", key, cause));
cacheManager.setCaffeine(caffeine);
cacheManager.setAllowNullValues(true);
return cacheManager;
}
@Bean("redisCacheManager")
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(2))
.serializeKeysWith(RedisCacheConfiguration.defaultKeySerializer())
.serializeValuesWith(RedisCacheConfiguration.defaultValueSerializer());
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
@Bean("doubleCacheManager")
public CacheManager doubleCacheManager(CacheManager caffeineCacheManager,
CacheManager redisCacheManager) {
return new CacheManager() {
@Override
public Cache getCache(String name) {
Cache localCache = caffeineCacheManager.getCache(name);
Cache remoteCache = redisCacheManager.getCache(name);
return new DoubleLevelCache(localCache, remoteCache);
}
@Override
public java.util.Collection<String> getCacheNames() {
return caffeineCacheManager.getCacheNames();
}
};
}
static class DoubleLevelCache implements Cache {
private final Cache localCache;
private final Cache remoteCache;
public DoubleLevelCache(Cache localCache, Cache remoteCache) {
this.localCache = localCache;
this.remoteCache = remoteCache;
}
@Override public String getName() { return localCache.getName(); }
@Override public Object getNativeCache() { return this; }
@Override public ValueWrapper get(Object key) {
ValueWrapper local = localCache.get(key);
if (local != null) return local;
ValueWrapper remote = remoteCache.get(key);
if (remote != null) {
localCache.put(key, remote.get());
return remote;
}
return null;
}
@Override public void put(Object key, Object value) {
localCache.put(key, value);
remoteCache.put(key, value);
}
@Override public void evict(Object key) {
localCache.evict(key);
remoteCache.evict(key);
}
@Override public void clear() {
localCache.clear();
remoteCache.clear();
}
}
}Service‑layer usage example:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
// Precise key cache
@Cacheable(value = "user_info_cache", key = "#id", cacheManager = "doubleCacheManager")
@Override
public User getUserById(Long id) {
return userMapper.selectById(id);
}
// Precise eviction after update
@CacheEvict(value = "user_info_cache", key = "#user.id", cacheManager = "doubleCacheManager")
@Override
public void updateUser(User user) {
userMapper.updateById(user);
}
}TTL Recommendations
Caffeine local cache: 30 min – 1 h (short TTL to limit stale window).
Redis remote cache: 2 h – 24 h (long TTL as fallback).
Comparison of the Two Second‑Level Cache Approaches
Cache Granularity : MyBatis – whole table (namespace); Service – custom business key per record.
Invalidation : MyBatis – clears entire table on any update; Service – precise key eviction.
Distributed Support : MyBatis native cache is JVM‑local; Redis‑backed MyBatis adds limited distribution but still coarse; Service cache is natively distributed via Redis.
JOIN Queries : MyBatis cache easily produces stale data; Service cache avoids this problem.
Cache Hit Rate : MyBatis – low; Service – high due to fine‑grained keys.
Business Control : MyBatis – weak; Service – fully controllable.
Production Recommendation : MyBatis second‑level cache is not recommended for distributed projects; Service‑level Caffeine + Redis is the universal enterprise standard.
Guidelines and Best Practices
Never enable native MyBatis second‑level cache in distributed clusters.
Avoid any MyBatis caching for multi‑table JOIN business logic.
Do not implement ad‑hoc local caches with plain HashMap (no eviction, OOM risk).
All hot queries, static configurations, and dictionary data should use Caffeine + Redis.
Use @CacheEvict for precise cache removal on updates/deletes.
Set a maximum size for local Caffeine caches to prevent memory overflow.
Local cache TTL must be shorter than Redis TTL to shrink the stale‑data window.
Conclusion
MyBatis first‑ and second‑level caches are designed solely to reduce duplicate SQL execution and are unsuitable for ensuring business data consistency in complex, distributed scenarios. In contrast, a Caffeine + Redis service‑layer cache provides fine‑grained control, proper TTL management, distributed consistency, and superior performance, making it the de‑facto standard for enterprise Java back‑ends.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
