MyBatis的缓存机制

MyBatis中的缓存

一级缓存(SqlSession级别的/本地缓存)

MyBatis默认情况下只开启一级缓存,一级缓存只是相对于同一个SqlSession而言,一级缓存是一直开启的,我们没办法关闭

所在在查询的参数和SQL语句不变的情况下,使用同一个SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL,在第一次进行查询之后,会将查询的结果存放到缓存当中,以后如果再次查询,如果没有声明需要刷新,并且缓存也没有超时的情况下,SqlSession会取出当前缓存的结果,而不是再次发送SQL到数据库。但是如果是不同的SqlSession对象,他们之间是相互隔离的,所以对于不同的SqlSession对象执行相同的SQL,还是会向数据库发起缓存。当SqlSession对象消失时,其对应的一级缓存同样消失。

一级缓存失效的4种情况:(没有使用到当前一级缓存的4种情况)

效果就是,还需要再向数据库发出查询语句

第一种,sqlSession不同,不能共享其一级缓存:

@Test//sqlSession不同
    public void test01() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //开启两个不同的sqlSession会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession2 =sqlSessionFactory.openSession();

        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        EmpMapper mapper1 = sqlSession2.getMapper(EmpMapper.class);
        Emp empById = mapper.getEmpById(1);
        Emp empById1 = mapper1.getEmpById(1);
        System.out.println(empById + "---" + empById1);
        System.out.println(empById == empById1);
    }

在这里插入图片描述

第二种,sqlSession相同,查询条件不同,不能使用一级缓存(当前一级缓存中还没有这个数据):

@Test//sqlSession相同,查询条件不同
    public void test02() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //开启两个不同的sqlSession会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        Emp emp1 = mapper.getEmpById(1);
        System.out.println(emp1);
        //进行不同员工的查询
        Emp emp2 = mapper.getEmpById(2);//观察发送sql语句的条数和次数
        System.out.println(emp2);
    }

在这里插入图片描述
第三种,sqlSession相同,两次查询之间执行了怎删改操作,会导致一级缓存被清空:
增删改可能对当前数据有影响

比如我们在两次查询之间,删除了刚才的那条数据,那就不能让其从一级缓存中拿取已经不存在的数据。

@Test//sqlSession相同,两次相同的查询之间有增删改的操作
    public void test03() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //开启两个不同的sqlSession会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        Emp emp1 = mapper.getEmpById(1);
        System.out.println(emp1);
        //执行增删改的任何一个操作
        mapper.addEmp(new Emp(null,"zhangfen","ds","0",null));
        //mapper.deletEmpById(1);//删除的方法
        Emp emp2 = mapper.getEmpById(1);
        System.out.println(emp2);
    }

在这里插入图片描述
第四种,sqlSession相同,手动清除了一级缓存,则不能使用一级缓存:(清空不等于关闭)
在这里插入图片描述

总结:sqlSession的一级缓存本质上是一个Map,将查出的数据放到map中,有新的数据就放进map,如果是旧的数据被查询(确保还是当前会话),直接从map中拿。

一级缓存的不足:

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:

  1. session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
  2. statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存。

二级缓存(全局缓存/namespace级别)

二级缓存的介绍:

一个namespace对应一个二级缓存,相当于一个xml/mapper相当于一个二级缓存。

  • 二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
  • 作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。
  • 实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

二级缓存工作机制:(开启二级缓存的情况下:)

  1. 一个会话,查询一条记录,这个数据就会被放到当前会话的一级缓存中;
  2. 如果会话关闭,一级缓存中的数据才会被保存到二级缓存中;新的会话查询信息,就可以参照二级缓存内容。
  3. 不同的namespace查出的数据会被放在自己对应的二级缓存中(即自己的map中)。
  4. 查出的数据都会默认先放在一级缓存中,只有当会话关闭或者提交后,一级缓存中的数据才会被转移到二级缓存中。

开启二级缓存的方法:

第一步:开启全局二级缓存(setting中开启cacheEnabled为true)
在这里插入图片描述
第二步:去xxxMapper.xml中配置使用二级缓存(cache标签):
要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行

<cache/>

在这里插入图片描述
第三步:我们的POJO需要实现序列化接口
public class Emp implements Serializable {}

然后测试:

