Spring+MyBatis环境下SqlSession管理机制详解

在生产环境中,我们几乎从不手动管理SqlSession,而是由Spring框架来管理。让我详细解释这套机制。

一、Spring+MyBatis的整合模式

1.1 核心组件:SqlSessionTemplate

java 复制代码
// Spring管理SqlSession的核心组件
public class SqlSessionTemplate implements SqlSession, DisposableBean {
    
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    
    // 关键:通过ThreadLocal为每个线程绑定SqlSession
    private final ThreadLocal<SqlSession> sqlSessionHolder = 
        new ThreadLocal<>();
    
    // 执行数据库操作
    public <T> T selectOne(String statement) {
        // 1. 获取当前线程的SqlSession
        SqlSession sqlSession = getSqlSession();
        try {
            // 2. 执行查询
            return sqlSession.selectOne(statement);
        } finally {
            // 3. 不关闭!由Spring管理生命周期
        }
    }
}

二、Spring如何管理SqlSession生命周期

2.1 三种主要模式

模式1:Mapper代理模式(最常用)

java 复制代码
// 配置类
@Configuration
@MapperScan("com.example.mapper")  // 自动扫描Mapper接口
public class MyBatisConfig {
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }
}

// Service中使用
@Service
public class UserService {
    // 直接注入Mapper接口,不需要知道SqlSession
    @Autowired
    private UserMapper userMapper;
    
    public User getUser(Long id) {
        // Spring自动管理SqlSession
        return userMapper.selectById(id);
    }
}

模式2:SqlSessionTemplate直接使用

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    
    public User getUser(Long id) {
        // 手动获取Mapper(较少使用)
        UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
        return mapper.selectById(id);
    }
}

2.2 核心问题解答:一次请求是否对应一个SqlSession?

答案是:取决于事务配置!

三、不同场景下的SqlSession管理

3.1 无事务方法:每次Mapper方法调用都是新的SqlSession

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    // 没有@Transactional注解
    public void demoNoTransaction() {
        System.out.println("=== 无事务方法演示 ===");
        
        // 第一次查询:创建新的SqlSession1
        User user1 = userMapper.selectById(1);
        System.out.println("查询1:创建SqlSession1,执行SQL");
        
        // 第二次查询:创建新的SqlSession2
        User user2 = userMapper.selectById(1);
        System.out.println("查询2:创建SqlSession2,执行SQL");
        
        // 结论:两次查询在两个不同的SqlSession中
        // 一级缓存不生效!会执行两次SQL
    }
}

3.2 有事务方法:整个方法使用同一个SqlSession

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Transactional  // 关键注解!
    public void demoWithTransaction() {
        System.out.println("=== 有事务方法演示 ===");
        
        // 第一次查询:创建SqlSession
        User user1 = userMapper.selectById(1);
        System.out.println("查询1:创建SqlSession,执行SQL");
        
        // 第二次查询:复用同一个SqlSession
        User user2 = userMapper.selectById(1);
        System.out.println("查询2:复用SqlSession,从一级缓存读取,无SQL");
        
        // 第三次查询不同参数
        User user3 = userMapper.selectById(2);
        System.out.println("查询3:参数不同,执行SQL");
        
        // 第四次查询相同参数
        User user4 = userMapper.selectById(2);
        System.out.println("查询4:从一级缓存读取,无SQL");
        
        // 方法结束:提交事务,关闭SqlSession
        System.out.println("方法结束:提交事务,关闭SqlSession");
        
        // 结论:整个方法在同一个SqlSession中
        // 一级缓存生效!相同查询只执行一次SQL
    }
}

3.3 Web请求场景:通常每个请求是一个事务

java 复制代码
@RestController
@RequestMapping("/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    // @Transactional  // Controller层一般不直接加事务
    public User getUser(@PathVariable Long id) {
        // 通常Service方法有@Transactional
        return userService.getUserWithCache(id);
    }
}

@Service
public class UserService {
    
