本篇博客主要讲了 mybatis 一二级缓存的构成,以及一些容易出错地方的示例分析;
一、mybatis 缓存体系mybatis 的一二级缓存体系大致如下:
首先当一二级缓存同时开启的时候,首先命中二级缓存;
一级缓存位于 BaseExecutor 中不能关闭,但是可以指定范围 STATEMENT、SESSION;
整个二级缓存虽然经过了很多事务相关的组件,但是最终是落地在 MapperStatement 的 Cache 中(Cache 的具体实例类型可以在 mapper xml 的 cache type 标签中指定,默认 PerpetualCache),而 MapperStatement 和 namespace 一一对应,所以二级缓存的作用域是 mapper namespace;
在使用二级缓存的时候,如果 cache 没有命中则向后查找,然后查询的结果不是直接放到 cache 中,而是首先放到 TransactionCache 的本地缓存中,这里区分 entriesToAddOnCommit、entriesMissedInCache 是为了统计命令率,最后在 sqlSession commit 的时候,才会将 TransactionCache 的本地缓存提交到 cache 中,此时 cache 才是对其他 sqlSession 可见的;
此外当需要分布式缓存的时候,就需要将二级缓存放到 JVM 之外,这里可以实现 cache 接口编写自己的 cache,此时在实现的 cache 中就可以使用 ehcache、redis 等外部缓存进行操作;
以上就大致是 mybatis 缓存的整体结构,下面将分模块拆分测试一二级缓存;
二、一级缓存mybatis 的一级缓存一般情况很少使用,其原因主要有两个:
一级缓存的生命周期同 SqlSession,所以容易出现脏读;
一级缓存的 cache 的实现只能是 PerpetualCache,所以不能指定容量等设置;
1. 脏读测试指定一级缓存范围为 SESSION:
<setting value="SESSION"/> @Test public void test01() { SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory(); try ( SqlSession sqlSession1 = sqlSessionFactory.openSession(true); SqlSession sqlSession2 = sqlSessionFactory.openSession(true); ) { UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); log.info("---get: {}", userMapper1.getUser(1L)); log.info("---get: {}", userMapper2.getUser(1L)); log.info("---update: {}", userMapper1.setNameById(1L, "LiSi")); log.info("---get: {}", userMapper1.getUser(1L)); log.info("---get: {}", userMapper2.getUser(1L)); } }结果如下:
[DEBUG] sanzao.db.UserMapper.getUser - ==> Preparing: select * from user where id = ? [DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long) [TRACE] sanzao.db.UserMapper.getUser - <== Columns: id, username, password, address [TRACE] sanzao.db.UserMapper.getUser - <== Row: 1, ZhangSan, 123456, TT [DEBUG] sanzao.db.UserMapper.getUser - <== Total: 1 [INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'} [DEBUG] org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection [DEBUG] org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 61073295. [DEBUG] sanzao.db.UserMapper.getUser - ==> Preparing: select * from user where id = ? [DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long) [TRACE] sanzao.db.UserMapper.getUser - <== Columns: id, username, password, address [TRACE] sanzao.db.UserMapper.getUser - <== Row: 1, ZhangSan, 123456, TT [DEBUG] sanzao.db.UserMapper.getUser - <== Total: 1 [INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'} [DEBUG] sanzao.db.UserMapper.setNameById - ==> Preparing: update user set username = ? where id = ? [DEBUG] sanzao.db.UserMapper.setNameById - ==> Parameters: LiSi(String), 1(Long) [DEBUG] sanzao.db.UserMapper.setNameById - <== Updates: 1 [INFO] sanzao.Test01 - ---update: 1 [DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long) [TRACE] sanzao.db.UserMapper.getUser - <== Columns: id, username, password, address [TRACE] sanzao.db.UserMapper.getUser - <== Row: 1, LiSi, 123456, TT [DEBUG] sanzao.db.UserMapper.getUser - <== Total: 1 [INFO] sanzao.Test01 - ---get: User{id=1, user_name='LiSi', password='123456', address='TT'} [INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}可以看到当 sqlSession1 更新的时候,sqlSession2 的缓存仍然有效所以出现了脏读;所以通常都设置一级缓存的范围为:STATEMENT;
2. 源码分析mybatis 的一级缓存主要和 Executor 整合比较多,所以建议先查看我上一篇博客 Executor 详解 ,详细了解缓存命中的整体流程;这里一级缓存的源码也很简单:
查询的时候,首先查缓存,命中则返回,未命中就查数据库,然后填充缓存;