自定义缓存实现
MyBatis 默认的二级缓存是基于内存的本地缓存,无法满足分布式部署、持久化存储等需求。通过实现 Cache 接口,可以灵活集成 Redis、Ehcache、Caffeine 等第三方缓存系统。
Cache 接口定义
MyBatis 提供了 org.apache.ibatis.cache.Cache 接口,自定义缓存只需实现该接口:
Java
public interface Cache {
/**
* 获取缓存唯一标识
* @return 缓存 ID(通常为 namespace)
*/
String getId();
/**
* 放入缓存
* @param key 缓存键
* @param value 缓存值
*/
void putObject(Object key, Object value);
/**
* 从缓存获取
* @param key 缓存键
* @return 缓存值,不存在返回 null
*/
Object getObject(Object key);
/**
* 从缓存删除
* @param key 缓存键
* @return 被删除的值
*/
Object removeObject(Object key);
/**
* 清空缓存
*/
void clear();
/**
* 获取缓存大小
* @return 缓存中存储的对象数量
*/
int getSize();
/**
* 获取读写锁(可选实现)
* @return 读写锁实例
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
接口方法说明
| 方法 | 调用时机 | 说明 |
|---|---|---|
getId() | 初始化时 | 返回 namespace 作为缓存标识 |
putObject() | 查询后写入缓存 | 存储查询结果 |
getObject() | 查询前读取缓存 | 命中缓存直接返回 |
removeObject() | 删除操作时 | 删除指定缓存项 |
clear() | 写操作后/刷新间隔 | 清空全部缓存 |
getSize() | 统计时 | 返回缓存项数量 |
getReadWriteLock() | 并发访问时 | 提供并发控制 |
集成 Redis 缓存
1. 引入依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
2. 实现 Redis 缓存类
Java
public class RedisCache implements Cache {
private final String id;
private final RedisTemplate<Object, Object> redisTemplate;
private final ObjectMapper objectMapper;
public RedisCache(String id) {
this.id = id;
// 从 Spring 容器获取 Bean
this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
this.objectMapper = SpringContextHolder.getBean(ObjectMapper.class);
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
String cacheKey = generateCacheKey(key);
try {
String json = objectMapper.writeValueAsString(value);
redisTemplate.opsForValue().set(cacheKey, json, 30, TimeUnit.MINUTES);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize cache value", e);
}
}
@Override
public Object getObject(Object key) {
String cacheKey = generateCacheKey(key);
String json = (String) redisTemplate.opsForValue().get(cacheKey);
if (json == null) {
return null;
}
try {
return objectMapper.readValue(json, Object.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize cache value", e);
}
}
@Override
public Object removeObject(Object key) {
String cacheKey = generateCacheKey(key);
Object value = getObject(key);
redisTemplate.delete(cacheKey);
return value;
}
@Override
public void clear() {
// 扫描所有匹配的 key 并删除
Set<Object> keys = redisTemplate.keys(id + ":*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
@Override
public int getSize() {
Set<Object> keys = redisTemplate.keys(id + ":*");
return keys == null ? 0 : keys.size();
}
/**
* 生成 Redis 缓存 Key
* 格式: namespace:md5(sql_cache_key)
*/
private String generateCacheKey(Object key) {
return id + ":" + DigestUtils.md5Hex(key.toString());
}
}
3. Spring 上下文工具类
Java
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext ctx) {
applicationContext = ctx;
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name, clazz);
}
}
4. Mapper 中配置
XML
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 使用自定义 Redis 缓存 -->
<cache type="com.example.cache.RedisCache"/>
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
集成 Ehcache 缓存
1. 引入依赖
XML
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.8</version>
</dependency>
2. 实现 Ehcache 缓存类
Java
public class EhcacheCache implements Cache {
private final String id;
private final CacheManager cacheManager;
private final org.ehcache.Cache<Object, Object> ehcache;
public EhcacheCache(String id) {
this.id = id;
this.cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
this.ehcache = cacheManager.createCache(id,
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Object.class, Object.class,
ResourcePoolsBuilder.heap(1000) // 堆内存最多 1000 条
.offheap(10, MemoryUnit.MB) // 堆外内存 10MB
)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(
Duration.ofMinutes(30))) // TTL 30 分钟
.build());
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
ehcache.put(key, value);
}
@Override
public Object getObject(Object key) {
return ehcache.get(key);
}
@Override
public Object removeObject(Object key) {
Object value = ehcache.get(key);
ehcache.remove(key);
return value;
}
@Override
public void clear() {
ehcache.clear();
}
@Override
public int getSize() {
return (int) ehcache.getRuntimeConfiguration().getCacheStoreBindingStatistics().getSize();
}
}
3. Mapper 配置
XML
<mapper namespace="com.example.mapper.OrderMapper">
<!-- 使用 Ehcache 缓存 -->
<cache type="com.example.cache.EhcacheCache"/>
</mapper>
集成 Caffeine 缓存
1. 引入依赖
XML
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
2. 实现 Caffeine 缓存类
Java
public class CaffeineCache implements Cache {
private final String id;
private final com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache;
public CaffeineCache(String id) {
this.id = id;
this.caffeineCache = Caffeine.newBuilder()
.maximumSize(1024) // 最大容量 1024
.expireAfterWrite(30, TimeUnit.MINUTES) // 写入后 30 分钟过期
.recordStats() // 开启统计
.build();
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
caffeineCache.put(key, value);
}
@Override
public Object getObject(Object key) {
return caffeineCache.getIfPresent(key);
}
@Override
public Object removeObject(Object key) {
Object value = caffeineCache.getIfPresent(key);
caffeineCache.invalidate(key);
return value;
}
@Override
public void clear() {
caffeineCache.invalidateAll();
}
@Override
public int getSize() {
return (int) caffeineCache.estimatedSize();
}
}
第三方缓存对比
| 缓存 | 分布式 | 持久化 | 内存占用 | 适用场景 | 复杂度 |
|---|---|---|---|---|---|
| Redis | 是 | 是 | 独立服务 | 分布式、高并发 | 中(需部署) |
| Ehcache | 可选(Terracotta) | 是(磁盘缓存) | 堆内+堆外 | 单机大缓存 | 中 |
| Caffeine | 否 | 否 | 堆内 | 高性能单机缓存 | 低 |
分布式缓存模式
多节点共享缓存
分布式部署下,每个应用实例都有独立的本地缓存,导致数据不一致。使用 Redis 等分布式缓存可以解决:
XML
┌──────────┐ ┌──────────┐
│ App 1 │ │ App 2 │
│ Local: A │ │ Local: B │ ← 本地缓存不一致
└────┬─────┘ └────┬─────┘
│ │
└───────┬───────┘
│
┌──────▼──────┐
│ Redis │ ← 共享缓存,数据一致
│ Cache │
└─────────────┘
配置示例
Java
<!-- 所有 Mapper 共享同一个 Redis 缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<cache type="com.example.cache.RedisCache"/>
</mapper>
<mapper namespace="com.example.mapper.OrderMapper">
<cache type="com.example.cache.RedisCache"/>
</mapper>
缓存 Key 命名规范
为避免不同 Mapper 的缓存冲突,Redis Key 应按以下格式命名:
text
格式: {namespace}:{md5(sql_key)}
示例:
com.example.mapper.UserMapper:a3f8b9c1...
com.example.mapper.OrderMapper:e7d2f4a6...
缓存统计与监控
text
public class CaffeineCache implements Cache {
// ...
// 打印缓存统计
public void printStats() {
CacheStats stats = caffeineCache.stats();
System.out.println("命中次数: " + stats.hitCount());
System.out.println("未命中次数: " + stats.missCount());
System.out.println("命中率: " + stats.hitRate());
System.out.println("加载成功次数: " + stats.loadSuccessCount());
System.out.println("加载失败次数: " + stats.loadFailureCount());
}
}
| 统计指标 | 说明 | 优化建议 |
|---|---|---|
| hitRate | 缓存命中率 | 低于 50% 说明缓存设计不合理 |
| evictionCount | 被淘汰的缓存项数 | 过大说明 size 设置太小 |
| loadFailureCount | 加载失败次数 | 应检查序列化/网络问题 |
注意事项
- 构造函数参数:MyBatis 通过反射调用
Cache(String id)构造函数,必须提供该构造函数- 线程安全:自定义缓存实现必须是线程安全的,Redis 等外部缓存通常已保证
- 序列化:缓存对象必须可序列化,Redis 通常使用 JSON 格式
- Key 设计:缓存 Key 应唯一标识 SQL 查询,推荐 namespace + MD5(sql_key)
- 清空调用:
clear()在分布式缓存中可能开销大,应考虑批量删除而非全量扫描
要点总结
- 实现
Cache接口即可接入任意第三方缓存系统,必须提供Cache(String id)构造函数 - Redis 适合分布式部署,多实例共享缓存数据,保证一致性
- Ehcache 支持堆外内存和磁盘缓存,适合单机大容量缓存
- Caffeine 性能最优,但仅支持堆内内存,适合高并发单机场景
- 分布式缓存通过共享存储解决多实例数据不一致问题
- 缓存 Key 应按
{namespace}:{md5(sql_key)}格式命名避免冲突 - 建议添加缓存统计,监控命中率指导参数调优
存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\缓存机制\自定义缓存实现.md
📝 发现内容有误?点击此处直接编辑