    @Transactional  // Service层加事务,一个HTTP请求对应一个事务
    public User getUserWithCache(Long id) {
        // 这个Service方法中的所有数据库操作
        // 都在同一个SqlSession中
        // 一级缓存生效
        return userMapper.selectById(id);
    }
}

四、Spring事务管理机制

4.1 事务管理器与SqlSession绑定

java 复制代码
// Spring的事务管理机制
public class SpringManagedTransaction implements Transaction {
    
    private final DataSource dataSource;
    private Connection connection;
    private boolean isConnectionTransactional;
    
    // 获取连接(关键:从事务同步管理器中获取)
    @Override
    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            openConnection();
        }
        return this.connection;
    }
    
    private void openConnection() throws SQLException {
        // 从Spring的事务同步管理器中获取连接
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(
            this.connection, this.dataSource);
    }
}

4.2 事务传播行为的影响

java 复制代码
@Service
public class ComplexService {
    
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private OrderMapper orderMapper;
    
    // 外层事务
    @Transactional(propagation = Propagation.REQUIRED)
    public void complexBusiness(Long userId) {
        // 在事务中:创建/获取SqlSession1
        User user = userMapper.selectById(userId);  // SqlSession1
        
        // 调用内层方法(默认REQUIRED,加入现有事务)
        processOrders(userId);  // 仍然使用SqlSession1
        
        // 结论:整个方法使用同一个SqlSession
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processOrders(Long userId) {
        // 创建新的事务,新的SqlSession2
        List<Order> orders = orderMapper.selectByUserId(userId);  // SqlSession2
        
        // 与complexBusiness方法不在同一个SqlSession
        // 一级缓存不共享
    }
    
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void logOperation(Long userId) {
        // 非事务执行,没有SqlSession绑定
        // 每次数据库操作可能使用不同的SqlSession
    }
}

五、如何判断当前是否在同一个SqlSession中

5.1 调试方法:查看SqlSession ID

java 复制代码
@Service
public class DebugService {
    
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    
    @Transactional
    public void debugSqlSession() {
        // 方法1:获取当前SqlSession(仅用于调试)
        SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory()
            .openSession();
        
        // 获取SqlSession的唯一标识
        System.out.println("SqlSession hashCode: " + sqlSession.hashCode());
        
        // 方法2:通过反射查看ThreadLocal中的SqlSession
        try {
            Field field = sqlSessionTemplate.getClass()
                .getDeclaredField("sqlSessionHolder");
            field.setAccessible(true);
            ThreadLocal<SqlSession> holder = (ThreadLocal<SqlSession>) field.get(sqlSessionTemplate);
            SqlSession currentSession = holder.get();
            System.out.println("Current SqlSession: " + currentSession.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.2 实践判断方法

java 复制代码
public class SqlSessionInspector {
    
    /**
     * 判断两个操作是否在同一个SqlSession中的实用方法
     * 
     * 规则:
     * 1. 在同一个@Transactional方法中 → 同一个SqlSession ✓
     * 2. 在同一个Service方法中(无@Transactional)→ 可能不同 ✗
     * 3. 跨Service方法调用 → 取决于事务传播行为
     */
    public static boolean isSameSqlSession() {
        // 实际判断逻辑:
        // 1. 查看当前线程是否有活跃事务
        TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
        return status != null && !status.isCompleted();
    }
}

六、生产环境配置示例

6.1 Spring Boot + MyBatis配置

yaml 复制代码
# application.yml
mybatis:
  configuration:
    cache-enabled: true  # 开启二级缓存
    local-cache-scope: statement  # 一级缓存作用域
    
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 123456
    hikari:
      maximum-pool-size: 20
      
  # 事务管理配置
  transaction:
    default-timeout: 30  # 事务超时时间30秒

6.2 事务配置类

java 复制代码
@Configuration
@EnableTransactionManagement  // 启用事务管理
public class TransactionConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager manager) {
        TransactionTemplate template = new TransactionTemplate(manager);
        template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        return template;
    }
}

七、实际案例分析

7.1 案例1:Web应用中的典型流程

java 复制代码
// HTTP请求:GET /users/1
// 1. 请求到达DispatcherServlet
// 2. 调用UserController.getUser(1)
// 3. 调用UserService.getUserWithCache(1)

@Slf4j
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Transactional(readOnly = true)
    public User getUserWithCache(Long id) {
        log.info("开始查询用户 {}", id);
        
        // 第一次查询
        long start1 = System.currentTimeMillis();
        User user1 = userMapper.selectById(id);
        log.info("第一次查询耗时: {}ms", System.currentTimeMillis() - start1);
        
        // 第二次查询(应该从一级缓存读取)
        long start2 = System.currentTimeMillis();
        User user2 = userMapper.selectById(id);
        log.info("第二次查询耗时: {}ms", System.currentTimeMillis() - start2);
        
        // 验证是同一个对象(一级缓存直接返回引用)
        boolean sameReference = (user1 == user2);
        log.info("两次查询返回同一对象: {}", sameReference);
        
        return user2;
    }
}

// 日志输出:
// 第一次查询耗时: 15ms  (执行SQL)
// 第二次查询耗时: 0ms   (一级缓存)
// 两次查询返回同一对象: true

7.2 案例2:批量操作优化

java 复制代码
@Service
public class BatchService {
    
