Resolving MyBatis Second-Level Cache Inconsistencies with Custom Relation Cache
This article explains MyBatis first‑ and second‑level caching, demonstrates how join queries can cause stale data when only the updated mapper's cache is cleared, and introduces a custom @CacheRelations annotation with a RelativeCache implementation that propagates invalidation across related mappers.
1. MyBatis Cache Overview
MyBatis supports caching; by default only the first‑level cache is enabled per SqlSession. The second‑level cache must be explicitly turned on.
First‑level cache is scoped to a single SqlSession /transaction: repeated queries within the same session return cached results unless an Insert, Update or Delete occurs.
Second‑level cache is application‑wide, shared across different SqlSession s. When enabled, the first query result for a mapper is stored in a global cache; subsequent identical queries retrieve data from this cache.
2. Issue with Second‑Level Cache
When a mapper performs a join query (e.g., UserMapper joins Organization), updating the Organization table does not invalidate the cached UserMapper result, leading to stale organization information.
2.1 Data Inconsistency Verification
SQL used:
SELECT u.*, o.name org_name FROM user u LEFT JOIN organization o ON u.org_id = o.id WHERE u.id = #{userId}Mapper method:
UserInfo queryUserInfo(@Param("userId") String userId);Service method:
public UserEntity queryUser(String userId) {
UserInfo userInfo = userMapper.queryUserInfo(userId);
return userInfo;
}Initial query with userId = 1 returns organization "组织1". After updating the organization name to "组织2", a subsequent query of the user still returns "组织1" because the cached UserMapper entry was not cleared.
The root cause is that only the cache of the mapper that performed the update ( OrganizationMapper) is cleared; caches of related mappers remain unaware of the change.
2.2 Solution Idea
Define relationships between mappers in the mapper definition.
When a cache instance (cache1) is created, load references to related caches (cache2).
Store a reference to cache2 inside cache1.
When cache1 is cleared, also clear cache2.
3. Implementing Related Cache Refresh
Enable second‑level cache in MyBatis‑Plus: mybatis-plus.configuration.cache-enabled=true Introduce a custom annotation @CacheRelations to declare dependent mappers, and implement a custom cache class RelativeCache that implements MyBatis Cache interface.
Key parts of RelativeCache:
public class RelativeCache implements Cache {
private Map<Object, Object> CACHE_MAP = new ConcurrentHashMap<>();
private List<RelativeCache> relations = new ArrayList<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
private String id;
private Class<?> mapperClass;
private boolean clearing;
public RelativeCache(String id) throws Exception {
this.id = id;
this.mapperClass = Class.forName(id);
RelativeCacheContext.putCache(mapperClass, this);
loadRelations();
}
@Override public String getId() { return id; }
@Override public void putObject(Object key, Object value) { CACHE_MAP.put(key, value); }
@Override public Object getObject(Object key) { return CACHE_MAP.get(key); }
@Override public Object removeObject(Object key) { return CACHE_MAP.remove(key); }
@Override public void clear() {
Lock lock = getReadWriteLock().writeLock();
lock.lock();
try {
if (clearing) return;
clearing = true;
try {
CACHE_MAP.clear();
relations.forEach(RelativeCache::clear);
} finally { clearing = false; }
} finally { lock.unlock(); }
}
@Override public int getSize() { return CACHE_MAP.size(); }
@Override public ReadWriteLock getReadWriteLock() { return readWriteLock; }
void loadRelations() { /* reads @CacheRelations and builds relation graph */ }
}The RelativeCacheContext holds global maps for cache instances and pending relations:
public class RelativeCacheContext {
public static final Map<Class<?>, RelativeCache> MAPPER_CACHE_MAP = new ConcurrentHashMap<>();
public static final Map<Class<?>, List<RelativeCache>> UN_LOAD_TO_RELATIVE_CACHES_MAP = new ConcurrentHashMap<>();
public static final Map<Class<?>, List<RelativeCache>> UN_LOAD_FROM_RELATIVE_CACHES_MAP = new ConcurrentHashMap<>();
public static void putCache(Class<?> clazz, RelativeCache cache) { MAPPER_CACHE_MAP.put(clazz, cache); }
}Usage example on UserMapper:
@Repository
@CacheNamespace(implementation = RelativeCache.class, eviction = RelativeCache.class, flushInterval = 30 * 60 * 1000)
@CacheRelations(from = OrganizationMapper.class)
public interface UserMapper extends BaseMapper<UserEntity> {
UserInfo queryUserInfo(@Param("userId") String userId);
}Corresponding XML must declare a cache reference so the result is cached:
<cache-ref namespace="com.mars.system.dao.UserMapper"/>Verification steps:
Query userId = 1 → returns organization "组织1".
Update organization name to "组织2" via OrganizationMapper.
Query the same user again → now returns organization "组织2", confirming that the related cache was cleared.
4. Conclusion
The custom RelativeCache together with the @CacheRelations annotation provides a systematic mechanism to propagate cache invalidation across dependent mappers, eliminating stale data problems caused by second‑level cache when join queries are involved.
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 Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.
