缓存机制
MyBatis 提供了一级缓存和二级缓存机制,可以有效减少数据库访问次数,提升应用性能。本章将详细介绍缓存的工作原理和配置方法。
缓存概述
缓存类型
| 类型 | 作用域 | 生命周期 | 默认开启 |
|---|---|---|---|
| 一级缓存 | SqlSession | SqlSession 关闭时失效 | 是 |
| 二级缓存 | Mapper 命名空间 | 应用生命周期 | 否 |
一级缓存
一级缓存是 SqlSession 级别的缓存,也称为本地缓存。
工作原理
验证一级缓存
@Test
void testLevel1Cache() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,查询数据库
User user1 = userMapper.selectById(1L);
System.out.println("第一次查询: " + user1);
// 第二次查询,从缓存获取
User user2 = userMapper.selectById(1L);
System.out.println("第二次查询: " + user2);
// 验证是否是同一个对象
System.out.println("是否相同: " + (user1 == user2)); // true
sqlSession.close();
}
一级缓存失效场景
@Test
void testLevel1CacheInvalidation() {
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 场景1:执行增删改操作
User user1 = userMapper.selectById(1L);
userMapper.update(user1); // 执行更新
User user2 = userMapper.selectById(1L); // 缓存失效,重新查询
System.out.println(user1 == user2); // false
// 场景2:手动清空缓存
User user3 = userMapper.selectById(1L);
sqlSession.clearCache(); // 清空缓存
User user4 = userMapper.selectById(1L); // 重新查询
System.out.println(user3 == user4); // false
// 场景3:关闭 SqlSession
sqlSession.close();
SqlSession newSession = SqlSessionUtil.getSqlSession();
UserMapper newUserMapper = newSession.getMapper(UserMapper.class);
User user5 = newUserMapper.selectById(1L); // 新 Session,重新查询
System.out.println(user1 == user5); // false
newSession.close();
}
一级缓存失效条件
| 条件 | 说明 |
|---|---|
| 执行 insert/update/delete | 增删改操作会清空缓存 |
| 调用 sqlSession.clearCache() | 手动清空缓存 |
| sqlSession.close() | 关闭会话 |
| sqlSession.commit() | 提交事务 |
| sqlSession.rollback() | 回滚事务 |
| 查询参数不同 | 参数不同不会命中缓存 |
一级缓存作用域
<settings>
<!-- SESSION: 整个会话期间(默认) -->
<!-- STATEMENT: 每次查询后清空缓存 -->
<setting name="localCacheScope" value="SESSION"/>
</settings>
二级缓存
二级缓存是 Mapper 命名空间级别的缓存,多个 SqlSession 可以共享。
工作原理
开启二级缓存
步骤一:全局配置
<settings>
<!-- 开启二级缓存(默认 true) -->
<setting name="cacheEnabled" value="true"/>
</settings>
步骤二:Mapper 配置
<!-- XML 方式 -->
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<!-- 或者详细配置 -->
<cache
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="true"/>
</mapper>
// 注解方式
@CacheNamespace
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
}
cache 属性说明
| 属性 | 说明 | 默认值 |
|---|---|---|
eviction | 缓存回收策略 | LRU |
flushInterval | 刷新间隔(毫秒) | 无 |
size | 缓存对象数量 | 1024 |
readOnly | 是否只读 | false |
缓存回收策略
| 策略 | 说明 |
|---|---|
LRU | 最近最少使用,移除最长时间不被使用的对象 |
FIFO | 先进先出,按对象进入缓存的顺序移除 |
SOFT | 软引用,基于垃圾回收器状态和软引用规则移除 |
WEAK | 弱引用,更积极地基于垃圾回收器状态和弱引用规则移除 |
验证二级缓存
@Test
void testLevel2Cache() {
// 第一个 Session
SqlSession session1 = SqlSessionUtil.getSqlSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
System.out.println("第一次查询: " + user1);
session1.close(); // 关闭 Session,写入二级缓存
// 第二个 Session
SqlSession session2 = SqlSessionUtil.getSqlSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L);
System.out.println("第二次查询: " + user2);
session2.close();
// 验证是否命中缓存(不是同一个对象,但数据相同)
System.out.println("是否相同: " + (user1 == user2)); // false(非只读模式)
System.out.println("数据相同: " + user1.equals(user2)); // true
}
实体类序列化
使用二级缓存时,实体类需要实现 Serializable 接口:
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
}
缓存配置示例
<!-- 基本配置 -->
<cache/>
<!-- 完整配置 -->
<cache
eviction="LRU"
flushInterval="60000"
size="1024"
readOnly="false"/>
<!-- 自定义缓存实现 -->
<cache type="com.example.mybatis.cache.MyCache">
<property name="host" value="localhost"/>
<property name="port" value="6379"/>
</cache>
禁用特定语句的缓存
<select id="selectById" resultType="User" useCache="false">
SELECT * FROM user WHERE id = #{id}
</select>
刷新缓存
<select id="selectById" resultType="User" flushCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
cache-ref 共享缓存
多个 Mapper 可以共享同一个缓存空间:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<cache/>
</mapper>
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mybatis.mapper.OrderMapper">
<!-- 引用 UserMapper 的缓存 -->
<cache-ref namespace="com.example.mybatis.mapper.UserMapper"/>
</mapper>
自定义缓存
MyBatis 支持自定义缓存实现,可以集成 Redis、Ehcache 等第三方缓存。
自定义缓存接口
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
default ReadWriteLock getReadWriteLock() {
return null;
}
}
Redis 缓存实现示例
public class RedisCache implements Cache {
private final String id;
private RedisTemplate<String, Object> redisTemplate;
public RedisCache(String id) {
this.id = id;
this.redisTemplate = SpringContextHolder.getBean("redisTemplate");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
String cacheKey = getKey(key);
redisTemplate.opsForValue().set(cacheKey, value, 30, TimeUnit.MINUTES);
}
@Override
public Object getObject(Object key) {
String cacheKey = getKey(key);
return redisTemplate.opsForValue().get(cacheKey);
}
@Override
public Object removeObject(Object key) {
String cacheKey = getKey(key);
redisTemplate.delete(cacheKey);
return null;
}
@Override
public void clear() {
Set<String> keys = redisTemplate.keys(id + ":*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
@Override
public int getSize() {
Set<String> keys = redisTemplate.keys(id + ":*");
return keys != null ? keys.size() : 0;
}
private String getKey(Object key) {
return id + ":" + key.toString();
}
}
配置自定义缓存
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<cache type="com.example.mybatis.cache.RedisCache">
<property name="host" value="localhost"/>
<property name="port" value="6379"/>
</cache>
</mapper>
缓存使用场景
适合使用缓存的场景
- 读多写少的数据
- 数据变更不频繁
- 对数据实时性要求不高
- 数据量适中
不适合使用缓存的场景
- 频繁更新的数据
- 对数据实时性要求高
- 数据量过大
- 敏感数据
缓存最佳实践
1. 合理设置缓存作用域
<settings>
<!-- 对于批量操作,使用 STATEMENT 避免内存溢出 -->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
2. 控制缓存大小
<!-- 设置合理的缓存大小 -->
<cache size="512" eviction="LRU"/>
3. 设置刷新间隔
<!-- 定时刷新缓存 -->
<cache flushInterval="60000"/>
4. 只读模式优化
<!-- 如果数据不会修改,使用只读模式提升性能 -->
<cache readOnly="true"/>
5. 避免缓存大对象
// 不推荐:缓存大对象
@Data
public class User {
private Long id;
private String username;
private List<Order> orders; // 可能很大的列表
private byte[] avatar; // 二进制数据
}
// 推荐:只缓存基本信息
@Data
public class User {
private Long id;
private String username;
private String email;
}
6. 使用分布式缓存
对于分布式系统,使用 Redis 等分布式缓存:
<cache type="com.example.mybatis.cache.RedisCache"/>
缓存与事务
重要:二级缓存在事务提交后才会写入,如果事务回滚则不会写入缓存。
常见问题
1. 脏读问题
问题:多个 SqlSession 同时操作同一数据,可能导致脏读。
解决方案:使用二级缓存或分布式缓存。
2. 内存溢出
问题:缓存对象过多导致内存溢出。
解决方案:
- 设置合理的缓存大小
- 使用合适的回收策略
- 对于批量操作使用 STATEMENT 作用域
3. 序列化异常
问题:实体类未实现 Serializable 接口。
解决方案:让实体类实现 Serializable 接口。
4. 缓存不一致
问题:多表关联查询时缓存不一致。
解决方案:使用 cache-ref 共享缓存。
<!-- UserMapper.xml -->
<cache/>
<!-- OrderMapper.xml -->
<cache-ref namespace="com.example.mybatis.mapper.UserMapper"/>
小结
本章详细介绍了 MyBatis 的缓存机制:
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper 命名空间 |
| 默认开启 | 是 | 否 |
| 生命周期 | SqlSession 关闭时失效 | 应用生命周期 |
| 跨 Session | 不支持 | 支持 |
| 配置方式 | 自动 | 需要配置 |
缓存使用建议:
- 简单查询使用一级缓存即可
- 读多写少的场景开启二级缓存
- 分布式系统使用 Redis 等分布式缓存
- 合理设置缓存大小和刷新策略