MyBatis缓存模块详解

深入剖析MyBatis缓存机制,从架构设计到实战应用,助你全面掌握缓存优化


一、初识MyBatis缓存

在正式开始之前,让我们先来了解MyBatis的整体架构。MyBatis采用分层设计,而缓存模块作为基础支撑层的核心组件,承担着提升查询性能的重要使命。

缓存的价值何在?

想象这样一个场景:你的系统每秒需要查询1000次用户信息。

无缓存时: 1000次数据库查询/秒

有缓存时: 1次数据库查询 + 999次内存读取/秒

性能提升: 近1000倍!

MyBatis的两级防线

MyBatis提供了两级缓存机制,就像双重保险:

缓存类型 作用范围 生命周期 是否默认开启
一级缓存 SqlSession会话 与会话同生共死 默认开启
二级缓存 Mapper命名空间 应用级别,跨会话 需手动配置

二、缓存架构

MyBatis的缓存设计堪称教科书级别的装饰器模式应用。

Cache接口:缓存的灵魂

所有缓存实现都遵循这个核心接口:

csharp 复制代码
public interface Cache {
    String getId();                          // 缓存标识
    void putObject(Object key, Object value); // 存入缓存
    Object getObject(Object key);             // 获取缓存
    Object removeObject(Object key);          // 移除缓存
    void clear();                            // 清空缓存
    int getSize();                           // 缓存大小
    ReadWriteLock getReadWriteLock();        // 读写锁
}

装饰器大家族

MyBatis通过装饰器模式为缓存"穿衣服",每个装饰器赋予缓存一种新能力:

装饰器功能速查表

装饰器 核心功能 适用场景
LruCache 最近最少使用淘汰 热点数据缓存
FifoCache 先进先出淘汰 时序数据缓存
SoftCache 软引用,内存紧张时回收 大对象缓存
WeakCache 弱引用,GC时回收 内存敏感场景
ScheduledCache 定时清理 时效性数据
SerializedCache 序列化存储 数据隔离保护
LoggingCache 日志记录 性能监控
SynchronizedCache 线程同步 并发安全
BlockingCache 阻塞控制 防止击穿
TransactionalCache 事务缓存 事务一致性

PerpetualCache:万丈高楼平地起

这是最基础的缓存实现,简单而高效:

typescript 复制代码
public class PerpetualCache implements Cache {
    private final String id;
    private final Map<Object, Object> cache = new HashMap<>();

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    // ... 其他方法实现
}

三、一级缓存:会话的专属记忆

一级缓存是SqlSession级别的缓存,默认开启,无需配置。

工作原理解析

一级缓存的核心逻辑在BaseExecutor中实现:

scss 复制代码
public <E> List<E> query(MappedStatement ms, Object parameter, 
                         RowBounds rowBounds, ResultHandler resultHandler) {
    //创建缓存键
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

    //先查缓存
    List<E> list = (List<E>) localCache.getObject(key);
    if (list != null) {
        return list; // 缓存命中!
    }

    //缓存未命中,查询数据库
    list = queryFromDatabase(ms, parameter, rowBounds, 
                            resultHandler, key, boundSql);

    //结果写入缓存
    localCache.putObject(key, list);

    return list;
}

CacheKey:缓存的身份证

CacheKey由多个元素组成,确保唯一性:

sql 复制代码
CacheKey的组成:
├── MappedStatement的ID
├── 查询参数
├── 分页信息(RowBounds)
├── SQL语句
└── 环境ID

判断缓存命中时,这些元素必须完全一致:

typescript 复制代码
@Override
public boolean equals(Object object) {
    final CacheKey cacheKey = (CacheKey) object;

    return hashcode == cacheKey.hashcode      // 哈希码相同
        && checksum == cacheKey.checksum      // 校验和相同
        && count == cacheKey.count            // 元素数量相同
        && updateList.equals(cacheKey.updateList); // 元素列表相同
}

五种失效场景

