美文网首页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随笔(八) 缓存解析

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