目录
3.1 一级缓存
3.1.1 一级缓存介绍
一级缓存也叫本地缓存,MyBatis 的一级缓存是默认开启的,不需要任何的配置。
一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问
每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。
一级缓存是为了解决资源浪费的问题,如果在数据库的一次会话中,在极短的时间内反复执行完全相同的查询语句,结果大多数时候是相同的,如果不使用缓存,那么每次查询都需要连接数据库,会造成巨大的资源浪费。所以将查到的数据进行缓存,当下次查询时,经过判断如果有完全一样的查询,会直接将结果从缓存中取出,不需要再进行一次数据库连接。
3.1.2 一级缓存的生命周期
因为一级缓存是SqlSession级别的,所以一级缓存会随着会话的出现而出现,随着会话的关闭而消失。
令sqlSession失效的四种方式
a>)大概过程是:MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
1)其中executor是MyBatis的sql顶层执行器接口,声明了对数据库所有的增删改查操作: query()和queryCursor()方法用于执行查询操作,update()方法用于执行插入、删除、修改操作。
1.1)BaseExecutor是Executor的抽象实现类,采用模板设计模式,它声明的抽象方法由子类SimpleExecutor、ReuseExecutor、BatchExecutor实现。
1.1.1)SimpleExecutor是基础的Executor,能够完成基本的增删改查操作
1.1.2)BatchExecutor则会对调用同一个Mapper执行的update、insert和delete操作,调用Statement对象的批量操作功能。
1.1.3)ResueExecutor对JDBC中的Statement对象做了缓存,当执行相同的SQL语句时,直接从缓存中取出Statement对象进行复用,避免了频繁创建和销毁Statement对象,从而提升系统性能
b>)如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
c>)如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
d>)SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
映射文件:
<cache readOnly="true"/>
<select id="selectAll" resultType="com.lwl.entity.Dept">
select * from dept
</select>
<delete id="deleteDept">
delete from dept where deptno=#{deptno}
</delete>
mapper接口:
public interface DeptMapper {
/**
* 查询所有部门
*/
List<Dept> selectAll();
/**
* 删除一个部门
*/
int deleteDept(Integer deptno);
}
//测试类
SqlSession sqlSession;
DeptMapper mapper;
@Before
public void before(){
sqlSession = SqlSessionFactoryUtil.getSqlSession();
mapper = sqlSession.getMapper(DeptMapper.class);
}
// 当两次查询中没有别的操作时,会通过一次缓存的数据直接取出,不会再次连接数据库。
@org.junit.Test
public void testSelect(){
List<Dept> depts = mapper.selectAll();
System.out.println("depts =++++++++++++++++++++++++++= " + depts);
List<Dept> depts1 = mapper.selectAll();
System.out.println("depts1 =*************************= " + depts1);
}
/*
* 查询一次后,会将查询的数据放在缓存中
* 如果在使用一次缓存后,把sqlSession关闭,在调用时会报这个错误
* Cause: org.apache.ibatis.executor.ExecutorException: Executor was closed.
* 如果再使用,需要重新创建sqlsession会话对象,而且查询时需要再次连接数据库
*/
@org.junit.Test
public void testSelectClose(){
List<Dept> depts = mapper.selectAll();
System.out.println("depts =++++++++++++++++++++++++++= " + depts);
sqlSession.close();
SqlSession sqlSession1 = SqlSessionFactoryUtil.getSqlSession();
DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
List<Dept> depts1 = mapper1.selectAll();
System.out.println("depts1 = " + depts1);
}
/*
* 查询一次后,会将查询的数据放在缓存中
* 如果在使用一次缓存后,把sqlSession刷新,
* 如果再使用查询时,不需要再次连接数据库
*/
@org.junit.Test
public void testSelectClearCache(){
List<Dept> depts = mapper.selectAll();
System.out.println("depts =++++++++++++++++++++++++++= " + depts);
sqlSession.clearCache();
List<Dept> depts1 = mapper.selectAll();
System.out.println("depts1 = " + depts1);
}
/*
* 查询一次后,会将查询的数据放在缓存中
* 当在一次查询后,进行了增删改操作,那么在进行下一次查询时,需要再次连接数据库
*/
@org.junit.Test
public void testSelectCUD(){
List<Dept> depts = mapper.selectAll();
System.out.println("depts =++++++++++++++++++++++++++= " + depts);
int i = mapper.deleteDept(20);
System.out.println("i = " + i);
List<Dept> depts1 = mapper.selectAll();
System.out.println("depts1 = " + depts1);
}
3.1.3 一级缓存的工作流程
1)对于一个查询,会根据statementid,params和rowbounds构建的key值对比Cache中的值。
2)如果Cache中有相等的值,代表了命中,代表是完全重复的查询,将该key值对应的value返回。
3)如果Cache中没有相等的值,代表没有命中,此时会连接数据库,从数据库中查找数据,并将key和查询的数据库信息作为value以键值对的形式存储到Cache中,将查询结果返回。
3.1.4 一级缓存的不足
一级缓存不能跨会话共享,不同的会话之间对于同样的数据可能有不一样的缓存,这样在多个会话或分布式的环境下,会存在脏数据的问题。如果想要解决这个问题就需要用到二级缓存,因为二级缓存可以跨会话。
mybatis中一级缓存(Local Cache)无法关闭。
3.2 二级缓存
二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取 。
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的, 可以被多个SqlSession共享(只要是同一个接口里面的相同方法,都可以共享)
3.2.1 二级缓存的配置
1)在核心配置文件<settings>中,设置全局配置属性cacheEnabled=“true”,这个属性默认值为true,所以可以不用配置
2)在映射文件中设置标签<cache/>
4)查询的数据所转换的实体类类型必须实现序列化的接口。
2)
<!-- 开启二级缓存-->
<cache/>
<select id="selectAll" resultType="com.lwl.entity.Dept">
select * from dept
</select>
<delete id="deleteDept">
delete from dept where deptno=#{deptno}
</delete>
SqlSession sqlSession;
DeptMapper mapper;
@Before
public void before(){
sqlSession = SqlSessionFactoryUtil.getSqlSession();
mapper = sqlSession.getMapper(DeptMapper.class);
}
@After
public void after(){
sqlSession.commit();
sqlSession.close();
}
/*
* 实体类未实现序列化接口时,会出现的错误
* Cause: java.io.NotSerializableException: com.lwl.entity.Dept
*
* SqlSession关闭之后,一级缓存中的数据会写入二级缓存,所以这里只能用close方法,而不能用刷新缓存的方法
* */
@Test
public void testCacheTwo(){
// 第一次查询,二级缓存中没有数据,所以查不到,一共查询一次,命中率为0/1=0.0
List<Dept> depts = mapper.selectAll();
System.out.println("depts = " + depts);
sqlSession.close();
// 创建了一个新的会话,不是原来的会话,所以这里是跨会话查询
sqlSession = SqlSessionFactoryUtil.getSqlSession();
mapper = sqlSession.getMapper(DeptMapper.class);
// 第二次查询,二级缓存中有数据,查不到,一共查询两次,命中率为1/2=0.5
List<Dept> depts1 = mapper.selectAll();
System.out.println("depts1 = " + depts1);
//第三次查询相同的数据,则命中率为0.66666
}
4)
@Data
public class Dept implements Serializable {
private Integer deptno;
private String dname;
private String loc;
}
在mapper配置文件中添加的cache标签可以设置一些属性:
1)eviction属性:缓存回收策略
1.1)LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。
1.2)FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。
1.3)SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
1.4)WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认的是 LRU。
2)flushInterval属性:刷新间隔,单位毫秒,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
3)size属性:引用数目,正整数代表缓存最多可以存储多少个对象,太大容易导致内存溢出
4)readOnly属性:只读,true/false
4.1)true:只读缓存;会给所有调用者返回缓存对象的相同实例(弱拷贝:共用一份缓存文件)。因此这些对象不能被修改。这提供了很重要的性能优势。
4.2)false:读写缓存;会返回缓存对象的拷贝(强拷贝:再复制一份缓存文件通过序列化)。这会慢一些,但是安全,因此默认是false。
mapper映射文件:
<!-- 开启二级缓存-->
<cache readOnly="true"/>
<select id="selectAll" resultType="com.lwl.entity.Dept">
select * from dept
</select>
测试类:
@Test
public void testResdonly(){
SqlSession sqlSession1 = SqlSessionFactoryUtil.getSqlSession();
SqlSession sqlSession2 = SqlSessionFactoryUtil.getSqlSession();
DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
DeptMapper mapper2 = sqlSession2.getMapper(DeptMapper.class);
// 第一次查询
List<Dept> depts = mapper1.selectAll();
sqlSession1.close();//关闭后,数据存到了二级缓存里面
// 第二次查询
List<Dept> depts1 = mapper2.selectAll();
// 将二级缓存中的数据取出进行修改
Dept dept = depts1.get(0);
dept.setDname("只读测试");
System.out.println("depts = " + depts);
System.out.println("depts1 = " + depts1);
// 当开启二级缓存中的只读属性时,相当于是查询出来的缓存只有一份(弱拷贝),第二个会话修改了缓存中的数据之后,第一个会话结果打印出来也是被修改过的
//Readonly=true:depts = [Dept(deptno=1, dname=只读测试, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
//Readonly=true:depts1 = [Dept(deptno=1, dname=只读测试, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
// 当不开启二级缓存的只读属性时,相当于是第二个会话将缓存复制了一份(强拷贝),即使是第二个会话修改了缓存中的数据,也不会更改第一个会话缓存中的数据
//Readonly=false:depts = [Dept(deptno=1, dname=学术部, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
//Readonly=false:depts1 = [Dept(deptno=1, dname=只读测试, loc=郑州), Dept(deptno=2, dname=行政部, loc=郑州), Dept(deptno=3, dname=马上国庆, loc=郑州)]
}
3.2.2 二级缓存的生命周期
二级缓存的生命周期和应用同步
令二级缓存失效的情况:
两次查询之间执行任意的增删改,会使二级缓存失效,同时也会使一级缓存失效。
3.2.3 二级缓存的相关问题
1)查询时二级缓存应该在一级缓存之前还是一级缓存之后?
作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession内部的,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。
2)二级缓存放在哪个对象中维护呢?
要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。
MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。
CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
3.2.4 二级缓存的不足
缓存是以namespace为单位的,不同namespace下的操作互不影响。
insert,update,delete操作会清空所在namespace下的全部缓存。
通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。
1) 针对一个表的某些操作不在他独立的namespace下进行,会导致数据不一致。
2)多表操作
这些都是不能使用二级缓存来实现的
3.3 MyBatis的查询顺序
先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。
如果二级缓存没有命中,再查询一级缓存
如果一级缓存也没有命中,则查询数据库
3.4 Cache使用的注意事项
1) 只能在【只有单表操作】的表上使用缓存 ,而且要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须在一个namespace下。
2) 在可以保证查询操作数远远大于insert,update,delete操作的情况下使用缓存