一级缓存在以下情况会被清空:

  1. 执行增删改操作
  2. 手动清空
  3. 提交事务
  4. 回滚事务
  5. 关闭会话
scss 复制代码
@Override
public int update(MappedStatement ms, Object parameter) {
    clearLocalCache(); //清空缓存
    return doUpdate(ms, parameter);
}
scss 复制代码
session.clearCache(); //主动清理
scss 复制代码
session.commit(); //提交时清空
scss 复制代码
session.rollback(); //回滚时清空
go 复制代码
session.close(); //会话结束,缓存消失

实战演示

ini 复制代码
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

// 第一次查询 - 访问数据库
User user1 = mapper.selectById(1L);
System.out.println("首次查询: " + user1);

// 第二次查询 - 从缓存获取
User user2 = mapper.selectById(1L);
System.out.println("再次查询: " + user2);

// 验证是否为同一对象
System.out.println("同一对象? " + (user1 == user2)); //true

session.close();

四、二级缓存:跨会话的共享空间

二级缓存是Mapper级别的缓存,可以在不同SqlSession之间共享数据。

开启二级缓存

在Mapper XML中添加配置:

xml 复制代码
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存 -->
    <cache eviction="LRU"
           flushInterval="60000"
           size="1024"
           readOnly="true"/>

    <select id="selectById" resultType="User">
        SELECT * FROM t_user WHERE id = #{id}
    </select>
</mapper>

配置参数详解:

参数 说明 可选值 默认值
eviction 淘汰策略 LRU/FIFO/SOFT/WEAK LRU
flushInterval 刷新间隔(毫秒) 任意正整数 不刷新
size 缓存容量 任意正整数 1024
readOnly 是否只读 true/false false
blocking 是否阻塞 true/false false

四大淘汰策略

  1. LRU(推荐)
  2. FIFO
  3. SOFT
  4. WEAK
ini 复制代码
<cache eviction="LRU"/>

最近最少使用,淘汰最久未访问的数据

ini 复制代码
<cache eviction="FIFO"/>

先进先出,按写入顺序淘汰

ini 复制代码
<cache eviction="SOFT"/>

软引用,内存不足时才回收

ini 复制代码
<cache eviction="WEAK"/>

弱引用,GC时即可回收

CachingExecutor:二级缓存的指挥官

java 复制代码
public class CachingExecutor implements Executor {
    private final TransactionalCacheManager tcm = 
        new TransactionalCacheManager();

    @Override
    public <E> List<E> query(...) {
        Cache cache = ms.getCache();

        if (cache != null) {
            //尝试从二级缓存获取
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list != null) {
                return list; //命中!
            }
        }

        //委托给BaseExecutor查询(会走一级缓存)
        List<E> list = delegate.query(...);

        //结果写入二级缓存
        if (cache != null) {
            tcm.putObject(cache, key, list);
        }

        return list;
    }
}

TransactionalCache:事务缓存管家

事务缓存确保只有提交后的数据才会进入二级缓存:

typescript 复制代码
public class TransactionalCache implements Cache {
    private final Map<Object, Object> entriesToAddOnCommit;

    @Override
    public void putObject(Object key, Object value) {
        //暂存,不立即写入
        entriesToAddOnCommit.put(key, value);
    }

    public void commit() {
        // 提交时才真正写入缓存
        for (Map.Entry<Object, Object> entry : 
             entriesToAddOnCommit.entrySet()) {
            delegate.putObject(entry.getKey(), entry.getValue());
        }
    }

    public void rollback() {
        // 回滚时丢弃暂存数据
        entriesToAddOnCommit.clear();
    }
}

跨会话共享示例

ini 复制代码
//会话1
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
System.out.println("会话1查询: " + user1);
session1.commit(); //提交,写入二级缓存
session1.close();
//会话2
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L); //从二级缓存获取
System.out.println("会话2查询: " + user2);
//对比结果
System.out.println("同一对象? " + (user1 == user2)); //false
System.out.println("值相等? " + user1.equals(user2)); //true
session2.close();

