美文网首页Mybatis随笔
Mybatis随笔(八) 缓存解析

Mybatis随笔(八) 缓存解析

作者: sunyelw | 来源:发表于2020-03-28 23:54 被阅读0次

mybatis的两个缓存:一级缓存与二级缓存


一级缓存

上篇我们已经提过一部分一级缓存的本质、使用、清除,这里简单提下失效原因

  • 多次调用同一条查询之间有更新操作
  • 一级缓存是SqlSession级别的, 不同 SqlSession 之间调用同一个查询用不了
  • 同一个SqlSession, 但是非同一个SQL
  • 手动执行缓存清除操作

二级缓存

二级缓存是多个SqlSession之间共享, 属于Configuration级别,而且先于一级缓存。

2.1 创建

通过前面的配置解析,这里我们直接看下 XmlConfigBuilder # configurationElement(XNode) 方法

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        // here 1
        cacheRefElement(context.evalNode("cache-ref"));
        // here 2
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}

缓存相关的就是这两个地方
<cache/>标签的子标签<cache-ref/>用于连接不同Mapper的命名空间
二级缓存的主要实现在<cache/>标签的子标签<cache/>

private void cacheElement(XNode context) {
    if (context != null) {
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}
  • 前面一堆都是对cache标签属性的解析(包含默认值)
  • 最后一个方法用于缓存的构建 MapperBuilderAssistant # useNewCache
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
    // 建造者模式
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

这里构建了一个缓存 Cache, 存入了Configuration且赋值给了 currentCache,看下这个 currentCache 哪边用到了

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      StatementType statementType,
      SqlCommandType sqlCommandType,
      Integer fetchSize,
      Integer timeout,
      String parameterMap,
      Class<?> parameterType,
      String resultMap,
      Class<?> resultType,
      ResultSetType resultSetType,
      boolean flushCache,
      boolean useCache,
      boolean resultOrdered,
      KeyGenerator keyGenerator,
      String keyProperty,
      String keyColumn,
      String databaseId,
      LanguageDriver lang,
      String resultSets) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // 构建 MappedStatement
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        // here
        // here
        // here
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

这里看到使用 MapperBuilderAssistant # addMappedStatement 将这个 currentCache 放入了 Configuration 的 mappedStatements 中。

2.2 使用

而二级缓存 CacheExecutor 的使用中 (CacheExecutor # query)

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // here
    // here
    // here
    // 1.获取二级缓存
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);

            // 2.尝试从二级缓存取值
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 3.未能从二级缓存获取到值,调用原查询方法
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 4.查完反塞二级缓存
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    // 5.没有二级缓存,直接调用原查询方法
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

可以看到,二级缓存是从 MapperStatement 中取出来的
流程如下

  • 存在二级缓存
    1.尝试从二级缓存取值
    2.未能从二级缓存获取到值,调用原查询方法
    3.查完反塞二级缓存
  • 不存在二级缓存,直接调用原查询方法

反塞与从缓存中取值是通过 TransactionalCacheManager 完成的

2.3 清除

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
        tcm.clear(cache);
    }
}

2.4 二级缓存种类

  • FifoCache:先进先出缓存淘汰策略的缓存
  • LoggingCache:日志能力的缓存
  • ScheduledCache:定时清空的缓存
  • BlockingCache:阻塞式缓存
  • SerializedCache:序列化能力的缓存
  • SynchronizedCache:进行同步控制的缓存

2.4 实现之一 BlockCaching

public class BlockingCache implements Cache {
    private long timeout;
    private final Cache delegate;
    private final ConcurrentHashMap<Object, ReentrantLock> locks;
    public BlockingCache(Cache delegate) {
        this.delegate = delegate;
        this.locks = new ConcurrentHashMap<>();
    }
}
  • 持有一个Cache 的实现
  • 维护了一把锁的Map

2.4.1获取锁 / 释放锁

