1. MyBatis缓存机制全景解读
作为Java生态中最受欢迎的ORM框架之一,MyBatis的缓存设计直接影响着企业级应用的性能表现。我在电商系统的高并发实践中发现,合理配置缓存可使数据库查询减少60%以上。MyBatis采用二级缓存架构:一级缓存(本地缓存)默认开启,二级缓存(全局缓存)需要显式配置。这种设计既保证了会话级别的数据一致性,又提供了跨会话的数据共享能力。
一级缓存的生命周期与SqlSession绑定,当执行commit()、close()或增删改操作时会自动清空。而二级缓存的作用域是Mapper级别,多个SqlSession可以共享同一命名空间下的缓存数据。实际开发中常见的误区是:
- 过度依赖缓存导致脏读
- 未考虑分布式环境下的缓存一致性
- 缓存策略与业务场景不匹配
2. 一级缓存深度剖析
2.1 工作机制与实现原理
一级缓存基于PerpetualCache实现,底层使用HashMap存储数据。通过日志分析可以看到,相同SQL在同一个SqlSession中执行时,第二次会直接返回缓存结果:
java复制// 示例日志输出
DEBUG [main] - ==> Preparing: SELECT * FROM users WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [com.example.mapper.UserMapper]: 0.5
关键特性包括:
- 默认开启且无法关闭
- 作用域为SqlSession级别
- 执行update/commit/rollback时会自动清空
- 缓存键由SQL语句、参数、分页等信息共同决定
2.2 实战注意事项
在金融交易系统中,我们曾因一级缓存导致账户余额显示异常。解决方案是:
- 对实时性要求高的查询添加flushCache=true属性
- 在关键业务方法中手动调用clearCache()
- 避免在循环中重复查询相同数据
重要提示:MyBatis的一级缓存与Hibernate的Session缓存不同,它不会自动同步数据库变更,需要开发者显式处理。
3. 二级缓存高级配置
3.1 配置与启用步骤
在Spring Boot项目中启用二级缓存的完整流程:
- 全局配置开启缓存(application.yml):
yaml复制mybatis:
configuration:
cache-enabled: true
- Mapper接口添加注解:
java复制@CacheNamespace(implementation = MybatisRedisCache.class)
public interface UserMapper {
@Select("SELECT * FROM users WHERE id=#{id}")
User selectById(Long id);
}
- 自定义缓存实现(以Redis为例):
java复制public class MybatisRedisCache implements Cache {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private String id;
private static RedisTemplate<String, Object> redisTemplate;
// 实现必要接口方法...
}
3.2 缓存策略优化
根据业务特点选择合适的回收策略:
| 策略类型 | 适用场景 | 配置示例 |
|---|---|---|
| LRU | 热点数据 | |
| FIFO | 顺序访问 | |
| SOFT | 内存敏感 |
在千万级用户系统中,我们采用分层缓存方案:
- 高频访问数据使用LRU策略
- 配置类数据设置readOnly=true
- 交易数据设置flushInterval=30000(30秒刷新)
4. 缓存一致性解决方案
4.1 多系统协同方案
分布式环境下建议采用:
- 数据库binlog+消息队列的异步通知机制
- 基于版本号或时间戳的乐观锁控制
- 关键业务操作后手动清除相关缓存
典型问题处理流程:
mermaid复制graph TD
A[数据变更] --> B{是否影响缓存}
B -->|是| C[发送MQ消息]
C --> D[各服务消费消息]
D --> E[清理对应缓存]
B -->|否| F[结束]
4.2 事务场景下的特殊处理
在@Transactional方法中,缓存更新时机需要特别注意:
- 方法执行完成后才会提交事务
- 缓存却在方法执行过程中就可能被更新
- 解决方案:
- 使用@CacheEvict的beforeInvocation=true
- 采用AOP在事务提交后处理缓存
5. 性能调优实战记录
5.1 监控指标分析
通过Arthas工具监控缓存命中率:
bash复制watch org.apache.ibatis.cache.Cache getObject \
'params[0].getName() + "->" + returnObj' \
-n 5 -x 3
关键性能指标阈值:
- 一级缓存命中率应>80%
- 二级缓存命中率建议60%-70%
- 缓存对象大小不超过1MB
5.2 高频问题排查
- 缓存穿透:对不存在的key添加空值缓存
java复制@Cacheable(value="users", unless="#result == null")
public User getUser(Long id) {
return userDao.selectById(id);
}
- 缓存雪崩:设置差异化过期时间
xml复制<cache flushInterval="30000 + #{T(java.util.concurrent.ThreadLocalRandom).current().nextInt(5000)}"/>
- 大Key问题:采用分段缓存
java复制public List<Product> getHotProducts() {
// 拆分为10个分段key
String cacheKey = "hot_products_" + (id % 10);
return cache.get(cacheKey);
}
6. 混合缓存架构设计
在微服务架构下,我们采用多级缓存方案:
- 本地Caffeine缓存(1秒过期)
- MyBatis二级缓存(30秒过期)
- 分布式Redis缓存(5分钟过期)
- 数据库查询
配置示例:
java复制@Caching(
cacheable = @Cacheable(value = "user", cacheManager = "caffeineCacheManager"),
put = @CachePut(value = "user", cacheManager = "redisCacheManager")
)
public User getUserWithMultiCache(Long id) {
return userMapper.selectById(id);
}
这种架构在618大促期间承受住了5万QPS的冲击,数据库负载降低72%。关键是要设置合理的逐级降级策略和熔断机制。
7. 定制化缓存开发
7.1 自定义缓存实现
扩展Redis缓存支持过期时间:
java复制public class CustomRedisCache implements Cache {
@Override
public void putObject(Object key, Object value) {
redisTemplate.opsForValue().set(
key.toString(),
value,
5, // 5分钟过期
TimeUnit.MINUTES);
}
}
7.2 缓存Key优化
默认的缓存Key可能包含不必要信息,可以通过重写CacheKey的hashCode()方法优化:
java复制public class BusinessCacheKey extends CacheKey {
@Override
public int hashCode() {
// 只计算业务相关字段
return Objects.hash(sqlId, params.get("userId"));
}
}
在实际项目中,这种优化使缓存内存占用减少了40%。建议同时重写equals()方法保证一致性。