缓存的概念大家都知道,但是Mybatis缓存你知道吗?也许很多人知道Mybatis有一级缓存和二级缓存但是不知道具体是什么。下面我们一起来探讨一下Mybatis的一级缓存
什么是Mybatis的缓存
所谓Mybatis的缓存就是在执行一条sql之后,Mybatis会将该sql语句缓存起来,当再次执行这条sql语句的时候会直接从缓存中取出来执行。
Mybatis的缓存分为一级缓存和二级缓存,一级缓存叫做sqlSession级别的缓存,二级缓存叫做表缓存,本文探讨的是一级缓存。
我们在执行一次数据库会话的时候可能会进行相同的sql,这个时候Mybatis的一级缓存就起到作用了,这个时候会优先命中一级缓存,避免了在数据库中查找,提高了性能
一级缓存的演示
首先创建一个mybaits项目,建立各种配置文件,创建测试类
1 public interface UserDao {
2
3 User findById(int id);
4 }
5
6 public class UserDaoTest {
7
8 public UserDao userDao;
9 public SqlSessionFactory sqlSessionFactory;
10
11 @Before
12 public void setUp() throws Exception {
13 String resource = "mybatis-config.xml";
14 InputStream inputStream = Resources.getResourceAsStream(resource);
15 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
16 }
17
18 @Test
19 public void findById() throws Exception {
20 SqlSession sqlSession = sqlSessionFactory.openSession();
21 UserDao userDao = sqlSession.getMapper(UserDao.class);
22 System.out.println(userDao.findById(1));
23 System.out.println(userDao.findById(1));
24 }
25
26 }
这个时候是会进行两次查询还是一次查询,大家可以猜测一下。显然他只会执行一次查询
1 2019-09-23 11:01:41,671 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
2 2019-09-23 11:01:41,714 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 1(Integer)
3 2019-09-23 11:01:41,759 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
4 com.mybatis.pojo.User@12f41634
5 com.mybatis.pojo.User@12f41634
上面的代码执行了两次相同的查询,但是只查询了一次。这就是用到了一级缓存
缓存失效
上面介绍了一级缓存的概念,那么什么时候缓存会失效呢?其实失效可以分为很多种
在两次相同查询之间执行插入操作导致缓存失效
1 @Test
2 public void findById() throws Exception {
3 SqlSession sqlSession = sqlSessionFactory.openSession();
4 UserDao userDao = sqlSession.getMapper(UserDao.class);
5 System.out.println(userDao.findById(1));
6 userDao.insetUser(new User(3,"dq","123456",25,"女",new Date())); //在查询之间新增一条数据
7 System.out.println(userDao.findById(1));
8 }
我们直接看看结果如何
1 2019-09-23 11:28:01,120 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
2 2019-09-23 11:28:01,230 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 1(Integer)
3 2019-09-23 11:28:01,281 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
4 com.mybatis.pojo.User@12f41634
5 2019-09-23 11:28:01,282 [main] [com.mybatis.dao.UserDao.insetUser]-[DEBUG] ==> Preparing: INSERT INTO tb_user VALUES (?,?,?,?,?,?)
6 2019-09-23 11:28:01,286 [main] [com.mybatis.dao.UserDao.insetUser]-[DEBUG] ==> Parameters: 3(Integer), dq(String), 123456(String), 25(Integer), 女(String), 2019-09-23 11:28:01.282(Timestamp)
7 2019-09-23 11:28:01,286 [main] [com.mybatis.dao.UserDao.insetUser]-[DEBUG] <== Updates: 1
8 2019-09-23 11:28:01,287 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
9 2019-09-23 11:28:01,287 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 1(Integer)
10 2019-09-23 11:28:01,289 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
11 com.mybatis.pojo.User@50d0686
可以看出,查询了两次,缓存失效了
不同的sqlSession,导致缓存的失效
我们知道,一级缓存是sqlSession级别的,那么我们更换不同的sqlSession的话也是会导致缓存失效的
1 @Test
2 public void findById() throws Exception {
3 SqlSession sqlSession = sqlSessionFactory.openSession();
4 UserDao userDao = sqlSession.getMapper(UserDao.class);
5 System.out.println(userDao.findById(1));
6 SqlSession sqlSession2 = sqlSessionFactory.openSession(); //创建一个新的sqlSession
7 UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
8 System.out.println(userDao2.findById(1));
9 }
看看结果如何
1 2019-09-23 11:33:50,928 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
2 2019-09-23 11:33:51,212 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 1(Integer)
3 2019-09-23 11:33:51,374 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
4 com.mybatis.pojo.User@12f41634
5 2019-09-23 11:33:51,424 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
6 2019-09-23 11:33:51,427 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 1(Integer)
7 2019-09-23 11:33:51,436 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
8 com.mybatis.pojo.User@a38d7a3
可以看到上面的查询执行了两次
不同的查询条件导致缓存失效
1 @Test
2 public void findById() throws Exception {
3 SqlSession sqlSession = sqlSessionFactory.openSession();
4 UserDao userDao = sqlSession.getMapper(UserDao.class);
5 System.out.println(userDao.findById(1));
6 System.out.println(userDao2.findById(2));
7 }
看结果
1 2019-09-23 11:38:13,148 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
2 2019-09-23 11:38:13,185 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 1(Integer)
3 2019-09-23 11:38:13,213 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
4 com.mybatis.pojo.User@12f41634
5 2019-09-23 11:38:13,215 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Preparing: SELECT * FROM tb_user WHERE id = ?
6 2019-09-23 11:38:13,215 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] ==> Parameters: 2(Integer)
7 2019-09-23 11:38:13,217 [main] [com.mybatis.dao.UserDao.findById]-[DEBUG] <== Total: 1
8 com.mybatis.pojo.User@371a67ec
因为查询条件的不同,查了两次
手动清除缓存也会导致缓存的失效
执行调用sqlSession.clearCache()方法就能清除缓存,这里就不做过多的演示了
从上面的分析中你大概已经知道了什么是一级缓存?什么情况下会命中缓存?什么情况下缓存失效?
接下来我们分析一级缓存的流程
一级缓存的执行过程
一级缓存是SqlSession级别的缓存,那么我们从SqlSession开始分析
所有方法里面也就这个clearCache()方法貌似和cache有关系
所以我们从这里入手
SqlSession缓存的流程
因为SqlSession是一个接口,所以看他的实现
public class DefaultSqlSession implements SqlSession
继续看这个类的实现方法
public void clearCache() {
this.executor.clearLocalCache();
}
接着点进去看
1 void clearLocalCache(); //Executor接口的方法
2 public abstract class BaseExecutor implements Executor //实现方法的类
3
4 public void clearLocalCache() {
5 if (!this.closed) {
6 this.localCache.clear();
7 this.localOutputParameterCache.clear();
8 }
9
10 }
继续跟踪
public void clear() {
this.cache.clear();
}
这个cache是什么
private Map<Object, Object> cache = new HashMap();
原来是一个map,清除的方法就是调用map的清除方法进行清除
现在知道了缓存是用map来存放的了,那么缓存在哪里创建的呢?从上面几个类中我们分析应该是在Executor执行器中
CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);
这是其中的一个方法,我们可以看看它的实现
1 public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
2 if (this.closed) {
3 throw new ExecutorException("Executor was closed.");
4 } else {
5 CacheKey cacheKey = new CacheKey();
6 cacheKey.update(ms.getId());
7 cacheKey.update(rowBounds.getOffset());
8 cacheKey.update(rowBounds.getLimit());
9 cacheKey.update(boundSql.getSql());
10 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
11 TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
12
13 for(int i = 0; i < parameterMappings.size(); ++i) {
14 ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
15 if (parameterMapping.getMode() != ParameterMode.OUT) {
16 String propertyName = parameterMapping.getProperty();
17 Object value;
18 if (boundSql.hasAdditionalParameter(propertyName)) {
19 value = boundSql.getAdditionalParameter(propertyName);
20 } else if (parameterObject == null) {
21 value = null;
22 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
23 value = parameterObject;
24 } else {
25 MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
26 value = metaObject.getValue(propertyName);
27 }
28
29 cacheKey.update(value);
30 }
31 }
32
33 return cacheKey;
34 }
35 }
大概可以看出来cacheKey里面存了id,offset,limit,sql信息,参数(这里就能知道了,参数不同,这个缓存的key也就不同,,所以没法命中缓存),还有一个environmentId这几项
现在缓存创建出来了。,看看查询的方法吧
1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
2 BoundSql boundSql = ms.getBoundSql(parameter);
3 CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql); //在这里创建缓存
4 return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
5 }
6
7 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
8 ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
9 if (this.closed) {
10 throw new ExecutorException("Executor was closed.");
11 } else {
12 if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
13 this.clearLocalCache();
14 }
15
16 List list;
17 try {
18 ++this.queryStack;
19 //主要看这个地方
20 list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
21 if (list != null) {
22 //处理存储过程的
23 this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
24 } else {
25 //去数据库中进行查询
26 list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
27 }
28 } finally {
29 --this.queryStack;
30 }
31
32 if (this.queryStack == 0) {
33 Iterator i$ = this.deferredLoads.iterator();
34
35 while(i$.hasNext()) {
36 BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();
37 deferredLoad.load();
38 }
39
40 this.deferredLoads.clear();
41 if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
42 this.clearLocalCache();
43 }
44 }
45
46 return list;
47 }
48 }
49
50
51 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
52 this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
53
54 List list;
55 try {
56 list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
57 } finally {
58 this.localCache.removeObject(key);
59 }
60 //放入缓存中
61 this.localCache.putObject(key, list);
62 if (ms.getStatementType() == StatementType.CALLABLE) {
63 this.localOutputParameterCache.putObject(key, parameter);
64 }
65
66 return list;
67 }
从上面的代码中大概是可以看出来缓存的执行流程了
我们想想为什么在两次查找之间新增数据缓存会失效,来看看update()方法吧
1 public int update(MappedStatement ms, Object parameter) throws SQLException {
2 ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
3 if (this.closed) {
4 throw new ExecutorException("Executor was closed.");
5 } else {
6 this.clearLocalCache(); //清除缓存
7 return this.doUpdate(ms, parameter);
8 }
9 }
可以看到在更新的时候清除了缓存,这下都能解释了
最后我们想想为什么不同的SqlSession不会走一级缓存?到这里我们应该很清晰了,因为每个SqlSession都会存在一个map引用,不同的SqlSession的话map是不同的
总结
通过上面的内容可以对一级缓存有个认识了