1. MyBatis缓存机制概述
作为Java生态中最流行的ORM框架之一,MyBatis的缓存设计直接影响着系统性能。我在实际项目中曾遇到一个典型场景:某电商平台的商品详情接口QPS突破2000后,数据库负载突然飙升到警戒线。通过引入二级缓存优化,最终将数据库查询量降低了72%。这个案例让我深刻认识到,合理利用MyBatis缓存机制是应对高并发场景的利器。
MyBatis提供了一套完整的缓存体系,包括:
- 一级缓存(本地缓存):默认开启,作用域为SqlSession
- 二级缓存(全局缓存):需要手动配置,作用域为Mapper级别
- 自定义缓存:可通过实现Cache接口扩展
重要提示:缓存虽好但不可滥用,特别是对于财务、库存等强一致性要求高的场景,需要谨慎评估缓存策略。
2. 一级缓存深度解析
2.1 工作机制与生命周期
一级缓存是SqlSession级别的缓存,其实现原理可以用"查询签名"的概念来理解。MyBatis会为每个查询构建一个CacheKey,包含:
- Mapper方法ID(如com.example.mapper.UserMapper.selectById)
- 分页参数(offset/limit)
- SQL语句本身
- 所有参数值
- 环境ID(environment)
java复制// 示例:CacheKey核心构成
public class CacheKey implements Cloneable, Serializable {
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList; // 存储影响缓存key的因素
}
当执行查询时,MyBatis会先根据这些因素生成CacheKey,然后在本地缓存(PerpetualCache)中查找。缓存命中时直接返回结果,否则查询数据库并缓存结果。
2.2 失效场景与注意事项
在实际使用中,我发现一级缓存有几个容易踩坑的地方:
- 跨SqlSession不共享:这是新手常犯的错误。比如下面这个反例:
java复制try(SqlSession session1 = sqlSessionFactory.openSession()) {
User user1 = session1.selectOne("selectUser", 1);
}
try(SqlSession session2 = sqlSessionFactory.openSession()) {
User user2 = session2.selectOne("selectUser", 1); // 会再次查询数据库
}
-
更新操作导致清空:任何INSERT/UPDATE/DELETE操作都会清空当前SqlSession的一级缓存,这是MyBatis的默认行为。在批量操作场景下要特别注意。
-
手动清除方式:
java复制sqlSession.clearCache(); // 清除一级缓存
实战经验:对于读写比例超过10:1的场景,可以考虑将多个读操作放在同一个SqlSession中,但要注意控制Session生命周期避免内存泄漏。
3. 二级缓存全面指南
3.1 配置与启用步骤
二级缓存需要显式开启,以下是标准配置流程:
- 全局开关(可选,默认true):
xml复制<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- Mapper级别声明:
xml复制<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
</mapper>
- 实体类实现Serializable:
java复制public class User implements Serializable {
// ...
}
3.2 核心配置参数详解
二级缓存支持多种策略配置,以下是关键参数对比:
| 参数名 | 可选值 | 默认值 | 说明 |
|---|---|---|---|
| eviction | LRU/FIFO/SOFT/WEAK | LRU | 缓存淘汰策略 |
| flushInterval | 毫秒数 | 无 | 定时刷新间隔 |
| size | 正整数 | 1024 | 缓存对象数量上限 |
| readOnly | true/false | false | 是否只读 |
| blocking | true/false | false | 是否使用阻塞缓存 |
3.3 事务性缓存问题
二级缓存的一个典型陷阱是事务隔离问题。考虑以下场景:
java复制// 事务1
@Transactional
public void updateUser(User user) {
userMapper.update(user); // 更新数据库
// 此时事务未提交,但缓存已被清除
}
// 事务2
@Transactional
public User getuser(Integer id) {
// 可能读取到旧数据,因为事务1尚未提交
return userMapper.selectById(id);
}
解决方案有两种:
- 在事务提交后再访问缓存(使用TransactionCache装饰器)
- 设置localCacheScope=STATEMENT(不推荐,影响性能)
4. 自定义缓存高级实践
4.1 集成Redis缓存
当需要分布式缓存时,可以集成Redis:
- 添加依赖:
xml复制<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
- 配置Mapper:
xml复制<cache type="org.mybatis.caches.redis.RedisCache"/>
- 创建redis.properties:
properties复制host=127.0.0.1
port=6379
password=
database=0
timeout=2000
4.2 自定义缓存实现
实现自定义缓存需要三个步骤:
- 实现Cache接口:
java复制public class CustomCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new ConcurrentHashMap<>();
public CustomCache(String id) {
this.id = id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
// 实现其他接口方法...
}
- 注册缓存实现:
xml复制<cache type="com.example.cache.CustomCache"/>
- 添加缓存装饰器(可选):
java复制public class ScheduledCache implements Cache {
private final Cache delegate;
private long flushInterval;
public ScheduledCache(Cache delegate) {
this.delegate = delegate;
}
// 实现定时清理逻辑...
}
5. 缓存问题排查手册
5.1 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 查询结果不更新 | 二级缓存未刷新 | 设置flushCache="true" |
| 内存溢出 | 缓存大小不受控 | 调整size参数或使用LRU策略 |
| 分布式环境数据不一致 | 节点间缓存不同步 | 改用Redis等集中式缓存 |
| 性能反而下降 | 缓存命中率低 | 分析访问模式调整策略 |
5.2 监控与调优建议
- 开启缓存统计(需要自定义缓存实现):
java复制public class StatsCache implements Cache {
private int hits;
private int misses;
@Override
public Object getObject(Object key) {
Object value = delegate.getObject(key);
if (value != null) {
hits++;
} else {
misses++;
}
return value;
}
public double getHitRatio() {
return (double)hits / (hits + misses);
}
}
- 推荐配置原则:
- 读多写少:启用二级缓存,设置较大size
- 写多读少:禁用二级缓存或设置短flushInterval
- 分布式环境:必须使用集中式缓存
- 日志分析技巧:
xml复制<logger name="org.mybatis" level="DEBUG"/>
通过日志可以观察到:
code复制DEBUG - Cache Hit Ratio [com.example.mapper.UserMapper]: 0.75
6. 最佳实践与性能优化
在实际项目中,我总结出这些经验法则:
- 缓存粒度控制:
- 小对象(<1KB)适合全缓存
- 大对象建议缓存关键字段或DTO
- 缓存更新策略:
java复制// 更好的更新模式
public void updateProduct(Product product) {
productMapper.update(product);
// 主动清除相关缓存
cacheManager.evict("productCache", product.getId());
}
- 多级缓存架构:
code复制客户端 → CDN缓存 → 应用本地缓存 → Redis集群 → 数据库
- 缓存预热技巧:
java复制@PostConstruct
public void preloadCache() {
List<Integer> hotIds = getHotProductIds();
hotIds.forEach(id -> productMapper.selectById(id));
}
对于特别敏感的数据,可以采用"先删缓存再更新DB"的策略:
java复制public void updateWithCacheClean(Product product) {
cache.evict(product.getId());
int rows = productMapper.update(product);
if (rows == 0) {
cache.put(product.getId(), product); // 补偿
}
}
最后提醒:任何缓存方案都要有降级策略,当缓存系统故障时,要确保核心业务仍能正常工作。我在项目中通常会实现这样的回退机制:
java复制public Product getProductWithFallback(Integer id) {
try {
return productMapper.selectById(id);
} catch (CacheException e) {
log.warn("Cache failed, fallback to DB", e);
return productMapper.selectByIdFromDB(id);
}
}