深入解析 MyBatis 的一级缓存和二级缓存
在大型互联网项目中,性能优化一直是技术团队关注的重点。而数据库访问的性能优化则是其中的核心之一。在使用 MyBatis 进行开发时,合理利用其内置的一级缓存和二级缓存可以显著减少数据库的访问频次,提升系统性能。本文将基于 MyBatis 源码和实际场景,深入探讨一级缓存和二级缓存的实现原理、使用方法以及常见问题。
1. 背景概述
在传统的数据库操作中,每次查询都会向数据库发送 SQL 请求并返回结果。这种频繁的 IO 操作不仅耗时,还对数据库造成较大压力。在高并发场景下,优化数据库访问次数成为必然选择。MyBatis 作为一种流行的 ORM 框架,通过引入缓存机制有效缓解了这一问题。
- 一级缓存:基于 SqlSession 的缓存,默认开启,生命周期与 SqlSession 相同。
- 二级缓存:基于 Mapper 映射文件 或 namespace,需要手动开启,生命周期与 SqlSessionFactory 相同。
2. 一级缓存
2.1 一级缓存的特点
- 作用域:一级缓存的作用范围是同一个 SqlSession。
- 默认启用:无需额外配置。
- 数据存储:通过内存中的
Map
数据结构存储。 - 刷新条件:
- 执行
insert
、update
、delete
等写操作。 - 调用
SqlSession.clearCache
方法。 - 当前 SqlSession 关闭。
- 执行
2.2 一级缓存源码解析
一级缓存的核心类是 org.apache.ibatis.executor.BaseExecutor
,它内部的 localCache
是实现缓存的关键。
源码片段:
private final PerpetualCache localCache = new PerpetualCache("LocalCache");
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) {
if (localCache.getObject(key) != null) {
return (List<E>) localCache.getObject(key);
} else {
List<E> result = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
localCache.putObject(key, result);
return result;
}
}
在每次查询时:
- MyBatis 会为每个查询生成一个
CacheKey
。 - 查询结果会缓存在
localCache
中。 - 后续相同的查询直接从缓存中获取。
2.3 使用示例
以下是一级缓存的简单应用:
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.selectUserById(1);
// 第二次查询,同样的 SQL 和参数
User user2 = mapper.selectUserById(1);
// user1 和 user2 是同一个对象
System.out.println(user1 == user2); // true
2.4 一级缓存失效的场景
// 数据变更
mapper.updateUser(user);
// 缓存失效,再次查询会重新访问数据库
User user3 = mapper.selectUserById(1);
3. 二级缓存
3.1 二级缓存的特点
- 作用域:作用范围是 SqlSessionFactory,即跨 SqlSession。
- 需要手动开启:需要在 MyBatis 的配置文件中开启,并在每个 Mapper 映射文件中声明使用缓存。
- 存储方式:可以自定义存储方式(如内存、文件等)。
- 数据隔离:与一级缓存不同,二级缓存是共享的。
3.2 二级缓存配置
全局配置:
在 application.yml
中开启二级缓存:
mybatis:
configuration:
cache-enabled: true
Mapper 文件配置:
<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
</mapper>
参数说明:
eviction
:缓存回收策略(LRU
、FIFO
等)。flushInterval
:刷新间隔(毫秒)。size
:缓存对象的个数。readOnly
:只读属性。
3.3 二级缓存的使用示例
// 第一次查询
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.selectUserById(1);
}
// 第二次查询,使用新的 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user2 = mapper.selectUserById(1);
}
若启用了二级缓存,user1
和 user2
实际上是同一个对象。
3.4 二级缓存的实现原理
二级缓存的核心接口是 org.apache.ibatis.cache.Cache
,默认实现是 PerpetualCache
,配合 LruCache
和 SerializedCache
提供高级功能。
3.5 自定义二级缓存
实现 Cache
接口可以自定义缓存:
public class MyCustomCache implements Cache {
private final String id;
// 省略方法实现
}
4. 一级缓存与二级缓存的对比
特性 | 一级缓存 | 二级缓存 |
---|---|---|
作用域 | SqlSession | SqlSessionFactory |
默认开启 | 是 | 否 |
刷新条件 | 写操作、手动清除、关闭会话 | 配置刷新间隔或手动清除 |
存储方式 | 内存 | 可自定义(内存、磁盘等) |
5. 实践案例
在一个高并发场景中,为了提升查询性能,我们利用二级缓存共享跨会话的查询结果。以下是美团点评的典型应用场景:
需求描述:
对一个热门商品的详情页查询,访问量巨大,数据库查询压力大。
解决方案:
- 开启二级缓存,并设置合理的过期时间。
- 对商品的写操作(如库存更新)时,主动清理缓存。
6. 总结
MyBatis 的一级和二级缓存为数据库性能优化提供了强大的支持。在使用时需要结合业务场景,选择合适的缓存策略,并注意缓存一致性的问题。
- 一级缓存更适合短时间内的重复查询。
- 二级缓存则适合高并发场景下的数据共享。
但其实在实际应用中,我们通常会把缓存关掉。比起数据库性能的消耗,查到脏数据带来的影响会更大。
推荐一篇美团技术团队的文章:https://tech.meituan.com/2018/01/19/mybatis-cache.html