乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

自定义缓存

导学

本节学习目标:

  • 掌握MyBatis Cache接口的契约与实现规范
  • 能够编写自定义缓存实现并接入MyBatis
  • 理解MyBatis与Ehcache、Redis集成的配置方式
  • 认识分布式缓存场景下的序列化、过期策略与一致性挑战

定义

MyBatis内置的二级缓存基于单机内存HashMap,容量和可靠性受限于JVM堆空间。自定义缓存机制解决的核心痛点是:将MyBatis缓存层外接到企业级缓存中间件(如Ehcache、Redis),实现分布式共享、持久化存储、大容量缓存和更精细的过期控制,使缓存从开发工具升级为生产基础设施。

核心原理

Cache接口契约

任何自定义缓存都必须实现org.apache.ibatis.cache.Cache接口,该接口定义了6个核心方法:

方法说明
String getId()返回缓存的唯一标识,通常为Mapper命名空间
void putObject(Object key, Object value)将键值对存入缓存
Object getObject(Object key)根据Key获取缓存值
Object removeObject(Object key)移除指定Key的缓存项
void clear()清空整个缓存
int getSize()返回当前缓存项数量

MyBatis在创建缓存时,会通过装饰器模式层层包装:

SynchronizedCache → LoggingCache → SerializedCache → LruCache → 自定义Cache
  • SynchronizedCache:同步锁,保证线程安全
  • LoggingCache:记录命中率日志
  • SerializedCache:序列化存储(readOnly=false时)
  • LruCache:LRU淘汰策略
  • 最底层:自定义Cache实现

自定义Cache集成架构

完整示例

场景说明

乐途公司人事系统需要将所有员工数据缓存到Redis,实现多节点共享。本节先实现一个带日志打印的自定义缓存(便于教学观察),再演示Ehcache集成,最后给出Redis集成思路。

操作前的数据库表结构及初始数据

CREATE TABLE student (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    age INT,
    major VARCHAR(20),
    score DECIMAL(5,2)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);

当前数据状态:

idnameagemajorscore
1大翔22计算机科学95.50
2白歌21软件工程88.00
3小崔20计算机科学92.00
4黄俪21信息安全90.50
5李眉22软件工程87.00

完整代码与配置

示例1:自定义日志缓存实现

LoggingCache.java

package com.fly.cache;

import org.apache.ibatis.cache.Cache;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 自定义缓存:带操作日志的内存缓存
 * 用于教学演示,观察MyBatis何时put、何时get、何时clear
 */
public class LoggingCache implements Cache {
    
    private final String id;
    private final Map<Object, Object> cache = new HashMap<>();
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
    public LoggingCache(String id) {
        this.id = id;
        System.out.println("[自定义缓存] 创建缓存实例,ID=" + id);
    }
    
    @Override
    public String getId() {
        return id;
    }
    
    @Override
    public void putObject(Object key, Object value) {
        System.out.println("[自定义缓存] PUT  key=" + key.hashCode() + ", value类型=" + value.getClass().getSimpleName());
        cache.put(key, value);
    }
    
    @Override
    public Object getObject(Object key) {
        Object value = cache.get(key);
        if (value != null) {
            System.out.println("[自定义缓存] HIT  key=" + key.hashCode());
        } else {
            System.out.println("[自定义缓存] MISS key=" + key.hashCode());
        }
        return value;
    }
    
    @Override
    public Object removeObject(Object key) {
        System.out.println("[自定义缓存] REMOVE key=" + key.hashCode());
        return cache.remove(key);
    }
    
    @Override
    public void clear() {
        System.out.println("[自定义缓存] CLEAR 清空全部缓存,当前size=" + cache.size());
        cache.clear();
    }
    
    @Override
    public int getSize() {
        return cache.size();
    }
    
    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }
}

StudentMapper.xml

<?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.fly.mapper.StudentMapper">
    
    <!-- 使用自定义缓存类型 -->
    <cache type="com.fly.cache.LoggingCache"/>
    
    <select id="selectById" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score FROM student WHERE id = #{id}
    </select>
    
    <update id="updateScoreById">
        UPDATE student SET score = #{score} WHERE id = #{id}
    </update>
    
</mapper>

CustomCacheDemo.java

package com.fly.demo;

