欢迎关注微信公众号 「思客潘」

MyBatis 二级缓存详解

1. 概述

1.1 什么是MyBatis二级缓存?

MyBatis二级缓存是Mapper级别(或namespace级别)的缓存,多个SqlSession可以共享。它在应用生命周期内有效,可以跨越多个SqlSession。

1.2 与一级缓存的区别

特性 一级缓存 二级缓存
作用域 SqlSession级别 Mapper级别
生命周期 SqlSession生命周期 应用生命周期
共享性 不能跨SqlSession共享 多个SqlSession共享
存储位置 进程内存 可配置(内存、Redis等)
默认状态 开启,无法关闭 默认关闭,需手动开启

2. 工作原理

2.1 缓存结构和工作流程

缓存命中

缓存未命中

缓存命中

SqlSession1查询数据

查询二级缓存

返回缓存数据

查询数据库

数据存入二级缓存

返回数据

SqlSession2查询相同数据

查询二级缓存

直接从缓存返回

执行增删改操作

清空对应namespace缓存

缓存失效,下次查询重新获取

2.2 缓存数据流向

查询流程:
1. 先查询二级缓存
2. 二级缓存没有,查询一级缓存
3. 一级缓存没有,查询数据库
4. 查询结果存入一级缓存
5. 事务提交时,一级缓存数据同步到二级缓存

数据更新:
1. 执行增删改操作
2. 清空对应namespace的二级缓存
3. 清空当前SqlSession的一级缓存

2.3 缓存键的生成

二级缓存的缓存键生成规则与一级缓存相同:

  • Mapper Id + Offset + Limit + SQL + 参数 + Environment

3. 配置和使用

3.1 全局开启二级缓存

<!-- mybatis-config.xml -->
<configuration>
<settings>
<!-- 开启全局二级缓存,默认值为true -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>

3.2 Mapper级别配置

3.2.1 XML方式配置
<!-- UserMapper.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.example.mapper.UserMapper">

<!-- 开启当前Mapper的二级缓存 -->
<cache
eviction="LRU"<!-- 回收策略:LRU、FIFO、SOFT、WEAK -->
flushInterval="60000"<!-- 刷新间隔:60秒 -->
size="512"<!-- 引用数目:最多缓存512个对象 -->
readOnly="true"<!-- 只读:true/false -->
/>

<!-- 或者使用简化配置 -->
<!-- <cache/> -->

<!-- 查询方法默认使用二级缓存 -->
<select id="selectUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 指定某个查询不使用缓存 -->
<select id="selectUserRealTime" resultType="User" useCache="false">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 指定某个查询执行前清空缓存 -->
<select id="refreshAndSelect" resultType="User" flushCache="true">
SELECT * FROM user WHERE id = #{id}
</select>

<!-- 更新操作会清空缓存 -->
<update id="updateUser" parameterType="User" flushCache="true">
UPDATE user SET name=#{name} WHERE id=#{id}
</update>
</mapper>
3.2.2 注解方式配置
package com.example.mapper;

import org.apache.ibatis.annotations.*;
import org.apache.ibatis.cache.decorators.LruCache;

@CacheNamespace(
eviction = LruCache.class,// 使用LRU策略
flushInterval = 60000,// 60秒刷新一次
size = 512,// 最多缓存512个对象
readWrite = true// 读写缓存
)
public interface UserMapper {

@Select("SELECT * FROM user WHERE id = #{id}")
@Options(useCache = true)// 默认就是true
User selectUserById(Long id);

@Select("SELECT * FROM user WHERE id = #{id}")
@Options(useCache = false)// 这个查询不使用缓存
User selectUserRealTime(Long id);

@Update("UPDATE user SET name=#{name} WHERE id=#{id}")
@Options(flushCache = Options.FlushCachePolicy.TRUE)
int updateUser(User user);
}

3.3 引用第三方缓存实现

<!-- 使用Ehcache作为二级缓存 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="timeToIdleSeconds" value="3600"/>
<property name="timeToLiveSeconds" value="3600"/>
<property name="maxEntriesLocalHeap" value="1000"/>
<property name="maxEntriesLocalDisk" value="100000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>

