本篇博客主要讲了 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 name="localCacheScope" 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 详解 , 详细了解缓存命中的整体流程; 这里一级缓存的源码也很简单:
查询的时候, 首先查缓存, 命中则返回, 未命中就查数据库, 然后填充缓存;
更新, 提交等操作情况缓存;
- @SuppressWarnings("unchecked")
- @Override
- public <E> List<E> 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."); }
- // 查询的时候一般不清楚缓存, 但是可以通过 xml 配置或者注解强制清除, queryStack == 0 是为了防止递归调用
- if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }
- List<E> list;
- try {
- queryStack++;
- // 首先查看一级缓存
- list = resultHandler == null ? (List<E>) 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();
- if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
- // 一级缓存本身不能关闭, 但是可以设置作用范围 STATEMENT, 每次都清除缓存
- clearLocalCache();
- }
- }
- return list;
- }
三, 二级缓存
mybatis 二级缓存要稍微复杂一点, 中间多了一步事务缓存:
首先无论是查询还是更新, 都会按要求清空缓存
flushCacheIfRequired
, 默认更新清空, 查询不清空, 也可以在 xml 或者注解中指定;
查询的时候, 先查缓存, 命中返回, 未命中查一级缓存, 数据库, 然后回填事务缓存, 注意这里不是直接填充到缓存中; 此时的事务缓存对任何的 SqlSession 都是不可见的, 因为自己查询的时候也是直接查询的目标缓存;
更新就直接委托给目标 Executor 执行;
最后 SqlSession 执行 commit 的时候, 将事务缓存刷新到目标缓存中;
1. 事务缓存测试
设置二级缓存:
- <setting name="cacheEnabled" value="true"/>
- <mapper namespace="***">
- <cache/>
- </mapper>
- @Test
- public void test02() {
- 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);
- User u1 = userMapper1.getUser(1L);
- System.out.println("---get u1:" + u1);
- User u2 = userMapper2.getUser(1L);
- System.out.println("---get u2:" + u2);
- User u3 = userMapper1.getUser(1L);
- System.out.println("---get u3:" + u3);
- }
- }
打印:
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get u1: User{
- id=1, user_name='sanzao', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - Opening JDBC Connection
- DEBUG [main] - Created connection 1613095350.
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get u2: User{
- id=1, user_name='sanzao', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- ---get u3: User{
- id=1, user_name='sanzao', password='123456', address='TT'
- }
可以看到:
SqlSession1 为提交事务缓存, 所以 SqlSession2 又从数据库中查了一次;
当 SqlSession1 再次查询的时候, 二级缓存未命中 Cache Hit Ratio 为 0, 但是命中了一级缓存, 所以并未再查数据库;
2. 二级缓存测试
这次我们提交缓存看看是否命中:
- @Test
- public void test03() {
- 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);
- User u1 = userMapper1.getUser(1L);
- System.out.println("---get u1:" + u1);
- sqlSession1.commit();
- User u2 = userMapper2.getUser(1L);
- System.out.println("---get u2:" + u2);
- int i = userMapper1.setNameById(1L, "LiSi");
- System.out.println("---update user:" + i);
- sqlSession1.commit();
- User u3 = userMapper1.getUser(1L);
- System.out.println("---get u3:" + u3);
- sqlSession1.commit();
- User u4 = userMapper2.getUser(1L);
- System.out.println("---get u4:" + u4);
- }
- }
打印:
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get u1: User{
- id=1, user_name='sanzao', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
- ---get u2: User{
- id=1, user_name='sanzao', password='123456', address='TT'
- }
- DEBUG [main] - ==> Preparing: update user set username = ? where id = ?
- DEBUG [main] - ==> Parameters: LiSi(String), 1(Long)
- DEBUG [main] - <== Updates: 1
- ---update user: 1
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get u3: User{
- id=1, user_name='LiSi', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
- ---get u4: User{
- id=1, user_name='LiSi', password='123456', address='TT'
- }
这次就能看到当 SqlSession1 提交事务缓存后, SqlSession2 就能看到了;
3. 缓存配置测试
此外还可以配置各种二级缓存策略, 比如大小, 刷新间隔时间, 淘汰策略等, 这里主要就是使用了 Cache 接口的装饰者模式:
LRU - 最近最少使用: 移除最长时间不被使用的对象.
FIFO - 先进先出: 按对象进入缓存的顺序来移除它们.
SOFT - 软引用: 基于垃圾回收器状态和软引用规则移除对象.
WEAK - 弱引用: 更积极地基于垃圾收集器状态和弱引用规则移除对象.
但是需要注意的是这里的策略也能用户本地缓存, 对于分布式缓存有些策略还是有问题; 比如:
<cache eviction="FIFO" flushInterval="60000" size="2" readOnly="true"/>
这里主要定义了缓存大小 2, 使用 FIFO 策略更新;
- @Test
- public void test04() {
- 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);
- System.out.println("---get user:" + userMapper1.getUser(1L));
- sqlSession1.commit();
- System.out.println("---get user:" + userMapper1.getUser(2L));
- sqlSession1.commit();
- System.out.println("---get user:" + userMapper1.getUser(3L));
- sqlSession1.commit();
- System.out.println("---get user:" + userMapper2.getUser(1L));
- System.out.println("---get user:" + userMapper2.getUser(2L));
- System.out.println("---get user:" + userMapper1.getUser(1L));
- sqlSession2.commit();
- System.out.println("------------");
- System.out.println("---get user:" + userMapper1.getUser(1L));
- System.out.println("---get user:" + userMapper1.getUser(2L));
- System.out.println("---get user:" + userMapper1.getUser(3L));
- }
- }
打印:
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get user: User{
- id=1, user_name='s1', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 2(Long)
- DEBUG [main] - <== Total: 1
- ---get user: User{
- id=2, user_name='s2', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 3(Long)
- DEBUG [main] - <== Total: 1
- ---get user: User{
- id=3, user_name='s3', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get user: User{
- id=1, user_name='s1', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2
- ---get user: User{
- id=2, user_name='s2', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.16666666666666666
- DEBUG [main] - ==> Preparing: select * from user where id = ?
- DEBUG [main] - ==> Parameters: 1(Long)
- DEBUG [main] - <== Total: 1
- ---get user: User{
- id=1, user_name='s1', password='123456', address='TT'
- }
- ------------
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2857142857142857
- ---get user: User{
- id=1, user_name='s1', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.25
- DEBUG [main] - ==> Parameters: 2(Long)
- DEBUG [main] - <== Total: 1
- ---get user: User{
- id=2, user_name='s2', password='123456', address='TT'
- }
- DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
- ---get user: User{
- id=3, user_name='s3', password='123456', address='TT'
- }
从日志中可以看到对于 SqlSession1, 大小 2,FIFO 是生效的, 但是 SqlSession2 提交了之后, 就发现缓存 s1,s2,s3 都命中了;
至于源码太多了就不一次分析了, 对于上面说的使用装饰者模式, 可以在 CacheBuilder 中看到;
- public Cache build() {
- setDefaultImplementations();
- Cache cache = newBaseCacheInstance(implementation, id);
- setCacheProperties(cache);
- // issue #352, do not apply decorators to custom caches
- if (PerpetualCache.class.equals(cache.getClass())) {
- for (Class<? extends Cache> decorator : decorators) {
- cache = newCacheDecoratorInstance(decorator, cache);
- setCacheProperties(cache);
- }
- cache = setStandardDecorators(cache);
- } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
- cache = new LoggingCache(cache);
- }
- return cache;
- }
总结
mybatis 一级缓存的生命周期和 SqlSession 是一样的, 通常情况下不建议使用一级缓存, 通常将一级缓存范围设置为 STATEMENT;
使用 mybatis 二级的时候, 务必记得 SqlSession.commit , 否则二级缓存是不生效的;
在配置 mybatis 分布式二级缓存的时候, 要确保缓存淘汰等策略是可以用于分布式缓存的;
来源: https://www.cnblogs.com/sanzao/p/11414305.html