全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

缓存读写策略

MyBatis 二级缓存的读写策略决定了缓存数据的访问方式与隔离性。合理配置 readOnly 属性,理解序列化机制,能有效避免并发环境下的脏读、缓存穿透等问题。

readOnly 与读写模式

<cache readOnly="..."/> 配置决定了缓存对象是以引用方式返回还是以拷贝方式返回。

readOnly=true(只读模式)

XML
<cache readOnly="true"/>

只读模式下,MyBatis 直接从缓存中返回对象的引用,不进行序列化拷贝:

Java
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1);
session1.commit();
session1.close();

SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1); // 命中缓存

// 只读模式下返回同一对象引用
System.out.println(user1 == user2); // true
特性说明
性能高(无序列化开销)
返回值缓存对象的引用
安全性低(多 Session 可修改同一对象)
线程安全

readOnly=false(默认,读写模式)

XML
<cache readOnly="false"/>

读写模式下,MyBatis 使用序列化机制创建缓存对象的深拷贝,每个 Session 获得独立的对象副本:

Java
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1);
session1.commit();
session1.close();

SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1); // 命中缓存,获得拷贝

// 读写模式下返回不同的对象副本
System.out.println(user1 == user2); // false
System.out.println(user1.equals(user2)); // true(内容相同)
特性说明
性能低(有序列化与反序列化开销)
返回值缓存对象的深拷贝
安全性高(各 Session 相互隔离)
线程安全

序列化机制

readOnly=false 时,MyBatis 使用序列化来隔离不同 Session 对缓存对象的访问:

Java
// 内部实现逻辑
public Object getObject(Object key) {
    Object value = cache.get(key);
    if (!readOnly && value != null) {
        // 序列化拷贝:深度复制对象
        value = serializeCopy(value);
    }
    return value;
}

private Object serializeCopy(Object obj) {
    // 1. 对象序列化
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(obj);
    
    // 2. 反序列化创建新对象
    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    return ois.readObject();
}

序列化要求

要求说明
实现 Serializable缓存对象必须实现 Serializable 接口
所有属性可序列化对象的每个成员变量都必须可序列化
定义 serialVersionUID推荐定义序列化版本 ID,避免版本升级问题
Java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private Long id;
    private String username;
    private List<String> roles; // 内部集合也必须可序列化
    
    // ...
}

并发问题与脏读

readOnly=true 时的脏读风险

只读模式下,多个 Session 共享同一对象引用,可能导致脏读:

Java
// Session 1 读取缓存对象
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1); // 从缓存获得引用
session1.close();

// Session 2 读取同一缓存对象
SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1); // 命中缓存,获得同一引用

// Session 2 修改对象属性
user2.setUsername("modified_by_session2");

// 如果 Session 1 再次访问,会看到被修改的值(脏读)
SqlSession session3 = factory.openSession();
UserMapper mapper3 = session3.getMapper(UserMapper.class);
User user3 = mapper3.selectById(1);
System.out.println(user3.getUsername()); // 输出 "modified_by_session2"

readOnly=true 下,缓存对象的修改会影响所有后续读取,这是典型的脏读问题。

readOnly=false 时的隔离性

Java
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1);
session1.close();

SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1); // 获得拷贝

// Session 2 修改对象
user2.setUsername("modified");

// Session 3 读取,不受 Session 2 影响
SqlSession session3 = factory.openSession();
UserMapper mapper3 = session3.getMapper(UserMapper.class);
User user3 = mapper3.selectById(1);
System.out.println(user3.getUsername()); // 输出原始值,不受修改影响

缓存穿透

什么是缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中无法命中,请求会穿透到数据库。如果大量恶意请求都查询不存在的 key,会对数据库造成压力。

Java
// 恶意查询不存在的的数据
for (int i = 0; i < 10000; i++) {
    User user = mapper.selectById(-i); // 不存在的数据
    // 每次查询都会访问数据库
}

防御策略

1. 缓存空结果

将空结果也缓存起来,避免重复查询:

Java
public User selectByIdWithCache(Long id) {
    // 伪代码:先查缓存
    User user = cache.get(id);
    if (user != null) {
        return user; // 命中缓存(包括空结果)
    }
    
    // 缓存未命中,查询数据库
    user = mapper.selectById(id);
    
    // 将结果(即使是 null)写入缓存
    if (user != null) {
        cache.put(id, user);
    } else {
        cache.put(id, NULL_OBJECT); // 缓存空对象
    }
    
    return user;
}

2. 使用布隆过滤器

在查询前使用布隆过滤器判断数据是否存在:

Java
// 使用布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(
    Funnels.longFunnel(),
    expectedInsertions,
    falsePositiveRate
);

// 查询前检查
if (!bloomFilter.mightContain(id)) {
    return null; // 数据一定不存在,直接返回
}

// 可能命中,再查数据库
User user = mapper.selectById(id);
方案优点缺点
缓存空结果简单,有效增加缓存内存占用
布隆过滤器空间效率高有误判率
参数校验过滤非法请求只能处理明显非法值

性能对比

场景readOnly=truereadOnly=false
单线程读取快(无序列化开销)慢(有序列化开销)
多线程读取快但可能脏读慢但安全
对象修改传播会传播到所有 Session不会传播
大对象缓存更高效序列化开销大
并发安全性

推荐配置

根据业务场景选择合适的读写策略:

场景推荐配置说明
数据只读,不修改readOnly="true"性能最佳
多 Session 并发访问readOnly="false"防止脏读
缓存大对象readOnly="true" + 业务层保证不修改平衡性能与安全
对一致性要求高readOnly="false"确保隔离
低并发环境readOnly="true"性能优先
XML
<!-- 高性能只读缓存 -->
<cache eviction="LRU" size="1024" readOnly="true"/>

<!-- 安全读写缓存 -->
<cache eviction="LRU" size="512" readOnly="false"/>

<!-- 定时刷新的只读缓存 -->
<cache eviction="FIFO" size="256" flushInterval="60000" readOnly="true"/>

注意事项

  1. readOnly=true 时禁止修改:业务代码绝对不能修改从缓存中取出的对象
  2. 对象引用传播:只读模式下,一个 Session 修改缓存对象会影响其他 Session
  3. 序列化性能:大对象在 readOnly=false 下序列化开销很大,可能导致延迟
  4. 缓存穿透防护:高频查询应考虑缓存空结果或布隆过滤器
  5. 线程安全:MyBatis 二级缓存默认不是线程安全的,需要外部缓存保证并发安全

要点总结

  • readOnly=true 返回对象引用,性能高但可能脏读;readOnly=false 返回深拷贝,安全但有序列化开销
  • 只读模式下,多个 Session 修改同一缓存对象会导致脏读传播
  • 读写模式下通过序列化创建对象副本,保证 Session 间隔离
  • 缓存对象必须实现 Serializable 接口,所有成员变量也必须可序列化
  • 缓存穿透可通过缓存空结果或布隆过滤器防御
  • 数据只读、低并发场景推荐 readOnly=true;多 Session 并发、一致性要求高场景推荐 readOnly=false

存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\缓存机制\缓存读写策略.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 缓存脏数据处理
下一篇 → 自定义缓存实现
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库