<!-- 使用Redis作为二级缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache">
<property name="host" value="localhost"/>
<property name="port" value="6379"/>
<property name="password" value=""/>
<property name="timeout" value="2000"/>
<property name="database" value="0"/>
</cache>

4. 核心特性详解

4.1 缓存回收策略(Eviction Policy)

public enum CacheEvictionPolicy {
/**
* LRU - 最近最少使用(默认)
* 移除最长时间不被使用的对象
*/
LRU("LRU"),

/**
* FIFO - 先进先出
* 按对象进入缓存的顺序来移除它们
*/
FIFO("FIFO"),

/**
* SOFT - 软引用
* 移除基于垃圾回收器状态和软引用规则的对象
*/
SOFT("SOFT"),

/**
* WEAK - 弱引用
* 更积极地移除基于垃圾收集器状态和弱引用规则的对象
*/
WEAK("WEAK")
}

4.2 只读 vs 读写缓存

<!-- 只读缓存(默认) -->
<cache readOnly="true"/>
<!-- 返回缓存对象的相同实例,性能高,但不安全 -->

<!-- 读写缓存 -->
<cache readOnly="false"/>
<!-- 返回缓存对象的拷贝,通过序列化实现,安全但性能稍低 -->

4.3 缓存刷新机制

public class CacheRefreshExample {

@Test
public void testCacheRefresh() {
SqlSessionFactory sqlSessionFactory = getSessionFactory();

// 第一个SqlSession
try (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectUserById(1L);// 查询并缓存
System.out.println("第一次查询: " + user1);
sqlSession1.commit();// 必须提交,数据才会进入二级缓存
}

// 第二个SqlSession
try (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectUserById(1L);// 从二级缓存获取
System.out.println("第二次查询(不同Session): " + user2);
}
}
}

5. 完整代码示例

5.1 基础示例

/**
* 二级缓存基础示例
*/
public class SecondLevelCacheBasicExample {

private SqlSessionFactory sqlSessionFactory;

@Before
public void setup() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}

/**
* 演示二级缓存的共享特性
*/
@Test
public void testCacheSharing() {
System.out.println("=== 测试二级缓存的共享特性 ===");

// 创建两个不同的SqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();

try {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

// Session1 第一次查询(访问数据库)
System.out.println("Session1 第一次查询:");
User user1 = mapper1.selectUserById(1L);
System.out.println("查询结果: " + user1);
sqlSession1.commit();// 必须提交,数据才会进入二级缓存

// Session2 查询相同数据(从二级缓存获取)
System.out.println("\nSession2 查询相同数据:");
User user2 = mapper2.selectUserById(1L);
System.out.println("查询结果: " + user2);
System.out.println("是否为同一对象: " + (user1 == user2)); // true(如果readOnly=true)

// Session1 再次查询(从一级缓存获取)
System.out.println("\nSession1 再次查询:");
User user3 = mapper1.selectUserById(1L);
System.out.println("查询结果: " + user3);
System.out.println("是否为同一对象: " + (user1 == user3)); // true

} finally {
sqlSession1.close();
sqlSession2.close();
}
}

/**
* 演示缓存失效机制
*/
@Test
public void testCacheInvalidation() {
System.out.println("\n=== 测试缓存失效机制 ===");

SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();

try {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

// 1. 初始查询并缓存
System.out.println("1. 初始查询并缓存:");
User user1 = mapper1.selectUserById(1L);
System.out.println("查询结果: " + user1);
sqlSession1.commit();

// 2. Session2 从缓存获取
System.out.println("\n2. Session2 从缓存获取:");
User user2 = mapper2.selectUserById(1L);
System.out.println("从缓存获取: " + user2);

// 3. Session1 更新数据(会清空缓存)
System.out.println("\n3. Session1 更新数据:");
user1.setName("UpdatedName");
mapper1.updateUser(user1);
sqlSession1.commit();// 提交时会清空对应namespace的二级缓存

// 4. Session2 再次查询(缓存已失效,重新查询数据库)
System.out.println("\n4. Session2 再次查询(缓存已失效):");
User user3 = mapper2.selectUserById(1L);
System.out.println("重新查询结果: " + user3);

} finally {
sqlSession1.close();
sqlSession2.close();
}
}
}

