1. Introduction to MyBatis cache
Mybatis supports caching, but without configuration, it only enables the L1 cache by default, and the L2 cache needs to be manually enabled.
The L1 cache is only relative to the same SqlSession.
That is, for the same transaction, execute the same query method of the same Mapper multiple times. After the first query, MyBatis will put the query results into the cache. If the data Update (Insert, Update and Delete) operations of the corresponding Mapper are not involved in the middle, the subsequent queries will be obtained from the cache instead of querying the database.
L2 caching is for application level caching, that is, caching for different sqlsessions.
When the L2 cache is enabled, MyBatis will store the first query results in the global cache for Mapper. If the Mapper data update operation is not performed in the middle, the subsequent same queries will be obtained from the cache.
2. L2 cache problem
According to the introduction of the secondary cache, if Mapper is only a single table query, there will be no problem, but if the query involved in Mapper is an associated table query, for example, UserMapper needs to associate the query organization information when querying the user information, that is, it needs to associate the user table with the organization table, and the organization Mapper will not update the cache of UserMapper when executing the update, As a result, when using UserMapper to query user information with the same conditions, it will wait until the organization information before updating, resulting in inconsistent data.
2.1 verification of data inconsistency
Query SQL
SELECT u.*, o.name org_name FROM user u LEFT JOIN organization o ON u.org_id = o.id WHERE u.id = #{userId}
UserMapper
UserInfo queryUserInfo(@Param("userId") String userId);
UserService
public UserEntity queryUser(String userId) { UserInfo userInfo = userMapper.queryUserInfo(userId); return userInfo; }
Call the query to get the query result (multiple queries to get cached data), where userId = 1 and data is the user query result
{ "code": "1", "message": null, "data": { "id": "1", "username": "admin", "password": "admin", "orgName": "Organization 1" } }
Query the corresponding organization information and the results
{ "code": "1", "message": null, "data": { "id": "1", "name": "Organization 1" } }
It is found that the data is consistent with the user cache.
Update organization, change organization 1 to organization 2, and query organization information again
{ "code": "1", "message": null, "data": { "id": "1", "name": "Organization 2" } }
Query the user information again and find that it is still obtained from the cache
{ "code": "1", "message": null, "data": { "id": "1", "username": "admin", "password": "admin", "orgName": "Organization 1" } }
The reason for this problem is that the organization data information update will only update the corresponding cached data of its Mapper, and will not notify some mappers of the associated table organization to update the corresponding cached data.
2.2 problem handling ideas
- When Mapper1 is defined, manually configure the corresponding associated Mapper2
- When Mapper1 cache cache1 is instantiated, read the cache cache2 related information of the associated Mapper2
- Store the reference information of cache2 in cache1
- When cache1 executes clear, the synchronization operation cache2 executes clear
3. Implementation of associated cache refresh
Open the L2 cache and use MyBatis Plus for local projects
mybatis-plus.configuration.cache-enabled=true
The user-defined annotation CacheRelations is mainly used. The user-defined cache implements RelativeCache and cache context RelativeCacheContext.
Annotate cacherelationships, which should be marked on the corresponding mapper when used
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface CacheRelations { // When the cache corresponding to mapper class in from is updated, the cache of the current annotation mapper needs to be updated Class<?>[] from() default {}; // When the current annotation label mapper's cache is updated, the cache corresponding to mapper class in to needs to be updated Class<?>[] to() default {}; }
Customize cache RelativeCache to implement MyBatis Cache interface
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() { ReadWriteLock readWriteLock = getReadWriteLock(); Lock lock = readWriteLock.writeLock(); lock.lock(); try { // Judge whether the current cache is being emptied. If it is, cancel the operation // Avoid circular relation in the cache, resulting in non termination of recursion and overflow of call stack 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; } public void addRelation(RelativeCache relation) { if (relations.contains(relation)){ return; } relations.add(relation); } void loadRelations() { // When loading other cache updates, you need to update the caches of this cache // Add this cache to the relations hips of these caches List<RelativeCache> to = UN_LOAD_TO_RELATIVE_CACHES_MAP.get(mapperClass); if (to != null) { to.forEach(relativeCache -> this.addRelation(relativeCache)); } // Some cache caches that need to be updated when loading this cache update // Add these cache caches to this cache relations hip List<RelativeCache> from = UN_LOAD_FROM_RELATIVE_CACHES_MAP.get(mapperClass); if (from != null) { from.forEach(relativeCache -> relativeCache.addRelation(this)); } CacheRelations annotation = AnnotationUtils.findAnnotation(mapperClass, CacheRelations.class); if (annotation == null) { return; } Class<?>[] toMappers = annotation.to(); Class<?>[] fromMappers = annotation.from(); if (toMappers != null && toMappers.length > 0) { for (Class c : toMappers) { RelativeCache relativeCache = MAPPER_CACHE_MAP.get(c); if (relativeCache != null) { // Add the found cache to the relations hips of the current cache this.addRelation(relativeCache); } else { // If the to cache cannot be found, it proves that the to cache has not been loaded. At this time, the corresponding relationship needs to be stored in UN_LOAD_FROM_RELATIVE_CACHES_MAP // That is, the cache corresponding to c needs to be updated when the current cache is updated List<RelativeCache> relativeCaches = UN_LOAD_FROM_RELATIVE_CACHES_MAP.putIfAbsent(c, new ArrayList<RelativeCache>()); relativeCaches.add(this); } } } if (fromMappers != null && fromMappers.length > 0) { for (Class c : fromMappers) { RelativeCache relativeCache = MAPPER_CACHE_MAP.get(c); if (relativeCache != null) { // Add the found cache to the relations hips of the current cache relativeCache.addRelation(this); } else { // If the from cache cannot be found, it proves that the from cache has not been loaded. At this time, the corresponding relationship needs to be stored in UN_LOAD_TO_RELATIVE_CACHES_MAP // That is, the current cache needs to be updated when the cache corresponding to c is updated List<RelativeCache> relativeCaches = UN_LOAD_TO_RELATIVE_CACHES_MAP.putIfAbsent(c, new ArrayList<RelativeCache>()); relativeCaches.add(this); } } } } }
Cache context RelativeCacheContext
public class RelativeCacheContext { // Mapping relationship of storing full cache public static final Map<Class<?>, RelativeCache> MAPPER_CACHE_MAP = new ConcurrentHashMap<>(); // Storing Mapper's corresponding cache requires to update the cache, but Mapper's corresponding cache has not been loaded at this time // That is, class <? > When the corresponding cache is updated, the cache in list < relativecache > needs to be updated public static final Map<Class<?>, List<RelativeCache>> UN_LOAD_TO_RELATIVE_CACHES_MAP = new ConcurrentHashMap<>(); // The cache corresponding to Mapper needs to be updated from, but these caches are not loaded when Mapper cache is loaded // The cache in < relativelist > needs to be updated Corresponding cache 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); } public static void getCache(Class<?> clazz) { MAPPER_CACHE_MAP.get(clazz); } }
Mode of use
UserMapper.java
@Repository @CacheNamespace(implementation = RelativeCache.class, eviction = RelativeCache.class, flushInterval = 30 * 60 * 1000) @CacheRelations(from = OrganizationMapper.class) //When the organization mapper is updated, the new cache reference is updated to the userMapper cache public interface UserMapper extends BaseMapper<UserEntity> { UserInfo queryUserInfo(@Param("userId") String userId); }
queryUserInfo is an interface implemented in xml, so it needs to be configured in the corresponding xml, otherwise the query results will not be cached. If the interface is implemented by BaseMapper, the query results will be cached automatically.
UserMapper.xml
<mapper namespace="com.mars.system.dao.UserMapper"> <cache-ref namespace="com.mars.system.dao.UserMapper"/> <select id="queryUserInfo" resultType="com.mars.system.model.UserInfo"> select u.*, o.name org_name from user u left join organization o on u.org_id = o.id where u.id = #{userId} </select> </mapper>
OrganizationMapper.java
@Repository @CacheNamespace(implementation = RelativeCache.class, eviction = RelativeCache.class, flushInterval = 30 * 60 * 1000) public interface OrganizationMapper extends BaseMapper<OrganizationEntity> { }
The flush interval in the CacheNamespace is invalid by default, which means that the cache will not be cleaned up regularly. ScheduledCache is the implementation of the flushInterval function. MyBatis's cache system is extended with decorators. Therefore, if you need to refresh regularly, you need to use ScheduledCache to add decoration to the RelativeCache.
At this point, the configuration and coding are completed.
Start validation:
Query user information with userId=1
{ "code":"1", "message":null, "data":{ "id":"1", "username":"admin", "password":"admin", "orgName":"Organization 1" } }
Update organization information and change organization 1 to organization 2
{ "code":"1", "message":null, "data":{ "id":"1", "name":"Organization 2" } }
Query user information again
{ "code":"1", "message":null, "data":{ "id":"1", "username":"admin", "password":"admin", "orgName":"Organization 2" } }
Meet expectations.