import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class CustomCacheDemo {
    public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        
        System.out.println("========== 第一次SqlSession 查询 ==========");
        SqlSession session1 = sqlSessionFactory.openSession();
        StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
        Student s1 = mapper1.selectById(4);  // 查询黄俪
        System.out.println("查询结果:" + s1.getName() + ",专业:" + s1.getMajor());
        session1.commit();
        session1.close();
        
        System.out.println("\n========== 第二次SqlSession 查询(应命中缓存)==========");
        SqlSession session2 = sqlSessionFactory.openSession();
        StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
        Student s2 = mapper2.selectById(4);  // 应命中二级缓存
        System.out.println("查询结果:" + s2.getName() + ",专业:" + s2.getMajor());
        session2.close();
        
        System.out.println("\n========== 第三次SqlSession 更新(应清空缓存)==========");
        SqlSession session3 = sqlSessionFactory.openSession();
        StudentMapper mapper3 = session3.getMapper(StudentMapper.class);
        mapper3.updateScoreById(4, 95.0);  // 更新黄俪分数
        session3.commit();  // commit触发缓存清空
        session3.close();
        
        System.out.println("\n========== 第四次SqlSession 再次查询 ==========");
        SqlSession session4 = sqlSessionFactory.openSession();
        StudentMapper mapper4 = session4.getMapper(StudentMapper.class);
        Student s4 = mapper4.selectById(4);  // 缓存已清空,重新查库
        System.out.println("查询结果:" + s4.getName() + ",分数:" + s4.getScore());
        session4.close();
    }
}

示例2:集成Ehcache

pom.xml依赖

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.1</version>
</dependency>

ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    
    <diskStore path="java.io.tmpdir"/>
    
    <defaultCache
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"/>
    
    <!-- 为学生数据单独配置缓存区 -->
    <cache name="com.fly.mapper.StudentMapper"
           maxElementsInMemory="500"
           eternal="false"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"
           overflowToDisk="true"/>
</ehcache>

StudentMapper.xml(Ehcache版)

<mapper namespace="com.fly.mapper.StudentMapper">
    <!-- 集成Ehcache -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    
    <select id="selectById" resultType="com.fly.entity.Student">
        SELECT id, name, age, major, score FROM student WHERE id = #{id}
    </select>
</mapper>

示例3:Redis集成思路

package com.fly.cache;

import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.io.*;

/**
 * Redis缓存实现思路(教学版,未完整实现连接池管理)
 */
public class RedisCache implements Cache {
    
    private final String id;
    private static JedisPool jedisPool;
    
    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(100);
        jedisPool = new JedisPool(config, "localhost", 6379, 2000, "password");
    }
    
    public RedisCache(String id) {
        this.id = id;
    }
    
    @Override
    public String getId() {
        return id;
    }
    
    @Override
    public void putObject(Object key, Object value) {
        try (Jedis jedis = jedisPool.getResource()) {
            // 使用序列化存储
            byte[] keyBytes = serialize(key);
            byte[] valueBytes = serialize(value);
            // 设置过期时间600秒
            jedis.setex(keyBytes, 600, valueBytes);
        } catch (Exception e) {
            throw new RuntimeException("Redis缓存写入失败", e);
        }
    }
    
    @Override
    public Object getObject(Object key) {
        try (Jedis jedis = jedisPool.getResource()) {
            byte[] valueBytes = jedis.get(serialize(key));
            return valueBytes != null ? deserialize(valueBytes) : null;
        } catch (Exception e) {
            throw new RuntimeException("Redis缓存读取失败", e);
        }
    }
    
    @Override
    public Object removeObject(Object key) {
        try (Jedis jedis = jedisPool.getResource()) {
            byte[] keyBytes = serialize(key);
            Object value = getObject(key);
            jedis.del(keyBytes);
            return value;
        } catch (Exception e) {
            throw new RuntimeException("Redis缓存删除失败", e);
        }
    }
    
    @Override
    public void clear() {
        try (Jedis jedis = jedisPool.getResource()) {
            // 按前缀删除该命名空间的所有缓存
            // 实际生产应使用Redis Scan或Hash结构
            jedis.del((id + ":*").getBytes());
        } catch (Exception e) {
            throw new RuntimeException("Redis缓存清空失败", e);
        }
    }
    
    @Override
    public int getSize() {
        // Redis中统计该命名空间key数量
        return 0;  // 简化示意
    }
    
    private byte[] serialize(Object object) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(object);
        oos.close();
        return baos.toByteArray();
    }
    
    private Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        Object obj = ois.readObject();
        ois.close();
        return obj;
    }
}

实际执行结果(自定义日志缓存版)

控制台输出:

