MyBatis缓存模块详解

75 阅读8分钟

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


一、初识MyBatis缓存

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

缓存的价值何在?

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

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

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

性能提升: 近1000倍!

MyBatis的两级防线

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

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

二、缓存架构

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

Cache接口:缓存的灵魂

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

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:万丈高楼平地起

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

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中实现:

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由多个元素组成,确保唯一性:

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

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

@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. 关闭会话
@Override
public int update(MappedStatement ms, Object parameter) {
    clearLocalCache(); //清空缓存
    return doUpdate(ms, parameter);
}
session.clearCache(); //主动清理
session.commit(); //提交时清空
session.rollback(); //回滚时清空
session.close(); //会话结束,缓存消失

实战演示

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中添加配置:

<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/WEAKLRU
flushInterval刷新间隔(毫秒)任意正整数不刷新
size缓存容量任意正整数1024
readOnly是否只读true/falsefalse
blocking是否阻塞true/falsefalse

四大淘汰策略

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

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

<cache eviction="FIFO"/>

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

<cache eviction="SOFT"/>

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

<cache eviction="WEAK"/>

弱引用,GC时即可回收

CachingExecutor:二级缓存的指挥官

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:事务缓存管家

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

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();
    }
}

跨会话共享示例

//会话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();

五、缓存命中流程

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

完整查询链路

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

源码实现

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:智能淘汰

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:定时清理

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:深拷贝保护

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:线程安全卫士

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);
    }
}

装饰器链的构建

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;
}

七、最佳实践

推荐做法

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

避免做法

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

性能优化技巧

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

常见问题速查

问题1:二级缓存不生效

<!-- 解决方案 -->
<!-- 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:数据不一致

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

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

问题3:内存溢出

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

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

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

实战案例

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

<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缓存的工作原理,才能在实战中游刃有余。