1. MyBatis缓存机制概述
作为Java开发者,MyBatis的缓存机制是我们日常开发中必须掌握的核心知识点。缓存机制的设计初衷是为了减少数据库访问次数,提升系统性能。但在实际应用中,如果对缓存机制理解不透彻,很容易引发各种问题,比如脏数据、缓存穿透等。
MyBatis提供了一级缓存和二级缓存两种机制:
- 一级缓存是SqlSession级别的缓存,默认开启
- 二级缓存是Mapper级别的缓存,需要手动配置开启
2. 一级缓存深度解析
2.1 一级缓存工作机制
一级缓存是MyBatis默认开启的缓存机制,它的作用范围仅限于单个SqlSession内部。当同一个SqlSession中执行相同的SQL查询时,MyBatis会优先从缓存中获取结果,而不是直接访问数据库。
一级缓存的工作流程如下:
- 执行查询时,MyBatis会根据SQL语句、参数、分页等信息生成CacheKey
- 先在本地缓存(LocalCache)中查找是否有对应的缓存结果
- 如果命中缓存,直接返回结果
- 如果未命中,则查询数据库并将结果存入缓存
2.2 一级缓存配置与实验
一级缓存有两个配置级别:
- SESSION(默认):同一个SqlSession内的所有查询共享缓存
- STATEMENT:仅对当前Statement有效
<setting name="localCacheScope" value="SESSION"/>通过实验可以验证一级缓存的特性:
@Test public void testLocalCache() { SqlSession sqlSession = sqlSessionFactory.openSession(); try { UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 第一次查询,会访问数据库 User user1 = mapper.selectById(1); // 第二次查询,直接从缓存获取 User user2 = mapper.selectById(1); // user1 == user2 为true } finally { sqlSession.close(); } }2.3 一级缓存失效场景
以下操作会导致一级缓存失效:
- 执行insert/update/delete操作
- 调用SqlSession的clearCache()方法
- 配置了localCacheScope=STATEMENT
- 手动修改了数据库数据(其他SqlSession或外部程序修改)
注意:在分布式环境下,一级缓存容易引发脏读问题,因为不同节点的SqlSession无法感知彼此的修改。
3. 二级缓存全面剖析
3.1 二级缓存工作机制
二级缓存是Mapper级别的缓存,多个SqlSession可以共享同一个Mapper的二级缓存。二级缓存的工作流程如下:
- 查询时先检查二级缓存
- 如果命中则直接返回
- 未命中则查询数据库,并将结果存入二级缓存
- 执行更新操作时,清空对应的二级缓存
3.2 二级缓存配置与使用
启用二级缓存需要两步配置:
- 全局配置中开启二级缓存:
<setting name="cacheEnabled" value="true"/>- 在Mapper XML中配置缓存策略:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>3.3 二级缓存实验验证
通过实验可以验证二级缓存的特性:
@Test public void testSecondLevelCache() { SqlSession sqlSession1 = sqlSessionFactory.openSession(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); try { UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); // 第一次查询,访问数据库 User user1 = mapper1.selectById(1); sqlSession1.commit(); // 必须提交才会放入二级缓存 // 第二次查询,从二级缓存获取 User user2 = mapper2.selectById(1); // user1 == user2 为false,因为二级缓存默认是序列化存储 } finally { sqlSession1.close(); sqlSession2.close(); } }3.4 二级缓存的局限性
二级缓存存在以下问题需要注意:
- 多表关联查询时容易出现脏数据
- 分布式环境下需要额外处理缓存同步
- 缓存策略配置不当可能导致内存溢出
- 事务提交后才会更新缓存
4. 缓存源码深度分析
4.1 一级缓存源码实现
一级缓存的核心实现位于BaseExecutor类中:
public abstract class BaseExecutor implements Executor { protected PerpetualCache localCache; public <E> List<E> query(...) { // 生成CacheKey CacheKey key = createCacheKey(...); // 尝试从缓存获取 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list == null) { // 查询数据库 list = queryFromDatabase(...); // 放入缓存 localCache.putObject(key, list); } return list; } }CacheKey的生成逻辑考虑了以下因素:
- MappedStatement的ID
- 分页参数(offset, limit)
- SQL语句本身
- 参数值
- 环境ID
4.2 二级缓存源码实现
二级缓存通过装饰器模式实现,核心类为CachingExecutor:
public class CachingExecutor implements Executor { private final Executor delegate; private final TransactionalCacheManager tcm = new TransactionalCacheManager(); public <E> List<E> query(...) { // 获取MappedStatement对应的Cache Cache cache = ms.getCache(); if (cache != null) { // 检查是否需要刷新缓存 flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { // 从二级缓存获取 list = (List<E>) tcm.getObject(cache, key); if (list == null) { // 委托给底层Executor查询 list = delegate.query(...); // 放入二级缓存 tcm.putObject(cache, key, list); } } } return list; } }5. 生产环境缓存最佳实践
5.1 缓存使用建议
- 一级缓存:
- 在单机环境下可以保留默认配置
- 分布式环境下建议设置为STATEMENT级别
- 对于读写频繁的场景,考虑手动清空缓存
- 二级缓存:
- 对于读多写少的场景可以考虑使用
- 需要实现Serializable接口
- 建议设置合理的回收策略和大小限制
5.2 常见问题解决方案
- 脏数据问题:
- 多表关联查询避免使用二级缓存
- 可以考虑使用cache-ref建立缓存引用关系
- 或者手动控制缓存清除
- 缓存穿透:
- 对空结果也进行缓存
- 使用布隆过滤器预先判断
- 缓存雪崩:
- 设置不同的过期时间
- 使用多级缓存策略
5.3 性能优化技巧
- 选择合适的回收策略:
- LRU(最近最少使用)
- FIFO(先进先出)
- SOFT(软引用)
- WEAK(弱引用)
- 批量操作优化:
// 批量插入优化示例 <insert id="batchInsert" parameterType="java.util.List"> INSERT INTO user(name,age) VALUES <foreach collection="list" item="item" separator=","> (#{item.name},#{item.age}) </foreach> </insert>- 局部缓存控制:
// 指定方法不使用缓存 <select id="selectById" resultMap="userMap" useCache="false"> SELECT * FROM user WHERE id = #{id} </select> // 强制刷新缓存 <select id="selectAll" resultMap="userMap" flushCache="true"> SELECT * FROM user </select>6. 高级应用与扩展
6.1 自定义缓存实现
MyBatis缓存支持自定义实现,只需要实现Cache接口:
public class RedisCache implements Cache { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final String id; private final RedisTemplate<String, Object> redisTemplate; public RedisCache(String id) { this.id = id; // 初始化RedisTemplate } public void putObject(Object key, Object value) { redisTemplate.opsForValue().set(key.toString(), value); } public Object getObject(Object key) { return redisTemplate.opsForValue().get(key.toString()); } // 实现其他方法... }配置使用自定义缓存:
<cache type="com.example.RedisCache"/>6.2 整合第三方缓存
- 整合Ehcache:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency> <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>- 整合Redis:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency> <cache type="org.mybatis.caches.redis.RedisCache"/>6.3 多级缓存架构
对于高性能场景,可以考虑多级缓存架构:
- 一级缓存:SqlSession级别
- 二级缓存:Mapper级别
- 分布式缓存:Redis等
- 本地缓存:Caffeine等
实现思路:
- 自定义Cache实现
- 采用装饰器模式组合多种缓存
- 设置合理的缓存过期策略
- 实现缓存同步机制
7. 常见面试问题解析
7.1 基础问题
- MyBatis的一级缓存和二级缓存有什么区别?
- 作用范围不同:一级缓存是SqlSession级别,二级缓存是Mapper级别
- 配置方式不同:一级缓存默认开启,二级缓存需要显式配置
- 存储结构不同:一级缓存使用HashMap,二级缓存可配置多种实现
- 什么情况下会导致MyBatis缓存失效?
- 执行insert/update/delete操作
- 调用clearCache方法
- 配置了flushCache="true"
- 事务提交(二级缓存)
- 缓存达到上限或过期
7.2 进阶问题
- MyBatis缓存是如何解决并发问题的?
- 一级缓存:SqlSession是线程独享的,天然避免并发问题
- 二级缓存:通过SynchronizedCache装饰器实现线程安全
- 事务缓存:通过TransactionalCache管理事务期间的缓存操作
- 如何设计一个高性能的MyBatis缓存方案?
- 根据业务特点选择合适的缓存粒度
- 读多写少场景使用二级缓存
- 写多读少场景考虑禁用或缩短缓存时间
- 分布式环境使用集中式缓存
- 对热点数据实现多级缓存
7.3 实战问题
- 如何处理多表关联查询的缓存问题?
- 方案1:避免在关联查询上使用二级缓存
- 方案2:使用cache-ref建立缓存关联
- 方案3:手动控制相关缓存的清除
- 方案4:将关联查询拆分为多个单表查询
- MyBatis缓存与Spring Cache如何选择?
- MyBatis缓存:ORM层缓存,自动管理,粒度较细
- Spring Cache:业务层缓存,更灵活,支持多种实现
- 最佳实践:MyBatis缓存处理数据库访问优化,Spring Cache处理业务逻辑优化
8. 个人实践经验分享
在实际项目中使用MyBatis缓存时,我总结了以下几点经验:
缓存监控很重要:需要实时监控缓存命中率、内存占用等指标,我们使用Prometheus+Grafana搭建了监控系统。
缓存key设计:对于复杂查询,需要精心设计cacheKey,避免因为参数顺序不同导致缓存未命中。
批量操作优化:对于批量插入场景,我们重写了Executor,实现了批量操作的缓存优化。
动态缓存配置:根据系统负载动态调整缓存策略,高峰期放宽缓存限制,低谷期收紧。
故障处理:建立缓存降级机制,当缓存出现问题时可以快速切换为直接访问数据库。
一个实用的工具类示例:
public class CacheHelper { public static void clearCache(SqlSessionFactory sqlSessionFactory, Class<?> mapperClass) { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Configuration config = sqlSession.getConfiguration(); Cache cache = config.getCache(mapperClass.getName()); if (cache != null) { cache.clear(); } } } public static void clearAllCache(SqlSessionFactory sqlSessionFactory) { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { sqlSession.clearCache(); Configuration config = sqlSession.getConfiguration(); for (String name : config.getCacheNames()) { config.getCache(name).clear(); } } } }最后,关于是否在生产环境使用MyBatis缓存,我的建议是:
- 对于简单的单表CRUD,可以使用一级缓存
- 对于复杂的查询场景,建议使用专门的缓存解决方案如Redis
- 使用缓存前一定要做好充分的测试和监控