跳到主要内容

缓存机制

MyBatis 提供了一级缓存和二级缓存机制,可以有效减少数据库访问次数,提升应用性能。本章将详细介绍缓存的工作原理和配置方法。

缓存概述

缓存类型

类型作用域生命周期默认开启
一级缓存SqlSessionSqlSession 关闭时失效
二级缓存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 的缓存机制:

特性一级缓存二级缓存
作用域SqlSessionMapper 命名空间
默认开启
生命周期SqlSession 关闭时失效应用生命周期
跨 Session不支持支持
配置方式自动需要配置

缓存使用建议:

  • 简单查询使用一级缓存即可
  • 读多写少的场景开启二级缓存
  • 分布式系统使用 Redis 等分布式缓存
  • 合理设置缓存大小和刷新策略