Mybatis一级缓存
缓存的概念大家都知道,但是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 }