@Test//sqlSession不同
    public void test01() throws IOException {
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        //开启两个不同的sqlSession会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        SqlSession sqlSession2 =sqlSessionFactory.openSession();

        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        EmpMapper mapper2 = sqlSession2.getMapper(EmpMapper.class);

        Emp empById = mapper.getEmpById(1);
        sqlSession.close();//关闭第一个会话
	
		//第二次查询是从二级缓存中拿到的数据,并没有发送新的sql
        Emp empById2 = mapper2.getEmpById(1);
        sqlSession2.close();//关闭第一个会话
        
        System.out.println(empById + "---" + empById2);
        System.out.println(empById == empById2);
    }

测试结果:
在这里插入图片描述

二级缓存cache 的其他参数的详细配置:

这些属性可以通过 cache 元素的属性来修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

eviction(缓存清除/回收策略),有:

  1. LRU – 最近最少使用:移除最长时间不被使用的对象。
  2. FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  3. SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  4. WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是一直不清空

size(缓存存放多少元素)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024

readOnly(只读)属性可以被设置为 true 或 false。true:只读,只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升,但是不安全;false:非只读,而非只读的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false

type:指定自定义缓存的全类名;自定义缓存实现Cache接口即可;

使用自定义缓存

除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache"/>

如何使用一个自定义的缓存实现。type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。

useCache配置每条sql语句的二级缓存,可以覆盖掉全局二级缓存配置:
请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外.
如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

注意:1.每个增删改的默认flushCache=“true”:所以增删改执行完毕后都会清除一级二级缓存
2.每一个查询的标签的默认flushCache=“false”
3.sqlSession.clearCache();只会清除当前会话的一级缓存
4.全局配置中的localCacheScope(本地/一级缓存作用域):MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行一级缓存

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

总结:全局的二级缓存就好像我们的家里的电器总闸,当我们开启总闸后默认每个电器使用总闸上的电(使用二级缓存/关闭二级缓存),而每条sql标签上的useCache也是控制二级缓存是否使用(类似于家电的启动开关),当我们显式的把我们家电的开关设置为false(默认全局开启二级缓存后,每条sql都默认使用二级缓存),则相当于本条sql不使用二级缓存(一级缓存一直开启),使用一级缓存。

缓存原理图:

在这里插入图片描述

什么时候开启二级缓存?

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?

因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
如果多个namespace 中有针对于同一个表的操作,比如Blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用。

Mybatis整合第三方缓存ehcache

第三方缓存主要是来壮大Mybatis的二级缓存。

第一步:下载相关jar包和需要的ehcache.xml

1.用maven导入ehcache-core:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.8</version>
</dependency>

2.下载mybatis和ehcache的适配包
或者maven导入mybatis-ehcache:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.0.3</version>
</dependency>

3.还需要一个ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
 <!-- 磁盘保存路径 -->
 <diskStore path="D:\cache\ehcache" />
 
 <defaultCache 
   maxElementsInMemory="1" 
   maxElementsOnDisk="10000000"
   eternal="false" 
   overflowToDisk="true" 
   timeToIdleSeconds="120"
   timeToLiveSeconds="120" 
   diskExpiryThreadIntervalSeconds="120"
   memoryStoreEvictionPolicy="LRU">
 </defaultCache>
</ehcache>
 
<!-- 
属性说明:
l diskStore:当内存中不够存储时,存储到指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
 
以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目 
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
 
以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
 diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
 -->

第二步:整合mybatis和ehcache

1.将需要的maven导入后,将ehcache.xml放到resources类路径下
2.然后在我们的xxxMapper.xml中配置使用二级缓存(二级缓存类型是EhcacheCache):

<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

测试:

@Test
public void testSecondLevelCache() throws IOException{
	SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
	SqlSession openSession = sqlSessionFactory.openSession();
	SqlSession openSession2 = sqlSessionFactory.openSession();
	try{
 
		EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
		EmployeeMapper mapper2 = openSession2.getMapper(EmployeeMapper.class);
		
		Employee emp01 = mapper.getEmpById(1);
		System.out.println(emp01);
		openSession.close();
		
		//第二次查询是从二级缓存中拿到的数据,并没有发送新的sql
		Employee emp02 = mapper2.getEmpById(1);
		System.out.println(emp02);
		openSession2.close();
		
	}finally{
		
	}
}

整合第三方的原理图:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值