目录
[MyBatis 事务深度解析:原理、配置与企业级实战](#MyBatis 事务深度解析:原理、配置与企业级实战)
[一、核心基础:MyBatis 事务的本质与依赖](#一、核心基础:MyBatis 事务的本质与依赖)
[1. 事务的核心价值](#1. 事务的核心价值)
[2. MyBatis 事务的底层依赖](#2. MyBatis 事务的底层依赖)
[3. MyBatis 事务的核心组件](#3. MyBatis 事务的核心组件)
[二、原生 JDBC 事务:MyBatis 手动控制事务(非 Spring 环境)](#二、原生 JDBC 事务:MyBatis 手动控制事务(非 Spring 环境))
[1. 核心原理](#1. 核心原理)
[2. 实战演示:原生 JDBC 事务手动控制](#2. 实战演示:原生 JDBC 事务手动控制)
[(1)环境准备(非 Spring)](#(1)环境准备(非 Spring))
[(2)Mapper 接口与 XML(下单减库存场景)](#(2)Mapper 接口与 XML(下单减库存场景))
[3. 核心注意事项](#3. 核心注意事项)
[三、Spring 声明式事务:MyBatis 企业级主流方案](#三、Spring 声明式事务:MyBatis 企业级主流方案)
[1. 核心原理](#1. 核心原理)
[2. 实战演示:Spring 声明式事务整合 MyBatis](#2. 实战演示:Spring 声明式事务整合 MyBatis)
[(1)环境准备(Spring Boot + MyBatis)](#(1)环境准备(Spring Boot + MyBatis))
[(2)Service 层添加 @Transactional 注解](#(2)Service 层添加 @Transactional 注解)
[(3)Controller 层测试](#(3)Controller 层测试)
[3. @Transactional 注解核心配置参数](#3. @Transactional 注解核心配置参数)
[四、MyBatis 事务常见问题与解决方案](#四、MyBatis 事务常见问题与解决方案)
[1. 事务不生效问题(高频问题)](#1. 事务不生效问题(高频问题))
[2. 事务并发问题(脏读、不可重复读、幻读)](#2. 事务并发问题(脏读、不可重复读、幻读))
[3. 事务提交失败问题](#3. 事务提交失败问题)
[五、企业级实战:MyBatis 事务最佳实践](#五、企业级实战:MyBatis 事务最佳实践)
[1. 分层事务控制原则](#1. 分层事务控制原则)
[2. 高频场景事务配置示例](#2. 高频场景事务配置示例)
[3. 事务监控与排查技巧](#3. 事务监控与排查技巧)
MyBatis 事务深度解析:原理、配置与企业级实战
事务是保证数据一致性的核心机制,尤其在多步数据库操作场景中(如 "下单减库存""转账转账"),事务的 ACID 特性(原子性、一致性、隔离性、持久性)能避免出现数据脏读、幻读、部分成功等问题。MyBatis 作为持久层框架,本身不直接管理事务,而是依赖底层数据源(如 JDBC)或整合 Spring 框架实现事务控制。本文将从事务核心概念出发,深入拆解 MyBatis 事务的实现原理、两种核心使用方式(原生 JDBC 事务、Spring 声明式事务)、隔离级别配置及企业级实战场景,帮助开发者彻底掌握 MyBatis 事务的正确用法。
一、核心基础:MyBatis 事务的本质与依赖
1. 事务的核心价值
在无事务控制的场景中,多步数据库操作可能出现 "部分成功" 的风险。例如 "用户下单" 流程需执行两步操作:① 插入订单记录;② 扣减商品库存。若第一步成功、第二步失败,会导致 "订单已创建但库存未扣减" 的脏数据。事务的核心价值就是通过 ACID 特性保证:多步操作 "要么全成功,要么全回滚",确保数据一致性。
2. MyBatis 事务的底层依赖
MyBatis 本身不提供事务管理器,事务控制依赖以下两种底层机制,核心是通过SqlSession控制数据库连接的事务行为:
- JDBC 事务 :依赖 JDBC 的
Connection对象,通过setAutoCommit(false)关闭自动提交,commit()提交事务,rollback()回滚事务,适用于非 Spring 环境; - Spring 事务 :Spring 提供统一的事务管理器(
DataSourceTransactionManager),MyBatis 通过SqlSessionFactory与 Spring 事务管理器整合,支持声明式事务(@Transactional注解),是企业级开发的主流方案。
3. MyBatis 事务的核心组件
- SqlSession :MyBatis 的核心会话对象,每个 SqlSession 绑定一个数据库连接(
Connection),事务的提交 / 回滚本质是通过 SqlSession 控制 Connection 的事务行为; - TransactionFactory :事务工厂,负责创建
Transaction对象,默认提供JdbcTransactionFactory(适配 JDBC 事务)和ManagedTransactionFactory(适配容器管理事务); - Transaction:事务接口,封装了事务的提交、回滚、关闭等操作,底层委托给 Connection 实现;
- Executor:MyBatis 的执行器,所有数据库操作通过 Executor 执行,Executor 会通过 Transaction 获取数据库连接,确保同一事务内的操作使用同一个 Connection。
二、原生 JDBC 事务:MyBatis 手动控制事务(非 Spring 环境)
在不使用 Spring 框架的场景中,MyBatis 通过原生 JDBC 事务实现手动控制,核心是通过SqlSession的commit()和rollback()方法管理事务,底层依赖 JDBC 的 Connection 对象。
1. 核心原理
- MyBatis 默认开启 "自动提交事务"(
autoCommit=true),即每个 SQL 语句独立成为一个事务,执行后自动提交; - 手动控制事务时,需先通过
sqlSessionFactory.openSession(false)关闭自动提交,此时 SqlSession 绑定的 Connection 处于 "事务未提交" 状态; - 多步 SQL 操作执行完成后,调用
sqlSession.commit()提交事务;若执行过程中抛出异常,调用sqlSession.rollback()回滚事务; - 事务提交 / 回滚后,需关闭 SqlSession 释放连接。
2. 实战演示:原生 JDBC 事务手动控制
(1)环境准备(非 Spring)
- 依赖配置(pom.xml):
XML
<!-- MyBatis核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 数据源(C3P0) -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
- MyBatis 配置文件(mybatis-config.xml):
XML
<configuration>
<!-- 环境配置:配置事务工厂和数据源 -->
<environments default="development">
<environment id="development">
<!-- 事务工厂:使用JDBC事务工厂 -->
<transactionManager type="JDBC"/>
<!-- 数据源:C3P0连接池 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test_mybatis?useSSL=false&serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="poolMaximumActiveConnections" value="10"/>
</dataSource>
</environment>
</environments>
<!-- 映射器扫描 -->
<mappers>
<mapper resource="mapper/OrderMapper.xml"/>
<mapper resource="mapper/ProductMapper.xml"/>
</mappers>
</configuration>
(2)Mapper 接口与 XML(下单减库存场景)
- OrderMapper.java(插入订单):
java
public interface OrderMapper {
int insertOrder(Order order);
}
- ProductMapper.java(扣减库存):
java
public interface ProductMapper {
int decreaseStock(@Param("productId") Long productId, @Param("num") Integer num);
}
- OrderMapper.xml:
XML
<mapper namespace="com.example.mapper.OrderMapper">
<insert id="insertOrder" parameterType="com.example.entity.Order">
INSERT INTO `order`(order_no, product_id, num, create_time)
VALUES(#{orderNo}, #{productId}, #{num}, NOW())
</insert>
</mapper>
- ProductMapper.xml:
XML
<mapper namespace="com.example.mapper.ProductMapper">
<update id="decreaseStock">
UPDATE product SET stock = stock - #{num} WHERE id = #{productId} AND stock >= #{num}
</update>
</mapper>
(3)手动控制事务代码
java
public class TransactionDemo {
public static void main(String[] args) {
// 1. 加载MyBatis配置,创建SqlSessionFactory
String resource = "mybatis-config.xml";
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 2. 开启SqlSession,关闭自动提交(false表示手动控制事务)
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
// 3. 获取Mapper接口
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
// 4. 模拟下单流程:① 插入订单;② 扣减库存
Order order = new Order();
order.setOrderNo(UUID.randomUUID().toString());
order.setProductId(1L);
order.setNum(2);
orderMapper.insertOrder(order); // 第一步:插入订单
int rows = productMapper.decreaseStock(1L, 2); // 第二步:扣减库存
if (rows == 0) {
throw new RuntimeException("库存不足,扣减失败");
}
// 5. 所有操作成功,提交事务
sqlSession.commit();
System.out.println("事务提交成功:下单流程完成");
} catch (Exception e) {
// 6. 发生异常,回滚事务
sqlSession.rollback();
System.out.println("事务回滚:" + e.getMessage());
} finally {
// 7. 关闭SqlSession,释放连接
sqlSession.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 核心注意事项
- 必须通过
openSession(false)关闭自动提交,否则事务控制失效; - 同一事务内的所有操作必须使用同一个 SqlSession(确保使用同一个数据库连接);
- 异常捕获后必须手动调用
rollback(),否则事务会一直处于未提交状态,导致连接泄露; - 事务提交 / 回滚后,SqlSession 不可重复使用,需重新创建。
三、Spring 声明式事务:MyBatis 企业级主流方案
在 Spring 框架中,MyBatis 与 Spring 事务管理器深度整合,支持声明式事务(通过@Transactional注解),无需手动控制SqlSession的提交 / 回滚,简化事务代码,是企业级开发的首选方案。
1. 核心原理
- Spring 提供
DataSourceTransactionManager作为事务管理器,该管理器通过 MyBatis 的SqlSessionFactory获取数据库连接,统一管理事务; - 当方法添加
@Transactional注解后,Spring 会通过 AOP 动态生成代理对象,在方法执行前开启事务(关闭 Connection 自动提交),方法执行成功后提交事务,执行失败(抛出异常)后回滚事务; - Spring 事务管理器与 MyBatis 的
SqlSession无缝协同:同一事务内,Spring 会确保所有 MyBatis 操作使用同一个SqlSession(绑定同一个 Connection),保证事务原子性。
2. 实战演示:Spring 声明式事务整合 MyBatis
(1)环境准备(Spring Boot + MyBatis)
- 依赖配置(pom.xml):
XML
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
</parent>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis + Spring Boot整合依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
-
配置文件(application.yml):
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test_mybatis?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
# Spring事务配置(可选,默认已配置DataSourceTransactionManager)
transaction:
rollback-on-commit-failure: true # 提交失败时回滚mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL,便于调试
(2)Service 层添加 @Transactional 注解
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductMapper productMapper;
/**
* 下单流程:插入订单 + 扣减库存
* @Transactional:声明式事务,默认 RuntimeException 触发回滚
*/
@Transactional(
rollbackFor = Exception.class, // 所有异常都回滚(默认仅RuntimeException回滚)
propagation = Propagation.REQUIRED, // 事务传播行为:默认,当前无事务则创建新事务
isolation = Isolation.DEFAULT // 隔离级别:默认,使用数据库默认隔离级别
)
public void createOrder(Order order) {
// 1. 插入订单
orderMapper.insertOrder(order);
// 2. 扣减库存
int rows = productMapper.decreaseStock(order.getProductId(), order.getNum());
if (rows == 0) {
throw new RuntimeException("库存不足,下单失败");
}
// 3. 模拟其他业务操作(如记录日志)
System.out.println("下单成功,订单号:" + order.getOrderNo());
}
}
(3)Controller 层测试
java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody Order order) {
try {
order.setOrderNo(UUID.randomUUID().toString());
orderService.createOrder(order);
return ResponseEntity.ok("下单成功");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("下单失败:" + e.getMessage());
}
}
}
3. @Transactional 注解核心配置参数
@Transactional注解提供丰富的配置参数,可根据业务需求调整事务行为,核心参数如下:
| 参数名 | 作用说明 | 可选值 |
|---|---|---|
rollbackFor |
指定触发回滚的异常类型(默认仅 RuntimeException 及其子类回滚) | 如Exception.class(所有异常回滚)、SQLException.class(特定异常回滚) |
noRollbackFor |
指定不触发回滚的异常类型 | 如BusinessException.class(业务异常不回滚) |
propagation |
事务传播行为(多事务方法嵌套时的行为) | REQUIRED(默认)、SUPPORTS、REQUIRES_NEW、NESTED 等 |
isolation |
事务隔离级别(解决脏读、不可重复读、幻读问题) | DEFAULT(默认,数据库隔离级别)、READ_UNCOMMITTED、READ_COMMITTED 等 |
timeout |
事务超时时间(秒),超过时间未完成则自动回滚 | 如30(30 秒超时) |
readOnly |
是否为只读事务(仅查询操作,设置为 true 可优化性能,不可执行增删改) | true/false(默认 false) |
关键参数详解:
- 事务传播行为 :最常用
REQUIRED(当前无事务则创建新事务,有事务则加入当前事务)和REQUIRES_NEW(无论当前是否有事务,都创建新事务); - 事务隔离级别 :企业级常用
READ_COMMITTED(避免脏读,大多数数据库默认级别),SERIALIZABLE(最高隔离级别,避免所有并发问题,但性能最低); - readOnly :纯查询方法建议设置
readOnly=true,Spring 会优化事务配置(如关闭写操作权限),提升查询性能。
四、MyBatis 事务常见问题与解决方案
1. 事务不生效问题(高频问题)
问题现象:
@Transactional注解添加后,事务未生效(部分操作成功、部分失败时未回滚)。
常见原因与解决方案:
- 原因 1:方法非 public 修饰 :Spring AOP 仅对 public 方法生成代理,非 public 方法的
@Transactional注解无效;解决方案:确保事务方法为 public 修饰; - 原因 2:异常被 catch 捕获未抛出 :Spring 事务仅在方法抛出指定异常时才回滚,若异常被 try-catch 捕获且未重新抛出,事务不会回滚;解决方案:捕获异常后手动抛出(如
throw new RuntimeException(e)),或使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动触发回滚; - 原因 3:数据源未被 Spring 管理 :MyBatis 的 SqlSessionFactory 未使用 Spring 的数据源,导致 Spring 事务管理器无法控制连接;解决方案:确保数据源通过
spring.datasource配置,由 Spring 自动注入; - 原因 4:内部方法调用 :同一类中无事务方法调用有事务方法,AOP 无法拦截,事务无效;解决方案:将事务方法抽取到其他 Service 类,或通过
AopContext.currentProxy()获取代理对象调用。
2. 事务并发问题(脏读、不可重复读、幻读)
问题说明:
多线程并发操作同一数据时,可能出现以下问题:
- 脏读:一个事务读取到另一个事务未提交的数据;
- 不可重复读:同一事务内多次查询同一数据,结果不一致;
- 幻读:同一事务内多次查询,结果集行数不一致。
解决方案:
- 调整事务隔离级别:如设置
isolation = Isolation.READ_COMMITTED(避免脏读)、Isolation.REPEATABLE_READ(避免脏读和不可重复读); - 加锁:通过数据库锁(如行锁、表锁)或分布式锁(Redis/ZooKeeper)控制并发访问;
- 避免长事务:长事务会占用数据库连接,增加并发冲突概率,尽量拆分长事务为短事务。
3. 事务提交失败问题
问题现象:
方法执行无异常,但事务提交失败(数据未入库)。
常见原因与解决方案:
- 原因 1:数据库引擎不支持事务:如 MySQL 的 MyISAM 引擎不支持事务,InnoDB 引擎支持;解决方案:将数据库表引擎改为 InnoDB;
- 原因 2:事务超时 :事务执行时间超过
timeout配置,被 Spring 自动回滚;解决方案:优化 SQL 执行效率,或适当增大timeout值; - 原因 3:连接池参数不合理 :连接池最大连接数不足,导致事务无法获取连接提交;解决方案:调整连接池参数(如
spring.datasource.hikari.max-active)。
五、企业级实战:MyBatis 事务最佳实践
1. 分层事务控制原则
- 事务应添加在 Service 层:Service 层负责业务逻辑整合,多步数据库操作的事务控制应在 Service 层统一管理,避免在 Controller 或 Mapper 层添加事务;
- 粒度适中:事务粒度不宜过大(避免长事务),也不宜过小(避免事务碎片化),以 "一个完整业务场景" 为单位(如 "下单""转账")。
2. 高频场景事务配置示例
(1)纯查询方法(只读事务)
java
// 纯查询方法,设置readOnly=true优化性能
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public List<Order> getOrderByUserId(Long userId) {
return orderMapper.selectByUserId(userId);
}
(2)转账业务(高一致性要求)
java
// 转账业务:扣减付款方余额 + 增加收款方余额,要求强一致性
@Transactional(
rollbackFor = Exception.class,
isolation = Isolation.READ_COMMITTED,
timeout = 30
)
public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) {
// 扣减付款方余额
userAccountMapper.decreaseBalance(fromUserId, amount);
// 增加收款方余额
userAccountMapper.increaseBalance(toUserId, amount);
}
(3)嵌套事务(REQUIRES_NEW)
java
// 主事务:创建订单
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
orderMapper.insertOrder(order);
// 调用子事务方法(创建订单日志,独立事务,即使失败不影响主事务)
logService.recordOrderLog(order.getId());
}
// 子事务:记录订单日志(独立事务)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void recordOrderLog(Long orderId) {
OrderLog log = new OrderLog();
log.setOrderId(orderId);
log.setOperateTime(LocalDateTime.now());
orderLogMapper.insert(log);
}
3. 事务监控与排查技巧
- 开启 Spring 事务日志:在 application.yml 中配置
logging.level.org.springframework.transaction=DEBUG,可查看事务开启、提交、回滚的详细日志; - 打印 SQL 执行日志:MyBatis 配置
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl,查看 SQL 执行顺序和参数; - 数据库事务日志:如 MySQL 的 binlog 日志,可排查事务提交是否写入数据库。
六、总结
MyBatis 事务的核心是 "通过 SqlSession 控制数据库连接的事务行为",底层依赖 JDBC 事务机制,企业级开发中主要与 Spring 声明式事务整合,通过@Transactional注解简化事务控制。掌握 MyBatis 事务的关键在于:
- 理解事务的 ACID 特性,明确事务的适用场景(多步数据库操作需保证一致性);
- 区分原生 JDBC 事务和 Spring 声明式事务的使用场景(非 Spring 环境用原生,Spring 环境用声明式);
- 熟练配置
@Transactional注解的核心参数(尤其是rollbackFor、propagation、isolation); - 规避常见问题(如事务不生效、并发冲突、提交失败),遵循分层事务控制原则。
在实际开发中,应根据业务场景选择合适的事务策略:简单场景用 Spring 默认配置,高一致性场景调整隔离级别和传播行为,并发场景结合锁机制,确保数据一致性和系统性能的平衡。