MyBatis缓存机制解析:从本地到分布式缓存实践
MyBatis缓存机制深度解析:从本地缓存到分布式方案
作为MyBatis的核心特性之一,缓存机制直接影响着应用性能表现。本文将系统剖析MyBatis的一级缓存、二级缓存实现原理,并深入探讨企业级应用中如何安全高效地使用自定义缓存。
一、一级缓存:SqlSession级别的本地缓存
1.1 基本特性
一级缓存是MyBatis默认开启的本地缓存,其生命周期与SqlSession绑定:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 第一次查询,访问数据库
User user1 = session.selectOne("org.example.mapper.UserMapper.findById", 1L);
// 第二次查询相同SQL,直接从缓存获取
User user2 = session.selectOne("org.example.mapper.UserMapper.findById", 1L);
System.out.println(user1 == user2); // 输出true,同一对象引用
}
1.2 缓存失效场景
以下操作会清空当前SqlSession的一级缓存:
- DML操作:执行任意insert/update/delete语句
- 手动清除:调用
sqlSession.clearCache()
- 配置关闭:设置
localCacheScope=STATEMENT
(默认SESSION)
1.3 实践建议
- 适合读多写少的场景,如配置数据、基础信息查询
- 注意长会话可能导致内存泄漏(建议及时关闭SqlSession)
- 跨方法调用时注意缓存一致性(可考虑
@Transactional
统一管理)
二、二级缓存:Mapper级别的全局缓存
2.1 启用配置
需在Mapper XML中显式声明(注意实体类必须实现Serializable):
<!-- UserMapper.xml -->
<cache/>
或通过注解配置:
@CacheNamespace
public interface UserMapper {
//...
}
2.2 跨会话共享机制
二级缓存存储在SqlSessionFactory级别,不同SqlSession可共享:
// 会话1
try (SqlSession session1 = sqlSessionFactory.openSession()) {
User user1 = session1.selectOne("UserMapper.findById", 1L);
session1.commit(); // 必须提交才会写入二级缓存
}
// 会话2
try (SqlSession session2 = sqlSessionFactory.openSession()) {
User user2 = session2.selectOne("UserMapper.findById", 1L); // 命中二级缓存
}
2.3 缓存策略与风险
特性 | 说明 | 风险点 |
---|---|---|
跨会话共享 | 提升查询性能 | 脏读风险(需合理设置缓存刷新策略) |
序列化存储 | 要求实体类实现Serializable | 影响性能,大对象慎用 |
作用域控制 | 可配置flushInterval等参数 | 配置不当会导致数据不一致 |
2.4 实践建议
- 严格只读数据适合使用(如省份字典、系统参数)
- 高并发写入场景建议禁用(可通过
<cache-ref/>
引用空缓存) - 分布式环境需要配合集中式缓存(如Redis)解决一致性问题
三、自定义缓存:集成第三方缓存方案
3.1 Redis集成示例
添加依赖:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
配置Mapper:
<cache type="org.mybatis.caches.redis.RedisCache" eviction="LRU" flushInterval="60000" size="1024"/>
配置redis.properties:
redis.host=127.0.0.1 redis.port=6379 redis.password= redis.maxTotal=100
3.2 自定义缓存实现
实现org.apache.ibatis.cache.Cache
接口:
public class CustomRedisCache implements Cache {
private final String id;
private final RedisTemplate<String, Object> redisTemplate;
public CustomRedisCache(String id) {
this.id = id;
this.redisTemplate = SpringContextHolder.getBean("redisTemplate");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
redisTemplate.opsForValue().set(key.toString(), value);
}
@Override
public Object getObject(Object key) {
return redisTemplate.opsForValue().get(key.toString());
}
// 其他方法实现...
}
3.3 缓存方案选型对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
本地缓存 | 零网络开销,速度最快 | 单机有效,容量有限 | 单体应用高频只读数据 |
Redis | 分布式支持,数据结构丰富 | 网络延迟,需要序列化 | 分布式系统,需要数据共享 |
Ehcache | 内存+磁盘二级缓存 | 集群支持较弱 | 大数据量本地缓存 |
Caffeine | 高性能,现代缓存算法 | 无持久化功能 | 高性能本地缓存 |
四、缓存最佳实践
监控与统计:实现Cache接口的统计方法,监控缓存命中率
@Override public int getSize() { Long size = redisTemplate.execute( (RedisCallback<Long>) connection -> connection.dbSize()); return size != null ? size.intValue() : 0; }
- 多级缓存策略:
缓存雪崩防护:
// 使用Redis的SETNX实现互斥锁 public Object getObjectWithLock(Object key) { Object value = getObject(key); if (value == null) { String lockKey = key.toString() + "_lock"; if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) { try { // 查询数据库 value = queryFromDB(key); putObject(key, value); } finally { redisTemplate.delete(lockKey); } } else { // 等待重试或返回旧数据 Thread.sleep(100); return getObject(key); } } return value; }
总结
MyBatis缓存机制提供了从本地到全局的多层次缓存方案,合理使用可以显著提升系统性能。但在实际应用中需要特别注意:
- 一级缓存适合短生命周期的重复查询
- 二级缓存在分布式环境下需要额外的一致性保障
- 生产环境推荐采用Redis等集中式缓存作为二级缓存实现
- 必须建立完善的监控和失效机制保障数据一致性
通过理解这些缓存特性及适用场景,开发者可以在性能和数据一致性之间找到最佳平衡点。