缓存
查询缓存主要是为了提高查询访问速度,即当用户执行一次查询后,会将该数据结果放到缓存中,当下次再执行此查询时就不会访问数据库了而是直接从缓存中获取该数据。 如果在缓存中找到了数据那叫做命中。
一级缓存
- MyBatis的一级查询缓存(也叫作本地缓存)是基于org.apache.ibatis.cache.impl.PerpetualCache 类的 HashMap本地缓存,其作用域是SqlSession
- 在同一个SqlSession中两次执行相同的 sql 查询语句,第一次执行完毕后,会将查询结果写入到缓存中,第二次会从缓存中直接获取数据,而不再到数据库中进行查询,这样就减少了数据库的访问,从而提高查询效率。
- 当一个 SqlSession 结束后,该 SqlSession 中的一级查询缓存也就不存在了。 myBatis 默认一级查询缓存是开启状态,且不能关闭。
- 增删改会清空缓存,无论是否commit
- 当SqlSession关闭和提交时,会清空一级缓存
同一sqlSession 多次查询同一SQL时会使用缓存
@Test
public void testLocalCache() throws Exception {
SqlSession sqlSession = factory.openSession(); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
// 第二三次会从缓存中拿数据,不查数据库
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
}
复制代码
同一sqlSession 有增删改时会清空缓存
@Test
public void testLocalCacheClear() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
// 增删改会清空缓存
System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
// 会从数据库查数据
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
}
复制代码
一级缓存实现
对SqlSession的操作mybatis内部都是通过Executor来执行的。Executor的生命周期和SqlSession是一致的。Mybatis在Executor中创建了一级缓存,基于PerpetualCache 类的 HashMap
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
// 执行器
private Executor executor;
private boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
}
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 缓存实例
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.configuration = configuration; this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.closed = false; this.wrapperExecutor = this;
//mybatis一级缓存,在创建SqlSession->Executor时候动态创建,随着sqlSession销毁而销毁
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
}
}
// 缓存实现类
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
}
复制代码
//SqlSession.selectList会调用此方法(一级缓存操作,总是先查询一级缓存,缓存中不存在再查询数据库)
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 (closed) {
//如果已经关闭,报错
throw new ExecutorException("Executor was closed.");
}
//先清一级缓存,再查询,但仅仅查询堆栈为0才清,为了处理递归调用
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//加一,这样递归调用到上面的时候就不会再清局部缓存了
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//如果查到localCache缓存,处理
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//从数据库查
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
}
finally {
queryStack--; //清空堆栈
}
if (queryStack == 0) {
//延迟加载队列中所有元素
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); //清空延迟加载队列
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
复制代码
一级缓存生命周期总结
- MyBatis在开启一个会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
- SqlSession中执行了任何一个update操作(update()、delete()、insert()),都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
二级缓存
- MyBatis的二级缓存是mapper范围级别的
- SqlSession关闭后才会将数据写到二级缓存区域
- 增删改操作,无论是否进行提交commit(),均会清空一级、二级缓存
- 二级缓存是默认开启的。
如果想要设置增删改操作的时候不清空二级缓存的话,可以在其insert或delete或update中添加属性flushCache=”false”,默认为 true。
<delete id="deleteStudent" flushCache="false">
DELETE FROM t_student where id=#{id}
</delete>
复制代码
开启二级缓存
// mybatis-config.xml 中配置
<settings>
<setting name="localCacheScope" value="SESSION"/>
默认值为 true。即二级缓存默认是开启的
<setting name="cacheEnabled" value="true"/>
</settings>
// 具体mapper.xml 中配置
<mapper namespace="cn.itcast.mybatis.mapper.UserMapper">
<!-- 开启本mapper的namespace下的二级缓存
type:指定cache接口的实现类的类型,mybatis默认使用PerpetualCache
要和ehcache整合,需要配置type为ehcache实现cache接口的类型-->
<cache />
<!-- 下面的一些SQL语句暂时略 -->
</mapper>
复制代码
不同SqlSession,同一Mapper
- SqlSession关闭后才会将数据写到二级缓存区域
@Test
public void testCacheWithCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
//sqlSession1关闭后,会将sqlsession1中的数据写到二级缓存区域
//不关闭的话不会写入二级缓存
sqlSession1.close();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
复制代码
- sqlSession未关闭,不会将数据写到二级缓存区域
@Test
public void testCacheWithoutCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
//sqlSession未关闭,不会将数据写到二级缓存区域,会从数据库中查询
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
复制代码
二级缓存关闭
- 全局关闭
<setting name="cacheEnabled" value="false"/>
复制代码
- 局部关闭 局部关闭是只关闭某个select查询的二级缓存,在select标签中将属性useCache设置为false,那么就会关闭该select查询的二级缓存。
<select id="selectStudentById" useCache="false" resultMap="studentMapper">
SELECT id,name,age,score,password FROM t_student where id=#{id}
</select>
复制代码
使用注意事项
-
在一个命名空间下使用二级缓存 二级缓存对于不同的命名空间namespace的数据是互不干扰的,倘若多个namespace中对一个表进行操作的话,就会导致这不同的namespace中的数据不一致的情况。
-
在单表上使用二级缓存 在做关联关系查询时,就会发生多表的操作,此时有可能这些表存在于多个namespace中,这就会出现上一条内容出现的问题了。
-
查询多于修改时使用二级缓存 在查询操作远远多于增删改操作的情况下可以使用二级缓存。因为任何增删改操作都将刷新二级缓存,对二级缓存的频繁刷新将降低系统性能。