MyBatis L2 cache Association refresh implementation

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.

Keywords: Java Redis Cache

Added by tready29483 on Thu, 10 Feb 2022 03:20:30 +0200