在生产环境中,我们几乎从不手动管理SqlSession,而是由Spring框架来管理。让我详细解释这套机制。
一、Spring+MyBatis的整合模式
1.1 核心组件:SqlSessionTemplate
- // 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代理模式(最常用)
- // 配置类
- @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直接使用
- @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
- @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
- @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请求场景:通常每个请求是一个事务
- @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绑定
- // 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 事务传播行为的影响
- @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
- @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 实践判断方法
- 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配置
- # 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 事务配置类
- @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应用中的典型流程
- // 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:批量操作优化
- @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("")
- List<User> selectByIds(@Param("ids") List<Long> ids);
- }
复制代码 八、最佳实践总结
8.1 判断规则总结
场景是否同一个SqlSession一级缓存是否生效同一个@Transactional方法内✓ 是✓ 生效同一个非事务方法内✗ 否(每次调用新建)✗ 不生效跨@Transactional方法调用取决于传播行为可能不生效Web请求(Service有@Transactional)✓ 是(每个请求一个)✓ 生效异步方法(@Async)✗ 否(不同线程)✗ 不生效8.2 生产环境建议
- Service层添加事务注解:
- @Service
- @Transactional(readOnly = true) // 类级别默认只读
- public class UserService {
-
- @Transactional // 写操作单独标记
- public User updateUser(User user) {
- // ...
- }
- }
复制代码 - 合理设置事务边界:
- // 合适的事务边界:一个完整的业务操作
- @Transactional
- public Order createOrder(OrderRequest request) {
- // 验证库存
- // 扣减库存
- // 创建订单
- // 记录日志
- // 所有操作在同一个事务中
- }
复制代码 - 监控SqlSession使用:
- // 添加监控点
- @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);
- }
- }
- }
- }
复制代码 - 一级缓存注意事项:
- 长事务可能导致一级缓存积累大量数据
- 考虑设置localCacheScope=STATEMENT(每次查询后清空一级缓存)
- 复杂对象修改可能影响缓存中的对象
九、常见问题解答
Q1:Spring Boot中默认的SqlSession管理策略是什么?
A:Spring Boot + MyBatis Starter默认配置:
- 使用SqlSessionTemplate管理SqlSession
- 默认不开启事务时,每个Mapper方法调用使用新的SqlSession
- 开启事务时,整个事务使用同一个SqlSession
Q2:如何查看当前是否有活跃的SqlSession?
- // 方法1:通过TransactionSynchronizationManager
- boolean hasActiveTransaction = TransactionSynchronizationManager
- .isActualTransactionActive();
- // 方法2:通过DataSourceUtils
- Connection conn = DataSourceUtils.getConnection(dataSource);
- boolean isTransactional = DataSourceUtils.isConnectionTransactional(conn, dataSource);
复制代码 Q3:一级缓存在微服务架构中有什么问题?
A:在微服务中:
- 每个实例有自己的JVM,一级缓存不共享
- 可能导致不同实例数据不一致
- 建议:关闭一级缓存或设置很短的作用域,使用分布式缓存
Q4:如何强制清空一级缓存?
- @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的生命周期:
- 没有事务:每次Mapper方法调用都创建新的SqlSession
- 有事务:整个事务使用同一个SqlSession(通常是一个HTTP请求对应一个事务)
关键判断标准:
- 查看方法是否有@Transactional注解
- 同一个@Transactional方法内 → 同一个SqlSession → 一级缓存生效
- 不同方法或没有事务 → 不同SqlSession → 一级缓存不生效
实际开发中,我们通常:
- 在Service层方法添加@Transactional
- 让Spring自动管理SqlSession
- 通过事务边界控制一级缓存的作用范围
- 监控长事务避免一级缓存过大
这样既能享受一级缓存的性能优势,又避免了手动管理SqlSession的复杂性。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |