缓存的概念大家都知道,但是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是不同的

总结

通过上面的内容可以对一级缓存有个认识了

02-13 20:13