• 从根上理解Mybatis的一级、二级缓存


    1. 写在前头

    这篇帖子主要讲一级缓存,它的作用范围和源码分析

    (本来想把一二级缓存合在一起,发现太长了)

    2. 准备工作

    2.1 两个要用的实体类

    public class Department {
    
        public Department(String id) {
            this.id = id;
        }
    
        private String id;
    
        /**
         * 部门名称
         */
        private String name;
    
        /**
         * 部门电话
         */
        private String tel;
    
        /**
         * 部门成员
         */
        private Set users;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    public class User {
    
        private String id;
    
        private String name;
    
        private Integer age;
    
        private LocalDateTime birthday;
    
        private Department department;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.2 Mapper.xml文件中要用的SQL

    • DepartmentMapper.xml,两条SQL,一条根据ID匹配,一条清除缓存,注意fulshCache标签
        
    
        
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • UserMapper.xml,简简单单的查询所有的user
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3. 一级缓存

    • 一级缓存是基于SQLSession的,同一条SQL执行第二遍的时候会直接从缓存中取,测试下看看
        public static void main(String[] args) throws IOException {
            InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
            // 开启二级缓存需要在同一个SqlSessionFactory下,二级缓存存在于 SqlSessionFactory 生命周期,如此才能命中二级缓存
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
    
            SqlSession sqlSession = sqlSessionFactory.openSession();
            DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
            System.out.println("----------department第一次查询 ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
            System.out.println("----------department一级缓存生效,控制台看不见SQL ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 可以发现控制台在第二次查询的时候,一级缓存生效,没有出现SQL

    • 我们清空下一级缓存再试试

    xml文件中flushCache标签 会清除所有namespace 的一级缓存 和 当前namespace 的二级缓存均会清除 默认是false

        public static void main(String[] args) throws IOException {
            InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
            // 开启二级缓存需要在同一个SqlSessionFactory下,二级缓存存在于 SqlSessionFactory 生命周期,如此才能命中二级缓存
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
    
            SqlSession sqlSession = sqlSessionFactory.openSession();
            DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
            System.out.println("----------department第一次查询 ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
            System.out.println("----------department一级缓存生效,控制台看不见SQL ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
            System.out.println("----------清除一级缓存 ↓------------");
            departmentMapper.cleanCathe();
            System.out.println("----------清除后department再一次查询,SQL再次出现 ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 控制台日志很清晰,清除缓存后又重新查了一遍

    3.1 一级缓存失效的情况

    3.1.1 不同SQLSession下同一条SQL一级缓存不生效
    • 创建一个新的sqlSession1执行相同的SQL,发现不同SQLSession下不共享一级缓存
            SqlSession sqlSession = sqlSessionFactory.openSession();
            SqlSession sqlSession1 = sqlSessionFactory.openSession();
            DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
            DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
            System.out.println("----------department第一次查询 ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
            System.out.println("----------sqlSession1下department执行相同的SQL,控制台出现SQL ↓------------");
            departmentMapper1.findById("18ec781fbefd727923b0d35740b177ab");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.1.2 两次相同查询SQL间有Insert、Delete、Update语句出现
    • 因为Insert、Delete、Update的flushCache标签 默认为 true ,执行它们时,必然会导致一级缓存的清空,从而引发之前的一级缓存不能继续使用的情况(这跟我们上边清除一级缓存的SQL例子一致)
    3.1.3 调用sqlSession.clearCache()方法
    • 这个方***将一级缓存清除,效果是一样的

    3.2 一级缓存源码:缓存被保存在了哪里?

    3.2.1 该如何找打它的位置
    • Mybatis顶层的缓存是接口Cache,查看它的实现类

      发现大部分实现类的包都是decorators(装饰器),只有PerpetualCache是Impl,所以我们确定的说,它就是我们要找的缓存实现类,点进去看看,发现只是组合了HashMap…

    public class PerpetualCache implements Cache {
    
      private final String id;
    
      // 看这里
      private final Map cache = new HashMap<>();
    
      ...
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 那这个PerpetualCache被放在哪里呢? 我们想到了一级缓存是基于SQLSession,那我们去DefaultSQLSession,它默认的实现类里看看
    public class DefaultSqlSession implements SqlSession {
    
      private final Configuration configuration;
      private final Executor executor;
    
      private final boolean autoCommit;
      private boolean dirty;
      private List> cursorList;
    
      ...
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 发现并没有哇!DefaultSqlSession还有两个东西,Configuration是全局的配置,这里边儿应该是没有,那我们只能再去Executor里看看了

    • 发现它是个接口,实现类有一个CachingExecutor!立马点进去!

    public class CachingExecutor implements Executor {
    
      private final Executor delegate;
      private final TransactionalCacheManager tcm = new TransactionalCacheManager();
    
      ...
    }
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 发现还是没有???

    • 但是Executor还有一个BaseExecutor,最后一家了,再在没有关了Idea睡觉了

    public abstract class BaseExecutor implements Executor {
    
      private static final Log log = LogFactory.getLog(BaseExecutor.class);
    
      protected Transaction transaction;
      protected Executor wrapper;
    
      protected ConcurrentLinkedQueue deferredLoads;
      // o??!! 不就在这呢嘛,小老弟
      protected PerpetualCache localCache;
      protected PerpetualCache localOutputParameterCache;
      protected Configuration configuration;
    
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 它来了,原来在这藏着呢呀,行了,这把知道它的位置了,我们直接看SQL执行的时候是怎么存的,怎么取的吧!
    3.2.2 query()方法
    • BaseExecutor的query()方法,看看注释,很简单
      public  List 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.");
        }
        // 是否需要清除一级缓存
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
          clearLocalCache();
        }
        List list;
        try {
          queryStack++;
          // 查询一级缓存中是否存在数据
          list = resultHandler == null ? (List) localCache.getObject(key) : null;
          if (list != null) {
            // 有数据直接取一级缓存
            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();
          // 全局localCacheScope设置为statement,则清空一级缓存
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
          }
        }
        return list;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    3.2.3 写两条Sql,Debug看一下
            System.out.println("----------department第一次查询 ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
            System.out.println("----------department一级缓存生效,控制台看不见SQL ↓------------");
            departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
    复制代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 哎,很对,第一次果然去数据库里查了

    • 哎,更对了,第二次果然取得缓存

    • 好嘛,真简单呀

    3.3 注意:一级缓存的查询结果被修改后,竟然…

    • 竟然会对之后取出的一级缓存有影响,测试下看看
            System.out.println("----------department第一次查询 ↓------------");
            Department department = departmentMapper.findById("18ec781fbefd727923b0d35740b177ab");
            System.out.println(department);
            department.setName("方圆把名字改了");
    
            System.out.println("----------department一级缓存生效,控制台看不见SQL ↓------------");
            System.out.println(departmentMapper.findById("18ec781fbefd727923b0d35740b177ab"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 第一次查询结果name为null,之后我们修改它的name,第二次查询取缓存的结果是更改name结果之后的

    • 这是因为存放的数据其实是对象的引用,导致第二次从一级缓存中查询到的数据,就是我们刚刚改过的数据

    3.4 文末

    一级缓存到这里就要跟大家说再见了,做个总结吧

    • 一级缓存是基于SQLSession的,不同SQLSession间不共享一级缓存
    • 执行Insert、Delete、Update语句会使一级缓存失效
    • 一级缓存在底层被存放在了BaseExecutor中,本质上就是个HashMap
    • 一级缓存存放的数据其实是对象的引用,若对它进行修改,则之后取出的缓存为修改后的数据
  • 相关阅读:
    Apache Dubbo 首个 Node.js 3.0-alpha 版本正式发布
    基于Java毕业设计志愿者管理系统演示录像2020源码+系统+mysql+lw文档+部署软件
    【PowerQuery】Excel 自动刷新PowerQuery连接
    QToolBar详解
    Cadence OrCAD Capture 新放置的元件为问号无法自动编号解决方法
    计算机网络的七层结构、五层结构和四层结构
    JavaSE——数组
    【分享15个Linux 实用技巧,提高工作效率】
    [HECTF 2022]—Web WirteUp
    Qt 对界面类重命名的步骤
  • 原文地址:https://blog.csdn.net/Huangjiazhen711/article/details/127700361