当前位置:   article > 正文

MyBatis基础应用 (四)——一篇文章从底层彻底弄明白MyBatis一级缓存和二级缓存_判断mybatis 是走二级缓存还是查询的数据库

判断mybatis 是走二级缓存还是查询的数据库

1.MyBatis缓存(面试题)

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

为什么使用缓存?

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

缓存的作用:减少数据库查询次数,减轻数据库查询压力,提高查询的速度!

缓存的缺点:以空间换时间技术,需要额外内存开销。如果数据库中的数据和缓存中的数据不同步,那么可能出现脏数据,这个问题缓存的一致性问题!

浏览器缓存:缓存静态资源,减轻Web服务器访问压力!

缓存中有,先查询缓存。缓存中没有,那么查询数据库。这样的话不用每次都查询数据库。减轻数据库的压力。提高查询效率!!!

第一次查询的时候,由于缓存中没有,那么去查询数据库返回给客户端。同时还会把这个次查询的数据放入缓存。

第二次查询同样的数据时候,发现缓存中曾经有查询过的数据,那么直接从缓存中读取。不必再次查询数据库,减轻数据库服务器压力,缓存中有就查缓存,缓存中没有就查数据库!

如果数据库中数据发生了修改,那么缓存就会清空,保持数据缓存的一致性!防止脏数据!

1.1 MyBatis缓存分析

mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。

 一级缓存SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

二级缓存mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession

2 .一级缓存

一级缓存指的就是在sqlsession中有一个map结构的数据区域,这个区域就是一级缓存区域。

一级缓存中的key,是由sql语句、条件、statement等信息组成一个唯一值

一级缓存中的value,就是查询出的结果对象。

一级缓存是session级别的,同一个session 1级缓存是系统自带,不需要手动开启!

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

  • 相同的 sql 和 参数

  • 必须是在一个会话 Session当中

  • 必须是执行 相同的方法

  • 必须是相同的 namespace(同一个命名空间 -> 同一个mapper文件)

  • 不能够在查询之前执行 clearCache

  • 中间不能执行 任何 updatedelete ,insert (会将SqlSession中的数据全部清空)

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

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

  1. /**
  2. * 测试1级缓存
  3. */
  4. public class TestCache1 {
  5. @Test
  6. public void test1() throws Exception{
  7. try {
  8. String resources = "mybatis-config.xml";
  9. InputStream inputStream = Resources.getResourceAsStream(resources);
  10. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  11. SqlSession sqlSession = sqlSessionFactory.openSession(true);
  12. UserMapper um = sqlSession.getMapper(UserMapper.class);
  13. User user1 = um.findById(1);
  14. User user2 = um.findById(1);
  15. System.out.println(user1==user2);
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. } finally {
  19. }
  20. }
  21. }

运行结果: 在这里插入图片描述 用两张图来总结: 第一次:查数据库,放入到缓存中。 在这里插入图片描述 第二次:直接从缓存中获取。 在这里插入图片描述 下面这段代码中就使用不到缓存

  1. @Test
  2. public void test2() throws Exception{
  3. try {
  4. String resources = "mybatis-config.xml";
  5. InputStream inputStream = Resources.getResourceAsStream(resources);
  6. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  7. SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
  8. SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
  9. UserMapper um1 = sqlSession1.getMapper(UserMapper.class);
  10. UserMapper um2 = sqlSession2.getMapper(UserMapper.class);
  11. User user1 = um1.findById(1);
  12. User user2 = um2.findById(1);
  13. System.out.println(user1==user2);
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. } finally {
  17. }
  18. }

运行结果: 在这里插入图片描述

用两张图来总结:

第一次查询:sqlSession1查询数据库,放入到缓存中。 在这里插入图片描述 第二次查询:sqlSession2查询数据库,放入到缓存中。 在这里插入图片描述 记住是一级缓存只能是同一个SqlSession对象就行了。

2.3. MyBatis关闭一级缓存

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

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

2.4. 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方法 在这里插入图片描述在这里插入图片描述

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

在这里插入图片描述

 DefaultSqlSession中有一个CacheExecutor

CacheExecutor 中有一个 Simpleexexutor

Simpleexexutor 中有一个叫 LocalCache (PerpetualCache类型)

LocalCache才是真正的存储缓存的地方

LocalCache 中有一个叫cache (Hashmap <Object,Object>类型的)

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

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

  1. @Override
  2. public void clearLocalCache() {
  3.    if (!closed) {
  4.      localCache.clear();
  5.      localOutputParameterCache.clear();
  6.   }
  7. }

