知识模块
☕ Java 知识模块
七、数据库与JDBC
MyBatis 缓存

MyBatis 缓存机制

缓存体系架构

MyBatis 提供两级缓存:

┌─────────────────────────────────────────┐
│              应用程序                     │
└─────────────────┬───────────────────────┘


┌─────────────────────────────────────────┐
│           二级缓存(SqlSessionFactory 级别)    │
│         跨 Session 共享,需要配置开启           │
└─────────────────┬───────────────────────┘


┌─────────────────────────────────────────┐
│           一级缓存(SqlSession 级别)          │
│         Session 内共享,默认开启               │
└─────────────────┬───────────────────────┘


┌─────────────────────────────────────────┐
│                数据库                      │
└─────────────────────────────────────────┘

一级缓存

基本特性

特性说明
作用范围SqlSession 级别
默认状态开启,无法关闭
生命周期SqlSession 生命周期
数据结构HashMap(本地内存)

工作原理

// 同一个 SqlSession 内
SqlSession session = sqlSessionFactory.openSession();
 
User user1 = session.selectOne("getUserById", 1); // 查询数据库,缓存结果
User user2 = session.selectOne("getUserById", 1); // 命中缓存
 
System.out.println(user1 == user2); // true,同一个对象
 
session.close();

执行流程:

查询请求 → 检查一级缓存 → 命中? → 返回缓存对象
                ↓ 未命中
           查询数据库 → 写入缓存 → 返回结果

缓存 Key 组成

一级缓存 Key 由以下因素决定:

CacheKey = hashCode + mappedStatementId + offset + limit + sql + parameter + environmentId

相同查询条件才会命中缓存:

// 同一个 Session,相同 SQL 和参数 → 命中缓存
User user1 = session.selectOne("getUserById", 1);
User user2 = session.selectOne("getUserById", 1);
 
// 不同参数 → 不命中
User user3 = session.selectOne("getUserById", 2);

缓存失效场景

SqlSession session = sqlSessionFactory.openSession();
 
// 1. 执行 INSERT/UPDATE/DELETE
session.insert("insertUser", user);
User user = session.selectOne("getUserById", 1); // 缓存失效,查数据库
 
// 2. 调用 session.clearCache()
session.clearCache();
 
// 3. 调用 session.commit()
session.commit(); // 清空一级缓存
 
// 4. 调用 session.rollback()
session.rollback();
 
// 5. SqlSession 关闭
session.close();

一级缓存问题

问题:不同 Session 无法共享缓存

SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
 
User user1 = session1.selectOne("getUserById", 1); // 查数据库
User user2 = session2.selectOne("getUserById", 1); // 又查数据库!
 
// session1 和 session2 各自独立,缓存不共享

二级缓存

基本特性

特性说明
作用范围SqlSessionFactory 级别(命名空间)
默认状态关闭,需要配置开启
生命周期应用生命周期
跨 Session可共享

开启配置

1. 全局配置(mybatis-config.xml):

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

2. Mapper 配置(UserMapper.xml):

<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存 -->
    <cache/>
    
    <!-- 或自定义缓存配置 -->
    <cache 
        eviction="LRU"           <!-- 淘汰策略 -->
        flushInterval="60000"    <!-- 刷新间隔(毫秒) -->
        size="1024"              <!-- 缓存大小 -->
        readOnly="true"/>        <!-- 只读 -->
</mapper>

3. 实体类实现序列化:

public class User implements Serializable {
    private Integer id;
    private String name;
    // ...
}

工作原理

// 不同 SqlSession,相同查询 → 命中二级缓存
SqlSession session1 = sqlSessionFactory.openSession();
User user1 = session1.selectOne("getUserById", 1); // 查数据库
session1.commit(); // 提交后写入二级缓存
 
SqlSession session2 = sqlSessionFactory.openSession();
User user2 = session2.selectOne("getUserById", 1); // 命中二级缓存
 
System.out.println(user1 == user2); // false,但数据相同(可能是序列化副本)

执行流程:

查询请求 → 检查二级缓存 → 命中? → 返回缓存对象
                ↓ 未命中
           检查一级缓存 → 命中? → 返回缓存对象
                ↓ 未命中
           查询数据库 → 写入一级缓存 → commit后写入二级缓存

缓存淘汰策略

策略说明适用场景
LRU(默认)最近最少使用通用场景
FIFO先进先出顺序访问
SOFT软引用,内存不足时清除内存敏感
WEAK弱引用,GC 时清除内存紧张
<!-- 配置 LRU 策略 -->
<cache eviction="LRU" size="1024"/>

