mybatis的缓存--深入源码分析

mybatis缓存介绍

缓存的作用时为了提高查询效率,减少访问数据库访问次数。mybatis有一级缓存和二级缓存。执行增删改操作commit时会清空缓存。

一级缓存

一级缓存是对sqlSession的,就是用同一个sqlSession查询两次相同的sql语句时,第一次在数据库中查询并将结果存放在session的缓存中,第二次查询直接在缓存中取结果而不必在查一次数据库。

//一级缓存相关
@Test
public void testFindbyId(){
    User user=userDao.findById(46);
    System.out.println(user);
    System.out.println(userDao.findById(46));
}

控制台输出
2020-08-31 16:47:03,033 4573506 [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Opening JDBC Connection
2020-08-31 16:47:05,583 4576056 [           main] DEBUG source.pooled.PooledDataSource  - Created connection 1439632660.
2020-08-31 16:47:05,585 4576058 [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@55cf0d14]
2020-08-31 16:47:05,645 4576118 [           main] DEBUG    cn.thg.dao.UserDao.findById  - ==>  Preparing: select * from user where id=? 
2020-08-31 16:47:06,743 4577216 [           main] DEBUG    cn.thg.dao.UserDao.findById  - ==> Parameters: 46(Integer)
2020-08-31 16:47:08,759 4579232 [           main] DEBUG    cn.thg.dao.UserDao.findById  - <==      Total: 1
User{id=46, username='小马宝莉', sex='女', birthday=Tue Mar 24 15:47:57 CST 2020, address='北京修正'}
2020-08-31 16:47:17,980 4588453 [           main] DEBUG             cn.thg.dao.UserDao  - Cache Hit Ratio [cn.thg.dao.UserDao]: 0.0
User{id=46, username='小马宝莉', sex='女', birthday=Tue Mar 24 15:47:57 CST 2020, address='北京修正'}
2020-08-31 16:47:18,053 4588526 [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@55cf0d14]
2020-08-31 16:47:18,055 4588528 [           main] DEBUG ansaction.jdbc.JdbcTransaction  - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@55cf0d14]

一级缓存只能作用在同一个session中,跨session无效。每开启一个sqlsession都会有它们自己的缓存。

二级缓存

二级缓存是对mapper而言的,就是无论是不是同一个sqlsession,查询两次相同的sql时,第一次会从数据库查并将结果存到缓存中,第二次直接从缓存中取。二级缓存时跨session的。
mybatis默认开启一级缓存,二级缓存需要手动开启。
关于开启二级缓存有两步,第一步是配置cacheEnable=true,第二步是在mapper.xml中配置。具体步骤百度一下吧,这里不多赘述。

mybatis查询过程涉及缓存的源码

@Test
public void testFindbyId(){
    User user=userDao.findById(46);
    System.out.println(user);
    System.out.println(userDao.findById(46));
}

userDao是sqlSession.getMapper(UserDao.class)得来的,是一个代理对象(具体如何代理先不讲,本篇主要讲缓存,有兴趣可以自行去翻一下源码),会调用MapperProxy类中的invoke方法,然后经过一系列的调用会执行到DefaultSqlSession类的selectList方法,selectList源码如下:

//DefaultSqlSession的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    List var5;
    try {
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        //调用执行器的查询方法,如果cacheEnable=true,执行器的类型是CachingExecutor,否则是SimpleExecutor
        var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception var9) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
    } finally {
        ErrorContext.instance().reset();
    }

    return var5;
}

关于执行器,这里使用SimpleExecutor,如果cacheEnable=true,在SqlSessionFactory类中会通过装饰者模式将SimpleExecutor包装成CachingExecutor。文章后面再简单看一下SqlSessionFactory,这里直接进入CachingExecutor。

//CachingExecutor的方法
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);
}

调用this.query(…)后进入CachingExecutor的如下方法:

//CachingExecutor的方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
	//获取二级缓存
    Cache cache = ms.getCache();
    //如果mapper中加上了<cache/>标签,cache就不会为空,就会进入这个分支
    if (cache != null) {
        this.flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            this.ensureNoOutParams(ms, boundSql);
            //尝试从二级缓存中获取结果
            List<E> list = (List)this.tcm.getObject(cache, key);
            if (list == null) {
            	//获取不到,调用SimpleExecutor的查询方法
            	//(可能会从数据库查,也可能会从一级缓存拿结果)
                list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                this.tcm.putObject(cache, key, list);
            }

            return list;
        }
    }
	//如果mapper中没有<cache/>标签,执行下面语句
    return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