update时,一级缓存会被清空。delete和insert都是调用这个update。可以从SqlSession的insert、update、delete方法跟踪。 在这里插入图片描述 LocalCacheScope.STATEMENT时,一级缓存会被清空。在BaseExecutor里的query方法中: 在这里插入图片描述 事务提交回滚时,一级缓存会被清空。 在这里插入图片描述 flushCache="true"时,一级缓存会被清空。 在这里插入图片描述

2.7. 一级缓存key是什么?

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

  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 cacheKey = new CacheKey();
  7.  cacheKey.update(ms.getId());
  8.  cacheKey.update(rowBounds.getOffset());
  9.  cacheKey.update(rowBounds.getLimit());
  10.  cacheKey.update(boundSql.getSql());
  11.  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  12.  TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
  13.  // mimic DefaultParameterHandler logic
  14.  for (ParameterMapping parameterMapping : parameterMappings) {
  15.    if (parameterMapping.getMode() != ParameterMode.OUT) {
  16.      Object value;
  17.      String propertyName = parameterMapping.getProperty();
  18.      if (boundSql.hasAdditionalParameter(propertyName)) {
  19.        value = boundSql.getAdditionalParameter(propertyName);
  20.     } else if (parameterObject == null) {
  21.        value = null;
  22.     } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
  23.        value = parameterObject;
  24.     } else {
  25.        MetaObject metaObject = configuration.newMetaObject(parameterObject);
  26.        value = metaObject.getValue(propertyName);
  27.     }
  28.      cacheKey.update(value);
  29.   }
  30. }
  31.  if (configuration.getEnvironment() != null) {
  32.    // issue #176
  33.    cacheKey.update(configuration.getEnvironment().getId());
  34. }
  35.  return cacheKey;
  36. }

在这里插入图片描述 key的生成策略:id + offset + namespace+ sql + param value + environment id,这些值都相同,生成的key就相同。

2.8. 一级缓存总结

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

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

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

  1. 执行updateinsertdeleterollback这些方法
  2. --------------------------------------
  3. 执行sqlSession.clearCache();
  4. --------------------------------------
  5. 在mybatis的配置文件中的setting里面 设置LocalCacheScope属性为STATEMENT

3、二级缓存

二级缓存指的就是同一个namespace下的mapper,二级缓存中,也有一个map结构,这个区域就是二级缓存区域。二级缓存中的key是由sql语句、条件、statement等信息组成一个唯一值。二级缓存中的value,就是查询出的结果对象。

二级缓存,可以跨session!二级缓存是要配置,然后手动开启!

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库

二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是可以横跨跨SqlSession的。

3.1 二级缓存的相关底层

作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。

实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。 在这里插入图片描述

3.2.二级缓存使用

二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,可以理解为二级缓存区域是根据mapper划分,也就是根据命名空间来划分的,如果两个mapper文件的命名空间一样,那样,不同的SqlSession之间就可以共享一个mapper缓存。

在这里插入图片描述

3.2.1.步骤一:主配置文件中开启二级缓存总开关

在 mybatis 中,二级缓存有全局开关和分开关, 全局开关, 在 mybatis-config.xml 中如下配置:

  1. <settings>
  2.  <!-- cacheEnabled是二是级缓存的总开关,置为false代表关闭二级缓存 -->
  3.  <setting name="cacheEnabled" value="true"/>
  4. </settings>

默认是为 true, 即默认开启总开关。

3.2.2.步骤二:映射文件中开启二级缓存分开关

由于mybaits的二级缓存是mapper范围级别,所以除了在 mybatis-config.xml设置二级缓存的总开关外,还要在具体的mapper.xml中开启二级缓存。

设置cache标签,同时在查询标签里面设置useCache的属性(通产来说useCache的默认值为true,但是可以通过设置为false来操作某条语句不支持缓存)

但是二级缓存还是有缺点, 如果当前操作是多表查询,查询之后结果也会缓存,但是如果是多线程场景下,另外一个线程将多表中的某一张表做了更新? 二级缓存数据不会过更行,会引发脏读问题。解决办法:在映射文件中引入

<cache-ref namespace="com.it.mapper.EmployeeMapper"/>标签

该标签的作用是共享namespace值得mappper文件

mappper.xml设置如下:

  1. <mapper namespace="com.it.mapper.EmployeeMapper">
  2. <cache-ref namespace="com.it.mapper.EmployeeMapper"/>
  3. <cache type="org.apache.ibatis.cache.impl.PerpetualCache" eviction="LRU" flushInterval="40000" size="1024" readOnly="false" blocking="false"/>
  4. <select id="finEmployeeById" resultMap="empMap" useCache="true">
  5. select * from tb_employee where id=#{id}
  6. </select>
  7. </mapper>

 缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:

  1. <select ... flushCache="false" useCache="true"/>
  2. <insert ... flushCache="true"/>
  3. <update ... flushCache="true"/>
  4. <delete ... flushCache="true"/>

鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

3.2.3.二级中实体类需要实现序列化接口

要用到二级缓存的每一个实体类都需要实现Serializable接口

  1. public class Employee implements Serializable {
  2. private static final long serialVersionUID = -5150219292948138785L;
  3. //标识主键自增
  4. @TableId(value = "ID",type = IdType.AUTO)
  5. private Integer id;
  6. }

3.2.4.sqlSession关闭

 这里要记住一个地方,第二次查询必须要等第一次的session关闭以后才可以。否则二级缓存没有作用,依然还会执行二次select语句发送至数据库。

  1. @Test
  2. public void testCache2() throws Exception {
  3. String resource = "mybatis-config.xml";
  4. Reader reader = Resources.getResourceAsReader(resource);
  5. SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
  6. SqlSession sqlSession1 = sqlSessionFactory.openSession();
  7. EmployeeMapper employeeMapper1 = sqlSession1.getMapper(EmployeeMapper.class);
  8. Employee employee1 = employeeMapper1.finEmployeeById(2);
  9. Employee employee2 = employeeMapper1.finEmployeeById(2);
  10. System.out.println(employee1);
  11. // 这里要记住一个地方,第二次查询必须要等第一次的session关闭以后才可以。否则二级缓存没有作用,依然还会执行二次select语句发送至数据库。
  12. sqlSession1.close();
  13. System.out.println(employee1==employee2);
  14. SqlSession sqlSession2 = sqlSessionFactory.openSession();
  15. EmployeeMapper employeeMapper2 = sqlSession2.getMapper(EmployeeMapper.class);
  16. Employee employee3 = employeeMapper2.finEmployeeById(2);
  17. System.out.println(employee3);
  18. sqlSession2.close();
  19. System.out.println(employee1==employee3);
  20. }

test的运行结果。

 以上结果, 分几个过程解释:

第一阶段:

  • 在第一个 SqlSession 中, 查询出 employee对象, 此时发送了 SQL 语句;

  • SqlSession 再次查询出 employee对象, 此时不发送 SQL 语句, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是因为此时sqlsession1没有关闭,二级缓存没有起作用,所以没有命中。二级缓存没有起作用,所以一级缓存起作用,

  • 由于是一级缓存, 因此, 此时两个对象是相同的。

  • 调用了 sqlSession.close(), 此时将数据序列化并保持到二级缓存中。

第二阶段:

  • 新创建一个 sqlSession.close() 对象;

  • 查询出 user对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时查了 3 个对象,但只有一个命中, 因此命中率 1/3=0.333333;

3.3.cache有一些可选的属性

3.3.1.type

type 用于指定缓存的实现类型, 默认是PERPETUAL, 对应的是 mybatis 本身的缓存实现类org.apache.ibatis.cache.impl.PerpetualCache

3.3.2.eviction

eviction 对应的是回收策略, 默认为 LRU

  • LRU: 最近最少使用, 移除最长时间不被使用的对象。

  • FIFO: 先进先出, 按对象进入缓存的顺序来移除对象。

  • SOFT: 软引用, 移除基于垃圾回收器状态和软引用规则的对象。

  • WEAK: 弱引用, 移除基于垃圾回收器状态和弱引用规则的对象

3.3.3.flushInterval

flushInterval 对应刷新间隔, 单位毫秒, 默认值不设置, 即没有刷新间隔, 缓存仅仅在刷新语句时刷新。

如果设定了之后, 到了对应时间会过期, 再次查询需要从数据库中取数据。

3.3.4.size

size对应为引用的数量,即最多的缓存对象数据, 默认为 1024

3.3.5 readOnly

readOnly 为只读属性, 默认为 false

  • false: 可读写, 在创建对象时, 会通过反序列化得到缓存对象的拷贝。 因此在速度上会相对慢一点, 但重在安全。

  • true: 只读, 只读的缓存会给所有调用者返回缓存对象的相同实例。 因此性能很好, 但如果修改了对象, 有可能会导致程序出问题。

3.3.6 blocking

blocking 为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache 进行封装。

使用 BlockingCache 会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。 在这里插入图片描述 

3.4.MyBatis的缓存机制整体设计以及二级缓存的工作模式

在这里插入图片描述 如上图所示,当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。 在这里插入图片描述CachingExecutorExecutor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,

CachingExecutorExecutor的接口的关系如下类图所示: 在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/159877
推荐阅读
相关标签
  

闽ICP备14008679号