    @Autowired
    private UserMapper userMapper;
    
    // 错误示例:非事务中批量查询
    public List<User> getUsersWrong(List<Long> ids) {
        List<User> users = new ArrayList<>();
        for (Long id : ids) {
            // 每次循环都创建新的SqlSession
            User user = userMapper.selectById(id);  // N次SQL查询
            users.add(user);
        }
        return users;
    }
    
    // 正确示例1:使用事务包装
    @Transactional(readOnly = true)
    public List<User> getUsersRight1(List<Long> ids) {
        List<User> users = new ArrayList<>();
        for (Long id : ids) {
            // 同一个SqlSession,但相同查询只执行一次
            User user = userMapper.selectById(id);
            users.add(user);
        }
        return users;
    }
    
    // 正确示例2:批量查询方法
    public List<User> getUsersRight2(List<Long> ids) {
        // 一次SQL查询,返回所有结果
        return userMapper.selectByIds(ids);
    }
    
    // 正确示例3:使用IN查询
    @Select("<script>" +
            "SELECT * FROM user WHERE id IN " +
            "<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
            "#{id}" +
            "</foreach>" +
            "</script>")
    List<User> selectByIds(@Param("ids") List<Long> ids);
}

八、最佳实践总结

8.1 判断规则总结

场景 是否同一个SqlSession 一级缓存是否生效
同一个@Transactional方法内 ✓ 是 ✓ 生效
同一个非事务方法内 ✗ 否(每次调用新建) ✗ 不生效
跨@Transactional方法调用 取决于传播行为 可能不生效
Web请求(Service有@Transactional) ✓ 是(每个请求一个) ✓ 生效
异步方法(@Async) ✗ 否(不同线程) ✗ 不生效

8.2 生产环境建议

  1. Service层添加事务注解

    java 复制代码
    @Service
    @Transactional(readOnly = true)  // 类级别默认只读
    public class UserService {
        
        @Transactional  // 写操作单独标记
        public User updateUser(User user) {
            // ...
        }
    }
  2. 合理设置事务边界

    java 复制代码
    // 合适的事务边界:一个完整的业务操作
    @Transactional
    public Order createOrder(OrderRequest request) {
        // 验证库存
        // 扣减库存
        // 创建订单
        // 记录日志
        // 所有操作在同一个事务中
    }
  3. 监控SqlSession使用

    java 复制代码
    // 添加监控点
    @Aspect
    @Component
    public class SqlSessionMonitor {
        
