1. MyBatis缓存机制深度解析
作为Java持久层框架中的核心组件,MyBatis的缓存机制直接影响着系统性能表现。在实际项目中,我见过太多因为不当使用缓存导致的性能问题和数据一致性问题。今天我们就来彻底拆解MyBatis的缓存体系,从原理到实践,让你真正掌握这把"双刃剑"的正确用法。
MyBatis提供两级缓存结构:一级缓存(本地缓存)和二级缓存(全局缓存)。一级缓存默认开启,作用于SqlSession生命周期;二级缓存需要显式配置,可跨SqlSession共享。理解这两级缓存的实现原理和适用场景,是避免常见坑点的关键。
重要提示:MyBatis缓存虽然能提升性能,但使用不当会导致严重的脏读问题。特别是在分布式环境下,需要谨慎评估是否启用二级缓存。
1.1 一级缓存工作机制
一级缓存是SqlSession级别的缓存,其实现原理值得深入探讨。当我们执行查询时,MyBatis会创建一个CacheKey作为唯一标识,这个key由以下要素组成:
- 执行的SQL语句(包括分页参数)
- 传递给语句的参数值
- 环境ID(比如不同的数据库环境)
- 映射文件的namespace
java复制// 典型的CacheKey生成逻辑示例
public class CacheKey implements Cloneable, Serializable {
private static final int DEFAULT_MULTIPLIER = 37;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public void update(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
}
在实际项目中,我发现一级缓存有几个容易忽视的特性:
- 非线程安全:每个SqlSession独立维护自己的缓存,这意味着不同线程间的操作不会相互影响
- 自动失效机制:执行INSERT/UPDATE/DELETE操作时,对应namespace下的所有缓存会自动清除
- 作用域限制:在Spring集成环境下,如果不开启事务,每个DAO方法调用都会创建新的SqlSession,导致一级缓存失效
1.2 二级缓存实现原理
二级缓存是Mapper级别的缓存,多个SqlSession可以共享。其核心实现依赖于装饰器模式,通过Cache接口的不同实现类组合完成各种功能:
java复制// MyBatis缓存接口的核心定义
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();
}
常见的二级缓存实现包括:
- PerpetualCache:基础缓存实现,使用HashMap存储
- LruCache:基于LRU算法的缓存实现
- FifoCache:基于FIFO算法的缓存实现
- SoftCache/WeakCache:基于JVM软引用/弱引用的缓存实现
- BlockingCache:带阻塞功能的缓存实现
- TransactionalCache:支持事务的缓存实现
在配置二级缓存时,有几个关键参数需要注意:
xml复制<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
实践经验:生产环境中建议设置合理的flushInterval(如5-10分钟),避免缓存长时间不更新导致数据不一致。对于读多写少的数据,可以设置readOnly="true"提升性能。
2. 缓存配置与优化实战
2.1 一级缓存调优策略
虽然一级缓存无法关闭,但可以通过以下方式优化其使用效果:
-
合理控制SqlSession生命周期:
- 在Spring环境中,使用@Transactional注解确保业务方法内使用同一个SqlSession
- 避免在循环中频繁创建/关闭SqlSession
-
选择性清空缓存:
java复制// 手动清空一级缓存
sqlSession.clearCache();
- 利用本地会话管理:
java复制try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.REUSE)) {
// REUSE执行器会重用预处理语句
UserMapper mapper = session.getMapper(UserMapper.class);
User user1 = mapper.selectById(1);
User user2 = mapper.selectById(1); // 命中缓存
}
2.2 二级缓存高级配置
对于二级缓存,我们需要更精细的控制策略:
-
缓存回收策略选择:
- LRU(最近最少使用):适合大多数场景
- FIFO(先进先出):适合按时间顺序访问的场景
- SOFT(软引用):适合内存敏感场景
- WEAK(弱引用):适合临时缓存场景
-
缓存刷新策略:
xml复制<!-- 设置自动刷新间隔为30分钟 -->
<cache flushInterval="1800000"/>
<!-- 特定语句不缓存 -->
<select id="selectRecent" flushCache="true" useCache="false">
SELECT * FROM user ORDER BY create_time DESC LIMIT 10
</select>
- 自定义缓存实现:
java复制public class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
private JedisPool jedisPool;
public RedisCache(String id) {
this.id = id;
this.jedisPool = new JedisPool("127.0.0.1", 6379);
}
// 实现各缓存方法...
}
2.3 缓存雪崩与穿透防护
在生产环境中,我们需要特别注意缓存异常情况:
-
缓存雪崩解决方案:
- 设置不同的过期时间
- 使用互斥锁防止并发重建缓存
java复制public User getUserWithMutex(Long id) { String cacheKey = "user:" + id; User user = cache.get(cacheKey); if (user == null) { synchronized (this) { user = cache.get(cacheKey); if (user == null) { user = userMapper.selectById(id); cache.put(cacheKey, user, 60 + new Random().nextInt(30)); } } } return user; } -
缓存穿透防护:
- 布隆过滤器拦截
- 缓存空对象
java复制public User getUserWithNullCache(Long id) { String cacheKey = "user:" + id; User user = cache.get(cacheKey); if (user != null) { return user instanceof NullUser ? null : user; } user = userMapper.selectById(id); if (user == null) { cache.put(cacheKey, new NullUser(), 60); return null; } cache.put(cacheKey, user, 3600); return user; }
3. 缓存监控与问题排查
3.1 缓存命中率监控
了解缓存使用效果至关重要,我们可以通过以下方式监控:
- 日志输出缓存统计:
java复制public class CacheStatisticsInterceptor implements Interceptor {
private static final Log logger = LogFactory.getLog(CacheStatisticsInterceptor.class);
private Map<String, CacheStats> statsMap = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
String cacheId = ((MappedStatement)invocation.getArgs()[0]).getCache().getId();
CacheStats stats = statsMap.computeIfAbsent(cacheId, k -> new CacheStats());
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long end = System.currentTimeMillis();
if (result == null) {
stats.miss();
logger.debug("Cache miss for " + cacheId + ", query took " + (end - start) + "ms");
} else {
stats.hit();
logger.debug("Cache hit for " + cacheId + ", saved " + (end - start) + "ms");
}
return result;
}
// 注册拦截器
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new CacheStatisticsInterceptor());
};
}
}
- JMX暴露缓存指标:
java复制public class CacheJmxExporter implements InitializingBean {
private MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
private Map<String, Cache> caches;
@Override
public void afterPropertiesSet() throws Exception {
for (Map.Entry<String, Cache> entry : caches.entrySet()) {
ObjectName name = new ObjectName("mybatis.cache:type=Cache,name=" + entry.getKey());
mBeanServer.registerMBean(new CacheStatsMXBean(entry.getValue()), name);
}
}
}
3.2 常见问题排查指南
在实际项目中,我总结了一些典型问题及其解决方案:
-
缓存不一致问题:
- 现象:数据库已更新,但查询仍返回旧数据
- 解决方案:
- 检查是否配置了正确的flushCache行为
- 确认事务已提交(Spring中需要@Transactional)
- 对于分布式环境,考虑使用消息队列通知各节点失效缓存
-
内存泄漏问题:
- 现象:长时间运行后OOM
- 解决方案:
- 检查缓存大小配置是否合理
- 对于大对象考虑使用SoftCache
- 定期监控缓存内存占用
-
性能不升反降:
- 现象:启用缓存后响应时间变长
- 解决方案:
- 检查缓存命中率,过低则考虑调整缓存策略
- 确认缓存key生成没有性能瓶颈
- 对于复杂对象,检查序列化/反序列化开销
4. 高级应用与最佳实践
4.1 多级缓存架构设计
对于高并发系统,可以考虑设计多级缓存体系:
- 典型三级缓存架构:
- 一级:MyBatis本地缓存(SqlSession级别)
- 二级:Redis集中式缓存(应用级别)
- 三级:本地Caffeine缓存(JVM级别)
java复制public class ThreeLevelCache implements Cache {
private Cache localCache; // Caffeine
private Cache remoteCache; // Redis
private Cache mybatisCache; // 原始MyBatis缓存
@Override
public Object getObject(Object key) {
Object value = localCache.getObject(key);
if (value == null) {
value = remoteCache.getObject(key);
if (value != null) {
localCache.putObject(key, value);
} else {
value = mybatisCache.getObject(key);
if (value != null) {
remoteCache.putObject(key, value);
localCache.putObject(key, value);
}
}
}
return value;
}
}
4.2 缓存预热策略
对于关键数据,合理的预热策略可以显著提升系统启动后的性能:
- 定时任务预热:
java复制@Scheduled(cron = "0 0 6 * * ?") // 每天6点执行
public void preloadHotData() {
List<Long> hotUserIds = getHotUserIdsFromLogAnalysis();
hotUserIds.parallelStream().forEach(id -> {
userMapper.selectById(id); // 触发缓存加载
});
}
- 启动时异步预热:
java复制@EventListener(ContextRefreshedEvent.class)
public void onApplicationEvent(ContextRefreshedEvent event) {
CompletableFuture.runAsync(() -> {
// 加载热点数据到缓存
});
}
4.3 领域驱动设计中的缓存应用
在DDD架构中,缓存的应用需要特别考虑:
-
聚合根缓存策略:
- 整个聚合根作为缓存单元
- 使用版本号控制并发更新
java复制public class ProductAggregate { private Long id; private String name; private List<SKU> skus; private Long version; // 用于乐观锁 // 自定义序列化逻辑 public byte[] serialize() { // ... } public static ProductAggregate deserialize(byte[] bytes) { // ... } } -
仓储层缓存实现:
java复制public class CachedProductRepository implements ProductRepository {
private ProductRepository delegate;
private Cache cache;
@Override
public Product findById(Long id) {
String cacheKey = "product:" + id;
Product product = cache.get(cacheKey);
if (product == null) {
product = delegate.findById(id);
if (product != null) {
cache.put(cacheKey, product);
}
}
return product;
}
}
经过多年实践,我发现MyBatis缓存的最佳使用方式是:默认使用一级缓存,谨慎评估后选择性使用二级缓存,对于高性能要求场景实现自定义缓存。记住,缓存不是万能的,没有银弹,只有适合特定场景的最佳实践。
