MyBatis 缓存机制分析,MyBatis 真的有二级缓存?

系列文章目录

这是 MyBatis 源码之旅的第五篇文章,MyBatis 版本号为 3.5.6,源码分析注释已上传到 Github ,前四篇的文章目录如下,建议按照顺序阅读。
  1. MyBatis 初探,使用 MyBatis 简化数据库操作(超详细)
  2. MyBatis Mapper 接口方法执行原理分析
  3. 一条 SQL 是如何在 MyBatis 中执行的
  4. 谈谈 MyBatis 的插件,除了分页你可能还有这些使用场景

前言

缓存主要用来提高查询效率。以计算机的 CPU 为例,CPU 具有三级缓存,性能依次降低,优先从一级缓存查询,一级缓存未命中时再从二级缓存查询,二级缓存未命中时再从三级缓存查询。

MyBatis 官网在缓存一节中提到:Mybatis 具有全局性的二级缓存。也许这也是网上一些资料说 MyBatis 具有二级缓存的来源。类比 CPU 三级缓存,乍一看 MyBatis 的二级缓存似乎也是这样使用的。经过仔细分析,MyBatis 官网提到的二级缓存并不是这么回事。

MyBatis 中的缓存分为 Session 和 Statement 两种类型,然而这两种类型的缓存并没有什么关系,如果 MyBatis 用 Statement 类型表述二级缓存,也许更为精确。这也告诉我们,即便是官网上的描述,也不一定准确,文档的撰写者和开发者很有可能不是同一人,因此从源码入手能够理解更为深刻。

然而 MyBatis 和 Spring 对比,其注释的量几乎可以忽略不计,好在 MyBatis 的设计也比较小巧优秀,可读性较强。这节我将对 MyBatis 的缓存进行分析,力争通过这一篇文章大家就能理解 MyBatis 的缓存机制。话不多说,我们开始今天的内容。


MyBatis 缓存抽象

最简单的缓存使用 Map 即可实现,然而由于需要支持不同的使用场景,因此 MyBatis 将缓存抽象出一个 Cache 接口,定义如下。

public interface Cache {
    // 获取当前缓存的标识符
    String getId();
    // 存入缓存对象
    void putObject(Object key, Object value);
    // 获取缓存对象
    Object getObject(Object key);
    // 移除缓存对象
    Object removeObject(Object key);
    // 清空缓存
    void clear();
    // 获取缓存的对象数量
    int getSize();
    // 废弃的接口,3.2.6 版本开始不再使用
    default ReadWriteLock getReadWriteLock() {
        return null;
    }
}

可以看到 Cache 主要提供的功能就是添加、移除对象,MyBatis 会根据配置使用不同的实现,各实现具体如下。

缓存类型特点
PerpetualCache永久存储对象的缓存,使用 Map 实现
BlockingCache缓存装饰器,使用 CountDownLatch 实现,支持阻塞式获取,获取不到值时当前线程会被阻塞,直到其他线程存入值
FifoCache缓存装饰器,使用 Deque 实现,最大存储 1024 个对象,超过最大值时使用先进先出的方式移除旧对象
LoggingCache缓存装饰器,获取对象时会打印日志,对命中率进行简单的统计
LruCache缓存装饰器,使用 LinkedHashMap 实现,最大存储 1024 个对象,超过最大值时使用最近最少使用的方式移除旧对象
ScheduledCache缓存装饰器,存储的对象具有一个小时的生命周期,存取或者移除对象时会将过期的对象移除
SerializedCache缓存装饰器,存放的对象必须实现 Serializable 以支持序列化,存放的是对象序列话后的数组,存取前后会进行序列化和反序列化操作
SoftCache缓存装饰器,使用 SoftReference 实现,以便内存不够时进行垃圾回收
SynchronizedCache缓存装饰器,方法前添加 synchronized,每个线程需要获取到锁才能存取对象
TransactionalCache缓存装饰器,commit 时才把对象刷新到目标 Cache 中
WeakCache缓存装饰器,使用 WeakReference 实现,每次垃圾回收都会清空缓存对象

从上面 Cache 的表格中我们可以看到,MyBatis 使用装饰器模式定义了很多 Cache 的实现,以 LogingCache 为例,我们看下 MyBatis 对装饰器模式的使用。

