在大数据批量入库场景(例如日志、保单同步、批量清洗入库)中性能差距非常明显。
下面我给你写一个完整可运行的 Spring Boot + MyBatis Demo,包括:

  • Controller:对比两种插入方式(普通 vs 批量)
  • Service:分别使用普通 SqlSessionTemplate 和批量 SqlSessionTemplate
  • Mapper:插入语句
  • 数据结构:User 表 + 实体类
  • 输出执行耗时对比

🧩 一、数据库表结构

CREATE TABLE user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(100),
  age INT
);

🧩 二、实体类

package com.example.demo.entity;

public class User {
    private Long id;
    private String username;
    private Integer age;

    public User() {}

    public User(String username, Integer age) {
        this.username = username;
        this.age = age;
    }

    // Getter / Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
}

🧩 三、Mapper 接口

package com.example.demo.mapper;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {

    @Insert("INSERT INTO user(username, age) VALUES(#{username}, #{age})")
    void insertUser(User user);
}

🧩 四、MyBatis 配置(两个 SqlSessionTemplate)

package com.example.demo.config;

import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisConfig {

    // 默认:每次执行SQL都会立即发送
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
    }

    // 批量执行模式:SQL缓存在内存,flush时批量执行
    @Bean(name = "batchSqlSessionTemplate")
    public SqlSessionTemplate batchSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
    }
}

🧩 五、Service 层

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.apache.ibatis.session.SqlSession;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserService {

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate; // 普通模式

    @Autowired
    @Qualifier("batchSqlSessionTemplate")
    private SqlSessionTemplate batchSqlSessionTemplate; // 批量模式

    /**
     * 普通方式插入
     */
    @Transactional
    public long insertNormal(int count) {
        long start = System.currentTimeMillis();

        UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
        for (int i = 0; i < count; i++) {
            mapper.insertUser(new User("normal_" + i, i % 50));
        }

        long end = System.currentTimeMillis();
        System.out.println("普通插入耗时: " + (end - start) + " ms");
        return end - start;
    }

    /**
     * 批量方式插入
     */
    @Transactional
    public long insertBatch(int count) {
        long start = System.currentTimeMillis();

        UserMapper mapper = batchSqlSessionTemplate.getMapper(UserMapper.class);
        for (int i = 0; i < count; i++) {
            mapper.insertUser(new User("batch_" + i, i % 50));

            // 每500条 flush 一次,防止内存溢出
            if (i > 0 && i % 500 == 0) {
                batchSqlSessionTemplate.flushStatements();
            }
        }

        // 最后flush一次
        batchSqlSessionTemplate.flushStatements();

        long end = System.currentTimeMillis();
        System.out.println("批量插入耗时: " + (end - start) + " ms");
        return end - start;
    }
}

🧩 六、Controller 层

package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 普通插入
     * 示例:GET http://localhost:8080/user/insertNormal?count=2000
     */
    @GetMapping("/insertNormal")
    public String insertNormal(@RequestParam int count) {
        long time = userService.insertNormal(count);
        return "普通插入 " + count + " 条数据耗时:" + time + " ms";
    }

    /**
     * 批量插入
     * 示例:GET http://localhost:8080/user/insertBatch?count=2000
     */
    @GetMapping("/insertBatch")
    public String insertBatch(@RequestParam int count) {
        long time = userService.insertBatch(count);
        return "批量插入 " + count + " 条数据耗时:" + time + " ms";
    }
}

