Mybatis一级缓存

一级缓存简介

        在常见的应用系统中,数据库是比较珍贵的资源,很容易成为整个系统的瓶颈。在设计和护系统时,会进行多方面的权衡,并且利用多种优化手段,减少对数据库的直接访问。使用缓存是一种比较有效的优化手段,使用缓存可以减少应用系统与数据库的网络交互、减少数据库访问次数、降低数据库的负担、降低重复创建和销毁对象等一系列开销,从而提高整个系统的性能。从另一方面来看,当数据库意外宕机时,缓存中保存的数据可以继续支持应用程序中的部分展示的功能,提高系统的可用性。
         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&amp;characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;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、一级缓存、延迟加载完成了循环依赖 

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值