像Mybatis、Hibernate这样的ORM框架,封装了JDBC的大部分操作,极大的简化了我们对数据库的操作。
当然,其还有一些优势功能,比如缓存就是这样的一个优势功能。
在实际项目中,我们发现在一个事务中查询同样的语句两次的时候,第二次没有进行数据库查询,直接返回了结果,实际这种情况我们就可以称为缓存。
框架针对这种查询做了一定了优化,那么缓存有几种类型?具体是如何优化的呢?能否从源码角度来分析一下这种优化是如何做的?
1.Mybatis的缓存级别
网上有很多这种概念的描述,笔者直接拿来
* 一级缓存(SqlSession级别的缓存,对于相同的查询,会直接从缓存中返回结果而不是查询数据库)
* 二级缓存(Mapper级别缓存,定义在Mapper文件的标签并需要开启此缓存,多个Mapper文件可以共用一个缓存,依赖标签配置)
2.Mybatis一级缓存的使用
一级缓存默认是开启的,这里笔者主要将个人测试的配置信息描述一下
1)maven引入(笔者依据SpringBoot来开发的)
org.springframework.boot
spring-boot-starter-parent
1.5.3.RELEASE
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.1.1
mysql
mysql-connector-java
2)创建实体类、配置文件
* Blog.java
package jdbc;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Blog implements Serializable{
private static final long serialVersionUID = 1L;
private int id;
private String name;
private String url;
}
* BlogMapper.java
package mybatis;
import jdbc.Blog;
public interface BlogMapper {
public Blog queryById(int id);
public void updateBlog(Blog blog);
}
* src/main/resources/config/下创建blog.xml
select * from blog where id = #{id}
update Blog set name = #{name},url = #{url} where id=#{id}
* src/main/resources/config/下创建configure.xml
3)创建测试类
public class Test {
private static SqlSessionFactory sqlSessionFactory;
private static Reader reader;
static {
try {
// 1.读取mybatis配置文件,并生成SQLSessionFactory
reader = Resources.getResourceAsReader("config/configure.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (Exception e) {
e.printStackTrace();
}
}
public static SqlSessionFactory getSession() {
return sqlSessionFactory;
}
public static void main(String[] args) {
// 2.获取session,主要的CRUD操作均在SqlSession中提供
SqlSession session = sqlSessionFactory.openSession();
SqlSession session1 = sqlSessionFactory.openSession();
try {
Blog blog = (Blog)session.selectOne("queryById",17);
// session.commit();
Blog blog2 = (Blog)session1.selectOne("queryById",17);
} finally {
session.close();
}
}
}
3.一级缓存的测试与结论
1)同一个session查询
public static void main(String[] args) {
SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = (Blog)session.selectOne("queryById",17);
Blog blog2 = (Blog)session.selectOne("queryById",17);
} finally {
session.close();
}
}
结论:只有一个DB查询
2)两个session分别查询
public static void main(String[] args) {
// 2.获取session,主要的CRUD操作均在SqlSession中提供
SqlSession session = sqlSessionFactory.openSession();
SqlSession session1 = sqlSessionFactory.openSession();
try {
Blog blog = (Blog)session.selectOne("queryById",17);
Blog blog2 = (Blog)session1.selectOne("queryById",17);
} finally {
session.close();
}
}
结论:进行了两次DB查询
3)同一个session,进行update之后再次查询
public static void main(String[] args) {
SqlSession session = sqlSessionFactory.openSession();
try {
Blog blog = (Blog)session.selectOne("queryById",17);
blog.setName("llll");
session.update("updateBlog",blog);
Blog blog2 = (Blog)session.selectOne("queryById",17);
} finally {
session.close();
}
}
结论:进行了两次DB查询
总结:在一级缓存中,同一个SqlSession下,查询语句相同的SQL会被缓存,如果执行增删改操作之后,该缓存就会被删除
4.一级缓存源码分析
既然所有的操作都在session.selectOne()之中,那么我们就来看下这个操作有何特殊之处
SqlSession的默认实现为DefaultSqlSession
1)DefaultSqlSession.selectOne()
@Override
public T selectOne(String statement, Object parameter) {
// 直接委托给selectList
List list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
// DefaultSqlSession.selectList()
@Override
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
// 委托给executor执行
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();
}
}
2)executor.query()executor默认实现为CachingExecutor
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 转换SQL,并获取CacheKey
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 执行查询
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//
@Override
public List 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, parameterObject, boundSql);
@SuppressWarnings("unchecked")
List list = (List) 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;
}
}
// 直接来到这里
// 实现为BaseExecutor.query()
return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
3)BaseExecutor.query()
@SuppressWarnings("unchecked")
@Override
public List 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 (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List list;
try {
queryStack++;
// 看这里,先从localCache中获取对应CacheKey的结果值
list = resultHandler == null ? (List) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 如果缓存中没有值,则从DB中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
4)BaseExecutor.queryFromDatabase()
我们先来看下这种缓存中没有值的情况,看一下查询后的结果是如何被放置到缓存中的
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 1.执行查询,获取list
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 2.将查询后的结果放置到localCache中,key就是我们刚才封装的CacheKey,value就是从DB中查询到的list
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
5)localCache的结构与操作
// 在BaseExecutor中我们看到 localCache如下所示:
protected PerpetualCache localCache;
// PerpetualCache.java
public class PerpetualCache implements Cache {
private String id;
private Map cache = new HashMap();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
...
总结:可以看到localCache本质上就是一个Map,key为我们的CacheKey,value为我们的结果值
6)SqlSession.update()是如何清除缓存的呢
还是按照刚才分析SqlSession.selectOne()的方式来一步步跟踪,我们跟踪到BaseExecutor.update()方法
// BaseExecutor.update()
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 看这里,执行了一个清理操作
clearLocalCache();
return doUpdate(ms, parameter);
}
// clearLocalCache();
@Override
public void clearLocalCache() {
if (!closed) {
// 直接将Map清空
localCache.clear();
localOutputParameterCache.clear();
}
}
总结:
针对于一级缓存还是比较简单的
主要就是在查询的时候,将结果值放入到一个Map中,map的key为查询条件的一系列封装,同一个session在执行相同查询的时候,则先从缓存中获取,缓存中没有,再去数据库中查询;
针对于SqlSession的增删改操作,会清空该缓存Map