5.2 复杂场景示例

/**
* 二级缓存复杂场景示例
*/
public class SecondLevelCacheAdvancedExample {

private SqlSessionFactory sqlSessionFactory;

/**
* 测试关联查询的缓存
*/
@Test
public void testAssociationCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();

try {
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);

// OrderMapper.xml 配置
// <cache/>
// <select id="selectOrderWithUser" resultMap="orderWithUser">
//SELECT o.*, u.name as user_name
//FROM orders o LEFT JOIN user u ON o.user_id = u.id
//WHERE o.id = #{id}
// </select>

System.out.println("第一次查询订单(包含用户信息):");
Order order1 = orderMapper.selectOrderWithUser(1L);
System.out.println("订单: " + order1 + ", 用户: " + order1.getUser());
sqlSession.commit();

System.out.println("\n第二次查询相同订单:");
Order order2 = orderMapper.selectOrderWithUser(1L);
System.out.println("订单: " + order2 + ", 用户: " + order2.getUser());

} finally {
sqlSession.close();
}
}

/**
* 测试多个Mapper共享缓存
*/
@Test
public void testMultipleMapperCache() {
// 在UserMapper.xml和DeptMapper.xml中引用同一个缓存
// UserMapper.xml:
// <cache-ref namespace="com.example.mapper.CommonCache"/>

// DeptMapper.xml:
// <cache-ref namespace="com.example.mapper.CommonCache"/>

// CommonMapper.xml:
// <cache eviction="LRU" size="1024"/>

SqlSession sqlSession = sqlSessionFactory.openSession();

try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);

// 查询用户
User user = userMapper.selectUserById(1L);
sqlSession.commit();

// 查询部门(如果UserMapper和DeptMapper共享缓存,会命中)
Dept dept = deptMapper.selectDeptById(user.getDeptId());

} finally {
sqlSession.close();
}
}

/**
* 测试自定义缓存实现
*/
@Test
public void testCustomCache() {
// 1. 实现自定义Cache
public class CustomCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new ConcurrentHashMap<>();

public CustomCache(String id) {
this.id = id;
}

@Override
public String getId() {
return id;
}

@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}

@Override
public Object getObject(Object key) {
return cache.get(key);
}

// ... 其他方法实现
}

// 2. 在Mapper中指定使用自定义缓存
// <cache type="com.example.cache.CustomCache"/>
}
}

5.3 实际应用示例

/**
* 电商系统二级缓存应用示例
*/
public class EcommerceCacheExample {

/**
* 商品信息缓存策略
*/
@Test
public void testProductCacheStrategy() {
// 商品信息变化不频繁,适合使用二级缓存
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);

// 热门商品缓存
List<Product> hotProducts = productMapper.selectHotProducts();
sqlSession.commit();

// 多个用户同时访问热门商品页面,都会从缓存获取
for (int i = 0; i < 10; i++) {
try (SqlSession session = sqlSessionFactory.openSession()) {
ProductMapper mapper = session.getMapper(ProductMapper.class);
List<Product> cachedProducts = mapper.selectHotProducts();
System.out.println("用户" + i + "获取热门商品,数量: " + cachedProducts.size());
}
}
}
}

/**
* 库存信息实时性要求高,不适合缓存
*/
@Test
public void testInventoryRealTimeQuery() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
InventoryMapper inventoryMapper = sqlSession.getMapper(InventoryMapper.class);

// 库存查询使用useCache="false"
// <select id="selectInventory" resultType="Inventory" useCache="false">
Inventory inventory = inventoryMapper.selectInventory(1001L);
System.out.println("实时库存: " + inventory.getStock());
}
}
}

6. 高级特性

6.1 缓存同步策略

