MyBatis-Plus 本身不提供事务管理功能,而是完全依赖 Spring 的事务管理机制,通过 @Transactional 注解实现声明式事务控制。在实际开发中,结合 IService 的批量操作、逻辑删除、自动填充等特性,正确使用 @Transactional 是保证数据一致性的关键。
一、核心概念
1. 什么是事务?
事务(Transaction)是数据库操作的逻辑工作单元,具有 ACID 特性:
- A(原子性):操作要么全部成功,要么全部失败回滚。
- C(一致性):事务前后数据状态保持一致。
- I(隔离性):并发事务之间互不干扰。
- D(持久性):事务提交后数据永久保存。
2. MyBatis-Plus 与 Spring 事务的关系
- MyBatis-Plus 基于 MyBatis 构建,而 MyBatis 的
SqlSession由 Spring 管理。 @Transactional是 Spring 提供的声明式事务注解,作用于方法或类。- 当方法被
@Transactional修饰时,Spring 会:- 开启数据库事务
- 绑定
SqlSession到当前线程(ThreadLocal) - 方法执行完毕后提交事务(无异常)或回滚(抛出未捕获异常)
3. 事务传播行为(Propagation)
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) |
有事务则加入,无则新建 |
REQUIRES_NEW |
每次都新建事务,挂起当前事务 |
SUPPORTS |
支持当前事务,无则非事务执行 |
NOT_SUPPORTED |
不支持事务,总是非事务执行 |
NEVER |
不支持事务,有事务则抛异常 |
MANDATORY |
必须有事务,否则抛异常 |
二、操作步骤(非常详细)
步骤 1:确保项目集成 Spring 与 MyBatis-Plus
<!-- Spring Boot 项目 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
步骤 2:启用事务支持(Spring Boot 默认开启)
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
✅ Spring Boot 默认启用
@EnableTransactionManagement,无需手动添加。
步骤 3:定义实体类与 Mapper
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private Integer status;
}
@Mapper
public interface UserMapper extends BaseMapper<User> {}
步骤 4:创建 Service 层并使用 @Transactional
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderService orderService; // 假设有订单服务
示例 1:基础事务方法(用户注册)
/**
* 用户注册:插入用户 + 初始化订单
* 两个操作必须同时成功或失败
*/
@Transactional // 开启事务
public void registerUser(String name, Integer age) {
// 1. 插入用户
User user = new User();
user.setName(name);
user.setAge(age);
user.setStatus(1);
userMapper.insert(user); // 使用 MyBatis-Plus 方法
// 2. 初始化默认订单(调用其他 Service)
orderService.createDefaultOrder(user.getId());
// 如果 createDefaultOrder 抛异常,整个事务回滚
}
示例 2:事务传播行为 REQUIRES_NEW(日志记录)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogService logService; // 日志服务,独立事务
@Transactional
public void updateUserWithLog(Long id, String name) {
// 主事务:更新用户
User user = userMapper.selectById(id);
user.setName(name);
userMapper.updateById(user);
try {
// 独立事务:记录操作日志,即使失败也不影响主事务
logService.saveOperationLog("update_user", id, "name changed");
} catch (Exception e) {
// 忽略日志异常
log.warn("Failed to save log", e);
}
}
@Service
public class LogService {
@Autowired
private LogMapper logMapper;
/**
* 日志记录使用独立事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveOperationLog(String op, Long targetId, String desc) {
Log log = new Log();
log.setOp(op);
log.setTargetId(targetId);
log.setDesc(desc);
logMapper.insert(log);
}
}
示例 3:批量操作事务(MyBatis-Plus 自动支持)
@Transactional
public void batchImportUsers(List<User> users) {
// MyBatis-Plus saveBatch 默认在事务中执行
boolean result = userService.saveBatch(users, 100);
if (!result) {
throw new RuntimeException("批量导入失败");
}
}
✅
saveBatch内部使用SqlSessionTemplate,天然支持 Spring 事务。
步骤 5:在 Controller 中调用
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public R<String> register(@RequestBody RegisterDTO dto) {
userService.registerUser(dto.getName(), dto.getAge());
return R.ok("注册成功");
}
}
三、常见错误与解决方案
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| 事务不生效 | 方法为 private 或 final |
改为 public |
| 事务不回滚 | 捕获了异常但未抛出 | throw 异常或 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() |
| 同一类中方法调用失效 | 自调用绕过代理 | 使用 AopContext.currentProxy() 或拆分到不同类 |
REQUIRES_NEW 不生效 |
异常被捕获 | 确保内层方法抛出异常 |
| 事务超时 | 长时间操作 | 设置 @Transactional(timeout = 30) |
| 死锁 | 并发更新顺序不一致 | 统一资源访问顺序 |
四、注意事项
@Transactional必须作用于public方法
Spring AOP 代理无法拦截非public方法。异常必须抛出到事务方法外部
被try-catch捕获且不抛出,事务不会回滚。避免在事务方法中做耗时操作(如 HTTP 调用)
可能导致事务超时、连接占用。this.method()调用不触发事务
因为绕过了代理对象,应拆分到不同Service类。读操作可使用
@Transactional(readOnly = true)
提示数据库优化,如使用只读事务。事务方法应尽量短小
减少锁持有时间,提升并发性能。
五、使用技巧
技巧 1:手动控制回滚
@Transactional
public void manualRollback() {
try {
userMapper.insert(user);
if (someError()) {
// 手动标记回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.error("业务规则校验失败,事务将回滚");
return;
}
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw e;
}
}
技巧 2:只读事务优化查询
@Transactional(readOnly = true)
public List<User> getUsers() {
return userMapper.selectList(null);
}
技巧 3:设置事务超时
@Transactional(timeout = 15) // 15 秒超时
public void processOrder() {
// 防止长时间阻塞
}
技巧 4:指定回滚异常类型
@Transactional(rollbackFor = BusinessException.class)
public void businessMethod() {
// 遇到 BusinessException 也回滚(默认只对 RuntimeException 回滚)
}
六、最佳实践
| 实践 | 说明 |
|---|---|
| ✅ 事务控制在 Service 层 | Controller 不应加 @Transactional |
| ✅ 方法粒度要合理 | 避免大事务,拆分逻辑 |
✅ 优先使用 REQUIRES_NEW 处理日志、审计 |
保证主流程不受影响 |
| ✅ 避免循环内数据库操作 | 改为批量操作 |
✅ 使用 readOnly = true 标记查询方法 |
性能与语义更清晰 |
| ✅ 异常统一处理 | 结合 @ControllerAdvice 统一回滚策略 |
七、性能优化
| 优化点 | 说明 |
|---|---|
| 减少事务范围 | 只包裹必要的数据库操作 |
| 避免长事务 | 设置 timeout,避免阻塞 |
| 批量操作 | 使用 saveBatch、updateBatchById 减少交互次数 |
| 连接池配置 | 合理设置 maxPoolSize、connectionTimeout |
| 索引优化 | 减少行锁持有时间 |
| 读写分离 | 查询走从库,减轻主库压力(需配合中间件) |
八、总结
| 项目 | 内容 |
|---|---|
| 核心机制 | Spring @Transactional + MyBatis SqlSession 绑定 |
| 作用位置 | Service 层 public 方法 |
| 默认传播 | REQUIRED |
| 回滚条件 | 抛出未捕获的 RuntimeException 或 Error |
| 批量支持 | saveBatch 等方法天然支持事务 |
| 关键原则 | 短事务、少锁、快提交 |
✅ 一句话总结:
MyBatis-Plus 的事务管理完全依赖 Spring 的 @Transactional,通过合理使用传播行为、异常控制、批量操作,可在保证数据一致性的同时提升系统性能。