二级缓存失效

// 在同一个命名空间内执行增删改
session.insert("insertUser", user);
session.commit(); // 该命名空间的二级缓存清空
 
// 不同命名空间的操作不会影响
session.insert("com.example.mapper.OrderMapper.insertOrder", order);
session.commit(); // UserMapper 的缓存不受影响

注意事项

1. 关联查询缓存问题:

<!-- UserMapper.xml -->
<cache/>
 
<!-- 关联查询 order 表 -->
<select id="getUserWithOrders" resultMap="userOrderMap">
    SELECT u.*, o.* FROM user u 
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>
 
<!-- 问题:OrderMapper 的更新不会清空 UserMapper 的缓存! -->

解决方案:使用 cache-ref

<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
    <!-- 引用 UserMapper 的缓存,共享缓存空间 -->
    <cache-ref namespace="com.example.mapper.UserMapper"/>
</mapper>

2. 分布式环境问题:

二级缓存是本地缓存,分布式环境下各节点缓存不一致。

解决方案:

  • 使用分布式缓存(Redis)
  • 或关闭二级缓存,使用分布式缓存层

一级缓存 vs 二级缓存

对比项一级缓存二级缓存
作用范围SqlSessionSqlSessionFactory(命名空间)
默认状态开启关闭
跨 Session❌ 不共享✅ 共享
生命周期Session 生命周期应用生命周期
失效时机增删改、commit、rollback命名空间内增删改
存储位置JVM 内存JVM 内存(可配置外部)

自定义缓存(Redis)

实现 Cache 接口

public class RedisCache implements Cache {
    private final String id;
    private RedisTemplate<String, Object> redisTemplate;
    
    public RedisCache(String id) {
        this.id = id;
    }
    
    @Override
    public String getId() {
        return id;
    }
    
    @Override
    public void putObject(Object key, Object value) {
        getRedisTemplate().opsForValue().set(key.toString(), value, 30, TimeUnit.MINUTES);
    }
    
    @Override
    public Object getObject(Object key) {
        return getRedisTemplate().opsForValue().get(key.toString());
    }
    
    @Override
    public Object removeObject(Object key) {
        getRedisTemplate().delete(key.toString());
        return null;
    }
    
    @Override
    public void clear() {
        getRedisTemplate().delete(getRedisTemplate().keys("*" + id + "*"));
    }
    
    @Override
    public int getSize() {
        return getRedisTemplate().keys("*" + id + "*").size();
    }
}

配置使用

<!-- UserMapper.xml -->
<cache type="com.example.cache.RedisCache"/>

缓存使用建议

适合使用缓存的场景

  1. 读多写少:数据变更频率低
  2. 实时性要求不高:可以接受短暂的数据不一致
  3. 查询结果小:避免占用过多内存

不适合使用缓存的场景

  1. 写多读少:缓存频繁失效
  2. 实时性要求高:金融交易等
  3. 查询结果大:大结果集占用内存
  4. 分布式环境:缓存不一致问题

最佳实践

// 1. 查询频繁但变更少的数据 → 开启二级缓存
// 2. 单次请求内的重复查询 → 自动使用一级缓存
// 3. 分布式环境 → 使用 Redis 等分布式缓存
// 4. 复杂关联查询 → 考虑 cache-ref 或手动缓存

面试高频问题

Q1: 一级缓存和二级缓存的区别?

对比项一级缓存二级缓存
范围SqlSessionSqlSessionFactory
开启默认开启需配置开启
共享Session 内跨 Session
生命周期Session应用

Q2: 为什么一级缓存无法关闭?

设计理念:同一 Session 内的重复查询应该避免多次访问数据库。

可以通过 session.clearCache()statement.flushCache=true 清空。

Q3: 二级缓存的问题和解决方案?

问题:

  1. 细粒度控制差(命名空间级别)
  2. 分布式环境不一致
  3. 关联表更新问题

解决方案:

  1. 使用 cache-ref 共享缓存
  2. 使用 Redis 分布式缓存
  3. 复杂场景手动缓存控制

Q4: 如何选择缓存策略?

单机环境 + 读多写少 → 二级缓存
分布式环境 → Redis 分布式缓存
实时性要求高 → 不使用缓存 / 短过期时间

总结

MyBatis 缓存要点:
1. 一级缓存:Session 级别,默认开启,增删改后失效
2. 二级缓存:SessionFactory 级别,需配置,跨 Session 共享
3. 淘汰策略:LRU/FIFO/SOFT/WEAK
4. 分布式问题:二级缓存是本地缓存,需用 Redis 替代