/**
* 演示缓存同步问题
*/
public class CacheSyncExample {

@Test
public void testCacheSyncIssue() {
// 问题:多个应用实例部署时,二级缓存不一致
// 解决方案:使用分布式缓存如Redis

// Redis缓存配置示例
// <cache type="org.mybatis.caches.redis.RedisCache">
//<property name="host" value="${redis.host}"/>
//<property name="port" value="${redis.port}"/>
// </cache>
}

/**
* 使用缓存通知机制
*/
@Test
public void testCacheNotification() {
// 当数据更新时,发送缓存失效通知
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 更新用户信息
User user = new User();
user.setId(1L);
user.setName("NewName");
userMapper.updateUser(user);

// 这里可以发送消息通知其他实例清空缓存
// messageQueue.send("cache:clear:user:1");

sqlSession.commit();
}
}
}

6.2 缓存预热

/**
* 缓存预热策略
*/
public class CacheWarmUp {

/**
* 应用启动时预热缓存
*/
@PostConstruct
public void warmUpCache() {
System.out.println("开始预热缓存...");

try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 预热热门商品
List<Product> hotProducts = productMapper.selectHotProducts();
System.out.println("预热热门商品: " + hotProducts.size() + " 个");

// 预热VIP用户信息
List<User> vipUsers = userMapper.selectVipUsers();
System.out.println("预热VIP用户: " + vipUsers.size() + " 个");

sqlSession.commit();
}

System.out.println("缓存预热完成");
}

/**
* 定时刷新缓存
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void refreshCache() {
System.out.println("定时刷新缓存...");

try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);

// 清除并重新加载商品分类缓存
sqlSession.clearCache(); // 清空当前Mapper缓存
List<ProductCategory> categories = productMapper.selectAllCategories();

sqlSession.commit();
}
}
}

7. 性能优化和监控

7.1 缓存命中率监控

/**
* 缓存监控工具类
*/
public class CacheMonitor {

private static final Map<String, CacheStats> cacheStatsMap = new ConcurrentHashMap<>();

/**
* 缓存统计信息
*/
@Data
public static class CacheStats {
private String cacheId;
private long hitCount;// 命中次数
private long missCount;// 未命中次数
private long putCount;// 放入次数
private long evictionCount; // 淘汰次数
private long totalRequestCount; // 总请求次数

public double getHitRate() {
return totalRequestCount == 0 ? 0 : (double) hitCount / totalRequestCount;
}
}

/**
* 自定义Cache包装器,用于统计
*/
public static class MonitoredCache implements Cache {
private final Cache delegate;
private final CacheStats stats;

public MonitoredCache(Cache delegate) {
this.delegate = delegate;
this.stats = new CacheStats();
this.stats.setCacheId(delegate.getId());
cacheStatsMap.put(delegate.getId(), stats);
}

@Override
public Object getObject(Object key) {
stats.setTotalRequestCount(stats.getTotalRequestCount() + 1);

Object value = delegate.getObject(key);
if (value != null) {
stats.setHitCount(stats.getHitCount() + 1);
} else {
stats.setMissCount(stats.getMissCount() + 1);
}

return value;
}

@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
stats.setPutCount(stats.getPutCount() + 1);
}

// ... 其他方法委托给delegate

public CacheStats getStats() {
return stats;
}
}
}

7.2 缓存配置调优

<!-- 根据业务场景调整缓存配置 -->
<cache
eviction="LRU"
flushInterval="1800000"<!-- 30分钟刷新一次,适合不常变的数据 -->
size="2048"<!-- 根据内存大小调整 -->
readOnly="false"<!-- 读写分离场景使用读写缓存 -->
/>

<!-- 分场景配置 -->
<!-- 用户基本信息:缓存时间长 -->
<cache
id="userBasicCache"
eviction="LRU"
flushInterval="3600000"<!-- 1小时 -->
size="5000"
/>

<!-- 用户动态信息:缓存时间短 -->
<cache
id="userDynamicCache"
eviction="LRU"
flushInterval="300000"<!-- 5分钟 -->
size="10000"
/>

