mybatis 一级、二级缓存源码详解

文章中若附原文链接,那部分内容对你有所帮助,请给原作者先点赞!
文章中若附原文链接,那部分内容对你有所帮助,请给原作者先点赞!
文章中若附原文链接,那部分内容对你有所帮助,请给原作者先点赞!

缓存

  • 什么是缓存?
    缓存就是存在于内存中的临时数据。
  • 为什么要使用缓存?
    为了减少和数据库交互的次数,提高执行效率。
  • 适用于缓存的数据
    经常查询并且不经常改变的数据。
    数据的正确与否对最终结果影响不大的。
  • 不适用于缓存的数据
    经常改变的数据。
    数据的正确与否对最终结果影响很大的。例如:商品的库存、银行的汇率、股市的牌价等。

一级缓存

一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flushclose,它就会存在。当调用 SqlSession 的修改、添加、删除、commit()、close()、clearCache() 等方法时,就会清空一级缓存。

一级缓存流程如下图:

一级缓存分析

  • 第一次发起查询用户 id 为 1 的用户信息,Mybatis 会先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息。
    得到用户信息,将用户信息存储到一级缓存中。
  • 如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),那么 Mybatis 就会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
  • 第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。

Mybatis 默认就是使用一次缓存的,不需要配置。
一级缓存中存放的是对象。(一级缓存其实就是 Map 结构,直接存放对象)

mybatis一级缓存,缓存在SqlSession中。

一级缓存是默认的,不需要配置,localCache中没有缓存时,才去执行queryFromDatabase方法,去查询数据库,并将结果缓存到localCache中。

MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement

源码
public abstract class BaseExecutor implements Executor {

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());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
        if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
            this.clearLocalCache();
        }

        List list;
        try {
            ++this.queryStack;
            list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
            if (list != null) {
                this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            --this.queryStack;
        }

        if (this.queryStack == 0) {
            Iterator var8 = this.deferredLoads.iterator();

            while(var8.hasNext()) {
                BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                deferredLoad.load();
            }

            this.deferredLoads.clear();
            if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                this.clearLocalCache();
            }
        }

        return list;
    }
}

二级缓存

  • 二级缓存是 Mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 SQL 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的

二级缓存流程如下图:

img

img

  • 当 sqlSession1 去查询用户信息的时候,Mybatis 会将查询数据存储到二级缓存中。
  • 如果 sqlSession3 去执行相同 Mapper 映射下的 SQL 语句,并且执行 commit 提交,那么 Mybatis 将会清空该 Mapper 映射下的二级缓存区域的数据。
  • sqlSession2 去查询与 sqlSession1 相同的用户信息,Mybatis 首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
  • 如果想使用 Mybatis 的二级缓存,那么应该做以下配置
    • 首先在 Mybatis 配置文件中添加配置 (这一步其实可以忽略,因为默认值为 true)
<settings>
    <!-- 开启缓存 -->
    <setting name="cacheEnabled" value="true"/>
</settings> 
  • 接着在映射文件中配置
<mapper namespace="cn.ykf.mapper.UserMapper">
    <!-- 使用缓存 -->
    <cache/>
</mapper>
  • 最后在需要使用二级缓存的操作上配置 (针对每次查询都需要最新数据的操作,要设置成 useCache="false",禁用二级缓存)
<select id="listAllUsers" resultMap="UserWithAccountsMap" useCache="true">
    SELECT * FROM user
</select>
  • 当我们使用二级缓存的时候,所缓存的类一定要实现 java.io.Serializable 接口,这样才可以使用序列化的方式来保存对象。
  • 由于是序列化保存对象,所以二级缓存中存放的是数据,而不是整个对象。
源码
  • 首先,excutor是在sqlsessionfactory创建sqlsession时创建的,用以作为sqlsession的参数传入
public class DefaultSqlSessionFactory implements SqlSessionFactory {

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
        Environment environment = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        Executor executor = this.configuration.newExecutor(tx, execType);
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }

    return var8;
}
  • 接着看下excutor有什么类型
public class Configuration {
 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }

        if (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);//装饰前面的三个executor
        }

        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }
}
  • 可以看出如果开启二级缓存,cachingExecutor会被作为装饰类,为什么说是装饰类呢
