二级缓存
二级缓存原理和一级缓存的原理是一样的,第一次查询,将数据放入到缓存中,第二次查询就会去缓存中取,但是一级缓存是基于 SqlSession 的,而二级缓存是基于 Mapper 的 namespace 的,也就是说,多个 SqlSession 可以共享一个 Mapper 中的二级缓存,并且如果两个 Mapper 的 namespace 相同的话,即使是两个 Mapper,也会缓存到相同的区域中。
MyBatis 提供了两级缓存机制:一级缓存和二级缓存。一级缓存是 SqlSession 级别的,作用域仅限于当前会话,默认开启。而二级缓存是跨 SqlSession 的,作用域为 Mapper 映射文件级别,需要手动配置开启。
工作原理
二级缓存是一个共享的缓存区域,存储的查询结果可以被多个 SqlSession 复用,从而减少数据库查询操作,提高性能。
查询时的流程:
- 先从二级缓存中查找数据
- 如果二级缓存未命中,则去一级缓存中查找
- 一级缓存也未命中,则执行 SQL 查询数据库,并将结果存入一级缓存和二级缓存
更新时的流程:
- 当某个操作(如 INSERT、UPDATE 或 DELETE)导致数据发生改变时,MyBatis 会自动清空与该 Mapper 相关的二级缓存
优缺点
优点:
- 减少数据库访问次数:提高系统性能
- 跨 SqlSession 共享:对频繁查询的数据特别有用
缺点:
- 数据一致性问题:如果数据库数据发生变化,缓存数据可能不一致
- 内存占用增加:缓存需要消耗内存存储数据
- 复杂度增加:需要额外管理缓存的清理和更新
开启缓存
二级缓存默认是关闭的,我们需要开启,需要修改 sqlMapConfig.xml:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次,我们需要到对应的 Mapper 中,进行缓存的开启,这里我们用 UserMapper 作为例子,在其中加入:
<!--开启二级缓存-->
<cache></cache>
可以看到 Mapper.xml 中文件就这么一个空的标签,这里其实是可以加一些参数的,如果不加的话,会用默认的实现类。我们也可以实现 Cache 接口来用我们自己的缓存类。(使用 type 参数来更换实现类)
注意事项
由于开启了二级缓存之后,还需要将缓存中的 model 对象实现 Serializable 接口,为了将缓存数据取出执行反序列化操作,因为二级缓存的数据可能会有不同的介质,不一定是在内存中存储的,如果是到 Redis、磁盘等,就需要进行序列化和反序列化。
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WzkUser implements Serializable {
private int id;
private String username;
private String password;
private Date birthday;
private List<WzkOrder> orderList;
private List<WzkRole> roleList;
}
编写代码
这里我们测试二级缓存。
public class WzkicuCache03 {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
List<WzkUser> users1 = userMapper1.findAll();
sqlSession1.close();
List<WzkUser> users2 = userMapper2.findAll();
sqlSession2.close();
}
}
测试代码
运行上述的代码,控制台的输出结果如下:
24/11/13 10:52:09 DEBUG UserMapper.findAll: ==> Preparing: select *,o.id oid from wzk_user u left join wzk_orders o on u.id=o.uid;
24/11/13 10:52:09 DEBUG UserMapper.findAll: ==> Parameters:
24/11/13 10:52:09 DEBUG UserMapper.findAll: <== Total: 3
开启了二级缓存之后,可以看到两个不同的 SqlSession,第二次查询依然不会发出 SQL。
编写代码
这里我们通过 commit 清空缓存来进行测试
public class WzkicuCache04 {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
List<WzkUser> users1 = userMapper1.findAll();
sqlSession1.close();
WzkUser wzkUser = WzkUser
.builder()
.id(1)
.username("wzk-update")
.password("123-update")
.build();
userMapper2.updateById(wzkUser);
sqlSession2.commit();
sqlSession2.close();
List<WzkUser> users3 = userMapper3.findAll();
sqlSession3.close();
}
}
测试代码
执行上述的代码,结果如下:
24/11/13 10:58:50 DEBUG UserMapper.updateById: ==> Preparing: UPDATE wzk_user SET username=?, password=? WHERE id = ?
24/11/13 10:58:50 DEBUG UserMapper.updateById: ==> Parameters: wzk-update(String), 123-update(String), 1(Integer)
24/11/13 10:58:50 DEBUG UserMapper.updateById: <== Updates: 1
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG pooled.PooledDataSource: Returned connection 1183888521 to pool.
24/11/13 10:58:50 DEBUG mapper.UserMapper: Cache Hit Ratio [icu.wzk.mapper.UserMapper]: 0.0
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Opening JDBC Connection
24/11/13 10:58:50 DEBUG pooled.PooledDataSource: Checked out connection 1183888521 from pool.
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG UserMapper.findAll: ==> Preparing: select *,o.id oid from wzk_user u left join wzk_orders o on u.id=o.uid;
24/11/13 10:58:50 DEBUG UserMapper.findAll: ==> Parameters:
可以看到,commit 之后,二级缓存也被清空了,所以后续的查询还是发出了 SQL。
额外配置
MyBatis 中还配以 useCache 和 flushCache 等配置项目,useCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache = false 可以禁用当前 select 语句的二级缓存,即每次查询都会发送 SQL 去查询,默认情况是 true,即该 SQL 使用二级缓存:
<select id="findAll" resultMap="userMap" useCache="false">
select *,o.id oid from wzk_user u left join wzk_orders o on u.id=o.uid;
</select>
一般情况下,执行完 commit 都需要刷新缓存,flushCache = true 表示刷新缓存,这样可以避免数据库脏读,所以不需要设置,默认即可。