1.Mybatis的缓存
缓存是一般的ORM框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate一样,Mybatis也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
特点:缓存数据不可靠(可能会丢失),存放热点数据(经常使用的数据)
优点:缓存大部分存在内存中,查询速度快
- 一级缓存基于SqlSession,
- 二级缓存基于namespace,同一个namespace下所有数据可以共享
在https://blog.csdn.net/zitian246/article/details/109086027这篇文章中介绍配置文件时曾说明namespace与二级缓存相关
一个mapper.xml用自己的namespace,另外也可以多个mapper(dao接口)共享同一个二级缓存namespace,即namespace取名可以相同
一级缓存与二级缓存调用顺序:
获取数据先去二级缓存获取,如果有得到,写入(更新)一级缓存
若没有得到,再去一级缓存,如果仍没有,再去数据库
优先级: 二级缓存--->一级缓存--->数据库
2.一级缓存
一级缓存:基于SqlSession,默认就是开启的。
特点:
- 查询第一次时,获取到数据写入一级缓存,再次查询时从缓存获取,不再执行sql语句
- 若当前SqlSession发生修改、增加、删除动作时,就会立即把当前缓存的所有数据清空
- 对于查询操作,只要SqlSession没有调用flush或者close方法,它就一直存在
@Test //一级缓存
public void firstLevelCacheTest(){
//查询所有时 可以缓存数据 但是一旦发生增加 或者删除 修改 就要清空
iStudentDao.findAllStudentWithScoreLazy();
//查询第一次 有执行sql select * from student_tb where id = ?
Student student1 = iStudentDao.findStudentById(2);
System.out.println("student:" + student1);
//查询第二次 没有执行sql 没有去数据库查数据 从一级缓存sqlSession获取数据
Student student2 = iStudentDao.findStudentById(2);
System.out.println("student:" + student2);
student2.setAge(22);
int num = iStudentDao.upateStudent(student2);
System.out.println("num:"+num);
//再次查询数据 需要去数据库查数据 为什么??
//应为 当前sqlSession 发生 修改、删除、增加动作时,就会把当前缓存的所有数据清空
Student student3 = iStudentDao.findStudentById(2);
System.out.println("student3:"+student3);
iStudentDao.findAllStudentWithScoreLazy();
}
3.二级缓存
二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。(也可以说是,在同一namespace下,共享一块缓存空间,如果多个mapper (dao.xml)共享同一namesapce 则也共享一块缓存,二级缓存是跨sqlsession,多个sqlsession可以去二级缓存获取数据。即可以针对同一个dao接口或者同一个命名空间(namespace)创建多个SqlSession )
特点:
- 只要发生增删改,就会将·同一命名空间(namespace)下的缓存清空
- 使用二级缓存实体类必须实现序列化,否则报错
- 使用查询语句,默认只写入一级缓存,只有调用close(),commit()方法,才会将数据提交到二级缓存,其他的sqlsession才能拿到,不再执行sql语句
3.1实现
1.开启二级缓存(mybatis配置文件中开启)
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
2.在mapper的命名空间下配置缓存(xxx.xml)
<!--配置缓存
1.开启<cache></cache>
2.配置参数
- flushinterval` 缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值
- `readOnly`: 是否只读;**true 只读**,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。**读写(默认)**:MyBatis 觉得数据可能会被修改
- `size` : 缓存存放多少个元素
- `type`: 指定自定义缓存的全类名(实现Cache 接口即可)
- `blocking`: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
-->
<cache flushInterval="10000" eviction="LRU" size="1000" readOnly="false" blocking="false"></cache>
eviction:缓存回收策略,有如下几种回收策略:
- LRU(常用,默认) -最近最少回收,移除最长时间不被使用的对象
- FIFO(不常用)-先进先出,按照缓存进入的顺序来移除他们
- SOFT(了解)-软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK(了解)-弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
默认是 LRU 最近最少回收策略
-
flushinterval
缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值 -
readOnly
: 是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改 -
size
: 缓存存放多少个元素 -
type
: 指定自定义缓存的全类名(实现Cache 接口即可) -
blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
3.使用
对于sql语句默认就开启了,不用另外配置,也可在此关闭缓存,这一条sql不使用不缓存
<!--
useCache="true" 使用缓存 一般不需要配置 默认就开启true
-->
<select id="findStudentById" resultType="Student" useCache="true" >
select * from student_tb where id = #{id}
</select>
4.测试
@Test //二级缓存
public void secondLevelCacheTest(){
//查询第一次 执行sql语句select * from student_tb where id = ?
Student student1 = iStudentDao.findStudentById(2);
System.out.println("student:" + student1);
//查询第二次,没有执行sql 从一级缓存sqlSession中获取,因为二级缓存中没有
Student student2 = iStudentDao.findStudentById(2);
System.out.println("student2:"+student2);
//如果不执行这句话 下面的代码仍会执行sql
//调用close(),commit()方法 才会将数据提交到二级缓存,其他的sqlsession才能拿到
//sqlSession.close();
sqlSession.commit();
// studentDao2 studentDao 来自于不同的sqlSession
iStudentDao2 = sqlSessionFactory.openSession(true).getMapper(IStudentDao.class);
Student student3 = iStudentDao2.findStudentById(2);
System.out.println("student3:"+student3);
}
4.一级缓存与二级缓存的调用顺序与区别(重要)
一级缓存 在sqlSession 中
二级缓存 多个sqlSession 可以二级缓存 二级缓存是以namesapce 进行划分 多个mapper 可以共享一个二级缓存
调用顺序:先查二级缓存,再查一级缓存,最后查数据库
共同点:查询缓存数据 , 只要发生增删改 立即将 一级二级全部清除
5.问题的产生,脏数据
从上面的分析,我们可以知道缓存的调用顺序,试想一下这种情况:
使用二级缓存,当有一个session发生修改时,将二级缓存清空了,然而另一个session缓存过以前的查询结果(此时二级缓存没数据,去一级缓存却拿到了之前的),则可能产生脏数据。
如上图:
- 第一步sqlsession1 去查询id=1的数据,并写入了sqlsession1的一级缓存与共同的二级缓存
- 第二步sqlsession2 去查询id=1的数据,发现二级缓存有数据,就不执行sql,并将其写入sqlsession2的一级缓存
- 第三步sqlsession2执行修改id=1的数据,清空了二级缓存与sqlsession2的一级缓存
- 第四步sqlsession1 再去查询id=1的数据,二级缓存没有,但却从sqlsession1的一级缓存读到了数据,但这却是修改之前的,就产生了脏数据
public class DirtyData {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取SqlSession就是一个会话连接 mybatis默认是开启事务的
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
IStudentDao iStudentDao1 = sqlSession1.getMapper(IStudentDao.class);
System.out.println("使用sqlSession1查询");
Student student1 = iStudentDao1.findStudentById(2);
System.out.println("student1:" + student1);
//sqlSession1.commit();
//从二级缓存中读取不再执行sql
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
IStudentDao iStudentDao2 = sqlSession2.getMapper(IStudentDao.class);
System.out.println("使用sqlSession2 查询");
Student student2 = iStudentDao2.findStudentById(2);
System.out.println("student2:"+student2);
student2.setAge(222);
iStudentDao2.upateStudent(student2);
//提交,使二级缓存失效
//sqlSession2.commit();
//因为二级缓存失效 iStudentDao1先去二级缓存存取,失效后用一级缓存
//此时能拿到数据但不是 session2修改后的最新数据,所以是脏数据
student1 = iStudentDao1.findStudentById(2);
System.out.println("student1:" + student1);
}
}
5.1解决方案1:关闭一级缓存,并将查询的sqlsession改为自动提交
在spring中也不一定发生,创建时一般为单例的mapper,每次查询都会创建新的sqlsession(若开启事务则是同一sqlsession),所以可以避免,不用担心。
public static void main(String[] args) {
InputStream inputStream = null;
SqlSession sqlSession1 = null;
SqlSession sqlSession2 = null;
try {
// 1.读取配置文件
inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2.配置连接功工厂
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
// 3.获取sqlSession 就是一个会话 连接 mybatis 默认是开启事务的
sqlSession1 = sqlSessionFactory.openSession(true);
IStudentDao iStudentDao1 = sqlSession1.getMapper(IStudentDao.class);
System.out.println("使用sqlSession1 查询");
Student student1 = iStudentDao1.findStudentById(63);
System.out.println("student1:"+student1);
//sqlSession1.close();
sqlSession2 = sqlSessionFactory.openSession();
IStudentDao iStudentDao2 = sqlSession2.getMapper(IStudentDao.class);
System.out.println("使用sqlSession2 查询");
Student student2 = iStudentDao2.findStudentById(63);
System.out.println("student2:"+student2);
student2 = iStudentDao2.findStudentById(63);
System.out.println("student2:"+student2);
// 在 session2中修改数据 此时二级缓存失效
student2.setHeight(333);
iStudentDao2.updateStudent(student2);
sqlSession2.commit();
Thread.sleep(4000);
// 因为二级缓存实现,iStudentDao1 先去二级缓存取,失效改用取一级缓存,此时能拿到数据但是不是 session2中修改数据的最新数据产生 脏数据
Student student3 = iStudentDao1.findStudentById(63);
System.out.println("student3:"+student3);
} catch (Exception e) {
e.printStackTrace();
// 有异常回滚
sqlSession1.rollback();
}finally {
// 5.释放资源
sqlSession1.close();
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.2解决方案2:不使用缓存
<select id="findStudentById" parameterType="java.lang.Integer" resultType="com.baidu.entity.Student" useCache="false" >
<include refid="selectAll"></include> where id = #{id}
</select>