[自定义缓存] 创建缓存实例,ID=com.fly.mapper.StudentMapper
========== 第一次SqlSession 查询 ==========
[自定义缓存] MISS key=-1423456789
[com.fly.mapper.StudentMapper.selectById] - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 4(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
[自定义缓存] PUT  key=-1423456789, value类型=Student
查询结果:黄俪,专业:信息安全

========== 第二次SqlSession 查询(应命中缓存)==========
[自定义缓存] HIT  key=-1423456789
查询结果:黄俪,专业:信息安全

========== 第三次SqlSession 更新(应清空缓存)==========
[com.fly.mapper.StudentMapper.updateScoreById] - ==>  Preparing: UPDATE student SET score = ? WHERE id = ?
[com.fly.mapper.StudentMapper.updateScoreById] - ==> Parameters: 95.0(Double), 4(Integer)
[com.fly.mapper.StudentMapper.updateScoreById] - <==    Updates: 1
[自定义缓存] CLEAR 清空全部缓存,当前size=1

========== 第四次SqlSession 再次查询 ==========
[自定义缓存] MISS key=-1423456789
[com.fly.mapper.StudentMapper.selectById] - ==>  Preparing: SELECT id, name, age, major, score FROM student WHERE id = ?
[com.fly.mapper.StudentMapper.selectById] - ==> Parameters: 4(Integer)
[com.fly.mapper.StudentMapper.selectById] - <==      Total: 1
[自定义缓存] PUT  key=-1423456789, value类型=Student
查询结果:黄俪,分数:95.0

分析

  1. 缓存创建:LoggingCache实例化时打印日志,ID为Mapper命名空间
  2. 第一次查询:缓存未命中(MISS),查库后PUT写入缓存,value类型为Student
  3. 第二次查询:缓存命中(HIT),无SQL输出,直接从自定义缓存获取黄俪的信息
  4. 更新操作:commit()触发clear(),打印CLEAR日志,当前缓存size=1被清空
  5. 第四次查询:缓存再次MISS,重新查库,获取更新后的分数95.0

自定义缓存让开发者完全掌控缓存的存储介质和行为,是接入外部中间件的必经之路。

易错场景与常见误区

误区正解
自定义Cache不需要处理线程安全MyBatis外层有SynchronizedCache包装,但自定义实现内部若用HashMap等结构,仍需考虑并发;建议返回ReentrantReadWriteLock
自定义缓存可以忽略getId()getId()必须返回Mapper命名空间,MyBatis靠它识别缓存归属,不可随意返回
Redis缓存不需要序列化自定义缓存接收的是Java对象,存入Redis必须序列化;且实体类需实现Serializable
clear()只需清空内存接入外部中间件时,clear()必须同步清空外部存储,否则造成数据不一致
自定义缓存比内置缓存更快网络型缓存(Redis)有IO开销,单次查询可能比内存缓存慢;优势在分布式共享和大容量

反例:未实现getReadWriteLock导致并发问题

// 反例:不实现getReadWriteLock,返回null
public class BadCache implements Cache {
    // ... 其他方法省略 ...
    
    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;  // 错误!MyBatis外层SynchronizedCache依赖此锁
    }
}

正解:始终返回有效的ReadWriteLock实例,如new ReentrantReadWriteLock()。

面试考点

Q1:实现MyBatis自定义缓存需要实现哪些方法?

必须实现Cache接口的6个方法:getId()、putObject()、getObject()、removeObject()、clear()、getSize()。此外建议实现getReadWriteLock()返回有效锁对象。

Q2:MyBatis如何集成Ehcache?

引入mybatis-ehcache依赖,在Mapper中配置<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>,并编写ehcache.xml定义缓存策略。Ehcache支持内存+磁盘两级存储,以及丰富的过期策略。

Q3:自定义Redis缓存需要注意哪些问题?

四方面:① 序列化(Java对象需转为字节数组存储);② Key设计(需包含命名空间避免冲突);③ 过期策略(利用Redis TTL替代flushInterval);④ 一致性(MyBatis的clear()需同步删除Redis中对应数据)。

Q4:MyBatis缓存装饰器有哪些?执行顺序是什么?

从外到内依次是:SynchronizedCache(线程同步)→ LoggingCache(命中率日志)→ SerializedCache(序列化,readOnly=false时)→ LruCache/FifoCache/SoftCache/WeakCache(淘汰策略)→ 自定义Cache实现。数据流从外向内到达存储层。

小结

自定义缓存是MyBatis二级缓存的"最后一公里",通过实现Cache接口,开发者可以将缓存层无缝对接到Ehcache、Redis、Caffeine等任意存储中间件。核心要点是:遵守接口契约、处理好序列化、保证clear()的一致性、合理设计Key与过期策略。自定义缓存让MyBatis从单机ORM框架进化为分布式架构中的标准数据访问层。

下一章引子

缓存是提升读性能的核心手段,但写操作的性能优化同样重要。MyBatis提供了三种Executor执行器类型——SIMPLE、REUSE、BATCH,分别对应不同的Statement创建与执行策略。下一节将深入讲解三种执行器的工作流程,并通过批量插入1000条记录的实测对比,揭示BATCH模式在写密集型场景下的巨大优势。

上一页
缓存配置详解
下一页
Executor执行器类型