MyBatis 二级缓存详解
·
欢迎关注微信公众号 「思客潘」
MyBatis 二级缓存详解
1. 概述
1.1 什么是MyBatis二级缓存?
MyBatis二级缓存是Mapper级别(或namespace级别)的缓存,多个SqlSession可以共享。它在应用生命周期内有效,可以跨越多个SqlSession。
1.2 与一级缓存的区别
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession级别 | Mapper级别 |
| 生命周期 | SqlSession生命周期 | 应用生命周期 |
| 共享性 | 不能跨SqlSession共享 | 多个SqlSession共享 |
| 存储位置 | 进程内存 | 可配置(内存、Redis等) |
| 默认状态 | 开启,无法关闭 | 默认关闭,需手动开启 |
2. 工作原理
2.1 缓存结构和工作流程
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 配置建议
- 按业务场景划分缓存:不同业务数据使用不同的缓存配置
- 合理设置缓存大小:根据数据量和内存情况调整
- 监控缓存命中率:定期分析,优化缓存策略
- 考虑序列化成本:读写缓存需要考虑对象的序列化性能
- 分布式环境:使用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二级缓存是提升应用性能的重要工具,但需要根据具体业务场景合理使用。关键要点:
- 理解原理:二级缓存是Mapper级别的,可跨SqlSession共享
- 合理配置:根据数据特性选择合适的回收策略、刷新间隔
- 注意事务:必须提交事务后数据才会进入二级缓存
- 监控优化:关注缓存命中率,及时调整策略
- 分布式环境:考虑使用Redis等分布式缓存方案
正确使用二级缓存可以显著提升系统性能,但不当使用可能导致数据不一致等问题。建议在充分理解业务需求和缓存特性的基础上,制定合适的缓存策略。
更多推荐




所有评论(0)