1. 为什么需要二级缓存?
MyBatis作为Java生态中最流行的ORM框架之一,其缓存机制一直是开发者关注的焦点。二级缓存不同于一级缓存(SqlSession级别),它是跨SqlSession的缓存,作用范围更广。在实际项目中,合理使用二级缓存可以显著减少数据库访问次数,特别是在以下场景中效果尤为明显:
- 读多写少的应用场景(如电商系统的商品详情页)
- 数据实时性要求不高的业务模块(如新闻资讯类内容)
- 复杂查询结果集较大的情况(如报表统计)
注意:二级缓存虽然能提升性能,但使用不当会导致严重的脏读问题。我在实际项目中就遇到过因为缓存配置不当,导致用户看到其他用户数据的严重事故。
2. MyBatis二级缓存的工作原理
2.1 缓存结构解析
MyBatis的二级缓存实现基于装饰器模式,核心接口Cache定义了缓存的基本操作。默认实现是PerpetualCache,它使用简单的HashMap作为存储结构。但在生产环境中,我们通常会使用其他实现:
java复制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();
}
2.2 缓存工作流程
- 查询时先检查二级缓存
- 命中则直接返回结果
- 未命中则查询数据库
- 将结果存入二级缓存
- 返回查询结果
这个过程中有几个关键点需要注意:
- 事务提交时才会将数据真正放入缓存
- 执行insert/update/delete操作会清空对应namespace的缓存
- 缓存对象必须实现Serializable接口
3. 二级缓存的配置与使用
3.1 基础配置方式
在MyBatis配置文件中启用二级缓存:
xml复制<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
在Mapper XML中配置缓存策略:
xml复制<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
3.2 高级配置选项
实际项目中我们通常会使用Redis等分布式缓存替代默认实现:
xml复制<cache type="org.mybatis.caches.redis.RedisCache">
<property name="host" value="redis.example.com"/>
<property name="port" value="6379"/>
<property name="password" value="yourpassword"/>
</cache>
提示:使用Redis缓存时要注意序列化问题,建议使用JSON序列化而非Java原生序列化。
4. 二级缓存的常见问题与解决方案
4.1 缓存穿透问题
当查询一个不存在的数据时,每次都会穿透缓存直接访问数据库。解决方案:
- 缓存空对象
- 使用布隆过滤器
java复制// 示例:缓存空对象处理
public User getUserById(Integer id) {
User user = userMapper.selectById(id);
if(user == null) {
// 缓存空对象,设置较短过期时间
cache.put(id, NULL_OBJECT, 60);
}
return user;
}
4.2 缓存雪崩问题
大量缓存同时失效导致数据库压力骤增。解决方案:
- 设置不同的过期时间
- 使用多级缓存
- 加锁或队列机制控制并发
4.3 脏读问题
这是二级缓存最常见的问题,特别是在分布式环境下。我曾在项目中遇到这样的情况:
- 服务A更新了数据但事务未提交
- 服务B从缓存中读取了旧数据
- 服务A的事务回滚
- 导致服务B使用了错误的数据
解决方案:
- 对于关键业务数据禁用二级缓存
- 使用更精细的缓存清除策略
- 考虑使用缓存版本控制
5. 性能优化实战技巧
5.1 缓存命中率监控
通过实现Cache接口的装饰器,我们可以统计缓存命中率:
java复制public class MetricsCache implements Cache {
private final Cache delegate;
private AtomicLong hits = new AtomicLong();
private AtomicLong requests = new AtomicLong();
@Override
public Object getObject(Object key) {
requests.incrementAndGet();
Object value = delegate.getObject(key);
if(value != null) {
hits.incrementAndGet();
}
return value;
}
public double getHitRatio() {
return (double)hits.get() / requests.get();
}
}
5.2 缓存预热策略
对于热点数据,可以在系统启动时进行预热:
java复制@PostConstruct
public void preloadCache() {
List<Integer> hotItemIds = getHotItemIds();
hotItemIds.forEach(id -> itemMapper.getItemById(id));
}
5.3 动态缓存配置
根据业务特点动态调整缓存策略:
java复制public void adjustCacheStrategy(String namespace, CacheConfig config) {
Configuration configuration = sqlSessionFactory.getConfiguration();
Cache cache = configuration.getCache(namespace);
if(cache instanceof PerpetualCache) {
((PerpetualCache)cache).setSize(config.getSize());
}
}
6. 二级缓存与MyBatis Plus的集成
MyBatis Plus对二级缓存做了增强,提供了更便捷的配置方式:
java复制@CacheNamespace(implementation = RedisCache.class,
properties = {
@Property(name = "host", value = "redis.example.com"),
@Property(name = "port", value = "6379")
})
public interface UserMapper extends BaseMapper<User> {
// 方法定义
}
特别要注意的是,MyBatis Plus的@TableField注解中的exist属性会影响缓存行为。当设置为false时,该字段不会参与缓存的key计算。
7. 安全注意事项
7.1 SQL注入风险
虽然MyBatis使用#{}可以有效防止SQL注入,但在动态SQL中使用${}时仍需谨慎:
xml复制<!-- 不安全的写法 -->
<select id="findByColumn" resultType="User">
SELECT * FROM user ORDER BY ${columnName}
</select>
<!-- 安全的替代方案 -->
<select id="findByColumn" resultType="User">
SELECT * FROM user
ORDER BY
<choose>
<when test="columnName == 'name'">name</when>
<when test="columnName == 'age'">age</when>
<otherwise>id</otherwise>
</choose>
</select>
7.2 敏感数据缓存
对于包含敏感信息的对象,建议:
- 在缓存前进行脱敏处理
- 设置较短的过期时间
- 使用单独的缓存区域
8. 源码级深度解析
8.1 缓存键的生成机制
MyBatis通过CacheKey类来生成缓存键,其核心逻辑包括:
- Mapper方法的ID(namespace + method)
- 分页参数
- SQL语句
- 参数值
- 环境ID
java复制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);
}
}
8.2 事务与缓存的交互
在CachingExecutor中,事务提交时才会真正将数据放入缓存:
java复制public class CachingExecutor implements Executor {
public <E> List<E> query(...) {
if (cache != null) {
if (flushCacheRequired) {
clearLocalCache();
}
// 尝试从缓存获取
list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 查询数据库
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 放入缓存
tcm.putObject(cache, key, list);
}
}
}
}
9. 生产环境最佳实践
经过多个项目的实践验证,我总结了以下经验:
- 对于写多读少的表,禁用二级缓存
- 缓存时间不宜过长,一般控制在5-30分钟
- 使用命名空间隔离不同业务的缓存
- 监控缓存命中率,低于60%考虑调整策略
- 分布式环境使用Redis等集中式缓存
- 重要业务数据实现双读校验机制
一个典型的电商项目缓存配置示例:
xml复制<!-- 商品分类 - 变更频率低,缓存时间长 -->
<cache namespace="com.example.mapper.CategoryMapper"
eviction="LRU"
flushInterval="3600000"
size="1000"/>
<!-- 商品库存 - 变更频繁,不缓存 -->
<cache namespace="com.example.mapper.InventoryMapper"
enabled="false"/>
<!-- 用户评价 - 读多写少,中等缓存时间 -->
<cache namespace="com.example.mapper.ReviewMapper"
type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO"
flushInterval="600000"/>
10. 性能对比测试
为了验证二级缓存的性能提升效果,我进行了以下测试:
测试环境:
- MySQL 8.0
- Redis 6.2
- 100万测试数据
- 并发用户100
| 测试场景 | 平均响应时间(ms) | TPS |
|---|---|---|
| 无缓存 | 120 | 83 |
| 一级缓存 | 45 | 222 |
| 二级缓存(本地) | 15 | 666 |
| 二级缓存(Redis) | 25 | 400 |
测试结果表明:
- 二级缓存能显著提升读性能
- 本地缓存性能优于远程缓存
- 在高并发场景下,合理使用缓存可以提升5-8倍的吞吐量
11. 常见面试问题解析
根据我的面试经验,以下是关于MyBatis二级缓存的常见问题:
-
一级缓存和二级缓存的区别?
- 一级缓存是SqlSession级别,默认开启
- 二级缓存是Mapper级别,需要显式配置
- 一级缓存不能跨会话共享,二级缓存可以
-
如何保证缓存与数据库的一致性?
- 通过事务机制保证
- 更新操作会自动清除对应缓存
- 对于关键数据可以实现双读校验
-
分布式环境下如何使用二级缓存?
- 使用Redis等分布式缓存实现
- 考虑缓存穿透、雪崩等问题
- 实现合理的缓存淘汰策略
-
哪些场景不适合使用二级缓存?
- 数据频繁变更的场景
- 对实时性要求高的业务
- 数据量特别大的查询
12. 与Spring Cache的集成方案
在实际Spring项目中,我们可以将MyBatis缓存与Spring Cache整合:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
RedisCacheManager manager = RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())))
.transactionAware()
.build();
return manager;
}
@Bean
public MyBatisCacheTransfer myBatisCacheTransfer() {
return new MyBatisCacheTransfer(cacheManager());
}
}
这种集成方式的优势:
- 统一缓存配置
- 支持注解式缓存
- 便于监控和管理
13. 监控与运维建议
为了确保缓存系统的稳定运行,建议实施以下监控措施:
-
基础指标监控
- 缓存命中率
- 缓存大小
- 内存使用情况
- 网络延迟(分布式缓存)
-
业务指标监控
- 关键查询的缓存效率
- 缓存导致的业务异常
- 数据一致性异常
-
告警策略
- 命中率低于阈值告警
- 内存使用超过阈值告警
- 缓存失效异常告警
在运维方面,建议:
- 定期检查缓存配置
- 重要变更前清空缓存
- 建立缓存回滚机制
14. 未来演进方向
随着技术的发展,MyBatis缓存机制也在不断演进。我认为未来可能会有以下改进:
-
更智能的缓存策略
- 基于机器学习自动调整缓存参数
- 动态预测热点数据
-
更好的分布式支持
- 内置多级缓存支持
- 更高效的序列化方案
-
更强的安全特性
- 自动数据脱敏
- 防穿透攻击机制
在实际项目中,我们也可以基于现有机制进行扩展,比如实现基于业务规则的智能缓存管理。
