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
,通过合理使用传播行为、异常控制、批量操作,可在保证数据一致性的同时提升系统性能。