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

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

前言


缓存主要用来提高查询效率。以计算机的 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 会根据配置使用不同的实现,各实现具体如下。


image.png


从上面 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 中对缓存的使用有两个场景。


BaseExecutor 使用缓存避免循环引用查询。

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 官网对二级缓存的描述可能不够准确。学习中,我们还是不能人云亦云,即便是官网的描述可能也不一定正确,需要我们时刻保持怀疑态度。


目录
相关文章
|
6天前
|
存储 缓存 运维
【Docker 专栏】Docker 镜像的分层存储与缓存机制
【5月更文挑战第8天】Docker 镜像采用分层存储,减少空间占用并提升构建效率。每个镜像由多个层组成,共享基础层(如 Ubuntu)和应用层。缓存机制加速构建和运行,通过检查已有层来避免重复操作。有效管理缓存,如清理无用缓存和控制大小,可优化性能。分层和缓存带来资源高效利用、快速构建和灵活管理,但也面临缓存失效和层管理挑战。理解这一机制对开发者和运维至关重要。
【Docker 专栏】Docker 镜像的分层存储与缓存机制
|
12天前
|
缓存 NoSQL Java
17:缓存机制-Java Spring
17:缓存机制-Java Spring
34 5
|
13天前
|
存储 缓存 自然语言处理
深入PHP内核:探索Opcode缓存机制
【5月更文挑战第1天】 在动态语言如PHP的执行过程中,每次脚本被请求时都需要经过一系列复杂的解析和编译步骤。为了优化这一过程并提高性能,PHP引入了Opcode缓存机制。本文将详细探讨Opcode的概念、作用以及它如何显著提升PHP应用的执行效率。我们将从缓存原理出发,分析几种常见的Opcode缓存工具,并通过实例说明如何在实际项目中实现和优化缓存策略。
|
14天前
|
缓存 NoSQL PHP
【PHP开发专栏】PHP缓存机制与实现
【4月更文挑战第29天】本文介绍了PHP缓存的基本概念、策略及实现方式。PHP缓存包括应用缓存、Web服务器缓存、数据库缓存和分布式缓存,常见策略有缓存预热、更新和懒加载。PHP的缓存实现包括文件缓存、APC、OPcache、Memcached和Redis。最佳实践包括缓存热点数据、控制粒度、设置失效策略、保证一致性和确保安全性。文中还提供了一个新闻列表和详情页的缓存实战示例,帮助开发者理解如何在实际项目中应用缓存。
|
19天前
|
Java 关系型数据库 数据库连接
MyBatis 执行流程分析
MyBatis 执行流程分析
17 2
|
20天前
|
缓存 流计算
缓存命中率和过期机制的一般思路
【4月更文挑战第20天】缓存命中率是评估缓存效果的关键,目标是达到90%以上,但某些频繁的小请求场景可能无法实现。过期机制可采用定时删除(精确但开销大)、延迟队列(精确但有队列开销)、懒惰删除(简单但时间不精确)或定期删除(简单但性能损耗不可控)。
19 4
|
20天前
|
缓存 Linux
linux系统缓存机制
linux系统缓存机制
|
29天前
|
缓存 数据库 开发者
Django视图中的缓存机制:提升页面加载速度
【4月更文挑战第15天】本文介绍了Django的缓存机制在提升页面加载速度中的作用。Django提供视图缓存和片段缓存,通过`cache_page`装饰器和`CacheMixin`实现视图级别的缓存,使用`{% cache %}`模板标签实现页面片段缓存。开发者可选择不同的缓存后端,并在设置中配置缓存参数。同时,注意合理控制缓存粒度、及时更新和管理缓存,以优化用户体验和网站性能。
|
1月前
|
缓存 Java 数据库连接
MyBatis三级缓存实战:高级缓存策略的实现与应用
MyBatis三级缓存实战:高级缓存策略的实现与应用
36 0
MyBatis三级缓存实战:高级缓存策略的实现与应用
|
1月前
|
XML 缓存 Java
MyBatis二级缓存解密:深入探究缓存机制与应用场景
MyBatis二级缓存解密:深入探究缓存机制与应用场景
70 2
MyBatis二级缓存解密:深入探究缓存机制与应用场景