public class LoggingCache implements Cache {
    // 日志对象
    private final Log log;
    // 目标 Cache
    private final Cache delegate;
    // 请求获取对象的数量
    protected int requests = 0;
    // 命中数量
    protected int hits = 0;

    public LoggingCache(Cache delegate) {
        this.delegate = delegate;
        this.log = LogFactory.getLog(getId());
    }

    @Override
    public void putObject(Object key, Object object) {
        delegate.putObject(key, object);
    }

    @Override
    public Object getObject(Object key) {
        requests++;
        final Object value = delegate.getObject(key);
        if (value != null) {
            hits++;
        }
        if (log.isDebugEnabled()) {
            log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
        }
        return value;
    }
    
    // 获取命中率
    private double getHitRatio() {
        return (double) hits / (double) requests;
    }
	
	... 省略部分方法
}

LogingCache 类对目标 Cache 进行装饰,持有目标 Cache 的引用,当调用 Cache 接口方法时委托给目标 Cache 处理,LogingCache 类对#getObject方法进行增强,记录了请求次数,命中次数,并且打印了日志。其他装饰器的实现和 LogingCache 类似。


如何在 MyBatis 中配置缓存

根据上面的内容,我们知道,MyBatis 中具有多种类型的 Cache,那么 MyBatis 到底使用哪个作为缓存实现呢?这是根据配置来的。

全局配置

MyBatis xml 配置文件或 Configuration 类中可以对缓存进行全局配置,以 xml 为例可以配置如下。

<configuration>
    <settings>
        <setting name="cacheEnabled" value="false"/>
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
</configuration>

请注意:这两个配置理解为开关更为合适,然而很难直观的通过这两个字段理解其含义,MyBatis 将其分别修改为 statementCacheEnabled、clearSessionCacheOnFinish 也许更为恰当。

首先我们要知道 MyBatis 中每个 SqlSession 和 SQL 语句分别有一个对应的 Cache,有了这个背景知识之后我们再来看这两个字段的含义。

  • cacheEnabled: 语句级 Cache 的开关,可取值 true 或 false ,用于开启或关闭语句的缓存。
  • localCacheScope:可取值为 SESSION 或 STATEMENT,如果为 STATEMENT 则 SqlSession 每次查询结束都会清空 对应的 Cache。

Mapper 配置

除了全局配置,MyBatis 还可以在 Mapper 的 xml 文件中对每个语句使用的 Cache 进行配置。

<mapper namespace="com.zzuhkp.blog.mybatis.UserMapper">
    <cache type="" blocking="" eviction="" flushInterval="" readOnly="" size=""/>
    <cache-ref namespace=""/>
</mapper>

每个 mapper 中的所有语句共享一个 Cache。使用的这个 Cache 可以通过 cache 或 cache-ref 节点来配置,cache-ref 节点的 namespace 属性可以指定使用哪个命名空间 mapper 下的 Cache,cache 节点相对复杂,会影响使用到的具体 Cache 类型,下面对其可以配置的属性进行详细介绍。

  • type:使用的 Cache 具体类型,如果不指定则默认为 PerpetualCache。
  • blocking:表示是否使用 BlockingCache 装饰 Cache,可取值为 true 或 false,如果不指定则默认为 false。
  • eviction:缓存清除策略,可取值为 FIFO、LRU、SOFT、WEAK,如果不指定则默认为 LRU,使用 LruCache 装饰 Cache。
  • flushInterval:缓存刷新间隔,单位 ms,如果设置了则使用 ScheduledCache 装饰 Cache。
  • readOnly:可取值 true 或 false ,如果设置为 true 则使用 SerializedCache 装饰 Cache,如果不指定则默认为 flase。
  • size:指定可缓存的对象的数量,FifoCache 或 LruCache 使用。

可以看到,cache 节点中的很多属性将影响 MyBatais 创建 Cache 的装饰器。

cache 或 cache-ref 只是配置 mapper 的语句中使用的缓存类型,那么每个查询都会使用缓存吗?MyBatis 在每个语句中提供了灵活配置的方式,具体如下。

<mapper namespace="com.zzuhkp.blog.mybatis.UserMapper">
    <select|insert|update|delete flushCache="true" useCache="true"/>
</mapper>