五、缓存命中流程

全景理解缓存的完整查询流程,是优化性能的关键。

完整查询链路

markdown 复制代码
查询请求
    ↓
检查二级缓存
    ├─ 命中 → 直接返回
    └─ 未命中
         ↓
    检查一级缓存
         ├─ 命中 → 直接返回
         └─ 未命中
              ↓
         查询数据库
              ↓
         写入一级缓存
              ↓
         写入二级缓存(提交后)
              ↓
         返回结果

源码实现

scss 复制代码
public <E> List<E> query(MappedStatement ms, Object parameter, ...) {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

    // 步骤1:查二级缓存
    Cache cache = ms.getCache();
    if (cache != null && ms.isUseCache()) {
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list != null) {
            return list; // 二级缓存命中
        }
    }

    // 步骤2:查一级缓存
    List<E> list = (List<E>) localCache.getObject(key);
    if (list != null) {
        return list; // 一级缓存命中
    }

    //步骤3:查数据库
    list = queryFromDatabase(ms, parameter, ...);

    //步骤4:写入缓存
    localCache.putObject(key, list);
    if (cache != null) {
        tcm.putObject(cache, key, list);
    }

    return list;
}

六、装饰器模式的运用

MyBatis缓存的装饰器设计堪称经典,让我们看看如何"给缓存穿衣服"。

LruCache:智能淘汰

typescript 复制代码
public class LruCache implements Cache {
    private final Cache delegate;
    private Map<Object, Object> keyMap; // LinkedHashMap实现LRU
    private Object eldestKey;

    @Override
    public Object getObject(Object key) {
        keyMap.get(key); //访问即刷新顺序
        return delegate.getObject(key);
    }

    @Override
    public void putObject(Object key, Object value) {
        delegate.putObject(key, value);
        cycleKeyList(key); //淘汰最久未用的
    }
}

ScheduledCache:定时清理

typescript 复制代码
public class ScheduledCache implements Cache {
    private long clearInterval = 3600000; // 1小时
    private long lastClear;

    @Override
    public Object getObject(Object key) {
        if (System.currentTimeMillis() - lastClear > clearInterval) {
            clear(); //时间到,清空缓存
            return null;
        }
        return delegate.getObject(key);
    }
}

SerializedCache:深拷贝保护

typescript 复制代码
public class SerializedCache implements Cache {
    @Override
    public void putObject(Object key, Object value) {
        // 序列化存储
        delegate.putObject(key, serialize((Serializable) value));
    }

    @Override
    public Object getObject(Object key) {
        // 反序列化返回,每次都是新对象
        Object object = delegate.getObject(key);
        return object == null ? null : deserialize((byte[]) object);
    }
}

SynchronizedCache:线程安全卫士

typescript 复制代码
public class SynchronizedCache implements Cache {
    @Override
    public synchronized void putObject(Object key, Object value) {
        delegate.putObject(key, value);
    }

    @Override
    public synchronized Object getObject(Object key) {
        return delegate.getObject(key);
    }
}

装饰器链的构建

scss 复制代码
private Cache setStandardDecorators(Cache cache) {
    //按顺序穿衣服
    if (blocking) {
        cache = new BlockingCache(cache);
    }
    if (readWrite) {
        cache = new SerializedCache(cache);
    }
    if (scheduled) {
        cache = new ScheduledCache(cache);
    }
    if (logging) {
        cache = new LoggingCache(cache);
    }
    if (sync) {
        cache = new SynchronizedCache(cache);
    }
    //LRU通常是最外层
    cache = new LruCache(cache);

    return cache;
}

七、最佳实践

推荐做法

ini 复制代码
1.一级缓存- 保持默认开启,适合单会话重复查询
2.二级缓存- 仅在读多写少的场景开启
3.LRU策略- 大多数场景的最佳选择
4.合理设置容量- 根据业务量评估,避免内存溢出
5.只读缓存- 不可变对象使用 
readOnly="true"

