MyBatis缓存专题-一文彻底搞懂MyBatis一级缓存

Posted IT老刘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis缓存专题-一文彻底搞懂MyBatis一级缓存相关的知识,希望对你有一定的参考价值。

带着问题来学习?

1.缓存的概念

1.1.什么是缓存

存在于内存中的临时数据。

1.2.为什么使用缓存

减少和数据库的交互次数,提高执行效率。(因为查询数据库是一件很费时很费效率的事,还涉及一些硬盘等io操作,而缓存是存在内存中的,读取都很快,而且效率高)

1.3.什么样的数据能使用缓存,什么样的数据不能使用

  • 适用于缓存:
经常查询并且不经常改变的。
数据的正确与否对最终结果影响不大的。
  • 不适用于缓存:
经常改变的数据
数据的正确与否对最终结果影响很大的。
例如:商品的库存,银行的汇率,股市的牌价。

2.什么是一级缓存

MyBatis 包含了一个非常强大的查询缓存特性,它可以非常方便地配置和定制。MyBatis 3 中的缓存实现的很多改进都已经实现了,使得它更加强大而且易于配置。mybatis 默认情况下只会开启一级缓存,也就是局部的 session 会话缓存

首先我们要知道什么是查询缓存?查询缓存又有什么作用?

功能:mybatis提供查询缓存,用于减轻数据压力,提高数据库性能。

如下图,每一个 session 会话都会有各自的缓存,这缓存是局部的,也就是所谓的一级缓存

3.什么情况下会命中一级缓存

  • 相同的 sql 和 参数
  • 必须是在一个会话 Session当中
  • 必须是执行 相同的方法
  • 必须是相同的 namespace (同一个命名空间 -> 同一个mapper文件)
  • 不能够在查询之前执行 clearCache
  • 中间不能执行 任何 updatedelete ,insert (会将SqlSession中的数据全部清空)

4.Mybatis的一级缓存机制详解

一级缓存是SqlSession级别的缓存。我们都知道在操作数据库时需要构造 sqlSession对象,而在sqlSession对象中有一个数据结构(HashMap)用于存储缓存数据。

如下图:

从图上,我们可以看出,一级缓存区域是根据SqlSession为单位划分的。每次查询都会先从缓存区域找,如果找不到就会从数据库查询数据,然后将查询到的数据写入一级缓存中。Mybatis内部存储缓存使用的是一个HashMap对象,key为 hashCode + sqlId + sql 语句。而value值就是从查询出来映射生成的java对象。而为了保证缓存里面的数据肯定是准确数据避免脏读,每次我们进行数据修改后(增、删、改操作)就会执行commit操作,清空缓存区域。

/**
 * 测试1级缓存
 */
public class TestCache1 {

    @Test
    public void test1() throws Exception{
        try {
            String resources = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resources);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            SqlSession sqlSession = sqlSessionFactory.openSession(true);
            UserMapper um = sqlSession.getMapper(UserMapper.class);
            User user1 = um.findById(1);
            User user2 = um.findById(1);
            System.out.println(user1==user2);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
    }
}

运行结果:

用两张图来总结:
第一次:查数据库,放入到缓存中。

第二次:直接从缓存中获取。

下面这段代码中就使用不到缓存

    @Test
    public void test2() throws Exception{
        try {
            String resources = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resources);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

            SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
            SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

            UserMapper um1 = sqlSession1.getMapper(UserMapper.class);
            UserMapper um2 = sqlSession2.getMapper(UserMapper.class);

            User user1 = um1.findById(1);
            User user2 = um2.findById(1);
            
            System.out.println(user1==user2);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
    }

运行结果:

用两张图来总结:
第一次查询:sqlSession1查询数据库,放入到缓存中。

第二次查询:sqlSession2查询数据库,放入到缓存中。

记住是一级缓存只能是同一个SqlSession对象就行了。

5.MyBatis关闭一级缓存

一级缓存也叫本地缓存(LocalCache),Mybatis的一级缓存是会话级别(SqlSession)层面进行缓存的。Mybatis的一级缓存是默认开启的。我们开发项目中不需要做任何配置,但是如果想关闭一级缓存,可以使用localCacheScopde=statement来关闭。

