MyBatis的二级缓存,慎用!
往期热门文章:
1、8种专坑同事的 SQL 写法,性能降低100倍,不来看看? 2、最近火起的 Bean Searcher 与 MyBatis Plus 到底有啥区别? 3、别再重复造轮子了,一个 Spring 注解轻松搞定循环重试功能! 4、2023最新互联网公司时长排行榜出炉! 5、弃用 Nginx 后!它成为了最受欢迎 Web 服务器。。。
为了增加查询的性能,MyBatis 提供了二级缓存架构,分为一级缓存和二级缓存。
这两级缓存最大的区别就是:一级缓存是会话级别的,只要出了这个 SqlSession,缓存就没用了。而二级缓存可以跨会话,多个会话可以使用相同的缓存!
一级缓存使用简单,默认就开启。二级缓存需要手动开启,相对复杂,而且要注意的事项也多,否则可能有隐患。
一级缓存
应用场景
订单表与会员表是存在一对多的关系,为了尽可能减少 join 查询,进行了分阶段查询。即先查询出订单表,再根据 member_id 字段查询出会员表,最后进行数据整合。而如果订单表中存在重复的 member_id,就会出现很多重复查询。
针对这种情况,MyBatis 通过一级缓存来解决:在同一次查询会话(SqlSession)中如果出现相同的语句及参数,就会从缓存中取出,不再走数据库查询。
一级缓存只能作用于查询会话中,所以也叫做会话缓存。
生效的条件
一级缓存要生效,必须满足以下条件条件:
必须是相同的会话
必须是同一个 mapper,即同一个 namespace
必须是相同的 statement,即同一个 mapper 中的同一个方法
必须是相同的 SQL 和参数
查询语句中间没有执行 session.clearCache() 方法
查询语句中间没有执行 insert/update/delete 方法(无论变动记录是否与缓存数据有无关系)
因为一级缓存是会话级别的,要生效的话,必须要在同一个 SqlSession 中。但是与 SpringBoot 集成的 MyBatis,默认每次执行 SQL 语句时,都会创建一个新的 SqlSession!所以一级缓存才没有生效。
当调用 mapper 的方法时,最终会执行到 SqlSessionUtils 的 getSqlSession 方法,在这个方法中会尝试在事务管理器中获取 SqlSession,如果没有开启事务,那么就会 new 一个 DefaultSqlSession。
所以说,即便在同一个方法中,通过同一个 mapper 连续调用两次相同的查询方法,也不会触发一级缓存。
解决与 SpringBoot 集成时一级缓存不生效问题
再看看源码中是什么时候将 SqlSession 设置到事务管理器中的。
SqlSessionUtils 中,在获取到 SqlSession 后,会调用 registerSessionHolder 方法注册 SessionHolder 到事务管理器:
具体是在 TransactionSynchronizationManager 的 bindResource 方法中操作的,将 SessionHolder 保存到线程本地变量 (ThreadLocal) resources 中,这是每个线程独享的:
然后在下次查询时,就可以从这里取出此 SqlSession,使用同一个 SqlSession 查询,一级缓存就生效了。
所以基本原理就是:如果当前线程存在事物,并且存在相关会话,就从 ThreadLocal 中取出。如果没有事务,就重新创建一个 SqlSession 并存储到 ThreadLocal 当中,共下次查询使用。
至于缓存查询数据的地方,是在 BaseExecutor 中的 queryFromDatabase 方法中。执行 doQuery 从数据库中查询数据后,会立马缓存到 localCache(PerpetualCache类型) 中:
public class ItemController {
private ItemMapper itemMapper;
public void getById( Long id) {
System.out.println("==================== begin ====================");
Item item = itemMapper.selectById(id);
System.out.println(JSON.toJSONString(item));
}
}
mybatis-plus:
configuration:
cache-enabled: true
2)Mapper 接口上添加 @CacheNamespace 注解
生效的条件
当会话提交或关闭之后才会填充二级缓存
必须是同一个 mapper,即同一个命名空间
必须是相同的 statement,即同一个 mapper 中的同一个方法
必须是相同的 SQL 语句和参数
如果 readWrite=true(默认就是true),实体对像必须实现 Serializable 接口
缓存清除条件
只有修改会话提交之后,才会执行清空操作
xml 中配置的 update 不能清空 @CacheNamespace 中的缓存数据
任何一种增删改操作都会清空整个 namespace 中的缓存
源码中是如何填充二级缓存的?
在生效条件中提到了,二级缓存必须要在会话提交或关闭之后,才能生效!
SpringBoot 集成 MyBatis 的话,如果没有开启事务,每次执行查询,都会创建新的 SqlSession,所以即使是在同一个方法中进行查询操作,那也是跨会话的。
查询时如何使用二级缓存?
在查询的时候,最终会调用 MybatisCachingExecutor 的 query 方法,里面会从 TransactionalCacheManager 中尝试根据 key 获取二级缓存的内容。
具体的调用层数实在太多,用到了装饰者模式,最终是在 PerpetualCache 中获取缓存的:
打印日志是在 LoggingCache 中:
public interface XxxMapper {
)
List
getPaymentVO(Long id);
}
private XxxMapper xxxMapper;
void test() {
System.out.println("==================== 查询PaymentVO ====================");
List
voList = xxxMapper.getPaymentVO(1L); System.out.println(JSON.toJSONString(voList.get(0)));
System.out.println("==================== 更新item表的name ==================== ");
Item item = itemMapper.selectById(1);
item.setName("java并发编程");
itemMapper.updateById(item);
System.out.println("==================== 重新查询PaymentVO ==================== ");
List
voList2 = xxxMapper.getPaymentVO(1L); System.out.println(JSON.toJSONString(voList2.get(0)));
}
上面的代码,test() 方法中前后两次调用了 xxxMapper.getPaymentVO 方法,因为没有加 @Transactional 注解,所以前后两次查询,是两个不同的会话,第一次查询完后,SqlSession 会自动 commit,所以二级缓存能够生效;
然后在中间进行了 Item 表的更新操作,修改了下名称;
由于 itemMapper 与 xxxMapper 不是同一个命名空间,所以 itemMapper 执行的更新操作不会影响到 xxxMapper 的二级缓存;
再次调用 xxxMapper.getPaymentVO,发现取出的值是走缓存的,itemName 还是老的。但实际上 itemName 在上面已经被改了!
转自:xujingyiss,
链接:blog.csdn.net/xujingyiss/article/details/123481116
往期热门文章:
1、计算机会成为下一个土木吗? 2、干掉Maven和Gradle!推出更强更快更牛逼的新一代构建工具,炸裂! 3、大公司为什么禁止SpringBoot项目使用Tomcat? 4、快速交付神器:阿里巴巴官方低代码引擎开源了! 5、为什么 Spring和IDEA 都不推荐使用 @Autowired 注解 6、程序员的悲哀是什么? 7、被问懵了:MySQL 自增主键一定是连续的吗? 8、点一下详情系统挂了,CPU100% 9、我说用count(*)统计行数,面试官让我回去等消息... 10、世界第三大浏览器正在消亡