public class CachingExecutor implements Executor {
    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }

    public Transaction getTransaction() {
        return this.delegate.getTransaction();
    }

    public void close(boolean forceRollback) {
        try {
            if (forceRollback) {
                this.tcm.rollback();
            } else {
                this.tcm.commit();
            }
        } finally {
            this.delegate.close(forceRollback);
        }

    }

    public boolean isClosed() {
        return this.delegate.isClosed();
    }

    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        this.flushCacheIfRequired(ms);//刷新二级缓存
        return this.delegate.update(ms, parameterObject);
    }

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, parameterObject, boundSql);
                List<E> list = (List)this.tcm.getObject(cache, key);二级缓存中存在则在二级缓存中读取
                if (list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);//保存到二级缓存中
                }

                return list;//返回
            }
        }

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    public List<BatchResult> flushStatements() throws SQLException {
        return this.delegate.flushStatements();
    }

    public void commit(boolean required) throws SQLException {
        this.delegate.commit(required);
        this.tcm.commit();
    }

    public void rollback(boolean required) throws SQLException {
        try {
            this.delegate.rollback(required);
        } finally {
            if (required) {
                this.tcm.rollback();
            }

        }

    }

}
  • 可以从commit等事务可以看出来是由传入的excutor(delegate)实现的,并且与TransactionalCacheManager tcm辅助完成功能,只有query自己实现了功能,即二级缓存,从中可以看出他是先读取二级缓存有无内容,没有再到一级缓存中查看,没有再去数据库查询,再保存到一级缓存(若commit或),最后返还给用户。

  • 因为一个二级缓存对应多个一级缓存,所以在其中一个sqlsession需要刷新导致二级缓存刷新时,其他sqlsession的与该sqlsession无关联的数据可以在一级缓存中获取就行,但是如果是相关联的数据,此时又用其他的sqlsession来查找,其他sqlsession的一级缓存数据是否存在过期现象。(不同命名空间?)这就涉及到一级缓存什么时候提交数据到二级缓存中的问题了

  • 只有会话关闭或提交后,一级缓存中的数据才会转移到二级缓存中,并且只有是同一个namespace所以可以获取到数据;因为mybatis的缓存会被一个transactioncache类包装住,所有的cache.putObject全部都会被暂时存到一个map里,等会话commit/close以后,这个map里的缓存对象才会被真正的cache类执行putObject操作。这么设计的原因是为了防止事务执行过程中出异常导致回滚,如果get到object后直接put进缓存,万一发生回滚,就很容易导致mybatis缓存被脏读

  • 这就保证了select就不会到达同的其他的sqlsession的cache中了,若是update成功,则commit成功,数据上传到二级缓存中,对该sqlsession的数据访问会直接在二级内存读取,若是失败,则不会commit,则不会上传到二级缓存中,数据因为失败仍应是原来的数据才是对的,二级缓存数据没有收到commit或close后传来的数据而不变,继续等待相应的sqlsession 进行commit/close

  • UserMapper有一个二级缓存区域(按namespace分,如果namespace相同则使用同一个相同的二级缓存区),其它mapper也有自己的二级缓存区域(按namespace分)

  • 每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。

  • mybatis二级缓存的局限性:

    细粒度缓存就是,针对某个商品,如果要修改其信息,只修改该商品的缓存数据,缓存区的其他数据不动(不会因为某个商品信息的修改就直接清空整个缓存区)

    mybatis二级缓存对细粒度级别的缓存实现不好,比如如下需求:
    对商品信息进行缓存, 由于商品信息查询访问量大,但是要求用户每次都能查询到最新的商品信息,此时如果使用mybatis的二级缓存,就无法实现当一个商品信息变化时,只刷新该商品的缓存信息,而不刷新其它商品的信息。这是因为mybatis的二级缓存区域以mapper为单位进行划分,当一个商品信息变化时(使用这个mapper的任意一个sqlSession执行commt操作),会将所有商品信息的缓存数据全部清空。解决此问题,需要在业务层根据需求对数据进行针对性的缓存(三级缓存)。

posted @ 2020-07-23 16:13  eternal_heathens  阅读(234)  评论(0编辑  收藏  举报