<settings>
    <!-- localCacheScope是本地缓存(一级缓存)的作用域,只有两种取值:SESSION和STATEMENT,取STATEMENT意味着关闭一级缓存-->
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

6.Mybatis的一级缓存机制源码分析

SqlSession和Executor都是接口 ,而实现是 DefaultSqlSession 和 CacheExecutor

  • Client相当于测试方法 Test1
  • User@Proxy 动态代理
  • Executor 接口 才是去数据库中拿数据的跑腿的
  • SqlSession 是接口,一级缓存的实现是通过 CacheExecutor 实现的

程序入口处断点

执行MapperProxy动态代理invoke

MapperMethod调用sqlsession的selectOne方法

本质调用DefaultSqlSession的selectList方法


调用BaseExecutor的query方法

BaseExecutor的query方法生成缓存的key

可以看出缓存key中是包含了 方法和namespace和会话 这些必须相同才会去做一个缓存命中
这里面封装了缓存唯一的key

继续执行BaseExecutor的query方法判断缓存是否存在

如果缓存中不存在,执行查询数据库

缓存中查询到数据存入缓存中


如果缓存中存在,执行查询缓存,PerpetualCache调用getObject方法

7.Mybatis的一级缓存机制源码分析图解总结

DefaultSqlSession中有一个CacheExecutor
CacheExecutor 中有一个 Simpleexexutor
Simpleexexutor 中有一个叫 LocalCache (PerpetualCache类型)
LocalCache才是真正的存储缓存的地方
LocalCache 中有一个叫cache (Hashmap <Object,Object>类型的)

8.一级缓存什么时候被清空?

在执行update、insert、delete、flushCache=“true”、commit、rollback、LocalCacheScope.STATEMENT等情况下,一级缓存就都会被清空。

@Override
public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
}

update时,一级缓存会被清空。delete和insert都是调用这个update。可以从SqlSession的insert、update、delete方法跟踪。

LocalCacheScope.STATEMENT时,一级缓存会被清空。在BaseExecutor里的query方法中:

事务提交回滚时,一级缓存会被清空。

flushCache="true"时,一级缓存会被清空。

9.一级缓存key是什么?

下面就是一级缓存key的创建过程

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  CacheKey cacheKey = new CacheKey();
  cacheKey.update(ms.getId());
  cacheKey.update(rowBounds.getOffset());
  cacheKey.update(rowBounds.getLimit());
  cacheKey.update(boundSql.getSql());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  // mimic DefaultParameterHandler logic
  for (ParameterMapping parameterMapping : parameterMappings) {
    if (parameterMapping.getMode() != ParameterMode.OUT) {
      Object value;
      String propertyName = parameterMapping.getProperty();
      if (boundSql.hasAdditionalParameter(propertyName)) {
        value = boundSql.getAdditionalParameter(propertyName);
      } else if (parameterObject == null) {
        value = null;
      } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
        value = parameterObject;
      } else {
        MetaObject metaObject = configuration.newMetaObject(parameterObject);
        value = metaObject.getValue(propertyName);
      }
      cacheKey.update(value);
    }
  }
  if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
  }
  return cacheKey;
}


key的生成策略:id + offset + limit + sql + param value + environment id,这些值都相同,生成的key就相同。

10.是否会存在数据不一致问题?

不会 因为数据库本身是具有事务隔离级别的 默认是可重复读 也就是说任何一个事务在它内部读到的数据都是一致的,所以并不存在数据不一致的问题

11.一级缓存总结

  • 一级缓存的生命周期和SqlSession对象的生命周期一致。所以缓存维护在SqlSession中的属性executor里。

  • 一级缓存默认开启。可以通过修改配置项把一级缓存关掉。

  • 清空一级缓存的方式有:

update、insert、delete
flushCache="true"
commit、rollback
LocalCacheScope.STATEMENT

以上是关于MyBatis缓存专题-一文彻底搞懂MyBatis一级缓存的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis缓存专题-一文彻底搞懂MyBatis二级缓存

带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能

带你彻底搞懂MyBatis的底层实现之缓存模块(Cache)-吊打面试官必备技能

手写Mybatis,彻底搞懂框架原理

两张图彻底搞懂MyBatis的Mapper原理!

一文搞懂Mybatis架构与工作原理