终于看到了Cache,其实Cache里面主要是通过key-value结构进行数据缓存的。到这里如果从二级缓存中拿到结果,那这次查询就结束返回了。
delegate是一个Executor,一般实现类是SimpleExecutor。
假设我们启动了二级缓存并且没有从二级缓存中获得结果(为了可以看到一级缓存的相关源码)。
继续调用this.delegate.query(…),因为SimpleExecutor没有覆写父类的query方法,故而调用到了父类BaseExecutor的query方法:

//BaseExecutor的query方法
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;
            //前面传参时给resultHandler传了Executor.NO_RESULT_HANDLER,为空。
            //尝试从一级缓存localCache中取结果
            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;
    }
}

至此,启动了二级缓存(一级缓存默认开启)的查询过程就找完了。
那么关于一级缓存和二级缓存主要放在哪里呢?

mybatis缓存位置

如果看博客时跟着用idea进行debug的人不难发现,二级缓存时存放在Configuration的MappedStatement这个类中的,一级缓存是放在SqlSession的执行器executor中的。

二级缓存的位置
//CachingExecutorf的方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
	//获取二级缓存
    Cache cache = ms.getCache();
    ...
    ...
}

二级缓存放在MappedStatement类中,那么MappedStatement是怎么来的呢,是什么东西呢?
往回看看调用上面方法的类:

//DefaultSqlSession的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    List var5;
    try {
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    ...
    ...
}

原来MappedStatement是从Configuration中拿到的,Configuration想必大家都知道是mybatis配置文件的解析出来的信息吧。MappedStatement对应解析出来的mapper.xml文件中的相关信息,一个MappedStatement对应xml的一条sql。
看看Configuration的相关代码:

//Configuration的代码
protected final Map<String, MappedStatement> mappedStatements;

public MappedStatement getMappedStatement(String id) {
    return this.getMappedStatement(id, true);
}

public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
    if (validateIncompleteStatements) {
        this.buildAllStatements();
    }

    return (MappedStatement)this.mappedStatements.get(id);
}

通过全限定类名加方法名作为id获取对应dao方法的MappedStatement对象。

一级缓存的位置

通过上面找到一级缓存的代码,ctrl+左键进去看看就知道:

public abstract class BaseExecutor implements Executor {
    ...
    ...
    protected PerpetualCache localCache;
    ...
    ...
}

PerpetualCache是Cache接口的实现类,里面是封装了key-value结构:

public class PerpetualCache implements Cache {
    private final String id;
    private Map<Object, Object> cache = new HashMap();
    ...
}

刷新缓存

一级缓存会在执行增删改commit或者rollback方法、update方法、close方法之后清空。

//BaseExecutor的方法
public void commit(boolean required) throws SQLException {
    if (this.closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
    	//清空缓存
        this.clearLocalCache();
        this.flushStatements();
        if (required) {
            this.transaction.commit();
        }

    }
}
//BaseExecutor的方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (this.closed) {
        throw new ExecutorException("Executor was closed.");
    } else {
    	//清空缓存
        this.clearLocalCache();
        return this.doUpdate(ms, parameter);
    }
}
//BaseExecutor的方法
public void rollback(boolean required) throws SQLException {
    if (!this.closed) {
        try {
        	//清空缓存
            this.clearLocalCache();
            this.flushStatements(true);
        } finally {
            if (required) {
                this.transaction.rollback();
            }

        }
    }

}
//BaseExecutor的方法
public void close(boolean forceRollback) {
    try {
        try {
            this.rollback(forceRollback);
        } finally {
            if (this.transaction != null) {
                this.transaction.close();
            }

        }
    } catch (SQLException var11) {
        log.warn("Unexpected exception on closing transaction.  Cause: " + var11);
    } finally {
        this.transaction = null;
        this.deferredLoads = null;
        //指向空,一样是清空缓存
        this.localCache = null;
        this.localOutputParameterCache = null;
        this.closed = true;
    }

}

装饰者模式相关代码

写得有点累了,看代码注释跟进去就好。

//DefaultSqlSessionFactory的方法
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;
}
//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) {
    	//如果cacheEnable=true,装饰成CachingExecutor
        executor = new CachingExecutor((Executor)executor);
    }

    Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
    return executor;
}

结束!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值