private void acquireLock(Object key) {
    Lock lock = getLockForKey(key);
    if (timeout > 0) {
        try {
            boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
            if (!acquired) {
                throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());
            }
        } catch (InterruptedException e) {
            throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
        }
    } else {
        lock.lock();
    }
}

private ReentrantLock getLockForKey(Object key) {
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
}

CurrentHashMap # computeIfAbsent 方法是线程安全的,可以确保多个线程进行put操作,得到的是同一个对象,也就是同一把锁

private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

2.4.2 获取对象 / 放入对象

@Override
public Object getObject(Object key) {
    acquireLock(key);
    Object value = delegate.getObject(key);
    if (value != null) {
        releaseLock(key);
    }
    return value;
}

如果没有获取到对象,则不释放锁,一直等待。这是一把可重入锁,每个此对象的获取线程会像链表一样链在此对象关联的监视器上,这是一种重量级锁。

那总不能一直等待,需要一个释放吧:

@Override
public void putObject(Object key, Object value) {
    try {
        delegate.putObject(key, value);
    } finally {
        releaseLock(key);
    }
}

当有对象放入后,就执行释放锁的操作来唤醒等待线程。

还有一种写法是用 ReentrantLock 的 Condition,每个 Condition 就是一个 key

其他几种二级缓存的实现可以自行看下,我感觉这个阻塞用了一个重入锁比较好玩~~

2.5 失效原因

  • 查询之间的更新操作会导致后面的查询用不到前面查询的二级缓存
  • 前次SqlSession未提交或回滚导致后面的SqlSession的查询无法命中二级缓存

可以写几个SqlSession来实验一下,这里就不赘述了。

2.6 最后

这里还有一点实现,跟前面创建放一起可能会有点多,眼花缭乱的,看到这里还有兴趣就继续看下去吧

public Cache build() {
    setDefaultImplementations();
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        if (readWrite) {
            cache = new SerializedCache(cache);
        }
        cache = new LoggingCache(cache);
        cache = new SynchronizedCache(cache);
        if (blocking) {
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}

简直把装饰器模式使用的淋漓尽致啊,一次看个爽。

参考
Mybatis 二级缓存全详解


天地英雄气,千秋尚凛然。

相关文章

  • Mybatis随笔(八) 缓存解析

    mybatis的两个缓存:一级缓存与二级缓存 一级缓存 上篇我们已经提过一部分一级缓存的本质、使用、清除,这里简单...

  • MyBatis一二级缓存

    【MyBatis源码解析】MyBatis一二级缓存

  • 深度解析Mybatis缓存

    本期看点 本文从源码分析Mybatis一级和二级缓存的应用,进而阐述Mybatis缓存的“坑”。 一级缓存:Sql...

  • mybatis缓存源码解析

    mybatis源码阅读 二级缓存 二级缓存之所以能够跨session是因为采用的装饰器模式对Executor进行了...

  • MyBatis缓存和注解

    Mybatis缓存和注解 学习目标 1、mybatis缓存 2、mybatis注解 学习内容 1、mybatis缓...

  • MyBatis缓存书目录

    MyBatis缓存 MyBatis介绍 MyBatis一级缓存 1、什么是一级缓存? 为什么使用一级缓存? 2、M...

  • MyBatis缓存配置

    这篇我们讲MyBatis的缓存配置,关于MyBatis缓存请参看MyBatis缓存。在xml配置爱中添加如下内容:...

  • Mybatis的缓存

    一 Mybatis缓存体系图 Mybatis缓存的基础实现是perpetualCache,但是mybatis利用装...

  • 【MyBatis】学习纪要八:缓存(二)

    缓存工具 MyBatis缓存 Ehcache Redis 缓存设计 缓存接口 我们来讨论一下,说到MyBatis,...

  • mybatis一级缓存和二级缓存

    MyBatis官网MyBatis拥有自带一级缓存和二级缓存 一级缓存: MyBatis是默认开启一级缓存,一级缓存...

网友评论

    本文标题:Mybatis随笔(八) 缓存解析

    本文链接:https://www.haomeiwen.com/subject/xqleuhtx.html