Mapper xml 配置文件中 select、insert、update、delete 每个节点都可以设置 flushCache、useCache 属性。

  • flushCache:SqlSession 执行查询或更新前是否清空 SqlSession 和语句对应的缓存,默认非 select 语句执行前清空缓存。
  • useCache:是否使用语句对应的 Cache 缓存查询结果。

MyBatis 缓存底层使用分析

这节对 MyBatis 底层对缓存的使用进行一个分析,为了了解 MyBatis 如何使用缓存的,我们可以从配置入手。全局配置都会存放到 Configuration 中,查看 cacheEnabled 的使用位置如下。

public class Configuration {
	// 创建新的 Executor
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        ... 省略部分代码
        Executor executor;
        ... 省略部分代码
        if (cacheEnabled) {
        	// 启用 Statement 级别的缓存
            executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }  
}

Configuration 提供了实例化 Executor 的方法#newExecutor,当设置 cachedEnabled 为 true 时 MyBatis 会创建一个 CachingExecutor,由这个 Executor 对语句级别的缓存进行支持。关于 Executor,不了解的小伙伴可以点击 MyBatis 初探,使用 MyBatis 简化数据库操作(超详细)查看。查看 CachingExecutor 关键代码如下。

public class CachingExecutor implements Executor {
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
        // 语句中的 Cache,值为 Mapper xml 文件中 cache 或 cache-ref 节点配置的缓存对象
        Cache cache = ms.getCache();
        if (cache != null) {
        	// 如果配置了 flushCache 则先清空语句对应的缓存
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    // 优先从缓存中获取,如果缓存不存在,则查询后放入缓存
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

CachingExecutor 执行查询或更新时,如果 Mapper xml select|insert|update|delete 语句中配置了 flushCache 会先清空语句对应的缓存,如果配置了 useCache 为 true 则执行查询时优先从缓存中获取结果,从数据库查询到结果后则会放入缓存中。

Configuration 中还有一个 localCacheScope 配置,查看其使用位置如下。

public abstract class BaseExecutor implements Executor {

    protected PerpetualCache localCache;
    
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ... 省略非关键代码
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            // 先清空缓存
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            // 优先从缓存获取
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // 缓存中没有数据,从数据库中查询
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            ... 省略非关键代码
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

}

BaseExecutor 查询的逻辑和 CachingExecutor 类似,只是 CachingExecutor 使用的是 STATEMENT 类型的缓存,而 BaseExecutor 自身持有一个 Cache 对象,这个对象就是 SESSION 类型的缓存。

BaseExecutor 执行查询前如果 select|insert|update|delete 语句中设置了 flushCache 也会先清空缓存,查询时优先从缓存获取数据,缓存中没有结果时再进行数据库查询。查询后如果配置中的缓存作用域设置的是 STATEMENT,则会清空 BaseExecutor 中的缓存。


MyBatis 缓存使用场景

综合上面的分析,我们可以得出 MyBatis 中对缓存的使用有两个场景。

  1. BaseExecutor 使用缓存避免循环引用查询。
  2. CachingExecutor 使用缓存加快查询速度。

对于大型项目,我们通常会拆分为多个微服务,并且每个服务部署多份,如果仍然使用 MyBatis 的缓存,很容易导致相同的查询条件在 A 服务器上的查询结果和在 B 服务器上查询的结果不一致,如果一定要使用缓存加快查询速度,建议使用 Redis 做集中式缓存。对于小型单机部署的服务,直接使用 MyBatis 的缓存则没有问题。为了关闭 Executor 和语句对应的缓存,我们可以进行如下的配置。

<configuration>
    <settings>
        <setting name="cacheEnabled" value="false"/>
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
</configuration>

cachedEnabled 用来禁用语句的缓存,localCacheScope 设置为 STATEMENT 则可以将 Executor 中的缓存在查询后清空。


总结

本篇主要介绍了 MyBatis 对缓存的抽象、如何在 MyBatis 中配置缓存以及 MyBatis 底层对缓存的使用。从上面的分析中,我们并没有看到 MyBatis 有类似先查询一级缓存、再查二级缓存的逻辑,而是在 Executor 中使用缓存避免循环引用以及加快查询,因此 MyBatis 官网对二级缓存的描述可能不够准确。学习中,我们还是不能人云亦云,即便是官网的描述可能也不一定正确,需要我们时刻保持怀疑态度。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值