目录
1、一级缓存
1.1 我们在一个 sqlSession 中,对 User 表根据id进行两次查询,查看他们发出sql语句的情况。
1.2 同样是对user表进行两次查询,只不过两次查询之间进行了一次update操作。
1.3 一级缓存查询过程
1.4 Mybatis与Spring整合导致一级缓存失效
1.5 总结
2、二级缓存
2.1 二级缓存的使用
2.1.1 创建一个POJO Bean并序列化
2.1.2 在映射文件中开启二级缓存
2.1.3 在 mybatis-config.xml中开启二级缓存
2.1.4 测试
2.1.5 使用spring整合Mybatis时使用二级缓存
2.2 二级缓存的缺陷
MyBatis为我们提供了一级缓存和二级缓存,可以通过下图来理解:
- 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
- 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。二级缓存的底层也是HashMap。(同一个Mapper本质就是同一个命名空间)
1、一级缓存
我们在一个 sqlSession 中,对 User 表根据id进行两次查询,查看他们发出sql语句的情况。
1.1 我们在一个 sqlSession 中,对 User 表根据id进行两次查询,查看他们发出sql语句的情况。
@Test
public void testSelectOrderAndUserByOrderId(){
//根据 sqlSessionFactory 产生 sqlsession
SqlSession sqlSession = sessionFactory.openSession();
String statement = "one.to.one.mapper.OrdersMapper.selectOrderAndUserByOrderID";
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查询,发出sql语句,并将查询的结果放入缓存中
User u1 = userMapper.selectUserByUserId(1);
System.out.println(u1);
//第二次查询,由于是同一个sqlSession,会在缓存中查找查询结果
//如果有,则直接从缓存中取出来,不和数据库进行交互
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
查看控制台打印情况:
1.2 同样是对user表进行两次查询,只不过两次查询之间进行了一次update操作。
@Test
public void testSelectOrderAndUserByOrderId(){
//根据 sqlSessionFactory 产生 sqlsession
SqlSession sqlSession = sessionFactory.openSession();
String statement = "one.to.one.mapper.OrdersMapper.selectOrderAndUserByOrderID";
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查询,发出sql语句,并将查询的结果放入缓存中
User u1 = userMapper.selectUserByUserId(1);
System.out.println(u1);
//第二步进行了一次更新操作,sqlSession.commit()
u1.setSex("女");
userMapper.updateUserByUserId(u1);
sqlSession.commit();
//第二次查询,由于是同一个sqlSession.commit(),会清空缓存信息
//则此次查询也会发出 sql 语句
User u2 = userMapper.selectUserByUserId(1);
System.out.println(u2);
sqlSession.close();
}
控制台打印情况:
1.3 一级缓存查询过程
1、第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
2、如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
3、第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。
1.4 Mybatis与Spring整合导致一级缓存失效
我们使用Spring整合MyBatis的时候,就会发现我们的一级缓存失效,即如果1.1的代码是运行在spring项目下的,那么两次查询都会执行sql语句。
这是因为spring整合Mybatis之后,sqlSession是要交给spring容器去管理的,在每一次执行sql之后,spring会将sqlSession关闭,关闭sqlSession会清空一级缓存,所以一级缓存就会失效。
1.5 总结
一级缓存:与数据库同一次会话(sqlSession)期间查询到的数据会放在本地缓存中,如果以后要获取相同的数据直接从缓存中获取,不会再次向数据库查询数据。
一个SqlSession拥有一个一级缓存,这个一级缓存是一个本地缓存。
MyBatis一直开启一级缓存,不同SqlSession的缓存,数据不可以共享
一级缓存还有失效的情况,一级缓存失效后,第二次查询相同的语句就还需向数据库发送sql。
一级缓存失效情况:
- sqlSession不同
- 当sqlSession对象相同的时候,查询的条件不同,原因是第一次查询时候一级缓存中没有第二次查询所需要的数据
- 当sqlSession对象相同,两次查询之间进行了插入的操作
- 当sqlSession对象相同,手动清除了一级缓存中的数据
一级缓存的生命周期有多长?
a、MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
b、如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,使PerpetualCache对象失效,以致一级缓存将不可用。
c、如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
d、SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。
2、二级缓存
二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去缓存中取。但是一级缓存是基于 sqlSession 的,而二级缓存是基于 mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
namespace默认就是Mapper类的全限定名。所以同一个Mapper也就保证了是同一个namespace
2.1 二级缓存的使用
2.1.1 创建一个POJO Bean并序列化
由于二级缓存的数据不一定都是存储到内存中,它的存储介质多种多样,所以需要给缓存的对象执行序列化。(如果存储在内存中的话,实测不序列化也可以的。)
public class Student implements Serializable{
private static final long serialVersionUID = 735655488285535299L;
private String id;
private String name;
private int age;
private Gender gender;
private List<Teacher> teachers;
setters&getters()....;
toString();
}
2.1.2 在映射文件中开启二级缓存
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yihaomen.mybatis.dao.StudentMapper">
<!--开启本mapper的namespace下的二级缓存-->
<!--
eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
(1) LRU,最近最少使用的,一处最长时间不用的对象
(2) FIFO,先进先出,按对象进入缓存的顺序来移除他们
(3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
(4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,
移除最长时间不用的对形象
flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当
SQL被执行的时候才会去刷新缓存。
size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
这里配置的是1024个对象
readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有
办法修改缓存,他的默认值是false,不允许我们修改
-->
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
<resultMap id="studentMap" type="Student">
<id property="id" column="id" />
<result property="name" column="name" />
<result property="age" column="age" />
<result property="gender" column="gender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler" />
</resultMap>
<resultMap id="collectionMap" type="Student" extends="studentMap">
<collection property="teachers" ofType="Teacher">
<id property="id" column="teach_id" />
<result property="name" column="tname"/>
<result property="gender" column="tgender" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
<result property="subject" column="tsubject" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/>
<result property="degree" column="tdegree" javaType="string" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<select id="selectStudents" resultMap="collectionMap">
SELECT
s.id, s.name, s.gender, t.id teach_id, t.name tname, t.gender tgender, t.subject tsubject, t.degree tdegree
FROM
student s
LEFT JOIN
stu_teach_rel str
ON
s.id = str.stu_id
LEFT JOIN
teacher t
ON
t.id = str.teach_id
</select>
<!--可以通过设置useCache来规定这个sql是否开启缓存,ture是开启,false是关闭-->
<select id="selectAllStudents" resultMap="studentMap" useCache="true">
SELECT id, name, age FROM student
</select>
<!--刷新二级缓存
<select id="selectAllStudents" resultMap="studentMap" flushCache="true">
SELECT id, name, age FROM student
</select>
-->
</mapper>
2.1.3 在 mybatis-config.xml中开启二级缓存
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!--这个配置使全局的映射器(二级缓存)启用或禁用缓存-->
<setting name="cacheEnabled" value="true" />
.....
</settings>
....
</configuration>
2.1.4 测试
① 测试二级缓存和sqlSession 无关
@Test
public void testTwoCache(){
//根据 sqlSessionFactory 产生 session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
String statement = "com.ys.twocache.UserMapper.selectUserByUserId";
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
//第一次查询,发出sql语句,并将查询的结果放入缓存中
User u1 = userMapper1.selectUserByUserId(1);
System.out.println(u1);
sqlSession1.close();//第一次查询完后关闭sqlSession
//第二次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
User u2 = userMapper2.selectUserByUserId(1);
System.out.println(u2);
sqlSession2.close();
}
可以看出上面两个不同的sqlSession,第一个关闭了,第二次查询依然不发出sql查询语句。
② 测试执行 commit() 操作,二级缓存数据清空
@Test
public void testTwoCache(){
//根据 sqlSessionFactory 产生 session
SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession();
SqlSession sqlSession3 = sessionFactory.openSession();
String statement = "com.ys.twocache.UserMapper.selectUserByUserId";
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper userMapper3 = sqlSession2.getMapper(UserMapper.class);
//第一次查询,发出sql语句,并将查询的结果放入缓存中
User u1 = userMapper1.selectUserByUserId(1);
System.out.println(u1);
sqlSession1.close();//第一次查询完后关闭sqlSession
//执行更新操作,commit()
u1.setUsername("aaa");
userMapper3.updateUserByUserId(u1);
sqlSession3.commit();
//第二次查询,由于上次更新操作,缓存数据已经清空(防止数据脏读),这里必须再次发出sql语句
User u2 = userMapper2.selectUserByUserId(1);
System.out.println(u2);
sqlSession2.close();
}
查看控制台情况:
2.1.5 使用spring整合Mybatis时使用二级缓存
只需要在Mapper接口类上加一个@CacheNamespace注解就开启二级缓存了。
@CacheNamespace
public interface demoMapper {
@Select("select * from demo")
public List<Map<String, Object>> list();
}
2.2 二级缓存的缺陷
MyBatis二级缓存使用的在某些场景下会出问题,来看一下为什么这么说。
假设我有一条select语句(开启了二级缓存):
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;
对于tableA与tableB的操作定义在两个Mapper中,分别叫做MapperA与MapperB,即它们属于两个命名空间,如果此时启用缓存:
- MapperA中执行上述sql语句查询这6个字段
- tableB更新了col1与col2两个字段
- MapperA再次执行上述sql语句查询这6个字段(前提是没有执行过任何insert、delete、update操作)
此时问题就来了,即使第(2)步tableB更新了col1与col2两个字段,第(3)步MapperA走二级缓存查询到的这6个字段依然是原来的这6个字段的值,因为我们从CacheKey的3组条件来看:
- <select>标签所在的Mapper的Namespace+<select>标签的id属性
- RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
- <select>标签中定义的sql语句
对于MapperA来说,其中的任何一个条件都没有变化,自然会将缓存中的原结果返回。
这个问题对于MyBatis的二级缓存来说是一个无解的问题,因此使用MyBatis二级缓存有一个前提:必须保证所有的增删改查都在同一个命名空间下才行。
最后说一下,MyBatis支持三种类型的二级缓存:
- MyBatis默认的缓存,type为空,Cache为PerpetualCache
- 自定义缓存(实现Cache接口)
- 第三方缓存(redis、caffeine等)
相关文章:
【MyBatis】MyBatis解析动态SQL原理分析-CSDN博客【MyBatis】一篇文章带你彻底搞懂MyBatis解析SQL映射文件底层原理_mybatis 如何解析sql-CSDN博客
【MyBatis】MyBatis内置数据源_mybatis 指定数据源-CSDN博客
【MyBatis】MyBatis解析全局配置文件源码详解_mybatis解析配置文件源码-CSDN博客
【MyBatis】MyBatis的日志实现_mybatis 日志-CSDN博客
【MyBatis】MyBatis的介绍和基本使用_dbutil mybaits-CSDN博客
【MyBatis】史上最全的MyBatis执行SQL原理分析_mybatis 执行sql-CSDN博客