最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
MyBatis缓存引发同一SqlSession中的数据不一致
时间:2026-07-24 09:10:14 编辑:袖梨 来源:一聚教程网
作为多年的Java开发经验,在开发过程中经常会踩一些坑,本系列想通过一些案例分享,帮助其他开发者避免这些问题。

注意:由于框架不同版本改造会有些使用的不同,因此本次系列中使用JDK版本使用的是open-jdk21。
1. 事情起因
在一次订单系统中,用户反馈订单状态更新后,页面显示的状态仍然是旧状态。经过排查发现,是因为MyBatis一级缓存导致的问题:在同一事务中,先查询了订单信息,然后外部系统更新了订单状态,再次查询时返回的是缓存中的旧数据。
问题代码如下:
参考代码 lesson14-mybatis-cache 中的MyBatisCacheDemo.java
package com.architect.pitfalls.mybatis.cause;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.cause")
public class MyBatisCacheDemo {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(MyBatisCacheDemo.class, args);
MyBatisCacheDemo demo = context.getBean(MyBatisCacheDemo.class);
System.out.println("n=== MyBatis一级缓存问题演示 ===n");
demo.demonstrateFirstLevelCache();
demo.demonstrateDataInconsistency();
demo.demonstrateCrossSessionBehavior();
context.close();
}
private final UserMapper userMapper;
private final SqlSessionFactory sqlSessionFactory;
private final JdbcTemplate jdbcTemplate;
public MyBatisCacheDemo(UserMapper userMapper, SqlSessionFactory sqlSessionFactory, JdbcTemplate jdbcTemplate) {
this.userMapper = userMapper;
this.sqlSessionFactory = sqlSessionFactory;
this.jdbcTemplate = jdbcTemplate;
}
public void demonstrateFirstLevelCache() {
System.out.println("========================================");
System.out.println("场景1: 同一SqlSession中的缓存行为");
System.out.println("========================================");
System.out.println();
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println("第一次查询用户ID=1:");
User user1 = mapper.selectById(1L);
System.out.println(" 结果: " + user1);
System.out.println();
System.out.println("第二次查询用户ID=1(相同SqlSession):");
User user2 = mapper.selectById(1L);
System.out.println(" 结果: " + user2);
System.out.println();
System.out.println("验证是否为同一对象:");
System.out.println(" user1 == user2: " + (user1 == user2));
System.out.println();
System.out.println("分析:");
System.out.println(" - 第二次查询没有执行SQL(观察日志)");
System.out.println(" - 返回的是缓存中的同一个对象引用");
System.out.println(" - 这就是MyBatis一级缓存的效果");
}
System.out.println();
}
@Transactional
public void demonstrateDataInconsistency() {
System.out.println("========================================");
System.out.println("场景2: 一级缓存导致的数据不一致问题");
System.out.println("========================================");
System.out.println();
System.out.println("模拟业务场景:");
System.out.println(" 1. 查询用户信息");
System.out.println(" 2. 外部系统直接修改数据库(绕过MyBatis)");
System.out.println(" 3. 再次查询同一用户");
System.out.println();
System.out.println("步骤1: 第一次查询用户ID=1");
User user1 = userMapper.selectById(1L);
System.out.println(" 查询结果: " + user1);
System.out.println();
System.out.println("步骤2: 模拟外部系统修改(使用JdbcTemplate绕过MyBatis缓存)");
jdbcTemplate.update("UPDATE "user" SET status = 'INACTIVE' WHERE id = 1");
System.out.println(" 已通过JdbcTemplate更新用户状态为INACTIVE");
System.out.println();
System.out.println("步骤3: 第二次查询用户ID=1(同一事务/SqlSession)");
User user2 = userMapper.selectById(1L);
System.out.println(" 查询结果: " + user2);
System.out.println();
System.out.println("问题分析:");
System.out.println(" - 数据库中status已更新为INACTIVE");
System.out.println(" - 但第二次查询返回的仍是缓存中的旧数据: " + user2.getStatus());
System.out.println(" - user1 == user2: " + (user1 == user2));
System.out.println(" - 这就是一级缓存导致的数据不一致问题");
System.out.println();
System.out.println("实际数据库状态验证(新SqlSession):");
try (SqlSession newSession = sqlSessionFactory.openSession()) {
UserMapper newMapper = newSession.getMapper(UserMapper.class);
User dbUser = newMapper.selectById(1L);
System.out.println(" 数据库实际状态: " + dbUser.getStatus());
}
System.out.println();
}
public void demonstrateCrossSessionBehavior() {
System.out.println("========================================");
System.out.println("场景3: 不同SqlSession的缓存隔离");
System.out.println("========================================");
System.out.println();
System.out.println("SqlSession1: 查询用户ID=1");
try (SqlSession session1 = sqlSessionFactory.openSession()) {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1L);
System.out.println(" 结果: " + user1);
System.out.println();
System.out.println("SqlSession2: 查询用户ID=1");
try (SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1L);
System.out.println(" 结果: " + user2);
System.out.println();
System.out.println("验证对象引用:");
System.out.println(" user1 == user2: " + (user1 == user2));
System.out.println();
System.out.println("分析:");
System.out.println(" - 不同SqlSession有各自独立的一级缓存");
System.out.println(" - 两个session都执行了SQL查询(观察日志)");
System.out.println(" - 返回的是不同的对象实例");
}
}
System.out.println();
}
}
运行结果:
=== MyBatis一级缓存问题演示 ===
========================================
场景1: 同一SqlSession中的缓存行为
========================================
第一次查询用户ID=1:
==> Preparing: SELECT * FROM "user" WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
第二次查询用户ID=1(相同SqlSession):
结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
验证是否为同一对象:
user1 == user2: true
分析:
- 第二次查询没有执行SQL(观察日志)
- 返回的是缓存中的同一个对象引用
- 这就是MyBatis一级缓存的效果
========================================
场景2: 一级缓存导致的数据不一致问题
========================================
模拟业务场景:
1. 查询用户信息
2. 外部系统直接修改数据库(绕过MyBatis)
3. 再次查询同一用户
步骤1: 第一次查询用户ID=1
==> Preparing: SELECT * FROM "user" WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
查询结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
步骤2: 模拟外部系统修改(使用JdbcTemplate绕过MyBatis缓存)
已通过JdbcTemplate更新用户状态为INACTIVE
步骤3: 第二次查询用户ID=1(同一事务/SqlSession)
查询结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
问题分析:
- 数据库中status已更新为INACTIVE
- 但第二次查询返回的仍是缓存中的旧数据: ACTIVE
- user1 == user2: true
- 这就是一级缓存导致的数据不一致问题
实际数据库状态验证(新SqlSession):
==> Preparing: SELECT * FROM "user" WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
数据库实际状态: INACTIVE
========================================
场景3: 不同SqlSession的缓存隔离
========================================
SqlSession1: 查询用户ID=1
==> Preparing: SELECT * FROM "user" WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
SqlSession2: 查询用户ID=1
==> Preparing: SELECT * FROM "user" WHERE id = ?
==> Parameters: 1(Long)
<== Total: 1
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
验证对象引用:
user1 == user2: false
分析:
- 不同SqlSession有各自独立的一级缓存
- 两个session都执行了SQL查询(观察日志)
- 返回的是不同的对象实例
2. 原因分析
通过深入分析MyBatis一级缓存机制,发现数据不一致的根本原因:
参考代码 lesson14-mybatis-cache 中的MyBatisCacheAnalysis.java
package com.architect.pitfalls.mybatis.analysis;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.util.Map;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.cause")
public class MyBatisCacheAnalysis {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(MyBatisCacheAnalysis.class, args);
MyBatisCacheAnalysis analysis = context.getBean(MyBatisCacheAnalysis.class);
System.out.println("n=== MyBatis一级缓存原理分析 ===n");
analysis.analyzeCacheStructure();
analysis.analyzeCacheKey();
analysis.analyzeCacheLifecycle();
context.close();
}
private final SqlSessionFactory sqlSessionFactory;
public MyBatisCacheAnalysis(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public void analyzeCacheStructure() {
System.out.println("========================================");
System.out.println("1. 一级缓存结构分析");
System.out.println("========================================");
System.out.println();
System.out.println("MyBatis一级缓存架构:");
System.out.println();
System.out.println(" ┌─────────────────────────────────────┐");
System.out.println(" │ SqlSession │");
System.out.println(" │ ┌───────────────────────────────┐ │");
System.out.println(" │ │ Executor │ │");
System.out.println(" │ │ ┌─────────────────────────┐ │ │");
System.out.println(" │ │ │ Local Cache (一级缓存) │ │ │");
System.out.println(" │ │ │ - PerpetualCache │ │ │");
System.out.println(" │ │ │ - HashMap存储 │ │ │");
System.out.println(" │ │ └─────────────────────────┘ │ │");
System.out.println(" │ └───────────────────────────────┘ │");
System.out.println(" └─────────────────────────────────────┘");
System.out.println();
System.out.println("核心组件:");
System.out.println(" 1. BaseExecutor: 基础执行器,维护localCache");
System.out.println(" 2. PerpetualCache: 永久缓存实现,内部使用HashMap");
System.out.println(" 3. CacheKey: 缓存键,用于唯一标识查询");
System.out.println();
System.out.println("源码位置:");
System.out.println(" - org.apache.ibatis.executor.BaseExecutor");
System.out.println(" - org.apache.ibatis.cache.impl.PerpetualCache");
System.out.println();
try (SqlSession session = sqlSessionFactory.openSession()) {
System.out.println("验证缓存结构:");
System.out.println(" SqlSession实现类: " + session.getClass().getName());
Object executor = getFieldValue(session, "executor");
if (executor != null) {
System.out.println(" Executor实现类: " + executor.getClass().getName());
Object localCache = getFieldValue(executor, "localCache");
if (localCache != null) {
System.out.println(" LocalCache实现类: " + localCache.getClass().getName());
Object cacheMap = getFieldValue(localCache, "cache");
if (cacheMap != null) {
System.out.println(" 内部存储结构: " + cacheMap.getClass().getName());
System.out.println(" 初始缓存大小: " + ((Map<?, ?>) cacheMap).size());
}
}
}
} catch (Exception e) {
System.out.println(" 反射获取失败: " + e.getMessage());
}
System.out.println();
}
public void analyzeCacheKey() {
System.out.println("========================================");
System.out.println("2. 缓存键(CacheKey)分析");
System.out.println("========================================");
System.out.println();
System.out.println("CacheKey的组成因素:");
System.out.println();
System.out.println(" 1. MappedStatement的id(SQL语句唯一标识)");
System.out.println(" 2. 结果集范围(分页参数)");
System.out.println(" 3. SQL语句本身");
System.out.println(" 4. 参数值");
System.out.println(" 5. Environment的id");
System.out.println();
System.out.println("CacheKey计算规则:");
System.out.println(" - 使用多重hash计算");
System.out.println(" - 所有因素组合生成唯一key");
System.out.println(" - 相同查询条件生成相同key");
System.out.println();
System.out.println("源码示例(BaseExecutor.createCacheKey):");
System.out.println();
System.out.println(" CacheKey cacheKey = new CacheKey();");
System.out.println(" cacheKey.update(ms.getId()); // MappedStatement id");
System.out.println(" cacheKey.update(rowBounds.getOffset()); // 分页偏移");
System.out.println(" cacheKey.update(rowBounds.getLimit()); // 分页大小");
System.out.println(" cacheKey.update(boundSql.getSql()); // SQL语句");
System.out.println(" // 遍历参数值");
System.out.println(" for (ParameterMapping pm : parameterMappings) {");
System.out.println(" cacheKey.update(parameterObject);");
System.out.println(" }");
System.out.println();
System.out.println("为什么两次查询返回同一对象:");
System.out.println(" - 相同SQL + 相同参数 = 相同CacheKey");
System.out.println(" - 从缓存中取出的是同一个Java对象引用");
System.out.println(" - 修改对象属性会影响缓存中的数据");
System.out.println();
}
public void analyzeCacheLifecycle() {
System.out.println("========================================");
System.out.println("3. 缓存生命周期分析");
System.out.println("========================================");
System.out.println();
System.out.println("一级缓存的生命周期:");
System.out.println();
System.out.println(" ┌──────────────────────────────────────────┐");
System.out.println(" │ SqlSession生命周期 │");
System.out.println(" │ │");
System.out.println(" │ 创建Session ──> 使用缓存 ──> 关闭Session│");
System.out.println(" │ │ │ │ │");
System.out.println(" │ ▼ ▼ ▼ │");
System.out.println(" │ 创建缓存 命中/未命中 清空缓存 │");
System.out.println(" └──────────────────────────────────────────┘");
System.out.println();
System.out.println("缓存失效场景:");
System.out.println();
System.out.println(" 1. SqlSession关闭");
System.out.println(" - session.close() 后缓存被清空");
System.out.println();
System.out.println(" 2. 执行更新操作");
System.out.println(" - insert/update/delete 会清空缓存");
System.out.println(" - 防止脏读,保证数据一致性");
System.out.println();
System.out.println(" 3. 手动清空");
System.out.println(" - session.clearCache() 主动清空");
System.out.println();
System.out.println(" 4. 配置flushCache=true");
System.out.println(" - 查询语句配置flushCache="true"");
System.out.println();
System.out.println("Spring集成下的特殊情况:");
System.out.println();
System.out.println(" @Transactional注解的影响:");
System.out.println(" - Spring事务会将多个操作绑定到同一SqlSession");
System.out.println(" - 事务内的多次查询会共享一级缓存");
System.out.println(" - 这可能导致事务内数据不一致问题");
System.out.println();
System.out.println("Spring管理SqlSession流程:");
System.out.println(" 1. 事务开始 -> 创建SqlSession");
System.out.println(" 2. 事务执行 -> 复用同一SqlSession");
System.out.println(" 3. 事务提交 -> 提交并关闭SqlSession");
System.out.println(" 4. 事务回滚 -> 回滚并关闭SqlSession");
System.out.println();
System.out.println("关键源码:");
System.out.println(" - SqlSessionUtils.getSqlSession()");
System.out.println(" - SpringManagedTransaction");
System.out.println(" - SqlSessionSynchronization");
System.out.println();
}
private Object getFieldValue(Object obj, String fieldName) {
try {
Class<?> clazz = obj.getClass();
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
} catch (Exception e) {
// ignore
}
return null;
}
}
运行结果:
=== MyBatis一级缓存原理分析 ===
========================================
1. 一级缓存结构分析
========================================
MyBatis一级缓存架构:
┌─────────────────────────────────────┐
│ SqlSession │
│ ┌───────────────────────────────┐ │
│ │ Executor │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Local Cache (一级缓存) │ │ │
│ │ │ - PerpetualCache │ │ │
│ │ │ - HashMap存储 │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
核心组件:
1. BaseExecutor: 基础执行器,维护localCache
2. PerpetualCache: 永久缓存实现,内部使用HashMap
3. CacheKey: 缓存键,用于唯一标识查询
源码位置:
- org.apache.ibatis.executor.BaseExecutor
- org.apache.ibatis.cache.impl.PerpetualCache
验证缓存结构:
SqlSession实现类: org.apache.ibatis.session.defaults.DefaultSqlSession
Executor实现类: org.apache.ibatis.executor.CachingExecutor
LocalCache实现类: org.apache.ibatis.cache.impl.PerpetualCache
内部存储结构: java.util.HashMap
初始缓存大小: 0
========================================
2. 缓存键(CacheKey)分析
========================================
CacheKey的组成因素:
1. MappedStatement的id(SQL语句唯一标识)
2. 结果集范围(分页参数)
3. SQL语句本身
4. 参数值
5. Environment的id
CacheKey计算规则:
- 使用多重hash计算
- 所有因素组合生成唯一key
- 相同查询条件生成相同key
源码示例(BaseExecutor.createCacheKey):
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); // MappedStatement id
cacheKey.update(rowBounds.getOffset()); // 分页偏移
cacheKey.update(rowBounds.getLimit()); // 分页大小
cacheKey.update(boundSql.getSql()); // SQL语句
// 遍历参数值
for (ParameterMapping pm : parameterMappings) {
cacheKey.update(parameterObject);
}
为什么两次查询返回同一对象:
- 相同SQL + 相同参数 = 相同CacheKey
- 从缓存中取出的是同一个Java对象引用
- 修改对象属性会影响缓存中的数据
========================================
3. 缓存生命周期分析
========================================
一级缓存的生命周期:
┌──────────────────────────────────────────┐
│ SqlSession生命周期 │
│ │
│ 创建Session ──> 使用缓存 ──> 关闭Session│
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 创建缓存 命中/未命中 清空缓存 │
└──────────────────────────────────────────┘
缓存失效场景:
1. SqlSession关闭
- session.close() 后缓存被清空
2. 执行更新操作
- insert/update/delete 会清空缓存
- 防止脏读,保证数据一致性
3. 手动清空
- session.clearCache() 主动清空
4. 配置flushCache=true
- 查询语句配置flushCache="true"
Spring集成下的特殊情况:
@Transactional注解的影响:
- Spring事务会将多个操作绑定到同一SqlSession
- 事务内的多次查询会共享一级缓存
- 这可能导致事务内数据不一致问题
Spring管理SqlSession流程:
1. 事务开始 -> 创建SqlSession
2. 事务执行 -> 复用同一SqlSession
3. 事务提交 -> 提交并关闭SqlSession
4. 事务回滚 -> 回滚并关闭SqlSession
关键源码:
- SqlSessionUtils.getSqlSession()
- SpringManagedTransaction
- SqlSessionSynchronization
核心原理:
- MyBatis一级缓存是基于SqlSession级别的,每个SqlSession有独立的缓存
- 缓存使用HashMap存储,key由SQL语句、参数等组成
- Spring事务会将多个操作绑定到同一SqlSession,导致缓存共享
- 在同一SqlSession中,相同查询条件会返回缓存中的同一对象引用
3. 解决方案
3.1 方案一:手动清空缓存
参考代码 lesson14-mybatis-cache 中的ClearCacheSolution.java
package com.architect.pitfalls.mybatis.solution;
import com.architect.pitfalls.mybatis.cause.Order;
import com.architect.pitfalls.mybatis.cause.OrderMapper;
import com.architect.pitfalls.mybatis.cause.User;
import com.architect.pitfalls.mybatis.cause.UserMapper;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.Transactional;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.cause")
public class ClearCacheSolution {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(ClearCacheSolution.class, args);
ClearCacheSolution solution = context.getBean(ClearCacheSolution.class);
System.out.println("n=== 方案一:手动清空缓存解决方案 ===n");
solution.demonstrateSolution();
context.close();
}
private final UserMapper userMapper;
private final SqlSessionFactory sqlSessionFactory;
public ClearCacheSolution(UserMapper userMapper, SqlSessionFactory sqlSessionFactory) {
this.userMapper = userMapper;
this.sqlSessionFactory = sqlSessionFactory;
}
public void demonstrateSolution() {
System.out.println("========================================");
System.out.println("手动清空缓存方案演示");
System.out.println("========================================");
System.out.println();
System.out.println("方案说明:");
System.out.println(" 在需要获取最新数据的场景,手动调用clearCache()清空缓存");
System.out.println();
System.out.println("适用场景:");
System.out.println(" 1. 知道数据可能被外部修改");
System.out.println(" 2. 需要确保获取最新数据");
System.out.println(" 3. 对性能影响不敏感的场景");
System.out.println();
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println("步骤1: 第一次查询用户ID=1");
User user1 = mapper.selectById(1L);
System.out.println(" 结果: " + user1);
System.out.println();
System.out.println("步骤2: 模拟外部修改数据");
User updateUser = new User();
updateUser.setId(1L);
updateUser.setUsername("张三");
updateUser.setEmail("[email protected]");
updateUser.setStatus("INACTIVE");
mapper.update(updateUser);
session.commit();
System.out.println(" 已更新用户状态为INACTIVE");
System.out.println();
System.out.println("步骤3: 手动清空缓存");
session.clearCache();
System.out.println(" 缓存已清空");
System.out.println();
System.out.println("步骤4: 再次查询用户ID=1");
User user2 = mapper.selectById(1L);
System.out.println(" 结果: " + user2);
System.out.println();
System.out.println("验证结果:");
System.out.println(" user1.status: " + user1.getStatus());
System.out.println(" user2.status: " + user2.getStatus());
System.out.println(" user1 == user2: " + (user1 == user2));
System.out.println();
System.out.println("分析:");
System.out.println(" ✅ 清空缓存后,第二次查询执行了SQL");
System.out.println(" ✅ 获取到了最新的数据库数据");
System.out.println(" ✅ user1和user2是不同的对象实例");
}
System.out.println();
System.out.println("代码示例:");
System.out.println();
System.out.println(" // 方式1:通过SqlSession清空");
System.out.println(" sqlSession.clearCache();");
System.out.println();
System.out.println(" // 方式2:在Mapper接口中定义清空方法");
System.out.println(" @Update("/* clear cache */ SELECT 1")");
System.out.println(" void clearCache();");
System.out.println();
System.out.println("优点:");
System.out.println(" - 实现简单,直接有效");
System.out.println(" - 可以精确控制清空时机");
System.out.println();
System.out.println("缺点:");
System.out.println(" - 需要手动管理,容易遗漏");
System.out.println(" - 清空整个缓存,可能影响其他查询性能");
System.out.println(" - 代码侵入性较强");
}
}
运行结果:
=== 方案一:手动清空缓存解决方案 ===
========================================
手动清空缓存方案演示
========================================
方案说明:
在需要获取最新数据的场景,手动调用clearCache()清空缓存
适用场景:
1. 知道数据可能被外部修改
2. 需要确保获取最新数据
3. 对性能影响不敏感的场景
步骤1: 第一次查询用户ID=1
结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
步骤2: 模拟外部修改数据
已更新用户状态为INACTIVE
步骤3: 手动清空缓存
缓存已清空
步骤4: 再次查询用户ID=1
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
验证结果:
user1.status: ACTIVE
user2.status: INACTIVE
user1 == user2: false
分析:
✅ 清空缓存后,第二次查询执行了SQL
✅ 获取到了最新的数据库数据
✅ user1和user2是不同的对象实例
代码示例:
// 方式1:通过SqlSession清空
sqlSession.clearCache();
// 方式2:在Mapper接口中定义清空方法
@Update("/* clear cache */ SELECT 1")
void clearCache();
优点:
- 实现简单,直接有效
- 可以精确控制清空时机
缺点:
- 需要手动管理,容易遗漏
- 清空整个缓存,可能影响其他查询性能
- 代码侵入性较强
优点:实现简单,直接有效
缺点:需要手动管理,容易遗漏
3.2 方案二:flushCache配置
参考代码 lesson14-mybatis-cache 中的FlushCacheSolution.java
package com.architect.pitfalls.mybatis.solution;
import com.architect.pitfalls.mybatis.cause.User;
import com.architect.pitfalls.mybatis.cause.UserMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.solution")
public class FlushCacheSolution {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(FlushCacheSolution.class, args);
FlushCacheSolution solution = context.getBean(FlushCacheSolution.class);
System.out.println("n=== 方案二:flushCache配置解决方案 ===n");
solution.demonstrateSolution();
context.close();
}
private final FlushUserMapper flushUserMapper;
public FlushCacheSolution(FlushUserMapper flushUserMapper) {
this.flushUserMapper = flushUserMapper;
}
public void demonstrateSolution() {
System.out.println("========================================");
System.out.println("flushCache配置方案演示");
System.out.println("========================================");
System.out.println();
System.out.println("方案说明:");
System.out.println(" 在Mapper方法上配置flushCache=true,每次查询前清空缓存");
System.out.println();
System.out.println("配置方式:");
System.out.println();
System.out.println(" // 注解方式");
System.out.println(" @Select(value = "SELECT * FROM user WHERE id = #{id}", ");
System.out.println(" flushCache = Options.FlushCachePolicy.TRUE)");
System.out.println(" User selectByIdWithFlush(Long id);");
System.out.println();
System.out.println(" // XML方式");
System.out.println(" <select id="selectByIdWithFlush" resultType="User" ");
System.out.println(" flushCache="true">");
System.out.println(" SELECT * FROM user WHERE id = #{id}");
System.out.println(" </select>");
System.out.println();
System.out.println("步骤1: 第一次查询用户ID=1");
User user1 = flushUserMapper.selectByIdWithFlush(1L);
System.out.println(" 结果: " + user1);
System.out.println();
System.out.println("步骤2: 模拟更新数据");
User updateUser = new User();
updateUser.setId(1L);
updateUser.setUsername("张三");
updateUser.setEmail("[email protected]");
updateUser.setStatus("INACTIVE");
flushUserMapper.update(updateUser);
System.out.println(" 已更新用户状态为INACTIVE");
System.out.println();
System.out.println("步骤3: 再次查询用户ID=1(使用flushCache=true)");
User user2 = flushUserMapper.selectByIdWithFlush(1L);
System.out.println(" 结果: " + user2);
System.out.println();
System.out.println("验证结果:");
System.out.println(" user1.status: " + user1.getStatus());
System.out.println(" user2.status: " + user2.getStatus());
System.out.println(" user1 == user2: " + (user1 == user2));
System.out.println();
System.out.println("分析:");
System.out.println(" ✅ 每次查询都会清空缓存");
System.out.println(" ✅ 总是获取最新数据");
System.out.println(" ✅ 配置简单,无需手动管理");
System.out.println();
System.out.println("优点:");
System.out.println(" - 声明式配置,代码简洁");
System.out.println(" - 自动生效,无需手动调用");
System.out.println(" - 可以针对特定查询配置");
System.out.println();
System.out.println("缺点:");
System.out.println(" - 每次查询都清空缓存,性能影响较大");
System.out.println(" - 可能影响同一事务中其他查询的缓存");
System.out.println(" - 不适合高频查询场景");
}
}
运行结果:
=== 方案二:flushCache配置解决方案 ===
========================================
flushCache配置方案演示
========================================
方案说明:
在Mapper方法上配置flushCache=true,每次查询前清空缓存
配置方式:
// 注解方式
@Select(value = "SELECT * FROM user WHERE id = #{id}",
flushCache = Options.FlushCachePolicy.TRUE)
User selectByIdWithFlush(Long id);
// XML方式
<select id="selectByIdWithFlush" resultType="User"
flushCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
步骤1: 第一次查询用户ID=1
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
步骤2: 模拟更新数据
已更新用户状态为INACTIVE
步骤3: 再次查询用户ID=1(使用flushCache=true)
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
验证结果:
user1.status: INACTIVE
user2.status: INACTIVE
user1 == user2: false
分析:
✅ 每次查询都会清空缓存
✅ 总是获取最新数据
✅ 配置简单,无需手动管理
优点:
- 声明式配置,代码简洁
- 自动生效,无需手动调用
- 可以针对特定查询配置
缺点:
- 每次查询都清空缓存,性能影响较大
- 可能影响同一事务中其他查询的缓存
- 不适合高频查询场景
优点:声明式配置,自动生效
缺点:每次查询都清空缓存,性能影响较大
3.3 方案三:useCache配置
参考代码 lesson14-mybatis-cache 中的StatementCacheSolution.java
package com.architect.pitfalls.mybatis.solution;
import com.architect.pitfalls.mybatis.cause.User;
import org.apache.ibatis.annotations.*;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.solution")
public class StatementCacheSolution {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(StatementCacheSolution.class, args);
StatementCacheSolution solution = context.getBean(StatementCacheSolution.class);
System.out.println("n=== 方案三:禁用特定查询缓存(useCache=false) ===n");
solution.demonstrateSolution();
context.close();
}
private final NoCacheUserMapper noCacheUserMapper;
public StatementCacheSolution(NoCacheUserMapper noCacheUserMapper) {
this.noCacheUserMapper = noCacheUserMapper;
}
public void demonstrateSolution() {
System.out.println("========================================");
System.out.println("useCache=false配置方案演示");
System.out.println("========================================");
System.out.println();
System.out.println("方案说明:");
System.out.println(" 在Mapper方法上配置useCache=false,跳过缓存直接查询数据库");
System.out.println();
System.out.println("配置方式:");
System.out.println();
System.out.println(" // 注解方式");
System.out.println(" @Select(value = "SELECT * FROM user WHERE id = #{id}", ");
System.out.println(" useCache = false)");
System.out.println(" User selectByIdNoCache(Long id);");
System.out.println();
System.out.println(" // XML方式");
System.out.println(" <select id="selectByIdNoCache" resultType="User" ");
System.out.println(" useCache="false">");
System.out.println(" SELECT * FROM user WHERE id = #{id}");
System.out.println(" </select>");
System.out.println();
System.out.println("步骤1: 第一次查询用户ID=1(useCache=false)");
User user1 = noCacheUserMapper.selectByIdNoCache(1L);
System.out.println(" 结果: " + user1);
System.out.println();
System.out.println("步骤2: 第二次查询用户ID=1(useCache=false)");
User user2 = noCacheUserMapper.selectByIdNoCache(1L);
System.out.println(" 结果: " + user2);
System.out.println();
System.out.println("验证结果:");
System.out.println(" user1 == user2: " + (user1 == user2));
System.out.println();
System.out.println("分析:");
System.out.println(" ✅ 每次查询都执行SQL(观察日志)");
System.out.println(" ✅ 不会将结果放入缓存");
System.out.println(" ✅ 不影响其他查询的缓存");
System.out.println();
System.out.println("对比flushCache和useCache:");
System.out.println();
System.out.println(" flushCache=true:");
System.out.println(" - 查询前清空整个一级缓存");
System.out.println(" - 影响同一SqlSession中所有缓存");
System.out.println(" - 适合需要确保数据绝对最新的场景");
System.out.println();
System.out.println(" useCache=false:");
System.out.println(" - 仅跳过当前查询的缓存");
System.out.println(" - 不影响其他查询的缓存");
System.out.println(" - 适合特定查询需要实时数据的场景");
System.out.println();
System.out.println("优点:");
System.out.println(" - 精确控制,只影响当前查询");
System.out.println(" - 不影响其他查询的缓存效果");
System.out.println(" - 配置简单,声明式");
System.out.println();
System.out.println("缺点:");
System.out.println(" - 每次都查询数据库,性能有一定影响");
System.out.println(" - 需要针对每个查询单独配置");
}
}
运行结果:
=== 方案三:禁用特定查询缓存(useCache=false) ===
========================================
useCache=false配置方案演示
========================================
方案说明:
在Mapper方法上配置useCache=false,跳过缓存直接查询数据库
配置方式:
// 注解方式
@Select(value = "SELECT * FROM user WHERE id = #{id}",
useCache = false)
User selectByIdNoCache(Long id);
// XML方式
<select id="selectByIdNoCache" resultType="User"
useCache="false">
SELECT * FROM user WHERE id = #{id}
</select>
步骤1: 第一次查询用户ID=1(useCache=false)
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
步骤2: 第二次查询用户ID=1(useCache=false)
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
验证结果:
user1 == user2: false
分析:
✅ 每次查询都执行SQL(观察日志)
✅ 不会将结果放入缓存
✅ 不影响其他查询的缓存
对比flushCache和useCache:
flushCache=true:
- 查询前清空整个一级缓存
- 影响同一SqlSession中所有缓存
- 适合需要确保数据绝对最新的场景
useCache=false:
- 仅跳过当前查询的缓存
- 不影响其他查询的缓存
- 适合特定查询需要实时数据的场景
优点:
- 精确控制,只影响当前查询
- 不影响其他查询的缓存效果
- 配置简单,声明式
缺点:
- 每次都查询数据库,性能有一定影响
- 需要针对每个查询单独配置
优点:精确控制,不影响其他查询
缺点:需要针对每个查询单独配置
3.4 方案四:全局配置缓存作用域
参考代码 lesson14-mybatis-cache 中的GlobalCacheSolution.java
package com.architect.pitfalls.mybatis.solution;
import com.architect.pitfalls.mybatis.cause.User;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.solution")
public class GlobalCacheSolution {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(GlobalCacheSolution.class, args);
GlobalCacheSolution solution = context.getBean(GlobalCacheSolution.class);
System.out.println("n=== 方案四:全局配置缓存作用域 ===n");
solution.demonstrateSolution();
context.close();
}
private final GlobalUserMapper globalUserMapper;
public GlobalCacheSolution(GlobalUserMapper globalUserMapper) {
this.globalUserMapper = globalUserMapper;
}
public void demonstrateSolution() {
System.out.println("========================================");
System.out.println("全局配置缓存作用域方案演示");
System.out.println("========================================");
System.out.println();
System.out.println("方案说明:");
System.out.println(" 通过配置local-cache-scope控制一级缓存的作用范围");
System.out.println();
System.out.println("配置选项:");
System.out.println();
System.out.println(" # application.yml");
System.out.println(" mybatis:");
System.out.println(" configuration:");
System.out.println(" local-cache-scope: statement # 禁用一级缓存");
System.out.println(" # local-cache-scope: session # 默认值,启用一级缓存");
System.out.println();
System.out.println("作用域说明:");
System.out.println();
System.out.println(" SESSION(默认):");
System.out.println(" - 缓存在SqlSession级别有效");
System.out.println(" - 同一SqlSession中相同查询返回缓存结果");
System.out.println(" - 可能导致数据不一致问题");
System.out.println();
System.out.println(" STATEMENT:");
System.out.println(" - 每次查询后立即清空缓存");
System.out.println(" - 相当于禁用一级缓存");
System.out.println(" - 总是获取最新数据");
System.out.println();
System.out.println("步骤1: 第一次查询用户ID=1");
User user1 = globalUserMapper.selectById(1L);
System.out.println(" 结果: " + user1);
System.out.println();
System.out.println("步骤2: 第二次查询用户ID=1");
User user2 = globalUserMapper.selectById(1L);
System.out.println(" 结果: " + user2);
System.out.println();
System.out.println("验证结果:");
System.out.println(" user1 == user2: " + (user1 == user2));
System.out.println();
System.out.println("分析:");
System.out.println(" ✅ 配置为statement后,每次查询都执行SQL");
System.out.println(" ✅ 全局生效,无需逐个配置");
System.out.println(" ✅ 彻底避免一级缓存问题");
System.out.println();
System.out.println("优点:");
System.out.println(" - 全局配置,一次生效");
System.out.println(" - 彻底解决一级缓存问题");
System.out.println(" - 适合对数据实时性要求高的系统");
System.out.println();
System.out.println("缺点:");
System.out.println(" - 完全禁用缓存,性能影响最大");
System.out.println(" - 可能影响不需要实时数据的查询性能");
System.out.println(" - 需要评估系统整体性能影响");
System.out.println();
System.out.println("适用场景:");
System.out.println(" 1. 数据实时性要求极高的系统");
System.out.println(" 2. 并发更新频繁的业务");
System.out.println(" 3. 对性能不敏感的管理后台");
System.out.println();
System.out.println("注意事项:");
System.out.println(" - 建议配合二级缓存使用");
System.out.println(" - 或者使用其他缓存方案(如Redis)");
System.out.println(" - 需要进行性能测试评估");
}
}
运行结果:
=== 方案四:全局配置缓存作用域 ===
========================================
全局配置缓存作用域方案演示
========================================
方案说明:
通过配置local-cache-scope控制一级缓存的作用范围
配置选项:
# application.yml
mybatis:
configuration:
local-cache-scope: statement # 禁用一级缓存
# local-cache-scope: session # 默认值,启用一级缓存
作用域说明:
SESSION(默认):
- 缓存在SqlSession级别有效
- 同一SqlSession中相同查询返回缓存结果
- 可能导致数据不一致问题
STATEMENT:
- 每次查询后立即清空缓存
- 相当于禁用一级缓存
- 总是获取最新数据
步骤1: 第一次查询用户ID=1
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
步骤2: 第二次查询用户ID=1
结果: User{id=1, username='张三', email='[email protected]', status='INACTIVE'}
验证结果:
user1 == user2: false
分析:
✅ 配置为statement后,每次查询都执行SQL
✅ 全局生效,无需逐个配置
✅ 彻底避免一级缓存问题
优点:
- 全局配置,一次生效
- 彻底解决一级缓存问题
- 适合对数据实时性要求高的系统
缺点:
- 完全禁用缓存,性能影响最大
- 可能影响不需要实时数据的查询性能
- 需要评估系统整体性能影响
适用场景:
1. 数据实时性要求极高的系统
2. 并发更新频繁的业务
3. 对性能不敏感的管理后台
注意事项:
- 建议配合二级缓存使用
- 或者使用其他缓存方案(如Redis)
- 需要进行性能测试评估
优点:全局配置,彻底解决
缺点:完全禁用缓存,性能影响最大
3.5 方案五:最佳实践综合方案
参考代码 lesson14-mybatis-cache 中的BestPracticeSolution.java
package com.architect.pitfalls.mybatis.solution;
import com.architect.pitfalls.mybatis.cause.User;
import org.apache.ibatis.annotations.*;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.Transactional;
@SpringBootApplication
@MapperScan("com.architect.pitfalls.mybatis.solution")
public class BestPracticeSolution {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(BestPracticeSolution.class, args);
BestPracticeSolution solution = context.getBean(BestPracticeSolution.class);
System.out.println("n=== 方案五:最佳实践综合方案 ===n");
solution.demonstrateSolution();
solution.showBestPractices();
context.close();
}
private final BestPracticeUserMapper userMapper;
public BestPracticeSolution(BestPracticeUserMapper userMapper) {
this.userMapper = userMapper;
}
@Transactional
public void demonstrateSolution() {
System.out.println("========================================");
System.out.println("最佳实践综合方案演示");
System.out.println("========================================");
System.out.println();
System.out.println("方案说明:");
System.out.println(" 根据不同场景选择合适的缓存策略");
System.out.println();
System.out.println("场景1: 普通查询 - 使用默认缓存");
System.out.println("----------------------------------------");
User user1 = userMapper.selectById(1L);
System.out.println(" 查询结果: " + user1);
User user2 = userMapper.selectById(1L);
System.out.println(" 再次查询(命中缓存): " + user2);
System.out.println(" 是否同一对象: " + (user1 == user2));
System.out.println();
System.out.println("场景2: 需要实时数据 - 使用useCache=false");
System.out.println("----------------------------------------");
User realtimeUser = userMapper.selectByIdRealtime(1L);
System.out.println(" 实时查询结果: " + realtimeUser);
System.out.println();
System.out.println("场景3: 更新后查询 - 自动清空缓存");
System.out.println("----------------------------------------");
User updateUser = new User();
updateUser.setId(1L);
updateUser.setUsername("张三");
updateUser.setEmail("[email protected]");
updateUser.setStatus("ACTIVE");
userMapper.update(updateUser);
System.out.println(" 已更新用户数据");
User afterUpdate = userMapper.selectById(1L);
System.out.println(" 更新后查询: " + afterUpdate);
System.out.println(" 说明: update操作自动清空了缓存");
System.out.println();
}
public void showBestPractices() {
System.out.println("========================================");
System.out.println("MyBatis一级缓存最佳实践总结");
System.out.println("========================================");
System.out.println();
System.out.println("1. 理解缓存机制");
System.out.println("----------------------------------------");
System.out.println(" - 一级缓存是SqlSession级别的");
System.out.println(" - Spring事务会复用同一SqlSession");
System.out.println(" - 更新操作会自动清空缓存");
System.out.println();
System.out.println("2. 场景化配置策略");
System.out.println("----------------------------------------");
System.out.println(" 【场景A:普通查询】");
System.out.println(" - 使用默认缓存配置");
System.out.println(" - 适合数据变更不频繁的场景");
System.out.println(" - 代码示例:@Select("SELECT * FROM user WHERE id = #{id}")");
System.out.println();
System.out.println(" 【场景B:需要实时数据】");
System.out.println(" - 配置useCache=false");
System.out.println(" - 适合对数据实时性要求高的查询");
System.out.println(" - 代码示例:@Select(value="...", useCache=false)");
System.out.println();
System.out.println(" 【场景C:关键业务查询】");
System.out.println(" - 配置flushCache=true");
System.out.println(" - 适合金融、交易等关键业务");
System.out.println(" - 代码示例:@Select(value="...", flushCache=true)");
System.out.println();
System.out.println("3. 事务设计建议");
System.out.println("----------------------------------------");
System.out.println(" - 避免长事务,减少缓存影响范围");
System.out.println(" - 更新操作后立即查询,利用自动清空机制");
System.out.println(" - 复杂业务考虑拆分事务");
System.out.println();
System.out.println("4. 代码规范");
System.out.println("----------------------------------------");
System.out.println(" - 在Mapper接口注释中说明缓存策略");
System.out.println(" - 关键查询方法命名体现缓存策略");
System.out.println(" 例如:selectByIdRealtime、selectByIdWithFlush");
System.out.println(" - 代码审查时关注缓存配置");
System.out.println();
System.out.println("5. 监控与告警");
System.out.println("----------------------------------------");
System.out.println(" - 监控缓存命中率");
System.out.println(" - 关注数据库查询频率");
System.out.println(" - 设置数据一致性校验机制");
System.out.println();
System.out.println("6. 架构层面建议");
System.out.println("----------------------------------------");
System.out.println(" - 考虑引入分布式缓存(Redis)替代本地缓存");
System.out.println(" - 使用读写分离架构,从库查询可放宽缓存要求");
System.out.println(" - CQRS模式:命令和查询分离,避免缓存问题");
System.out.println();
System.out.println("方案对比总结:");
System.out.println();
System.out.println(" ┌─────────────────┬─────────────────────────────────────────┐");
System.out.println(" │ 方案 │ 适用场景 │");
System.out.println(" ├─────────────────┼─────────────────────────────────────────┤");
System.out.println(" │ 手动clearCache │ 特定场景需要最新数据 │");
System.out.println(" │ flushCache=true │ 关键业务,数据绝对实时 │");
System.out.println(" │ useCache=false │ 特定查询需要实时数据 │");
System.out.println(" │ 全局statement │ 整体系统对实时性要求高 │");
System.out.println(" │ 场景化配置 │ 推荐方案,灵活平衡性能与一致性 │");
System.out.println(" └─────────────────┴─────────────────────────────────────────┘");
}
}
运行结果:
=== 方案五:最佳实践综合方案 ===
========================================
最佳实践综合方案演示
========================================
方案说明:
根据不同场景选择合适的缓存策略
场景1: 普通查询 - 使用默认缓存
----------------------------------------
查询结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
再次查询(命中缓存): User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
是否同一对象: true
场景2: 需要实时数据 - 使用useCache=false
----------------------------------------
实时查询结果: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
场景3: 更新后查询 - 自动清空缓存
----------------------------------------
已更新用户数据
更新后查询: User{id=1, username='张三', email='[email protected]', status='ACTIVE'}
说明: update操作自动清空了缓存
========================================
MyBatis一级缓存最佳实践总结
========================================
1. 理解缓存机制
----------------------------------------
- 一级缓存是SqlSession级别的
- Spring事务会复用同一SqlSession
- 更新操作会自动清空缓存
2. 场景化配置策略
----------------------------------------
【场景A:普通查询】
- 使用默认缓存配置
- 适合数据变更不频繁的场景
- 代码示例:@Select("SELECT * FROM user WHERE id = #{id}")
【场景B:需要实时数据】
- 配置useCache=false
- 适合对数据实时性要求高的查询
- 代码示例:@Select(value="...", useCache=false)
【场景C:关键业务查询】
- 配置flushCache=true
- 适合金融、交易等关键业务
- 代码示例:@Select(value="...", flushCache=true)
3. 事务设计建议
----------------------------------------
- 避免长事务,减少缓存影响范围
- 更新操作后立即查询,利用自动清空机制
- 复杂业务考虑拆分事务
4. 代码规范
----------------------------------------
- 在Mapper接口注释中说明缓存策略
- 关键查询方法命名体现缓存策略
例如:selectByIdRealtime、selectByIdWithFlush
- 代码审查时关注缓存配置
5. 监控与告警
----------------------------------------
- 监控缓存命中率
- 关注数据库查询频率
- 设置数据一致性校验机制
6. 架构层面建议
----------------------------------------
- 考虑引入分布式缓存(Redis)替代本地缓存
- 使用读写分离架构,从库查询可放宽缓存要求
- CQRS模式:命令和查询分离,避免缓存问题
方案对比总结:
┌─────────────────┬─────────────────────────────────────────┐
│ 方案 │ 适用场景 │
├─────────────────┼─────────────────────────────────────────┤
│ 手动clearCache │ 特定场景需要最新数据 │
│ flushCache=true │ 关键业务,数据绝对实时 │
│ useCache=false │ 特定查询需要实时数据 │
│ 全局statement │ 整体系统对实时性要求高 │
│ 场景化配置 │ 推荐方案,灵活平衡性能与一致性 │
└─────────────────┴─────────────────────────────────────────┘
优点:灵活平衡性能与一致性
缺点:需要根据场景选择合适策略
4. 架构思考
4.1 一级缓存方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动clearCache | 实现简单,精确控制 | 需要手动管理,容易遗漏 | 特定场景需要最新数据 |
| flushCache=true | 声明式配置,自动生效 | 每次清空缓存,性能影响大 | 关键业务,数据绝对实时 |
| useCache=false | 精确控制,不影响其他查询 | 每次查数据库,性能影响 | 特定查询需要实时数据 |
| 全局statement | 全局生效,彻底解决 | 完全禁用缓存,性能影响最大 | 整体系统对实时性要求高 |
| 场景化配置 | 灵活平衡性能与一致性 | 需要根据场景选择策略 | 推荐方案 |
4.2 最佳实践总结
代码层面:
- ✅ 理解一级缓存是SqlSession级别的
- ✅ Spring事务会复用同一SqlSession
- ✅ 更新操作会自动清空缓存
- ✅ 根据场景选择合适的缓存策略
- ❌ 不要在长事务中依赖缓存
团队规范:
- 强制规范:关键业务查询必须配置缓存策略
- 代码审查:重点检查缓存配置是否合理
- 命名规范:方法名体现缓存策略(如selectByIdRealtime)
- 文档说明:在代码注释中说明缓存策略
架构设计:
- 缓存选型:考虑引入分布式缓存(Redis)替代本地缓存
- 读写分离:从库查询可放宽缓存要求
- CQRS模式:命令和查询分离,避免缓存问题
- 监控告警:监控缓存命中率和数据一致性
4.3 Spring Boot 3.1.0 + MyBatis 3.5.x注意事项
在Spring Boot 3.1.0 + MyBatis 3.5.x版本中:
- 一级缓存机制未变:仍然存在上述问题
- 默认配置:
local-cache-scope默认为session - 推荐做法:
- 对于普通查询,使用默认缓存
- 对于需要实时数据的查询,配置
useCache=false - 对于关键业务查询,配置
flushCache=true - 对于实时性要求极高的系统,配置
local-cache-scope: statement
通过深入理解MyBatis一级缓存机制,不仅能避免生产环境的数据不一致问题,更能提升对系统设计的整体思考。在实际项目中,正确选择缓存策略至关重要,唯有深入理解底层原理,才能构建真正稳定可靠的系统。
相关文章
- 2026年三角洲行动账号交易:6大交易平台综合对比及安全选购指南 07-26
- 无限暖暖万相境88血怪怎么打 07-26
- 三角洲行动幸运鸟窝什么时候开启:三角洲行动幸运鸟窝介绍 07-26
- 三角洲行动游戏账号交易平台推荐及安全交易指南 07-26
- 三角洲行动阿萨拉暖冬季活动玩法详解:任务流程 奖励获取及新手攻略 07-26
- 地下城堡4龙痕竞技场徽记的作用 地下城堡4龙痕竞技场徽记获取方法 07-26