        @Around("@within(org.springframework.stereotype.Service)")
        public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
            String methodName = joinPoint.getSignature().getName();
            long start = System.currentTimeMillis();
            
            try {
                return joinPoint.proceed();
            } finally {
                long duration = System.currentTimeMillis() - start;
                if (duration > 1000) {  // 超过1秒警告
                    log.warn("方法 {} 执行时间过长: {}ms", methodName, duration);
                }
            }
        }
    }
  4. 一级缓存注意事项

    • 长事务可能导致一级缓存积累大量数据
    • 考虑设置localCacheScope=STATEMENT(每次查询后清空一级缓存)
    • 复杂对象修改可能影响缓存中的对象

九、常见问题解答

Q1:Spring Boot中默认的SqlSession管理策略是什么?

A:Spring Boot + MyBatis Starter默认配置:

  • 使用SqlSessionTemplate管理SqlSession
  • 默认不开启事务时,每个Mapper方法调用使用新的SqlSession
  • 开启事务时,整个事务使用同一个SqlSession

Q2:如何查看当前是否有活跃的SqlSession?

java 复制代码
// 方法1:通过TransactionSynchronizationManager
boolean hasActiveTransaction = TransactionSynchronizationManager
    .isActualTransactionActive();

// 方法2:通过DataSourceUtils
Connection conn = DataSourceUtils.getConnection(dataSource);
boolean isTransactional = DataSourceUtils.isConnectionTransactional(conn, dataSource);

Q3:一级缓存在微服务架构中有什么问题?

A:在微服务中:

  • 每个实例有自己的JVM,一级缓存不共享
  • 可能导致不同实例数据不一致
  • 建议:关闭一级缓存或设置很短的作用域,使用分布式缓存

Q4:如何强制清空一级缓存?

java 复制代码
@Service
public class CacheService {
    
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    
    public void clearLocalCache() {
        // 获取当前SqlSession并清空缓存
        sqlSessionTemplate.clearCache();
    }
    
    @Transactional
    public void updateAndClear() {
        // 更新操作会自动清空相关缓存
        userMapper.updateUser(user);
        
        // 如果需要手动清空
        sqlSessionTemplate.clearCache();
    }
}

总结

在生产环境中,Spring通过事务管理自动控制SqlSession的生命周期

  1. 没有事务:每次Mapper方法调用都创建新的SqlSession
  2. 有事务:整个事务使用同一个SqlSession(通常是一个HTTP请求对应一个事务)

关键判断标准

  • 查看方法是否有@Transactional注解
  • 同一个@Transactional方法内 → 同一个SqlSession → 一级缓存生效
  • 不同方法或没有事务 → 不同SqlSession → 一级缓存不生效

实际开发中,我们通常:

  1. 在Service层方法添加@Transactional
  2. 让Spring自动管理SqlSession
  3. 通过事务边界控制一级缓存的作用范围
  4. 监控长事务避免一级缓存过大

这样既能享受一级缓存的性能优势,又避免了手动管理SqlSession的复杂性。

相关推荐
lazy★boy1 年前
Spring中每次访问数据库都要创建SqlSession吗?
spring·mybatis·sqlsession
码农爱java1 年前
MyBatis 源码分析-- getMapper(获取Mapper)
mybatis·源码·mapper·sqlsession·getmapper
Davieyang.D.Y2 年前
互联网轻量级框架整合之MyBatis核心组件
mybatis·生命周期·mapper·sqlsession·sessionfactory
丁总学Java2 年前
mybatis数据输入-实体类型的参数
mybatis·commit·manager·sqlsession·transaction·insert·resources
wsdhla2 年前
动态数据源自定义SqlSessionFactoryBean时mybatis plus配置失效
mybatis·mybatis-plus·config·datasource·sqlsession·dynamic
金刚猿2 年前
76、SpringBoot 整合 MyBatis------使用 sqlSession 作为 Dao 组件(就是ssm那一套,在 xml 写sql)
xml·spring boot·mybatis·sqlsession