Spring与MyBatis整合原理及事务管理

目录

[🎯 先说说我被整合坑惨的经历](#🎯 先说说我被整合坑惨的经历)

[✨ 摘要](#✨ 摘要)

[1. 整合的三种方式:你用的是哪一种?](#1. 整合的三种方式:你用的是哪一种?)

[1.1 传统XML配置:最经典也最复杂](#1.1 传统XML配置:最经典也最复杂)

[1.2 注解配置:Spring JavaConfig](#1.2 注解配置:Spring JavaConfig)

[1.3 Spring Boot自动配置:最简单](#1.3 Spring Boot自动配置:最简单)

[2. 整合的核心:SqlSession管理](#2. 整合的核心:SqlSession管理)

[2.1 为什么需要SqlSessionTemplate?](#2.1 为什么需要SqlSessionTemplate?)

[2.2 SqlSessionTemplate的线程安全设计](#2.2 SqlSessionTemplate的线程安全设计)

[2.3 性能影响测试](#2.3 性能影响测试)

[3. Mapper接口的自动注册](#3. Mapper接口的自动注册)

[3.1 @MapperScan是怎么工作的?](#3.1 @MapperScan是怎么工作的?)

[3.2 MapperScannerConfigurer:扫描器的核心](#3.2 MapperScannerConfigurer:扫描器的核心)

[3.3 MapperFactoryBean:Mapper的工厂](#3.3 MapperFactoryBean:Mapper的工厂)

[4. Spring事务管理在MyBatis中的实现](#4. Spring事务管理在MyBatis中的实现)

[4.1 事务管理器配置](#4.1 事务管理器配置)

[4.2 事务的传播行为测试](#4.2 事务的传播行为测试)

[4.3 事务与SqlSession的绑定](#4.3 事务与SqlSession的绑定)

[5. 整合中的性能陷阱与优化](#5. 整合中的性能陷阱与优化)

[5.1 连接池配置不当](#5.1 连接池配置不当)

[5.2 一级缓存失效问题](#5.2 一级缓存失效问题)

[5.3 批量操作性能优化](#5.3 批量操作性能优化)

[6. 多数据源整合](#6. 多数据源整合)

[6.1 多数据源配置](#6.1 多数据源配置)

[6.2 动态数据源切换](#6.2 动态数据源切换)

[7. 事务失效的常见场景](#7. 事务失效的常见场景)

[7.1 自调用问题](#7.1 自调用问题)

[7.2 异常类型不匹配](#7.2 异常类型不匹配)

[7.3 方法修饰符问题](#7.3 方法修饰符问题)

[8. 性能监控与调优](#8. 性能监控与调优)

[8.1 监控指标配置](#8.1 监控指标配置)

[8.2 关键监控指标](#8.2 关键监控指标)

[8.3 慢SQL监控](#8.3 慢SQL监控)

[9. 企业级最佳实践](#9. 企业级最佳实践)

[9.1 我的"整合配置军规"](#9.1 我的"整合配置军规")

[📜 第一条:明确数据源配置](#📜 第一条:明确数据源配置)

[📜 第二条:合理使用事务](#📜 第二条:合理使用事务)

[📜 第三条:监控到位](#📜 第三条:监控到位)

[📜 第四条:代码规范](#📜 第四条:代码规范)

[📜 第五条:测试充分](#📜 第五条:测试充分)

[9.2 生产环境配置模板](#9.2 生产环境配置模板)

[10. 故障排查指南](#10. 故障排查指南)

[10.1 常见问题排查清单](#10.1 常见问题排查清单)

问题1:事务不生效

问题2:连接泄露

问题3:性能下降

[10.2 调试技巧](#10.2 调试技巧)

[11. 最后的话](#11. 最后的话)

[📚 推荐阅读](#📚 推荐阅读)

官方文档

源码学习

最佳实践

监控工具


🎯 先说说我被整合坑惨的经历

三年前我们团队接手一个老系统,Spring 4 + MyBatis 3.2。看起来标准配置,运行也正常。直到双十一大促,系统突然开始报"连接泄露"。我们查了三天三夜,最后发现是SqlSession没正确关闭。

更绝的是,有次线上事务不生效,数据出现不一致。排查发现是有人用了@Transactional,但方法里调用了this.xxx(),导致AOP代理失效。

去年迁移到Spring Boot,以为用mybatis-spring-boot-starter就万事大吉了。结果批量插入性能下降70%,排查发现是默认配置不对。

这些事让我明白:不懂整合原理,就等于开组装车不知道零部件怎么连接的,早晚要散架

✨ 摘要

Spring与MyBatis整合的核心在于SqlSession的动态管理、Mapper接口的自动注册、以及Spring事务管理的无缝集成。本文深度解析整合原理,从SqlSessionTemplate的线程安全设计、MapperScannerConfigurer的扫描机制,到Spring声明式事务在MyBatis中的实现。通过源码级分析、性能测试数据和实战案例,揭示整合过程中的常见陷阱和优化策略。

1. 整合的三种方式:你用的是哪一种?

1.1 传统XML配置:最经典也最复杂

先看看最传统的整合方式,现在还有很多老项目在用:

XML 复制代码
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx">
    
    <!-- 1. 数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    
    <!-- 2. SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>
    
    <!-- 3. 事务管理器 -->
    <bean id="transactionManager" 
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
    <!-- 4. 开启注解事务 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
    <!-- 5. Mapper扫描 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.example.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
    
</beans>

代码清单1:传统Spring+MyBatis XML配置

问题:配置繁琐,容易出错,依赖顺序很重要。

1.2 注解配置:Spring JavaConfig

Spring 3.0之后,推荐用JavaConfig:

java 复制代码
@Configuration
@ComponentScan("com.example")
@EnableTransactionManagement
@MapperScan("com.example.mapper")
public class AppConfig {
    
    @Bean
    public DataSource dataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(env.getProperty("jdbc.url"));
        ds.setUsername(env.getProperty("jdbc.username"));
        ds.setPassword(env.getProperty("jdbc.password"));
        return ds;
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml")
        );
        return factoryBean.getObject();
    }
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

代码清单2:JavaConfig配置方式

优点:类型安全,IDE友好,不容易配置错。

1.3 Spring Boot自动配置:最简单

现在最流行的方式:

复制代码
# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.model
  configuration:
    map-underscore-to-camel-case: true
java 复制代码
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

代码清单3:Spring Boot自动配置

但问题来了:这么简单的配置背后,Spring Boot到底干了什么?

2. 整合的核心:SqlSession管理

2.1 为什么需要SqlSessionTemplate?

在纯MyBatis中,我们这样用:

java 复制代码
// 传统MyBatis用法
try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    User user = mapper.findById(1L);
    session.commit();
}

问题

  1. 需要手动管理SqlSession生命周期

  2. 事务管理复杂

  3. 线程不安全

Spring的解决方案:SqlSessionTemplate

java 复制代码
@Repository
public class UserDaoImpl implements UserDao {
    
    // 直接注入SqlSessionTemplate
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    
    public User findById(Long id) {
        // 不用关心SqlSession的创建和关闭
        return sqlSessionTemplate.selectOne(
            "com.example.mapper.UserMapper.findById", id);
    }
}

代码清单4:使用SqlSessionTemplate

2.2 SqlSessionTemplate的线程安全设计

这是Spring整合MyBatis最精妙的设计。看源码:

java 复制代码
public class SqlSessionTemplate implements SqlSession, DisposableBean {
    
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy;  // 关键:动态代理
    
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration()
            .getDefaultExecutorType());
    }
    
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        
        // 创建动态代理
        this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[] { SqlSession.class },
            new SqlSessionInterceptor()  // 关键:拦截器
        );
    }
    
    // 所有SqlSession方法都委托给代理
    @Override
    public <T> T selectOne(String statement) {
        return sqlSessionProxy.selectOne(statement);
    }
    
    // 内部拦截器类
    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 关键:每次调用都获取新的SqlSession
            SqlSession sqlSession = getSqlSession(
                sqlSessionFactory, executorType, EXCEPTION);
            
            try {
                Object result = method.invoke(sqlSession, args);
                // 不提交事务!由Spring事务管理器控制
                return result;
            } catch (Throwable t) {
                // 异常处理...
                throw ExceptionUtil.unwrapThrowable(t);
            } finally {
                // 关闭SqlSession
                closeSqlSession(sqlSession, sqlSessionFactory);
            }
        }
    }
    
    // 获取SqlSession:与当前事务关联
    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, 
                                          ExecutorType executorType, 
                                          PersistenceExceptionTranslator exceptionTranslator) {
        
        // 关键:检查当前线程是否已有SqlSession
        SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager
            .getResource(sessionFactory);
        
        if (holder != null && holder.isSynchronizedWithTransaction()) {
            if (holder.getExecutorType() != executorType) {
                throw new TransientDataAccessResourceException(
                    "Cannot change ExecutorType when there is an existing transaction");
            }
            
            // 增加引用计数
            holder.requested();
            return holder.getSqlSession();
        }
        
        // 没有事务,创建新的SqlSession
        SqlSession session = sessionFactory.openSession(executorType);
        
        // 注册到事务同步管理器
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
        
        return session;
    }
}

代码清单5:SqlSessionTemplate核心源码

用图来表示更清楚:

图1:SqlSessionTemplate执行流程

关键点

  1. 用动态代理拦截所有方法调用

  2. 每次方法调用都可能创建新SqlSession

  3. 事务期间复用同一个SqlSession

  4. 事务结束后自动关闭SqlSession

2.3 性能影响测试

这么复杂的机制,性能影响大吗?我做了测试:

测试场景:单线程执行1000次查询

使用方式 总耗时(ms) 平均耗时(ms) SqlSession创建次数
原生MyBatis 1250 1.25 1
SqlSessionTemplate(无事务) 1450 1.45 1000
SqlSessionTemplate(有事务) 1320 1.32 1

结论

  1. 无事务时,每次调用都创建SqlSession,性能损失约16%

  2. 有事务时,复用SqlSession,性能接近原生

  3. 生产环境大部分操作在事务中,性能影响可接受

3. Mapper接口的自动注册

3.1 @MapperScan是怎么工作的?

很多人用@MapperScan,但不知道它干了什么。看源码:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MapperScannerRegistrar.class)  // 关键:导入配置类
@Repeatable(MapperScans.class)
public @interface MapperScan {
    String[] value() default {};
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    // ...
}

// 配置类
class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar {
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, 
                                      BeanDefinitionRegistry registry) {
        
        // 解析@MapperScan注解
        Map<String, Object> annotationAttributes = importingClassMetadata
            .getAnnotationAttributes(MapperScan.class.getName());
        
        // 创建扫描器配置
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition(MapperScannerConfigurer.class);
        
        // 设置扫描包
        builder.addPropertyValue("basePackage", 
            StringUtils.arrayToCommaDelimitedString((String[]) 
                annotationAttributes.get("basePackages")));
        
        // 注册Bean
        registry.registerBeanDefinition(
            MapperScannerConfigurer.class.getName(), 
            builder.getBeanDefinition());
    }
}

代码清单6:@MapperScan工作原理

3.2 MapperScannerConfigurer:扫描器的核心

真正干活的是MapperScannerConfigurer

java 复制代码
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean {
    
    private String basePackage;
    private SqlSessionFactory sqlSessionFactory;
    private SqlSessionTemplate sqlSessionTemplate;
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 创建ClassPath扫描器
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        
        // 设置过滤条件:只扫描接口
        scanner.setAnnotationClass(Mapper.class);
        scanner.registerFilters();
        
        // 设置SqlSessionFactory
        if (sqlSessionFactory != null) {
            scanner.setSqlSessionFactory(sqlSessionFactory);
        }
        if (sqlSessionTemplate != null) {
            scanner.setSqlSessionTemplate(sqlSessionTemplate);
        }
        
        // 扫描并注册
        scanner.scan(StringUtils.tokenizeToStringArray(
            this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
}

// 自定义扫描器
class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    
    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        // 1. 扫描包,获取Bean定义
        Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
        
        if (beanDefinitions.isEmpty()) {
            logger.warn("No MyBatis mapper was found...");
        } else {
            // 2. 处理每个Bean定义
            processBeanDefinitions(beanDefinitions);
        }
        
        return beanDefinitions;
    }
    
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        for (BeanDefinitionHolder holder : beanDefinitions) {
            GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
            
            // 3. 设置Bean类为MapperFactoryBean
            definition.setBeanClass(MapperFactoryBean.class);
            
            // 4. 设置构造函数参数:Mapper接口
            definition.getConstructorArgumentValues()
                .addGenericArgumentValue(definition.getBeanClassName());
            
            // 5. 设置属性:SqlSessionFactory/SqlSessionTemplate
            if (sqlSessionFactory != null) {
                definition.getPropertyValues()
                    .add("sqlSessionFactory", sqlSessionFactory);
            }
            if (sqlSessionTemplate != null) {
                definition.getPropertyValues()
                    .add("sqlSessionTemplate", sqlSessionTemplate);
            }
        }
    }
}

代码清单7:MapperScannerConfigurer核心逻辑

用图表示扫描过程:

图2:Mapper接口扫描注册流程

3.3 MapperFactoryBean:Mapper的工厂

这是Spring创建Mapper代理的关键:

java 复制代码
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    
    private Class<T> mapperInterface;
    private boolean addToConfig = true;
    
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
    
    @Override
    public T getObject() throws Exception {
        // 从SqlSessionTemplate获取Mapper
        return getSqlSession().getMapper(this.mapperInterface);
    }
    
    @Override
    public Class<T> getObjectType() {
        return this.mapperInterface;
    }
    
    @Override
    public boolean isSingleton() {
        return true;
    }
    
    @Override
    protected void checkDaoConfig() {
        super.checkDaoConfig();
        
        // 验证配置
        Configuration configuration = getSqlSession().getConfiguration();
        if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
            try {
                // 将Mapper添加到MyBatis配置
                configuration.addMapper(this.mapperInterface);
            } catch (Exception e) {
                logger.error("Error while adding the mapper...", e);
                throw new IllegalArgumentException(e);
            }
        }
    }
}

代码清单8:MapperFactoryBean核心代码

4. Spring事务管理在MyBatis中的实现

4.1 事务管理器配置

这是整合中最容易出错的部分:

java 复制代码
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // 关键:必须用DataSourceTransactionManager
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource);
        
        // 可选配置
        tm.setDefaultTimeout(30);  // 默认30秒超时
        tm.setNestedTransactionAllowed(true);  // 允许嵌套事务
        tm.setRollbackOnCommitFailure(true);  // 提交失败时回滚
        
        return tm;
    }
}

为什么必须是DataSourceTransactionManager?

因为MyBatis底层用的是JDBC,而DataSourceTransactionManager是Spring为JDBC事务提供的实现。

4.2 事务的传播行为测试

Spring的7种传播行为在MyBatis中表现如何?我做了测试:

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void outerMethod() {
        userMapper.insert(new User("user1"));
        
        try {
            innerMethod();  // 调用内部方法
        } catch (Exception e) {
            // 处理异常
        }
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerMethod() {
        userMapper.insert(new User("user2"));
        throw new RuntimeException("测试异常");
    }
}

代码清单9:事务传播行为测试

测试结果

传播行为 结果 说明
REQUIRED user1插入成功,user2回滚 同一个事务,一起回滚
REQUIRES_NEW user1插入成功,user2回滚 新事务,独立回滚
NESTED user1插入成功,user2回滚 嵌套事务,可部分回滚
SUPPORTS 无事务运行 沿用当前事务状态
NOT_SUPPORTED 挂起当前事务 无事务运行
NEVER 抛出异常 不能在事务中运行
MANDATORY 必须在事务中 否则抛异常

注意 :MySQL的InnoDB不支持真正的嵌套事务,NESTED会被降级为REQUIRED

4.3 事务与SqlSession的绑定

这是理解事务整合的关键。看源码:

java 复制代码
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager {
    
    @Override
    protected Object doGetTransaction() {
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        
        // 获取当前连接
        ConnectionHolder conHolder = (ConnectionHolder) 
            TransactionSynchronizationManager.getResource(this.dataSource);
        
        txObject.setConnectionHolder(conHolder, false);
        return txObject;
    }
    
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        
        Connection con = null;
        try {
            // 获取数据库连接
            con = this.dataSource.getConnection();
            
            // 设置事务属性
            con.setAutoCommit(false);
            con.setTransactionIsolation(definition.getIsolationLevel());
            
            // 绑定到当前线程
            ConnectionHolder holder = new ConnectionHolder(con);
            holder.setSynchronizedWithTransaction(true);
            
            TransactionSynchronizationManager.bindResource(
                this.dataSource, holder);
            
            txObject.setConnectionHolder(holder);
            
        } catch (Throwable ex) {
            // 清理资源
            DataSourceUtils.releaseConnection(con, this.dataSource);
            throw new CannotCreateTransactionException("Could not open JDBC Connection", ex);
        }
    }
}

代码清单10:DataSourceTransactionManager事务绑定

在MyBatis中,SqlSession会使用这个绑定的连接:

java 复制代码
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, 
                                      ExecutorType executorType, 
                                      PersistenceExceptionTranslator exceptionTranslator) {
    
    // 检查当前线程是否已绑定连接
    ConnectionHolder holder = (ConnectionHolder) 
        TransactionSynchronizationManager.getResource(sessionFactory);
    
    if (holder != null) {
        // 有事务,使用事务中的连接创建SqlSession
        SqlSession session = holder.getSqlSession();
        if (session == null) {
            session = sessionFactory.openSession(executorType);
            holder.setSqlSession(session);
        }
        return session;
    }
    
    // 无事务,创建新的SqlSession
    return sessionFactory.openSession(executorType);
}

用图表示事务、连接、SqlSession的关系:

图3:事务与连接绑定关系

5. 整合中的性能陷阱与优化

5.1 连接池配置不当

我见过最多的性能问题就是连接池配置不对:

复制代码
# 错误配置
spring:
  datasource:
    hikari:
      maximum-pool-size: 100  # 太大,浪费资源
      minimum-idle: 50        # 太大,启动慢
      connection-timeout: 30000  # 太长

# 正确配置(根据实际负载调整)
spring:
  datasource:
    hikari:
      maximum-pool-size: 20   # 根据CPU核心数*2-4
      minimum-idle: 5         # 小一点,按需创建
      connection-timeout: 5000  # 5秒超时
      idle-timeout: 600000    # 10分钟空闲超时
      max-lifetime: 1800000   # 30分钟最大生命周期
      leak-detection-threshold: 30000  # 30秒泄露检测

性能测试数据(100并发查询):

连接池大小 QPS 平均响应时间(ms) 连接使用率
10 850 45 100%
20 1550 32 75%
50 1600 31 40%
100 1620 30 20%

结论:连接池不是越大越好,20-50是比较合理的范围。

5.2 一级缓存失效问题

在Spring整合MyBatis中,一级缓存很容易失效:

java 复制代码
@Service
public class UserService {
    
    @Transactional
    public User getUser(Long id) {
        // 第一次查询
        User user1 = userMapper.findById(id);  // 查数据库
        
        // 中间有其他操作
        doSomething();
        
        // 第二次查询
        User user2 = userMapper.findById(id);  // 期望从缓存获取
        
        return user2;
    }
    
    private void doSomething() {
        // 如果这里执行了insert/update/delete
        userMapper.updateSomething();  // 会导致一级缓存清空!
    }
}

解决方案

  1. 合理安排方法顺序

  2. 使用二级缓存

  3. 在Service层做缓存

5.3 批量操作性能优化

批量操作是性能优化的重点:

java 复制代码
@Service
public class BatchService {
    
    // 错误:每次insert都提交
    public void batchInsertWrong(List<User> users) {
        for (User user : users) {
            userMapper.insert(user);  // 每次都有事务开销
        }
    }
    
    // 正确:使用批量模式
    public void batchInsertRight(List<User> users) {
        // 使用Batch执行器
        SqlSessionTemplate template = new SqlSessionTemplate(
            sqlSessionFactory, ExecutorType.BATCH);
        
        UserMapper mapper = template.getMapper(UserMapper.class);
        
        for (User user : users) {
            mapper.insert(user);
        }
        
        // 手动提交
        template.commit();
    }
    
    // 更优:使用MyBatis的foreach
    public void batchInsertBest(List<User> users) {
        userMapper.batchInsert(users);  // 一次插入多条
    }
}

// Mapper中的批量插入
<insert id="batchInsert" parameterType="list">
    INSERT INTO users (name, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.email})
    </foreach>
</insert>

代码清单11:批量操作优化

性能对比(插入1000条记录):

方式 耗时(ms) 内存占用 推荐指数
循环单条插入 1250
Batch执行器 350 ⭐⭐⭐
foreach批量插入 120 ⭐⭐⭐⭐⭐

6. 多数据源整合

6.1 多数据源配置

企业级应用经常需要多数据源:

java 复制代码
@Configuration
public class DataSourceConfig {
    
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean(name = "primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory(
            @Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/primary/*.xml"));
        return factoryBean.getObject();
    }
    
    @Bean(name = "secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactory(
            @Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/secondary/*.xml"));
        return factoryBean.getObject();
    }
    
    // 事务管理器也要配多个
    @Bean(name = "primaryTransactionManager")
    public PlatformTransactionManager primaryTransactionManager(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    @Bean(name = "secondaryTransactionManager")
    public PlatformTransactionManager secondaryTransactionManager(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

代码清单12:多数据源配置

6.2 动态数据源切换

更复杂的场景需要动态切换数据源:

java 复制代码
// 1. 定义动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

// 2. 数据源上下文
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }
    
    public static String getDataSource() {
        return contextHolder.get();
    }
    
    public static void clearDataSource() {
        contextHolder.remove();
    }
}

// 3. 自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "primary";
}

// 4. 切面实现切换
@Aspect
@Component
public class DataSourceAspect {
    
    @Around("@annotation(dataSource)")
    public Object around(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable {
        String oldDataSource = DataSourceContextHolder.getDataSource();
        
        try {
            DataSourceContextHolder.setDataSource(dataSource.value());
            return joinPoint.proceed();
        } finally {
            if (oldDataSource != null) {
                DataSourceContextHolder.setDataSource(oldDataSource);
            } else {
                DataSourceContextHolder.clearDataSource();
            }
        }
    }
}

代码清单13:动态数据源切换

7. 事务失效的常见场景

7.1 自调用问题

这是最常见的问题:

java 复制代码
@Service
public class UserService {
    
    public void createUser(User user) {
        // 自调用,事务失效!
        this.validateAndSave(user);
    }
    
    @Transactional
    public void validateAndSave(User user) {
        // 事务不会生效
        validator.validate(user);
        userMapper.insert(user);
    }
}

解决方案

java 复制代码
// 方案1:注入自己
@Service
public class UserService {
    
    @Autowired
    private UserService self;  // 注入代理对象
    
    public void createUser(User user) {
        self.validateAndSave(user);  // 通过代理调用
    }
}

// 方案2:拆分Service
@Service
public class UserService {
    
    @Autowired
    private UserTransactionService transactionService;
    
    public void createUser(User user) {
        transactionService.validateAndSave(user);
    }
}

@Service
class UserTransactionService {
    @Transactional
    public void validateAndSave(User user) {
        // 事务生效
    }
}

7.2 异常类型不匹配

java 复制代码
@Service
public class OrderService {
    
    @Transactional  // 默认只回滚RuntimeException
    public void createOrder(Order order) throws Exception {
        // ...
        throw new Exception("业务异常");  // 不会回滚!
    }
}

正确做法

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) throws Exception {
    // 现在任何异常都会回滚
}

7.3 方法修饰符问题

java 复制代码
@Service
public class UserService {
    
    @Transactional
    private void internalSave(User user) {  // 私有方法,事务失效!
        userMapper.insert(user);
    }
}

原因:Spring AOP基于代理,只能代理public方法。

8. 性能监控与调优

8.1 监控指标配置

生产环境必须监控:

复制代码
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}
      
# 自定义监控
mybatis:
  metrics:
    enabled: true
    log-slow-sql: true
    slow-sql-threshold: 1000

8.2 关键监控指标

java 复制代码
@Component
public class MyBatisMetrics {
    
    private final MeterRegistry meterRegistry;
    
    // SQL执行统计
    private final Timer sqlTimer;
    private final Counter sqlErrorCounter;
    
    public MyBatisMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        sqlTimer = Timer.builder("mybatis.sql.duration")
            .description("SQL执行耗时")
            .publishPercentiles(0.5, 0.95, 0.99)  // 50%, 95%, 99%分位
            .register(meterRegistry);
        
        sqlErrorCounter = Counter.builder("mybatis.sql.errors")
            .description("SQL执行错误次数")
            .register(meterRegistry);
    }
    
    public void recordSqlExecution(long duration, boolean success) {
        sqlTimer.record(duration, TimeUnit.MILLISECONDS);
        
        if (!success) {
            sqlErrorCounter.increment();
        }
    }
}

8.3 慢SQL监控

java 复制代码
@Intercepts({
    @Signature(type = StatementHandler.class, 
               method = "query", 
               args = {Statement.class, ResultHandler.class})
})
@Component
@Slf4j
public class SlowSqlInterceptor implements Interceptor {
    
    private static final long SLOW_SQL_THRESHOLD = 1000;  // 1秒
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            return invocation.proceed();
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            
            if (costTime > SLOW_SQL_THRESHOLD) {
                StatementHandler handler = (StatementHandler) invocation.getTarget();
                BoundSql boundSql = handler.getBoundSql();
                
                log.warn("慢SQL检测 - 耗时: {}ms, SQL: {}, 参数: {}", 
                    costTime, boundSql.getSql(), boundSql.getParameterObject());
                
                // 发送告警
                alertSlowSql(boundSql.getSql(), costTime);
            }
        }
    }
}

9. 企业级最佳实践

9.1 我的"整合配置军规"

经过多年实践,我总结了一套最佳实践:

📜 第一条:明确数据源配置
  • 生产环境必须用连接池(HikariCP)

  • 根据实际负载调整连接数

  • 开启监控和泄露检测

📜 第二条:合理使用事务
  • 事务要短小,不要在事务中做RPC调用

  • 明确指定回滚异常类型

  • 合理设置事务超时时间

📜 第三条:监控到位
  • 监控SQL执行时间

  • 监控连接池使用情况

  • 设置慢SQL告警

📜 第四条:代码规范
  • Mapper接口要有@Repository或@Mapper注解

  • SQL写在XML中,复杂的用动态SQL

  • 使用resultMap明确映射关系

📜 第五条:测试充分
  • 单元测试要覆盖事务场景

  • 集成测试要验证多数据源

  • 性能测试要模拟生产负载

9.2 生产环境配置模板

复制代码
# application-prod.yml
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${DB_HOST:localhost}:3306/${DB_NAME}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: ${DB_USER}
    password: ${DB_PASSWORD}
    hikari:
      pool-name: HikariPool
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 5000
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      leak-detection-threshold: 30000
      
mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.model
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    lazy-loading-enabled: true
    aggressive-lazy-loading: false
    default-statement-timeout: 30
    
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true

10. 故障排查指南

10.1 常见问题排查清单

问题1:事务不生效
  1. 检查是否配置了@EnableTransactionManagement

  2. 检查方法是否是public

  3. 检查是否自调用

  4. 检查异常类型是否匹配

问题2:连接泄露
  1. 检查是否在事务外使用了SqlSession

  2. 检查连接池配置

  3. 使用Druid的监控界面查看

  4. 检查是否有未关闭的ResultSet/Statement

问题3:性能下降
  1. 检查SQL是否有全表扫描

  2. 检查是否缺少索引

  3. 检查连接池是否过小

  4. 检查是否有N+1查询问题

10.2 调试技巧

复制代码
// 开启MyBatis日志
logging:
  level:
    com.example.mapper: DEBUG
    org.mybatis: DEBUG
    org.springframework.jdbc: DEBUG

// 查看事务状态
@Component
public class TransactionDebug {
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    public void debugTransaction() {
        TransactionStatus status = transactionManager.getTransaction(
            new DefaultTransactionDefinition());
        
        System.out.println("是否新事务: " + status.isNewTransaction());
        System.out.println("是否有保存点: " + status.hasSavepoint());
        
        transactionManager.commit(status);
    }
}

11. 最后的话

Spring整合MyBatis就像婚姻,表面看起来和谐美满,实际上需要双方不断磨合。理解整合原理,就是理解这段"婚姻"的相处之道。

我见过太多团队在整合上栽跟头:有的因为事务配置不当导致数据不一致,有的因为连接泄露导致系统崩溃,有的因为性能问题导致用户体验差。

记住:框架是工具,整合是艺术。理解原理,掌握细节,才能在关键时刻解决问题。

📚 推荐阅读

官方文档

  1. **MyBatis-Spring官方文档**​ - 最权威的整合指南

  2. **Spring Framework事务管理**​ - Spring事务官方文档

源码学习

  1. **mybatis-spring源码**​ - 整合模块源码

  2. **mybatis-spring-boot-starter**​ - Spring Boot整合源码

最佳实践

  1. **阿里巴巴Java开发手册**​ - MyBatis章节必看

  2. **Spring Boot最佳实践**​ - 官方最佳实践

监控工具

  1. **Druid监控**​ - 数据库连接池监控

  2. **Arthas诊断**​ - Java应用诊断工具


最后建议 :找个老项目,把它的Spring+MyBatis配置从头到尾看一遍,然后尝试优化。实战一次,胜过看十篇文章。记住:先理解,后配置;先测试,后上线

相关推荐
yaoxin5211232 小时前
278. Java Stream API - 限制与跳过操作全解析
java·开发语言·python
短剑重铸之日2 小时前
《深入解析JVM》第五章:JDK 8之后版本的优化与JDK 25前瞻
java·开发语言·jvm·后端
love530love2 小时前
【探讨】“父级/基环境损坏,子环境全部失效”,如何避免 .venv 受父级 Python 损坏影响?
java·开发语言·人工智能·windows·python·编程·ai编程
java硕哥3 小时前
Spring源码debug方法
java·后端·spring
杂货铺的小掌柜3 小时前
MAC版IDEA常用快捷键
java·macos·intellij-idea
xjz18423 小时前
JVM虚拟线程:JEP 444开启Java并发编程新纪元
java
JH30733 小时前
Spring Retry 实战:优雅搞定重试需求
java·后端·spring
czlczl200209253 小时前
实战:基于 MyBatis-Plus 实现无感知的“数据权限”自动过滤
spring boot·mybatis
蓝眸少年CY3 小时前
测试Java性能
java·开发语言·python