MyBatis缓存引发同一SqlSession中的数据不一致

作者:袖梨 2026-07-24

作为多年的Java开发经验,在开发过程中经常会踩一些坑,本系列想通过一些案例分享,帮助其他开发者避免这些问题。

MyBatis缓存导致同一SqlSession中的数据不一致

注意:由于框架不同版本改造会有些使用的不同,因此本次系列中使用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版本中:

  1. 一级缓存机制未变:仍然存在上述问题
  2. 默认配置local-cache-scope默认为session
  3. 推荐做法
    • 对于普通查询,使用默认缓存
    • 对于需要实时数据的查询,配置useCache=false
    • 对于关键业务查询,配置flushCache=true
    • 对于实时性要求极高的系统,配置local-cache-scope: statement

通过深入理解MyBatis一级缓存机制,不仅能避免生产环境的数据不一致问题,更能提升对系统设计的整体思考。在实际项目中,正确选择缓存策略至关重要,唯有深入理解底层原理,才能构建真正稳定可靠的系统。

相关文章

精彩推荐