作为一名长期使用MyBatis的开发老兵,今天我想和大家深入探讨一个看似基础但实际开发中经常踩坑的话题——MyBatis一级缓存的失效场景。一级缓存作为MyBatis最基础的缓存机制,理解它的工作原理和失效条件对写出高性能的数据库访问代码至关重要。
先明确一个概念:MyBatis的一级缓存是SqlSession级别的缓存,默认开启且无法关闭。当同一个SqlSession执行相同的查询时,第二次查询会直接从内存中获取结果,而不再访问数据库。这个机制在大多数场景下能显著提升性能,但如果我们不了解它的失效条件,就可能遇到各种"缓存不生效"的诡异问题。
这是最基础但也最容易理解的一点:每个SqlSession拥有自己独立的一级缓存。这意味着:
java复制// 场景1:不同SqlSession
try (SqlSession session1 = sqlSessionFactory.openSession()) {
User user1 = session1.selectOne("getUserById", 1);
}
try (SqlSession session2 = sqlSessionFactory.openSession()) {
User user2 = session2.selectOne("getUserById", 1); // 会再次查询数据库
}
实际开发中常见误区:在Web应用中,如果没有正确管理SqlSession生命周期(比如每次请求都创建新SqlSession),就会导致一级缓存几乎不起作用。
即使同一个SqlSession,如果查询条件发生变化,也会被视为不同的查询:
java复制try (SqlSession session = sqlSessionFactory.openSession()) {
// 第一次查询
User user1 = session.selectOne("getUserById", 1);
// 第二次查询,参数不同
User user2 = session.selectOne("getUserById", 2); // 会查询数据库
// 即使参数相同但查询方法不同
User user3 = session.selectOne("getUserByName", "张三"); // 也会查询数据库
}
这里有个细节需要注意:MyBatis判断查询是否相同的依据包括:
这是最容易踩坑的场景。任何INSERT、UPDATE、DELETE操作都会清空整个一级缓存:
java复制try (SqlSession session = sqlSessionFactory.openSession()) {
// 第一次查询
User user1 = session.selectOne("getUserById", 1);
// 执行更新操作
session.update("updateUser", new User(1, "新名字"));
// 再次查询
User user2 = session.selectOne("getUserById", 1); // 会查询数据库
}
实际开发建议:如果业务上需要保证数据实时性,可以在关键查询前手动清空缓存(见下节),而不是依赖自动清空机制。
我们可以通过代码显式清空一级缓存:
java复制try (SqlSession session = sqlSessionFactory.openSession()) {
// 第一次查询
User user1 = session.selectOne("getUserById", 1);
// 清空缓存
session.clearCache();
// 再次查询
User user2 = session.selectOne("getUserById", 1); // 会查询数据库
}
理解这些失效场景的背后,需要了解一级缓存的实现机制。MyBatis的一级缓存实际上是基于一个简单的HashMap实现:
java复制public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
// ...
}
缓存Key的生成逻辑比较复杂,主要包括:
这也是为什么查询条件变化会导致缓存失效——因为生成的Key完全不同了。
让我们通过实际测试来验证这些失效场景。以下是基于JUnit的测试案例:
java复制public class CacheTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void setUp() throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
}
java复制@Test
public void testDifferentSession() {
try (SqlSession session1 = sqlSessionFactory.openSession()) {
User user1 = session1.selectOne("getUserById", 1);
System.out.println("第一次查询:" + user1);
}
try (SqlSession session2 = sqlSessionFactory.openSession()) {
User user2 = session2.selectOne("getUserById", 1);
System.out.println("第二次查询:" + user2); // 会打印SQL日志
}
}
java复制@Test
public void testAfterUpdate() {
try (SqlSession session = sqlSessionFactory.openSession()) {
User user1 = session.selectOne("getUserById", 1);
System.out.println("第一次查询:" + user1);
// 执行更新
User updateUser = new User(1, "新名字");
session.update("updateUser", updateUser);
session.commit();
User user2 = session.selectOne("getUserById", 1);
System.out.println("更新后查询:" + user2); // 会打印SQL日志
}
}
在实际项目开发中,关于一级缓存有几个需要特别注意的点:
Web应用中的缓存有效性:在典型的Web应用中,通常建议每个请求使用独立的SqlSession(即"每次请求一个Session"模式)。这种情况下,一级缓存的作用会非常有限,因为很少有同一个请求中多次查询相同数据的场景。
事务边界与缓存:在长时间运行的事务中,一级缓存可能导致"脏读"问题——因为缓存不会自动感知其他事务对数据的修改。
批量操作的影响:批量插入/更新操作会清空整个一级缓存,这可能导致后续查询性能下降。
缓存与延迟加载:一级缓存也会影响关联对象的延迟加载行为,可能导致N+1查询问题。
基于对一级缓存的理解,我们可以采取以下优化策略:
合理规划SqlSession生命周期:对于批处理任务等需要重复查询相同数据的场景,可以适当延长SqlSession的生命周期。
避免不必要的缓存清空:在事务中合理安排操作顺序,把只读操作放在写操作之前。
选择性使用缓存清空:在明确知道数据已经变更的场景下,主动调用clearCache()避免脏读。
结合二级缓存使用:对于跨SqlSession的缓存需求,可以考虑启用MyBatis的二级缓存。
在实际开发中,我们可能会遇到以下与一级缓存相关的问题:
问题1:为什么我的查询没有走缓存?
问题2:为什么我看到了脏数据?
问题3:批量操作后系统变慢?
对于需要更精细控制缓存的场景,我们可以考虑:
实现自定义缓存:通过实现Cache接口,可以替换默认的PerpetualCache。
缓存Key调优:理解缓存Key的生成逻辑,可以避免不必要的缓存失效。
监控缓存命中率:通过AOP等技术,可以统计缓存的使用效果。
java复制// 自定义缓存示例
public class CustomCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new CustomConcurrentHashMap<>();
// 实现Cache接口方法
// ...
}
在实际项目中,我遇到过一个典型案例:一个批处理任务因为频繁清空缓存导致性能低下。通过分析,我们发现80%的清空操作都是不必要的。最终我们重构了任务流程,把只读阶段和写入阶段明确分离,性能提升了3倍以上。