在高并发场景下,数据库往往成为整个系统的性能瓶颈。为了缓解数据库压力,MyBatis提供了一级和二级缓存机制。然而,默认的二级缓存是基于应用内存的,在分布式部署架构下,多个节点之间的缓存数据无法同步,极易导致数据不一致的问题。将MyBatis二级缓存与Redis进行整合,通过自定义Cache接口实现分布式缓存存储,是解决这一痛点的有效方案。

MyBatis二级缓存机制原理解析与局限性分析
MyBatis的二级缓存是Mapper(命名空间)级别的缓存,它的作用域跨越了SqlSession的生命周期。当开启二级缓存后,不同的SqlSession执行同一个Mapper下的查询操作时,会优先从缓存中获取数据。MyBatis默认的二级缓存实现类是PerpetualCache,它本质上是一个基于HashMap的本地内存缓存。虽然这种机制在单机环境下能有效提升查询效率,但在现代微服务架构中却暴露出明显的局限性。
最大的痛点在于分布式环境下的数据一致性。假设我们的应用部署了A和B两个节点,当节点A执行了更新操作并清除了本地缓存时,节点B的缓存中依然存有旧数据。此时如果有请求打到节点B,就会读取到脏数据,这在金融或电商等对数据准确性要求极高的业务中是不可接受的。此外,本地内存缓存还会受到JVM堆内存大小的限制,当缓存数据量急剧膨胀时,极易引发内存溢出异常,甚至导致整个应用服务崩溃。
为了彻底解决这些问题,我们需要引入分布式缓存中间件。Redis作为目前流行的分布式缓存解决方案,不仅具备高性能的读写能力,还提供了丰富的数据结构和过期策略。通过将MyBatis的缓存数据托管至Redis,所有应用节点共享同一份缓存数据,从根本上消除了本地缓存不一致的隐患。
实现自定义Cache接口与Redis的深度整合
MyBatis的缓存机制设计得非常灵活,它提供了一个org.apache.ibatis.cache.Cache接口。只要我们实现这个接口,就可以将缓存存储逻辑接管过来。该接口主要包含获取缓存ID、存入数据、取出数据、清空缓存等核心方法。在Windows开发环境中,我们通常会将Redis安装在本机,其配置文件一般位于C:\Program Files\Redis\redis.windows.conf。确保Redis服务正常启动后,我们就可以开始编写自定义的缓存实现类了。
在编写实现类时,我们需要注入RedisTemplate来操作Redis。由于MyBatis实例化Cache对象时无法直接使用Spring的依赖注入,我们需要通过Spring上下文工具类来获取RedisTemplate的Bean。另外,缓存对象必须实现序列化接口,因为数据需要通过网络传输到Redis服务器。为了处理复杂的对象序列化,建议配置Jackson2JsonRedisSerializer作为值的序列化器。
package com.example.cache;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RedisCache implements Cache {
private final String id;
private RedisTemplate<Object, Object> redisTemplate;
// 读写锁保证并发安全
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
// 获取RedisTemplate实例
redisTemplate = SpringContextHolder.getBean("redisTemplate");
// 设置缓存数据,并指定过期时间防止内存堆积
redisTemplate.opsForValue().set(key, value, 3600, java.util.concurrent.TimeUnit.SECONDS);
}
@Override
public Object getObject(Object key) {
redisTemplate = SpringContextHolder.getBean("redisTemplate");
return redisTemplate.opsForValue().get(key);
}
@Override
public Object removeObject(Object key) {
redisTemplate = SpringContextHolder.getBean("redisTemplate");
return redisTemplate.delete(key);
}
@Override
public void clear() {
redisTemplate = SpringContextHolder.getBean("redisTemplate");
// 使用通配符删除当前Mapper命名空间下的所有缓存
Set<Object> keys = redisTemplate.keys("*" + this.id + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
}
@Override
public int getSize() {
redisTemplate = SpringContextHolder.getBean("redisTemplate");
Long size = redisTemplate.getConnectionFactory().getConnection().dbSize();
return size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return this.readWriteLock;
}
}上述代码中,我们通过构造函数接收Mapper的命名空间作为缓存ID。在putObject方法中,我们不仅将数据存入Redis,还设置了过期时间,这是非常重要的一个优化点。如果只存不删,Redis的内存会无限增长。在clear方法中,我们利用了Redis的keys命令配合通配符来批量删除当前命名空间下的所有缓存键,确保在执行更新或删除操作时,相关的查询缓存能被有效清空。
整合配置与分布式环境下的缓存一致性保障
编写完自定义缓存类后,我们需要在MyBatis的配置文件中启用它。假设我们的项目配置文件位于D:\workspace\mybatis-redis\src\main\resources\mybatis-config.xml,我们需要在对应的Mapper映射文件中添加<cache>标签,并指定type为我们刚才编写的RedisCache类。同时,为了确保缓存数据的准确性,还需要在实体类映射配置中关闭方法级别的延迟加载,因为延迟加载可能会与二级缓存的序列化机制产生冲突。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存,指定自定义的RedisCache实现类 -->
<cache type="com.example.cache.RedisCache">
<property name="eviction" value="LRU"/>
<property name="flushInterval" value="60000"/>
<property name="size" value="1024"/>
<property name="readOnly" value="false"/>
</cache>
<!-- 查询语句配置使用缓存 -->
<select id="selectUserById" parameterType="int" resultType="com.example.entity.User" useCache="true">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>在分布式环境下,虽然Redis解决了多节点缓存共享的问题,但仍然存在一些极端情况下的不一致性。例如,当节点A执行更新数据库操作并调用clear方法清除Redis缓存时,如果在清除操作完成之前,节点B恰好从Redis中读取到了尚未被清除的旧数据,依然会发生短暂的脏读现象。为了缓解这个问题,可以在业务层引入分布式锁,或者在更新数据库时采用延迟双删策略。
此外,对于缓存穿透和缓存击穿问题,单纯依靠MyBatis的二级缓存机制是无法完全解决的。我们还需要在Redis层面配置空值缓存,或者使用布隆过滤器来拦截非法的查询请求。在实际的Windows开发联调过程中,可以通过查看Redis客户端工具中的键值对变化,来验证自定义Cache接口的各个方法是否被正确触发。只有将MyBatis的缓存生命周期与Redis的存储机制完美融合,才能构建出既高效又稳定的高并发系统架构。
MyBatis二级缓存Redis整合自定义Cache接口修改时间:2026-08-20 18:35:35