全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

自定义缓存实现

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加载失败次数应检查序列化/网络问题

注意事项

  1. 构造函数参数:MyBatis 通过反射调用 Cache(String id) 构造函数,必须提供该构造函数
  2. 线程安全:自定义缓存实现必须是线程安全的,Redis 等外部缓存通常已保证
  3. 序列化:缓存对象必须可序列化,Redis 通常使用 JSON 格式
  4. Key 设计:缓存 Key 应唯一标识 SQL 查询,推荐 namespace + MD5(sql_key)
  5. 清空调用clear() 在分布式缓存中可能开销大,应考虑批量删除而非全量扫描

要点总结

  • 实现 Cache 接口即可接入任意第三方缓存系统,必须提供 Cache(String id) 构造函数
  • Redis 适合分布式部署,多实例共享缓存数据,保证一致性
  • Ehcache 支持堆外内存和磁盘缓存,适合单机大容量缓存
  • Caffeine 性能最优,但仅支持堆内内存,适合高并发单机场景
  • 分布式缓存通过共享存储解决多实例数据不一致问题
  • 缓存 Key 应按 {namespace}:{md5(sql_key)} 格式命名避免冲突
  • 建议添加缓存统计,监控命中率指导参数调优

存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\缓存机制\自定义缓存实现.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 缓存读写策略
下一篇 → Interceptor 接口实现
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库