zoukankan      html  css  js  c++  java
  • mybatis一级缓存原理及源码分析

    前言

      mybatis的一级缓存是SqlSession级别的,默认开启。

    透过现象

    1)在⼀个SqlSession中,对User表根据id进⾏两次查询,查看他们发出sql语句的情况。

     1 @Test
     2 public void test5() throws IOException {
     3     InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
     4     SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
     5     SqlSession sqlSession = factory.openSession();
     6     IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
     7     // 第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中
     8     User user1 = mapper.findById(1);
     9     System.out.println(user1);
    10     // 第⼆次查询,由于是同⼀个SqlSession,会在缓存中查询结果
    11     // 如果有,则直接从缓存中取出来,不和数据库进⾏交互
    12     User user2 = mapper.findById(1);
    13     System.out.println(user2);
    14     sqlSession.close();
    15}

      查看控制台打印情况:

      只打印了一次SQL语句,打印了两次查询结果,由此可见,第二次查询并没有向数据库发出SQL执行。

    2)同样是对user表进⾏两次查询,只不过两次查询之间进⾏了⼀次update操作。

     1 @Test
     2 public void test6() throws IOException {
     3     InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
     4     SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
     5     SqlSession sqlSession = factory.openSession();
     6     IUserMapper mapper = sqlSession.getMapper(IUserMapper.class);
     7     // 第⼀次查询,发出sql语句,并将查询出来的结果放进缓存中
     8     User user1 = mapper.findById(1);
     9     System.out.println(user1);
    10     // 第⼆步进⾏了⼀次更新操作,SqlSession.commit()
    11     User user = new User();
    12     user.setId(1);
    13     user.setUsername("max");
    14     mapper.updateById(user);
    15     sqlSession.commit();
    16     // 第⼆次查询,由于是同⼀个sqlSession.commit(),会清空缓存信息
    17     // 则此次查询也会发出sql语句
    18     User user2 = mapper.findById(1);
    19     System.out.println(user2);
    20     sqlSession.close();
    21 }

      查看控制台打印情况:

      第一次查询发出SQL语句后,由于更新操作,缓存清除了,因此第二次查询继续发出SQL语句。

    3)总结

    1、第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,从 数据库查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。

    2、 如果中间SqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀级缓存,这样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。

    3、 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直 接从缓存中获取⽤户信息。

    看本质

      既然一级缓存是SqlSession级别的,那么就从SqlSession入手,看看有没有创建缓存或者与缓存有关的属性或者⽅法。

      貌似只有clearCache()和缓存沾上点儿关系啊,那么就直接从这个⽅ 法⼊⼿吧。分析源码时,我们要看它(此类)是谁,它的⽗类和⼦类分别⼜是谁,对如上关系了解了,你才 会对这个类有更深的认识,分析了⼀圈,你可能会得到如下这个流程图。

      再深⼊分析,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤其cache.clear()⽅法,那么这个cache是什么东⻄呢?点进去发现,cache其实就是private Map<Object, Object> cache = new HashMap();也就是⼀个HashMap,所以说cache.clear()其实就是Map.clear(),也就是说,缓存其实就是本地存放的⼀个Map对象,每⼀个SqISession都会存放⼀个Map对象的引⽤,那么这个cache是何 时创建的呢?

      你觉得最有可能创建缓存的地⽅是哪⾥呢?我觉得是Executor,为什么这么认为?因为Executor是 执⾏器,⽤来执⾏SQL请求,⽽且清除缓存的⽅法也在Executor中执⾏,所以很可能缓存的创建也很有可能在Executor中,看了⼀圈发现Executor中有⼀个createCacheKey⽅法,这个⽅法很像是创建缓存的⽅法啊,跟进去看看,你发现createCacheKey⽅法是由BaseExecutor执⾏的,代码如下:

     1 @Override
     2 public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
     3     if (closed) {
     4         throw new ExecutorException("Executor was closed.");
     5     }
     6     // 创建 CacheKey 对象
     7     CacheKey cacheKey = new CacheKey();
     8     // 设置 id、offset、limit、sql 到 CacheKey 对象中
     9     cacheKey.update(ms.getId());
    10     cacheKey.update(rowBounds.getOffset());
    11     cacheKey.update(rowBounds.getLimit());
    12     cacheKey.update(boundSql.getSql());
    13     ......
    14     // 设置 Environment.id 到 CacheKey 对象中
    15     if (configuration.getEnvironment() != null) {
    16         // issue #176
    17         cacheKey.update(configuration.getEnvironment().getId());
    18     }
    19     return cacheKey;
    20 }

      创建缓存key会经过⼀系列的update⽅法,udate⽅法由⼀个CacheKey这个对象来执⾏的,这个update⽅法最终由updateList的list来把五个值存进去,对照上⾯的代码和下⾯的图示,你应该能理解这五个值都是什么了。

      这⾥需要注意⼀下最后⼀个值,configuration.getEnvironment().getId()这是什么,这其实就是定义在mybatis-config.xml中的标签,⻅如下。

     1 <environments default="development">
     2     <environment id="development">
     3         <transactionManager type="JDBC"/>
     4         <dataSource type="POOLED">
     5             <property name="driver" value="${jdbc.driver}"/>
     6             <property name="url" value="${jdbc.url}"/>
     7             <property name="username" value="${jdbc.username}"/>
     8             <property name="password" value="${jdbc.password}"/>
     9         </dataSource>
    10     </environment>
    11 </environments>

      那么我们回归正题,那么创建完缓存之后该⽤在何处呢?总不会凭空创建⼀个缓存不使⽤吧?绝对不会的,经过我们对⼀级缓存的探究之后,我们发现⼀级缓存更多是⽤于查询操作,毕竟⼀级缓存也叫做查询缓存吧,为什么叫查询缓存我们⼀会⼉说。我们先来看⼀下这个缓存到底⽤在哪了,我们跟踪到query⽅法如下:

     1 //此方法在SimpleExecutor的父类BaseExecutor中实现
     2 @Override
     3 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
     4     //根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
     5     BoundSql boundSql = ms.getBoundSql(parameter);
     6     //为本次查询创建缓存的Key
     7     CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
     8     // 查询
     9     return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    10 }
    11 
    12 @SuppressWarnings("unchecked")
    13 @Override
    14 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    15     ......
    16         ......
    17         // 从一级缓存中,获取查询结果
    18         list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    19         // 获取到,则进行处理
    20         if (list != null) {
    21             handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    22             // 获得不到,则从数据库中查询
    23         } else {
    24             list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    25         }
    26         ......
    27     ......
    28 }
     1 // 从数据库中读取操作
     2 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
     3     List<E> list;
     4     // 在缓存中,添加占位对象。此处的占位符,和延迟加载有关,可见 `DeferredLoad#canLoad()` 方法
     5     localCache.putObject(key, EXECUTION_PLACEHOLDER);
     6     try {
     7         // 执行读操作
     8         list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
     9     } finally {
    10         // 从缓存中,移除占位对象
    11         localCache.removeObject(key);
    12     }
    13     // 添加到缓存中
    14     localCache.putObject(key, list);
    15     // 暂时忽略,存储过程相关
    16     if (ms.getStatementType() == StatementType.CALLABLE) {
    17         localOutputParameterCache.putObject(key, parameter);
    18     }
    19     return list;
    20 }

      如果查不到的话,就从数据库查,在queryFromDatabase中,会对localCache进⾏写⼊。 localCache对象的put⽅法最终交给Map进⾏存放。

    1 //缓存容器
    2 private Map<Object, Object> cache = new HashMap<>();
    3 
    4 @Override
    5 public void putObject(Object key, Object value) {
    6     cache.put(key, value);
    7 }
  • 相关阅读:
    网站备案 应该找域名商还是空间商备案
    备案的问题
    js发送邮件确定email地址
    How to Create a First Shell Script
    虚拟主机和网站空间有什么区别?
    linux 单引号,双引号,反引号
    linux 中的单引号 和双引号有什么区别吗
    linux awk命令详解
    shell中$0,$?,$!等的特殊用法
    深圳测试协会第二次管理层会议成功召开!
  • 原文地址:https://www.cnblogs.com/mgyboom/p/14290802.html
Copyright © 2011-2022 走看看