一级缓存简介
在常见的应用系统中,数据库是比较珍贵的资源,很容易成为整个系统的瓶颈。在设计和护系统时,会进行多方面的权衡,并且利用多种优化手段,减少对数据库的直接访问。使用缓存是一种比较有效的优化手段,使用缓存可以减少应用系统与数据库的网络交互、减少数据库访问次数、降低数据库的负担、降低重复创建和销毁对象等一系列开销,从而提高整个系统的性能。从另一方面来看,当数据库意外宕机时,缓存中保存的数据可以继续支持应用程序中的部分展示的功能,提高系统的可用性。
MyBatis 作为一个功能强大的ORM框架,也提供了缓存的功能,其缓存设计为两层结构,分别为一级缓存和二级缓存。一级缓存是会话级别的缓存,在MyBatis中每创建一个SqlSession对象,就表示开启一次数据库会话。在一次会话中,应用程序可能会在短时间内,例如一个事务内,反复执行完全相同的查询语句,如果不对数据进行缓存,那么每一次查询都会执行一次数据库查询操作,而多次完全相同的、时间间隔较短的查询语句得到的结果集极有可能完全相同,这也就造成了数据库资源的浪费。
MyBatis 中的 SqlSession是通过Executor对象完成数据库操作的,为了避免上述问题,在Executor对象中会建立一个简单的缓存,它会将每次查询的结果对象缓存起来。在执行查询操作时,会先查询一级缓存,如果其中存在完全一样的查询语句,则直接从一级缓存中取出相应的结果对象并返回给用户,这样不需要再访问数据库了,从而减小了数据库的压力。
一级缓存的生命周期与SqlSession相同,其实也就与SqlSession中封装的 Executor 对象的生命周期相同。当调用Executor对象的close()方法时,该Executor对象对应的一级缓存就变得不可用。一级缓存中对象的存活时间受很多方面的影响,例如,在调用Executor.update()方法时,也会先清空一级缓存。一级缓存默认是开启的,一般情况下,不需要用户进行特殊配置。
一级缓存命中现象演示
创建配置文件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>
<properties>
<property name="driver" value="com.mysql.cj.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true" />
<property name="username" value="root" />
<property name="password" value="123456" />
</properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<environments default="default">
<environment id="default">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/FirstCacheMapper.xml" />
</mappers>
</configuration>
创建FirstCacheMapper接口
public interface FirstCacheMapper {
List<EmployeeDO> listAllEmployeeByDeptId(Integer deptId);
List<EmployeeDO> listAllEmployeeByDeptIdCopy(Integer deptId);
int updateEmployeeAgeById(@Param("age") Integer age, @Param("id") Integer id);
}
创建FirstCacheMapper.xml
<?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.ys.mybatis.mapper.FirstCacheMapper">
<select id="listAllEmployeeByDeptId" resultType="com.ys.mybatis.DO.EmployeeDO">
select * from employee where dept_id = #{deptId}
</select>
<select id="listAllEmployeeByDeptIdCopy" resultType="com.ys.mybatis.DO.EmployeeDO" >
select * from employee where dept_id = #{deptId}
</select>
<update id="updateEmployeeAgeById">
update employee set age = #{age} where id = #{id}
</update>
</mapper>
创建测试类FirstCacheTest
@Slf4j
public class FirstCacheTest {
private SqlSessionFactory sqlSessionFactory;
private Configuration configuration;
@BeforeEach
public void before() {
InputStream inputStream = ConfigurationTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
configuration = sqlSessionFactory.getConfiguration();
}
@Test
public void hitFirstLevelCacheTest() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
}
运行测试方法hitFirstLevelCacheTest
现象 : SQL只编译一次且只执行一次,两次查询的结果相等
源码简析
通过源码我们得出结论 : 每次查询会生成一个cacheKey,当我们的查询未命中缓存则查询数据库,否则直接返回缓存的内容
cacheKey的组成
cacheKey的组成
- statementId
- rowBounds
- sql
- 参数
- environment : 主要针对二级缓存,一级缓存是session级别的缓存,当environment不同,则sqlSession肯定不是同一对象。对于二级缓存来说如果environment不同,即使sql 、参数、rowBounds等条件一致,也不会命中缓存
演示cacheKey不同的几种情况
@Test
public void differentStatementId() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptIdCopy", 1, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
@Test
public void differentRowBounds() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, new RowBounds(0, 10));
System.out.println(firstQuery == secondQuery);
}
@Test
public void differentParameters() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 2, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
一级缓存失效场景
除了因为cacheKey导致的缓存未命中,其他原因也有可能导致一级缓存未命中
1.手动清空缓存
@Test
public void manualClearing() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
// 手动清空
sqlSession.clearCache();
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
2.flushCache = true
@Test
public void flushCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
相关源码BaseExecutor#query
3.两次查询之间存在更新操作
@Test
public void updateInfoBetweenTwoQueries() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
// 更新信息
FirstCacheMapper mapper = sqlSession.getMapper(FirstCacheMapper.class);
mapper.updateEmployeeAgeById(20, 2);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
相关源码BaseExecutor#update
4.作用域为STATEMENT
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
@Test
public void statementScope() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> firstQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
List<Object> secondQuery = sqlSession.selectList("com.ys.mybatis.mapper.FirstCacheMapper.listAllEmployeeByDeptId", 1, RowBounds.DEFAULT);
System.out.println(firstQuery == secondQuery);
}
相关源码BaseExecutor#query
解决循环依赖
mybatis一级缓存不仅能减轻数据库的压力,还可以解决循环依赖
比如说现在有这样一个场景 : 博客里面有评论信息,评论里面有博客信息
@Data
public class Blog {
private Integer id;
private String title;
private List<Comment> comments;
}
@Data
public class Comment {
private Integer blogId;
private String content;
private Blog blog;
}
<resultMap id="blogMap" type="com.ys.mybatis.DO.Blog">
<id column="id" property="id"/>
<result column="title" property="title"/>
<collection property="comments" column="id" select="getCommentByBlogId"/>
</resultMap>
<resultMap id="commentMap" type="com.ys.mybatis.DO.Comment">
<result property="blogId" column="blog_id"/>
<result property="content" column="content"/>
<association property="blog" column="blog_id" select="getBlogInfoById"/>
</resultMap>
<select id="getBlogInfoById" resultMap="blogMap">
select * from blog where id = #{id}
</select>
<select id="getCommentByBlogId" resultMap="commentMap">
select * from comment where blog_id = #{blogId}
</select>
上述情景,会出现循环依赖,那么mybatis是如何解决循环依赖的,我们查看相关源码
BaseExecutor#query
BaseExecutor#queryFromDatabase
DefaultResultSetHandler#getNestedQueryMappingValue
相关源码比较多,还有很多流程是重复了,这里就标注了比较重要的在步骤。整体流程,详见下方流程图
mybatis利用queryStack、一级缓存、延迟加载完成了循环依赖