面试mybatis必问二级缓存,都知道有二级缓存,那mybatis是怎么实现的,本系列文章以mybatis-3.5.6版本为例。啰里啰唆写了,想直接看缓存的跳到缓存部分。
一、mybatis流程
写mybatis代码的时候,逃不过这几步
// 1.获取配置文件
InputStream in =Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml");
//2.开启SqlSession工厂
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(in);
//3.开启SqlSession
SqlSession sqlSession = build.openSession();
//4.执行sql并返回
// 4.1 直接用
sqlSession.selectOne("com.faith.mybatiscode.mapper.ProductConfigMapper.selectOne", 1);
// 4.2 用mapper代理
ProductConfigMapper mapper = sqlSession.getMapper(ProductConfigMapper.class);
List<ProductConfigVo> productConfigVos = mapper.selectProductConfigs(Arrays.asList(1));
各种写法大同小异,区别主要是两点,第3步的时候是用单例sqlSession还是多实例的,第4步直接用sqlSession进行操作还是用mapper代理的方式。
1.结构
还记得我们写jdbc代码的时候怎么写的吗?大概这么几步
String url = "jdbc:mysql://127.0.0.1:3306/Supermarket?characterEncoding=utf-8";
String username = "root";
String password = "123";
try{
// 1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
// 2.获得数据库链接
Connection conn = DriverManager.getConnection(url, username, password);
// 3.通过数据库的连接操作数据库,实现增删改查(使用Statement类)
String name="张三";
String sql="select * from product_config where id_no=?";
PreparedStatement statement = conn.prepareStatement(sql);
statement.setString(1, name);
// 4.处理数据库的返回结果(使用ResultSet类)
ResultSet rs = statement.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("UserName") + " " + rs.getString("Password"));
}
// 5.关闭资源
rs.close();
statement.close();
conn.close();
}catch(SQLException | ClassNotFoundException se){
System.out.println("数据库连接失败!");
se.printStackTrace() ;
}
那mybatis是怎么帮我们做这些步骤的呢?先看这几个对象:
1.SqlSessionFactory
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
看SqlSessionFactory接口,做了两件事,获取SqlSession和Configuration,看他的默认实现类DefaultSqlSessionFactory持有一个Configuration对象,看这个Configuration里有什么
要了命了,这么多变量,冷静下来看下,基本上包括我们在Mybatis-config.xml里配置的所有信息,其他的不认识,碰到再说。看 new SqlSessionFactoryBuilder().build(in) 做了什么,主要是
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
所以就是解析配置文件为Configuration对象。
openSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
只是返回一个DefaultSession对象,注意这里openSession一次就new一个SqlSession,缓存分级就从这里区别开来了。从而我们知道每个SqlSession持有相同的Configuration对象,不同的Executor对象,即
sqlSession.selectOne
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
从代码我们可知,SqlSession是用到了Configuration对象组装好MappedStatement,然后用executor执行得到结果,对比jdbc,似乎有这种感觉。
1.configuration在解析xml的时候就组装好带占位符的sql语句了。在这里取出来给executor用。
2.executor管理了参数的拼装,即PreparedStatement的实例化和参数设置,获取连接,执行,返回ResultSet和解析ResultSet到封装的对象。
如果设这样的话,缓存一定在executor里了。
缓存
1.在mybatis-config.xml里配置<cache/>标签,表示开启二级缓存,一级缓存是默认开启的。看CachingExecutor里的代码逻辑。
2.二级
@Override
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) {
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);
}
先从缓存里取,取不到再执行查询,这一级的cache是MappedStatement里的,这一步还没到事务呢,所以是跨事务的。实际上代码里的tcm就是一个TransactionalCacheManager,
真正的数据缓存是这个map的value,TransactionalCache,
主要关注delegate和entriesToAddOnCommit,delegate实际上和一级缓存一样,是一个
无论一级缓存还是二级缓存,最终都是存进这个cache的Map对象里。
而entriesToAddOnCommit这个map,看名字就知道是一组entries,调用SqlSession.commit()了之后才会刷到PerpetualCache的cache里,这里头才是真正的缓存数据。其实SqlSession.close()也会刷。
3.一级
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
这个localCache就是上边提到的 PerpetualCache,所以只要是查询都会缓存。
问题
1.一级缓存二级缓存都存到一个Map里,而且key一样,所有如果没有配置外部的二级缓存,内存里只存了一份缓存吧?
2.网上说二级缓存不要轻易开启,容易导致内存撑爆,这是什么道理?就算开启了二级缓存,内存里只有一份缓存数据,怎么会因为开启二级缓存而导致内存占用变大呢?除非是一直SqlSession不commit(),entriesToAddOnCommit这个map把内存撑爆,或者是一次SqlSession查出来的数据太大,在commit之前就爆了(如果这中情况的话一级缓存也爆)。
3.就算配置了外部二级缓存,要想使用上二级缓存,第一次必须查数据库,意味着一级缓存里也被存进去了。所以一级缓存该占用的内存还是占用啊,那二级缓存的意义在哪里?难道是为了多应用的架构,让别的应用直接去外部的二级缓存里取,而不用查数据库?