<!-- 系统配置:永久缓存 -->
<cache
id="configCache"
eviction="FIFO"
flushInterval="0"<!-- 0表示不自动刷新 -->
size="100"
readOnly="true"
/>

8. 常见问题解决方案

8.1 缓存穿透问题

/**
* 防止缓存穿透的解决方案
*/
public class CachePenetrationSolution {

/**
* 方案1:缓存空对象
*/
public User getUserWithNullCache(Long id) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

// 先查缓存
User user = userMapper.selectUserById(id);

if (user == null) {
// 缓存空对象,设置较短过期时间
user = new User();
user.setId(id);
user.setName("NULL_OBJECT");
// 这里需要自定义Cache实现来支持特殊空对象标记
}

return user;
}
}

/**
* 方案2:布隆过滤器
*/
public User getUserWithBloomFilter(Long id) {
// 使用布隆过滤器判断数据是否存在
BloomFilter bloomFilter = getBloomFilter();

if (!bloomFilter.mightContain(id)) {
return null; // 肯定不存在
}

// 存在则正常查询
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.selectUserById(id);
}
}
}

8.2 缓存雪崩问题

/**
* 防止缓存雪崩的解决方案
*/
public class CacheAvalancheSolution {

/**
* 方案1:设置不同的过期时间
*/
@Test
public void testRandomExpireTime() {
// 在自定义Cache实现中,为不同key设置随机的过期时间
// 避免大量缓存同时失效

Random random = new Random();
int baseExpireTime = 3600000; // 1小时
int randomRange = 300000;// 5分钟随机范围

int expireTime = baseExpireTime + random.nextInt(randomRange);
// 设置缓存过期时间
}

/**
* 方案2:热点数据永不过期
*/
@Test
public void testHotDataNeverExpire() {
// 对热点数据设置永不过期
// 通过后台线程异步更新

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
// 异步更新热点数据缓存
refreshHotDataCache();
}, 0, 5, TimeUnit.MINUTES); // 每5分钟更新一次
}
}

9. 最佳实践总结

9.1 使用场景推荐

场景 是否使用二级缓存 配置建议
读多写少的数据 ✅ 推荐使用 flushInterval 适当调长
实时性要求高的数据 ❌ 不建议使用 useCache="false"
数据量小的配置表 ✅ 推荐使用 readOnly="true"
频繁更新的数据 ❌ 不建议使用 不使用缓存或极短时间
分布式环境 ⚠️ 谨慎使用 使用Redis等分布式缓存

9.2 配置建议

  1. 按业务场景划分缓存:不同业务数据使用不同的缓存配置
  2. 合理设置缓存大小:根据数据量和内存情况调整
  3. 监控缓存命中率:定期分析,优化缓存策略
  4. 考虑序列化成本:读写缓存需要考虑对象的序列化性能
  5. 分布式环境:使用Redis等共享缓存替代本地缓存

9.3 代码规范

// 1. 明确缓存范围
@CacheNamespace // 明确标注使用缓存
public interface UserMapper {

// 2. 明确标注是否使用缓存
@Options(useCache = true)
User selectUserById(Long id);

// 3. 更新操作明确清空缓存
@Options(flushCache = Options.FlushCachePolicy.TRUE)
int updateUser(User user);

// 4. 实时查询明确禁用缓存
@Options(useCache = false)
User selectUserRealTime(Long id);
}

总结

MyBatis二级缓存是提升应用性能的重要工具,但需要根据具体业务场景合理使用。关键要点:

  1. 理解原理:二级缓存是Mapper级别的,可跨SqlSession共享
  2. 合理配置:根据数据特性选择合适的回收策略、刷新间隔
  3. 注意事务:必须提交事务后数据才会进入二级缓存
  4. 监控优化:关注缓存命中率,及时调整策略
  5. 分布式环境:考虑使用Redis等分布式缓存方案

正确使用二级缓存可以显著提升系统性能,但不当使用可能导致数据不一致等问题。建议在充分理解业务需求和缓存特性的基础上,制定合适的缓存策略。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