细品mybatis源码系列(三):Mybatis的缓存分析

前言

本文将介绍mybatis的缓存内容

mybatis缓存介绍

读者都知道mybatis有两个缓存,一级缓存和二级缓存。
在这里插入图片描述

  • 一级缓存是sqlsession级别的缓存,操作数据库需要构建sqlsession对象,对象中有个hashmap用于存储查询数据,不同的sqlsession之间缓存数据是互相隔离互不影响的
  • 二级缓存是mapper级别的缓存,多个sqlsession操作同一个mapper的sql语句,多少sqlsession可以共用二级缓存,二级缓存是跨sqlsession的

一级缓存

既然提到一级缓存是sqlsession级别的,那就观察sqlsession的源码
在这里插入图片描述
观察sqlsession这个接口的所有方法,没有发现有关创建缓存的方法,只发现一个void clearCache();清除缓存的方法,于是从这里作为突破口
找其接口的实现类,一步一步追踪下去,调用链如下
在这里插入图片描述

发现最终走进了Perpetualcache类下的clear()方法

 	/**
     * 缓存容器
     */
    private Map<Object, Object> cache = new HashMap<>();
  @Override
    public void clear() {
        cache.clear();
    }

Perpetualcache类中定义了一个名为cache的hashmap,所以可以确定的说缓存就是本地存储的一个map,每个sqlsession都会存放一个map的引用。

缓存的创建

深追sqlsession发现了提供了清除cache的方法,却没有提供创建cached方法,所以缓存是何时候创建的。
如果读者研读过mybatis的源码或者读者笔者的上一小节,知道executor是mybatis的执行器,清除缓存也是通过调用executor的接口来实现,所以创建缓存也可以推断由executor实现。
观察executor接口

 // 创建 CacheKey 对象
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

有一个创建缓存key的方法,看其实现类

  @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 创建 CacheKey 对象
        CacheKey cacheKey = new CacheKey();
        // 设置 id、offset、limit、sql 到 CacheKey 对象中
        // MappedStatement的id,即sql语句所在的位置 包名+类名+sql名称 
        cacheKey.update(ms.getId());
        // 偏移量 值为0
        cacheKey.update(rowBounds.getOffset());
        // integer的最大值
        cacheKey.update(rowBounds.getLimit());
        // 具体的sql语句
        cacheKey.update(boundSql.getSql());
        // 设置 ParameterMapping 数组的元素对应的每个 value 到 CacheKey 对象中
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        // mimic DefaultParameterHandler logic 这块逻辑,和 DefaultParameterHandler 获取 value 是一致的。
        for (ParameterMapping parameterMapping : parameterMappings) {
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                cacheKey.update(value);
            }
        }
        // 设置 Environment.id 到 CacheKey 对象中
        if (configuration.getEnvironment() != null) {
            // issue #176
            cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
    }

发现创建缓存key会经过一系列的update方法,这个update方法由一个Cachekey对象来执行。
在这里插入图片描述
至于code中提到的configuration.getEnvironment().getId() 是读取配置文件中的environment标签解析得到了

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>

缓存的使用

既然明白了缓存的创建,就探究下缓存会何时才被使用
依然从executor下手

 @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        //根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
        BoundSql boundSql = ms.getBoundSql(parameter);
        //为本次查询创建缓存的Key
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        // 查询
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

调用重载方法

  @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        // 已经关闭,则抛出 ExecutorException 异常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            // queryStack + 1
            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 - 1
            queryStack--;
        }
        if (queryStack == 0) {
            // 执行延迟加载
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            // 清空 deferredLoads
            deferredLoads.clear();
            // 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

如果读者看过上一节介绍,就会发现这和上节介绍中执行sql流程介绍到的代码一致,可知查询结果优先从缓存查询,没有查询则进行数据库查询,查询完成回写到cache中。

缓存的清空

读者知道一级缓存再经过增删改操作后就会失效,其失效的操作是在哪触发的


    @Override
    public void commit(boolean required) throws SQLException {
        // 已经关闭,则抛出 ExecutorException 异常
        if (closed) {
            throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        // 清空本地缓存
        clearLocalCache();
        // 刷入批处理语句
        flushStatements();
        // 是否要求提交事务。如果是,则提交事务。
        if (required) {
            transaction.commit();
        }
    }

在进行事物提交的过程中会进行清除缓存

二级缓存

二级缓存的实现思想和一级缓存大同小异,都是第一次查询结果缓存,第二次查询即从缓存中取结果。无非一个是基于sqlsession级别,一个是基于mapper文件中的namespace,也即sqlsession可以共享一个二级缓存,如果两个mapper的namespace相同,那么这两个mapper中查询到的数据也将缓存到同一片二级缓存中。
在这里插入图片描述

二级缓存的开启

二级缓存开启需要手动开启,不像一级缓存自动开启,首先要修改全局配置文件sqlMapConfig.xml

<!--二级缓存开启 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

其次要在具体的mapper.xml文件进行配置

<!-- 二级缓存开启-->
<cache></cache>

二级缓存和一级缓存一样,底层也是hashmap

// 默认的缓存实现类,也可以自定义缓存实现类
public class PerpetualCache implements Cache {

    /**
     * 标识
     */
    private final String id;
    /**
     * 缓存容器
     */
    private Map<Object, Object> cache = new HashMap<>();

    public PerpetualCache(String id) {
        this.id = id;
    }

相较于自动开启的一级缓存,二级缓存可以说是相当鸡肋了,生产中很少用到,相比较redis这种可以分布式缓存而言,mybatis的只能单节点的二级缓存更没优势,所以本文也不会重点介绍。

总结

本文从代码层面介绍了mybatis中缓存的作用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值