• notice
  • Congratulations on the launch of the Sought Tech site

MyBatis L2 cache association refresh implementation

1. Introduction to MyBatis cache

Mybatis provides support for caching, but by default, it only enables the first-level cache, and the second-level cache needs to be manually enabled.

The first level cache is only relative to the same SqlSession. That is, for the same transaction, the same query method of the same Mapper is executed multiple times. After the first query, MyBatis will put the query result into the cache, and the data update (Insert, Update and Delete) operations of the corresponding Mapper are not involved in the middle. , subsequent queries will be fetched from the cache without querying the database.

The second-level cache is for application-level caching, that is, for different SqlSessions. When the second-level cache is enabled, MyBatis will store the first query result in the global cache for Mapper. If the data update operation of the Mapper is not performed in the middle, subsequent identical queries will be obtained from the cache.

2. Second-level cache problem

According to the introduction of the second-level cache, it is found that if the Mapper is only a single table query, there will be no problem, but if the query involved in the Mapper has a joint table query, such as UserMapper needs to associate query organization information when querying user information, that is, the user table is required. Associated with the organization table, the OrganizationMapper will not update the UserMapper cache when performing the update. As a result, when the UserMapper is used to query user information with the same conditions, it will wait for the organization information before it is updated, resulting in data inconsistency.

2.1, data inconsistency problem verification

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 the cached data), here userId = 1, data is the user query result

{
 "code""1",
 "message"null,
 "data": {
   "id""1",
   "username""admin",
   "password""admin",
   "orgName""组织1"
 }
}

Query the corresponding organization information, the result

{
 "code""1",
 "message"null,
 "data": {
   "id""1",
   "name""组织1"
 }
}

It is found that the data is consistent with the user cache data.

Perform the update organization operation, change the organization 1 to the organization 2, and query the organization information again

{
 "code""1",
 "message"null,
 "data": {
   "id""1",
   "name""组织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""组织1"
 }
}

The reason for this problem is that the organization data information update will only cache data corresponding to its own Mapper, and will not notify some Mappers of the associated table organization to update the corresponding cache data.

2.2, problem solving ideas

  • When Mapper1 is defined, manually configure the corresponding associated Mapper2
  • When the 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 synchronous operation cache2 executes clear

3. Association cache refresh implementation

Open the second level cache, use MyBatis Plus for local projects

mybatis-plus.configuration.cache-enabled=true

The custom annotation CacheRelations is mainly used, and the custom cache implements RelativeCache and the cache context RelativeCacheContext.

Annotate CacheRelations, which needs to be marked on the corresponding mapper when using

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheRelations {
    // from中mapper class对应的缓存更新时,需要更新当前注解标注mapper的缓存
    Class<?>[] from() default {};
    // 当前注解标注mapper的缓存更新时,需要更新to中mapper class对应的缓存
    Class<?>[] to() default {};
}

Custom cache RelativeCache implements 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 {
            // 判断 当前缓存是否正在清空,如果正在清空,取消本次操作
            // 避免缓存出现 循环 relation,造成递归无终止,调用栈溢出
            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() {
        // 加载 其他缓存更新时 需要更新此缓存的 caches
        // 将 此缓存 加入至这些 caches 的 relations 中
        List<RelativeCache> to = UN_LOAD_TO_RELATIVE_CACHES_MAP.get(mapperClass);
        if (to != null) {
            to.forEach(relativeCache -> this.addRelation(relativeCache));
        }
        // 加载 此缓存更新时 需要更新的一些缓存 caches
        // 将这些缓存 caches 加入 至 此缓存 relations 中
        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) {
                    // 将找到的缓存添加到当前缓存的relations中
                    this.addRelation(relativeCache);
                } else {
                    // 如果找不到 to cache,证明to cache还未加载,这时需将对应关系存放到 UN_LOAD_FROM_RELATIVE_CACHES_MAP
                    // 也就是说 c 对应的 cache 需要 在 当前缓存更新时 进行更新
                    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) {
                    // 将找到的缓存添加到当前缓存的relations中
                    relativeCache.addRelation(this);
                } else {
                    // 如果找不到 from cache,证明from cache还未加载,这时需将对应关系存放到 UN_LOAD_TO_RELATIVE_CACHES_MAP
                    // 也就是说 c 对应的 cache 更新时需要更新当前缓存
                    List<RelativeCache> relativeCaches = UN_LOAD_TO_RELATIVE_CACHES_MAP.putIfAbsent(c, new ArrayList<RelativeCache>());
                    relativeCaches.add(this);
                }
            }
        }
    }

}

Cache context RelativeCacheContext

public class RelativeCacheContext {

    // 存储全量缓存的映射关系
    public static final Map<Class<?>, RelativeCache> MAPPER_CACHE_MAP = new ConcurrentHashMap<>();
    // 存储 Mapper 对应缓存 需要to更新缓存,但是此时 Mapper 对应缓存还未加载
    // 也就是 Class<?> 对应的缓存更新时,需要更新 List<RelativeCache> 中的缓存
    public static final Map<Class<?>, List<RelativeCache>> UN_LOAD_TO_RELATIVE_CACHES_MAP = new ConcurrentHashMap<>();
    // 存储 Mapper 对应缓存 需要from更新缓存,但是在 加载 Mapper 缓存时,这些缓存还未加载
    // 也就是 List<RelativeCache> 中的缓存更新时,需要更新 Class<?> 对应的缓存
    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);
    }

}

How to use

UserMapper.java

@Repository
@CacheNamespace(implementation = RelativeCache.classeviction = RelativeCache.classflushInterval 30 * 60 * 1000)
@CacheRelations(from = OrganizationMapper.class)
public interface UserMapper extends BaseMapper<UserEntity
{
    UserInfo queryUserInfo(@Param("userId") String userId);
}

queryUserInfo is an interface implemented by xml, so it needs to be configured in the corresponding xml <cache-ref namespace=“com.mars.system.dao.UserMapper”/>, otherwise the query results will not be cached. If the interface is implemented by BaseMapper, the query results will be automatically cached.

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.classeviction = RelativeCache.classflushInterval 30 * 60 * 1000)
public interface OrganizationMapper extends BaseMapper<OrganizationEntity{
}

The flushInterval in CacheNamespace is invalid by default, which means that the cache will not be cleaned regularly. ScheduledCache is the implementation of the flushInterval function. The cache system of MyBatis is extended with decorators. Therefore, if you need to refresh regularly, you need to use ScheduledCache to add decoration to RelativeCache.

At this point, configuration and coding are complete.

Start verification:

Query user information with userId=1

{
    "code":"1",
    "message":null,
    "data":{
        "id":"1",
        "username":"admin",
        "password":"admin",
        "orgName":"组织1"
    }
}

Update organization information, change organization 1 to organization 2

{
    "code":"1",
    "message":null,
    "data":{
        "id":"1",
        "name":"组织2"
    }
}

Query user information again

{
    "code":"1",
    "message":null,
    "data":{
        "id":"1",
        "username":"admin",
        "password":"admin",
        "orgName":"组织2"
    }
}

In line with expectations.

Source: blog.csdn.net/qq_38245668/article/

details/105803298


Tags

Technical otaku

Sought technology together

Related Topic

0 Comments

Leave a Reply

+