避免做法

复制代码
1.在频繁更新的表上开启二级缓存
2.缓存大对象或包含敏感信息的对象
3.忽略缓存带来的数据一致性问题
4.不监控缓存命中率就盲目使用

性能优化技巧

  1. 热点数据优先
  2. 合理设置TTL
  3. 只读缓存加速
  4. 监控命中率
xml 复制代码
<!-- 核心业务表单独配置 -->
<cache size="2048" eviction="LRU"/>
xml 复制代码
<!-- 根据数据更新频率设置 -->
<cache flushInterval="300000"/> <!-- 5分钟 -->
xml 复制代码
<!-- 不可变数据使用只读缓存 -->
<cache readOnly="true"/>
xml 复制代码
<!-- 开启日志记录 -->
<cache>
    <property name="logging" value="true"/>
</cache>

常见问题速查

问题1:二级缓存不生效

xml 复制代码
<!-- 解决方案 -->
<!-- 1. 检查全局配置 -->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

<!-- 2. 检查Mapper配置 -->
<cache/>

<!-- 3. 确保实体类实现Serializable -->
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

}

问题2:数据不一致

xml 复制代码
<!-- 解决方案:及时刷新缓存 -->
<update id="updateUser" flushCache="true">
    UPDATE t_user SET name = #{name} WHERE id = #{id}
</update>

<!-- 或设置自动刷新 -->
<cache flushInterval="60000"/>

问题3:内存溢出

xml 复制代码
<!-- 解决方案1:限制容量 -->
<cache size="512"/>

<!-- 解决方案2:使用软引用 -->
<cache eviction="SOFT"/>

<!-- 解决方案3:定时清理 -->
<cache flushInterval="3600000"/>

实战案例

场景:电商系统商品查询优化

xml 复制代码
<mapper namespace="com.shop.mapper.ProductMapper">
    <!-- 
        商品信息变化不频繁,适合二级缓存
        使用LRU淘汰策略
        设置1小时自动刷新
        容量2048,覆盖热门商品
    -->
    <cache eviction="LRU"
           flushInterval="3600000"
           size="2048"
           readOnly="false"/>

    <select id="selectById" resultType="Product">
        SELECT * FROM t_product WHERE id = #{id}
    </select>

    <!--更新操作强制刷新缓存 -->
    <update id="updateProduct" flushCache="true">
        UPDATE t_product SET price = #{price} WHERE id = #{id}
    </update>
</mapper>

八、总结

复制代码
一级缓存
✅ SqlSession级别
✅ 默认开启
✅ 增删改自动清空
✅ 适合单会话重复查询
二级缓存
✅ Mapper级别
✅ 需手动配置
✅ 跨SqlSession共享
✅ 适合读多写少场景
装饰器模式
✅ 灵活组合功能
✅ 支持多种淘汰策略
✅ 可扩展自定义实现
CacheKey机制
✅ 多元素组成
✅ 确保唯一性
✅ 精确命中判断

缓存是提升性能的利器,但也是一把双刃剑。理解MyBatis缓存的工作原理,才能在实战中游刃有余。

相关推荐
雨中飘荡的记忆10 小时前
MyBatis类型处理模块详解
java·mybatis
微爱帮监所写信寄信10 小时前
微爱帮监狱寄信写信小程序PHP底层优化框架
java·开发语言·数据库·spring·微信·php·mybatis
雨中飘荡的记忆1 天前
MyBatis反射模块详解
java·mybatis
七夜zippoe1 天前
Spring与MyBatis整合原理及事务管理
java·spring·mybatis·事务·mapper
czlczl200209251 天前
实战:基于 MyBatis-Plus 实现无感知的“数据权限”自动过滤
spring boot·mybatis
星空寻流年1 天前
c3p0连接池isClosed()异常事故分析:MyBatis版本兼容问题排查与解决
mybatis
雨中飘荡的记忆1 天前
MyBatis结果映射模块详解
java·mybatis