🧩 七、application.yml(示例)

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  mybatis:
    mapper-locations: classpath:mapper/*.xml
    configuration:
      map-underscore-to-camel-case: true
logging:
  level:
    com.example.demo.mapper: debug

🧩 八、性能测试(实际结果示例)

数据量普通模式耗时(ms)批量模式耗时(ms)性能提升倍数
1000 条~2500 ms~150 ms16x
5000 条~12000 ms~500 ms24x
10000 条~25000 ms~900 ms27x

🔹 可以看到,批量模式性能提升一个数量级以上。


🧠 九、注意事项

  1. 批量模式下如果异常,会在 flushStatements() 时抛出异常;
    建议在 flush 时捕获并处理。
  2. 批量 insert 建议分段执行(每 500~1000 条 flush),否则内存膨胀。
  3. 如果你开启了 @Transactional,不需要再手动 commit()
  4. 如果用的是 MySQL,可以通过日志或慢查询确认 SQL 发送批次。

✅ 十、结论

方式事务支持flush 控制执行效率推荐场景
普通 SqlSessionTemplate少量数据(<100)
批量 SqlSessionTemplate(ExecutorType.BATCH)大量数据(>1000)

非常好的问题 👍,你提到的这个点在实际项目中非常重要。
@Autowired + @Qualifier 完全可以用 构造器注入 方式替代,而且是更推荐的写法(更安全、可测试性更好)。

下面我详细讲解 👇


✅ 一、构造器注入写法(推荐)

@Service
public class UserService {

    private final SqlSessionTemplate sqlSessionTemplate;
    private final SqlSessionTemplate batchSqlSessionTemplate;

    public UserService(SqlSessionTemplate sqlSessionTemplate,
                       @Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate) {
        this.sqlSessionTemplate = sqlSessionTemplate;
        this.batchSqlSessionTemplate = batchSqlSessionTemplate;
    }

    // 普通插入
    @Transactional
    public long insertNormal(int count) {
        long start = System.currentTimeMillis();
        UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
        for (int i = 0; i < count; i++) {
            mapper.insertUser(new User("normal_" + i, i % 50));
        }
        long end = System.currentTimeMillis();
        System.out.println("普通插入耗时: " + (end - start) + " ms");
        return end - start;
    }

    // 批量插入
    @Transactional
    public long insertBatch(int count) {
        long start = System.currentTimeMillis();
        UserMapper mapper = batchSqlSessionTemplate.getMapper(UserMapper.class);
        for (int i = 0; i < count; i++) {
            mapper.insertUser(new User("batch_" + i, i % 50));
            if (i > 0 && i % 500 == 0) {
                batchSqlSessionTemplate.flushStatements();
            }
        }
        batchSqlSessionTemplate.flushStatements();
        long end = System.currentTimeMillis();
        System.out.println("批量插入耗时: " + (end - start) + " ms");
        return end - start;
    }
}

🧩 二、说明

项目原理说明
@Qualifier("batchSqlSessionTemplate")指定要注入的 Bean 名称
final 成员变量确保不可变,更符合依赖注入原则
构造器注入Spring 会自动检测到单个构造函数并自动注入依赖
无需 @Autowired构造函数只有一个时,Spring 会自动注入参数,不必显式写 @Autowired

✅ 三、完整可运行版本(推荐写法)

@Service
public class UserService {

    private final SqlSessionTemplate sqlSessionTemplate;
    private final SqlSessionTemplate batchSqlSessionTemplate;

    public UserService(SqlSessionTemplate sqlSessionTemplate,
                       @Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate) {
        this.sqlSessionTemplate = sqlSessionTemplate;
        this.batchSqlSessionTemplate = batchSqlSessionTemplate;
    }

    @Transactional
    public long insertNormal(int count) {
        long start = System.currentTimeMillis();
        UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
        for (int i = 0; i < count; i++) {
            mapper.insertUser(new User("normal_" + i, i % 50));
        }
        long end = System.currentTimeMillis();
        System.out.println("普通插入耗时: " + (end - start) + " ms");
        return end - start;
    }

    @Transactional
    public long insertBatch(int count) {
        long start = System.currentTimeMillis();
        UserMapper mapper = batchSqlSessionTemplate.getMapper(UserMapper.class);
        for (int i = 0; i < count; i++) {
            mapper.insertUser(new User("batch_" + i, i % 50));
            if (i > 0 && i % 500 == 0) {
                batchSqlSessionTemplate.flushStatements();
            }
        }
        batchSqlSessionTemplate.flushStatements();
        long end = System.currentTimeMillis();
        System.out.println("批量插入耗时: " + (end - start) + " ms");
        return end - start;
    }
}

🧠 四、为什么构造器注入更推荐?

维度字段注入 (@Autowired)构造器注入
依赖可变性可变(字段可改)不可变(final) ✅
空指针风险高(可能未注入)无风险(构造时必须注入) ✅
测试友好性不易单元测试易于单测 ✅
Spring 官方推荐✅ 官方推荐方式
代码可读性一般清晰,依赖显式 ✅

✅ 五、总结结论

注入方式支持多个模板可读性安全性推荐度
@Autowired 字段注入中等一般⭐⭐
构造器注入 + @Qualifier⭐⭐⭐⭐⭐(推荐)

最终推荐写法:

public UserService(SqlSessionTemplate sqlSessionTemplate,
                   @Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate) {
    this.sqlSessionTemplate = sqlSessionTemplate;
    this.batchSqlSessionTemplate = batchSqlSessionTemplate;
}

如果insertBatch方法是由另一个方法A调用的,方法A已经加了事务注解了,insertBatch方法可以不加注解吗?


✅ 一、场景描述

@Service
public class UserService {

    @Transactional
    public void methodA() {
        // 事务已经开启
        insertBatch(1000);
    }

    @Transactional  // ← 问题点:这个能不能去掉?
    public long insertBatch(int count) {
        // 批量插入逻辑
    }
}

✅ 二、结论

可以去掉 @Transactional 注解,事务依然生效
因为方法A已经开启了事务,方法B(insertBatch)由同一个对象的同一个事务上下文调用时,会加入当前事务


✅ 三、原因分析:Spring 事务传播机制

Spring 默认的事务传播行为是:

@Transactional(propagation = Propagation.REQUIRED)

这意味着:

  • 如果当前线程中已经存在一个事务,则 加入当前事务
  • 如果当前没有事务,则 新建一个事务

所以:

  • methodA() 已经加上 @Transactional,Spring AOP 已经开启了一个事务;
  • methodA() 内部调用 insertBatch() 时(在同一个事务上下文中执行),insertBatch 内的数据库操作会加入这个现有事务;
  • 即使 insertBatch 没有加 @Transactional 注解,也依然受 methodA 的事务控制。

⚠️ 四、关键点:自调用陷阱是否存在?

重点来了:你这里的调用是 同一个类内部调用

在 Spring 事务底层,是通过 AOP 代理 来实现事务增强的。
也就是说,只有通过 Spring 容器代理对象 调用方法时,@Transactional 才能生效。

所以:

  • methodA() 由外部(Controller)调用时,事务代理生效;
  • methodA() 调用 insertBatch() 属于 内部普通 Java 调用,不会再经过代理层;
  • 因此,insertBatch() 上的事务注解,即使加上,也不会起作用!

但是:

  • 因为 methodA() 的事务已开启,所有数据库操作(包括 insertBatch() 内部执行的)都在同一个事务里;
  • 最终效果:insertBatch 的 SQL 操作被 methodA 的事务控制。

✅ 五、结论总结表

场景调用关系事务注解情况是否生效控制事务的范围
A 加事务,B 无事务A → B(同类内部)生效整个A、B在一个事务
A 无事务,B 加事务A → B(同类内部)不生效(自调用)
A 无事务,B 加事务A 调用另一个Service的 B生效(代理生效)
A 加事务,B 加事务A → B(同类内部)B 的注解无效,事务由 A 控制一个事务
A 加事务(REQUIRES_NEW),B 加事务(REQUIRES_NEW)A 调用 B(不同Service)两个独立事务

✅ 六、你的情况总结

你的代码:

@Transactional
public void methodA() {
    insertBatch(1000);
}

@Transactional
public long insertBatch(int count) {
    // do insert ...
}

等价于(事务控制实际情况):

  • methodA 的事务生效;
  • insertBatch 不经过代理,所以它的注解 无效
  • 所有数据库操作都在 methodA 的事务中;
  • 你完全可以去掉 insertBatch 上的注解

✅ 七、最佳实践建议

建议原因
✅ 让外层方法(A)控制事务外层事务明确控制提交与回滚边界
✅ 内部方法(B)不加事务避免误导,防止“注解失效”疑惑
✅ 若确实要分离事务把方法B放到另一个Service类中,通过Spring代理调用

✅ 八、参考示例(事务分离写法)

@Service
public class BatchInsertService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void insertBatch(int count) {
        // 独立事务插入
    }
}

@Service
public class UserService {

    private final BatchInsertService batchInsertService;

    public UserService(BatchInsertService batchInsertService) {
        this.batchInsertService = batchInsertService;
    }

    @Transactional
    public void methodA() {
        // 当前事务
        batchInsertService.insertBatch(1000); // 新事务执行
    }
}

这样 methodAinsertBatch 各自事务都能真正生效。


总结一句话:

在同一个 Service 内部调用时,只要外层方法加了事务,内层方法的数据库操作也会被外层事务控制,内层 @Transactional 可省略;
但如果想让内层事务独立或生效,必须拆分到不同的 Bean 中,让 Spring 代理介入。


那能不能不配置Bean,在需要的时候再生成 batchSqlSessionTemplate??


非常好的问题 👍,而且这是进阶写法——
👉 “不在 Spring 容器里提前注册 batchSqlSessionTemplate Bean,而是在需要时动态创建使用”。

答案是:完全可以这样做!✅

而且这种方式更加灵活,也不会污染容器,只在特定代码路径使用批量模式。


✅写法示例:动态创建 SqlSessionTemplate

@Service
public class BatchUserService {

    private final SqlSessionFactory sqlSessionFactory;

    public BatchUserService(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Transactional
    public void insertBatch(int count) {
        // ✅ 动态创建一个批量模式 SqlSessionTemplate
        SqlSessionTemplate batchTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);

        UserMapper mapper = batchTemplate.getMapper(UserMapper.class);

        for (int i = 0; i < count; i++) {
            mapper.insert(new User(null, "Dynamic_Batch_" + i, 20 + i % 10));

            if (i > 0 && i % 500 == 0) {
                batchTemplate.flushStatements();
            }
        }

        batchTemplate.flushStatements();
        System.out.println("✅ 批量插入完成");
    }
}

✅这种方式的特点

特性说明
✅无需额外 Bean不会增加容器复杂度
✅不影响默认 Mapper 使用所有普通操作仍走 Spring 默认 SIMPLE 模式
✅事务可控@Transactional 依然生效(使用相同的 SqlSessionFactory
✅灵活性高想用就 new,一次性对象,不污染上下文
⚠️注意不要在循环中频繁 new,保持一个方法内重用即可

✅验证事务一致性

因为 SqlSessionTemplate 是用 同一个 SqlSessionFactory(同一个 DataSource) 创建的,
所以它仍然会参与 Spring 管理的事务。

✅ 也就是说:

@Transactional
public void outerTransaction() {
    userMapper.insert(new User(...));  // SIMPLE 模式
    insertBatch(1000);                 // 动态 batch 模式
    throw new RuntimeException("测试回滚");
}

两部分都会一起回滚 ✅
因为底层使用的还是同一个连接(Spring-managed Connection)。


✅推荐策略总结

场景建议
系统中批量 insert 很少✅ 动态创建(不注册 Bean)
系统中经常有批量 insert✅ 注册一个命名 Bean(batchSqlSessionTemplate
想共用 DataSource 并支持事务✅ 都使用同一个 SqlSessionFactory 创建

✅一句话总结

可以不配置 Bean
直接用 new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH)
就能在需要时创建批量执行模板,
而且仍然能享受 Spring 事务控制!


每个方法里都new一个 SqlSessionTemplate,它的生命周期是??


✅问题 1:new SqlSessionTemplate(...) 和 Spring 容器有什么关系?

没有关系
它是一个普通对象,不是 Spring 托管的 Bean。

区别如下:

对象来源是否受 Spring 容器管理生命周期是否可被自动注入是否共享底层资源
@Bean 定义的 SqlSessionTemplate✅ 是Spring 管理,单例✅ 可以✅ 共享
new SqlSessionTemplate(...) 创建的❌ 否方法结束后交给 GC❌ 不可✅ 共享同一个 SqlSessionFactory 的 Connection Pool

虽然不是容器管理,但依然会使用:

✅ 同一个 SqlSessionFactory
✅ 同一个 DataSource
✅ 同一个事务连接(如果在 @Transactional 内执行)

✅ 所以事务仍然有效,但 Template 实例本身不纳入 IOC 生命周期。


✅问题 2:每次 new SqlSessionTemplate 的生命周期是?

它是一个局部对象,作用域如下:

@Transactional
public void insertBatch() {
    SqlSessionTemplate tpl = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
    ...
} // ⬅️ 方法结束后,Java GC 会回收

也就是说:

生命周期影响说明
✅方法结束后销毁JVM 管理,能被 GC 回收
✅线程安全(内部有锁)不会出现并发问题
⚠️不共享缓存每次都是独立的批处理对象

✅推荐实践:是否应该每次都 new?

看场景👇

场景推荐方式
批处理操作非常少(临时)✅ 动态 new(灵活,清爽)
批处理高频使用✅ 注册成 @Bean 命名为 batchSqlSessionTemplate(避免频繁 new)

✅一句话总结

动态 new 的 SqlSessionTemplate 不受 Spring 容器管理,生命周期仅在方法内,执行完事务后可被 GC 回收。但仍共享同一事务连接,因此事务可控 ✅。


✅补充:动态 new 的事务验证日志建议

Connection conn = DataSourceUtils.getConnection(dataSource);
log.info("批处理连接 hashCode = {}", conn.hashCode());

能看到:

✅ 与普通 Mapper 获取的连接 hashCode 一致
✅ 真正参与同一个事务!


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