Java后端事务机制详解
本文聚焦Java后端开发中的事务知识,从基础概念到Spring实战,覆盖ACID特性、隔离级别、传播行为、失效场景、使用时机,帮助你建立完整的事务知识体系。
目录
1. 什么是事务
事务(Transaction)是数据库操作的最小工作单元,它将一组操作绑定为一个不可分割的整体。这一组操作要么全部成功,要么全部回滚,不存在中间状态。
通俗理解: 转账场景——A给B转100元,包含两步:A账户减100,B账户加100。这两步必须同时成功或同时失败,否则就会出现钱丢失或凭空产生的问题。
-- 事务示例:转账 BEGIN TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; -- 两步都成功才提交 COMMIT; -- 如果任何一步失败,执行回滚 -- ROLLBACK;
没有事务的世界:
步骤1:A账户减100元 → 成功,余额变为900 步骤2:B账户加100元 → 失败(网络超时/数据库宕机) 结果:A少了100元,B没收到 → 100元凭空消失 ❌ 有了事务: 步骤1:A账户减100元 → 成功 步骤2:B账户加100元 → 失败 事务回滚:A的余额恢复为1000 ✅
2. 事务的四大特性(ACID)
ACID是事务的四个基本特性的首字母缩写,是数据库事务的理论基石。
2.1 原子性(Atomicity)
事务中的操作要么全部完成,要么全部不执行,不会停留在中间状态。
类比:银行转账,"扣A的钱"和"加B的钱"是一个原子操作 要么都发生,要么都不发生
实现原理: 数据库通过 undo log(回滚日志) 实现原子性。事务执行过程中,数据库会记录每一步操作的逆操作。如果事务失败,就按照undo log逐步回滚到事务开始前的状态。
2.2 一致性(Consistency)
事务执行前后,数据库从一个一致状态转换到另一个一致状态。数据始终满足所有约束规则(如外键约束、唯一约束、业务规则)。
类比:转账前A和B的总额是1100元,转账后总额仍然是1100元 数据的"合法性"始终被维护 一致性是目的,原子性、隔离性、持久性是手段
2.3 隔离性(Isolation)
多个并发事务之间互不干扰。每个事务都感觉自己在独立操作数据库,感知不到其他事务的存在。
类比:你在ATM机取钱,旁边有人也在取钱 你们各自看到的余额是独立的,互不影响 实现原理:通过锁机制 + MVCC(多版本并发控制)
2.4 持久性(Durability)
事务一旦提交,其对数据库的修改就是永久性的,即使系统崩溃也不丢失。
类比:你提交了转账,即使银行系统马上重启 转账结果也不会丢失 实现原理:通过 redo log(重做日志)实现 事务提交时先写日志,再写磁盘(WAL机制)
2.5 四个特性的关系
┌─────────┐ │ 一致性 │ ← 最终目标 │(Consistency)│ └────┬────┘ │ ┌──────────────┼──────────────┐ │ │ │ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │ 原子性 │ │ 隔离性 │ │ 持久性 │ │(Atomicity) │ │(Isolation)│ │(Durability)│ └───────────┘ └───────────┘ └───────────┘ │ │ │ undo log 锁 + MVCC redo log
一致性是目的,原子性、隔离性、持久性是保证一致性的手段。
3. 并发事务带来的问题
当多个事务同时操作同一份数据时,如果没有适当的隔离控制,会出现以下问题。
3.1 脏读(Dirty Read)
一个事务读到了另一个事务尚未提交的数据。
时间线: 事务A 事务B ───── ───── 开始事务 开始事务 读取余额 = 1000 UPDATE余额 = 800(扣200,但未提交) 读取余额 = 800 ← 脏读! (以为余额是800) ROLLBACK(回滚,余额恢复1000) 基于"余额800"做了业务决策 结果:决策基于一个根本不存在的数据 ❌
危害: 读到了最终被回滚的数据,导致业务逻辑基于错误数据做判断。
3.2 不可重复读(Non-Repeatable Read)
同一事务内,两次读取同一行数据,结果不同。原因是另一个事务在两次读取之间修改并提交了数据。
时间线: 事务A 事务B ───── ───── 开始事务 开始事务 读取余额 = 1000 UPDATE余额 = 800 COMMIT(提交) 读取余额 = 800 ← 不可重复读! (同一个事务内,两次读取结果不同) COMMIT
与脏读的区别: 脏读读到的是未提交的数据,不可重复读读到的是已提交的数据。不可重复读的问题在于同一事务内前后不一致。
3.3 幻读(Phantom Read)
同一事务内,两次执行相同的范围查询,返回的行数不同。原因是另一个事务在两次查询之间插入或删除了行。
时间线:
事务A 事务B
───── ─────
开始事务 开始事务
SELECT COUNT(*) FROM orders
WHERE amount > 100 → 结果:3条
INSERT INTO orders(amount) VALUES(200)
COMMIT
SELECT COUNT(*) FROM orders
WHERE amount > 100 → 结果:4条 ← 幻读!
(凭空多出来一条记录)
COMMIT
与不可重复读的区别: 不可重复读针对的是某一行的数据被修改,幻读针对的是行数发生变化(新增/删除)。
3.4 三个问题的对比
| 问题 | 本质 | 涉及操作 | 严重程度 |
|---|---|---|---|
| 脏读 | 读到未提交的数据 | 读+写 | 最严重 |
| 不可重复读 | 同一行数据前后不一致 | 读+写(UPDATE) | 中等 |
| 幻读 | 查询结果的行数变化 | 读+写(INSERT/DELETE) | 较轻 |
4. 事务隔离级别
为了解决上述并发问题,SQL标准定义了四种隔离级别。隔离级别越高,数据越安全,但并发性能越低。
4.1 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 | 最高 |
| 读已提交(READ COMMITTED) | 不可能 | 可能 | 可能 | 较高 |
| 可重复读(REPEATABLE READ) | 不可能 | 不可能 | 可能 | 中等 |
| 串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 | 最低 |
安全性递增 → READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE 性能最高 安全性最高 安全性最低 性能最低
4.2 各隔离级别详解
READ UNCOMMITTED(读未提交)
最低的隔离级别。一个事务可以读到其他事务尚未提交的数据。
特点:什么都不隔离,什么都能读到 问题:脏读、不可重复读、幻读都会出现 使用场景:几乎不在生产环境使用
READ COMMITTED(读已提交)
一个事务只能读到其他事务已经提交的数据。
特点:每次SELECT都生成一个新的快照 解决:脏读 残留:不可重复读、幻读 使用场景:Oracle默认级别,大多数互联网项目使用此级别
REPEATABLE READ(可重复读)
保证同一事务内多次读取同一行数据的结果一致。
特点:事务开始时创建一个快照,整个事务期间使用这个快照 解决:脏读、不可重复读 残留:理论上存在幻读(但MySQL通过MVCC+间隙锁基本解决了幻读) 使用场景:MySQL默认级别,金融、支付等对一致性要求高的场景
SERIALIZABLE(串行化)
最高的隔离级别。所有事务串行执行,完全隔离。
特点:事务排队执行,并发度最低 解决:脏读、不可重复读、幻读,全部解决 残留:无 使用场景:几乎不在生产环境使用,除非对数据一致性要求极高且并发量很低
4.3 主流数据库的默认隔离级别
| 数据库 | 默认隔离级别 |
|---|---|
| MySQL | REPEATABLE READ |
| Oracle | READ COMMITTED |
| PostgreSQL | READ COMMITTED |
| SQL Server | READ COMMITTED |
4.4 MySQL如何解决幻读
MySQL在REPEATABLE READ级别下,通过两种机制解决幻读:
1. MVCC(多版本并发控制) - 快照读(普通SELECT):读取的是事务开始时的快照,不会看到其他事务的修改 - 每行数据保留多个版本,根据事务的ReadView决定读哪个版本 2. Next-Key Lock(临键锁 = 行锁 + 间隙锁) - 当前读(SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE): 通过临键锁锁定记录及其之间的间隙,阻止其他事务在间隙中插入新记录
4.5 在Spring中设置隔离级别
@Service
public class TransferService {
// 设置隔离级别为读已提交
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transfer(String from, String to, BigDecimal amount) {
deduct(from, amount);
add(to, amount);
}
// 设置隔离级别为可重复读
@Transactional(isolation = Isolation.REPEATABLE_READ)
public BigDecimal queryBalance(String userId) {
// 整个事务期间多次查询结果一致
BigDecimal balance1 = accountMapper.getBalance(userId);
// ... 其他操作 ...
BigDecimal balance2 = accountMapper.getBalance(userId);
// balance1 == balance2
return balance2;
}
}
Isolation枚举的所有值:
public enum Isolation {
DEFAULT, // 使用数据库默认隔离级别
READ_UNCOMMITTED, // 读未提交
READ_COMMITTED, // 读已提交
REPEATABLE_READ, // 可重复读
SERIALIZABLE // 串行化
}
4.6 如何选择隔离级别
选择原则:在满足业务需求的前提下,选择尽可能低的隔离级别 普通业务系统(CMS、内容管理) → READ COMMITTED 电商系统(订单、库存、支付) → READ COMMITTED 或 REPEATABLE READ 金融系统(转账、账务) → REPEATABLE READ 对数据一致性要求极高的场景 → SERIALIZABLE(慎用,性能很差)
5. Spring事务管理
Spring提供了两种事务管理方式:声明式事务(@Transactional注解)和编程式事务(TransactionTemplate)。实际开发中以声明式事务为主。
5.1 @Transactional 注解详解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
// 事务传播行为
Propagation propagation() default Propagation.REQUIRED;
// 隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 超时时间(秒),超时后自动回滚
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
// 是否只读事务(只读事务可以进行一些优化)
boolean readOnly() default false;
// 指定哪些异常触发回滚(默认只对RuntimeException和Error回滚)
Class<? extends Throwable>[] rollbackFor() default {};
// 指定哪些异常不触发回滚
Class<? extends Throwable>[] noRollbackFor() default {};
}
5.2 基本使用
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockMapper stockMapper;
@Autowired
private AccountMapper accountMapper;
/**
* 创建订单:扣库存 + 扣余额 + 插入订单
* 任何一步失败,全部回滚
*/
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 扣减库存
int stockRows = stockMapper.deduct(dto.getProductId(), dto.getQuantity());
if (stockRows == 0) {
throw new BusinessException("库存不足");
}
// 2. 扣减余额
BigDecimal totalAmount = dto.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity()));
int balanceRows = accountMapper.deductBalance(dto.getUserId(), totalAmount);
if (balanceRows == 0) {
throw new BusinessException("余额不足");
}
// 3. 创建订单
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(dto.getUserId());
order.setProductId(dto.getProductId());
order.setQuantity(dto.getQuantity());
order.setTotalAmount(totalAmount);
order.setStatus(0);
orderMapper.insert(order);
// 如果上面任何一步抛出异常,整个方法的操作全部回滚
}
private String generateOrderNo() {
return "ORD" + System.currentTimeMillis();
}
}
5.3 rollbackFor 详解
这是最重要的知识点之一。 Spring默认只对 RuntimeException 和 Error 进行回滚,受检异常(Checked Exception)不会触发回滚。
public class BusinessException extends RuntimeException {
// 继承RuntimeException,默认会回滚 ✓
}
public class BusinessException extends Exception {
// 继承Exception(受检异常),默认不会回滚 ✗
// 需要指定 rollbackFor = Exception.class
}
@Service
public class UserService {
// ❌ 默认行为:只对RuntimeException回滚
@Transactional
public void method1() throws IOException {
// 如果抛出IOException(受检异常),不会回滚!
throw new IOException("文件读取失败");
}
// ✅ 正确做法:指定rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class)
public void method2() throws IOException {
// 任何Exception都会触发回滚
throw new IOException("文件读取失败");
}
}
最佳实践:永远写 rollbackFor = Exception.class。
5.4 只读事务
@Service
public class UserService {
/**
* 只读事务:Spring会对只读事务进行优化
* - 不会生成undo log(因为不需要回滚)
* - 数据库可能会使用更高效的读取策略
* - 如果尝试在只读事务中执行写操作,会抛出异常
*/
@Transactional(readOnly = true)
public UserVO getUserDetail(Long userId) {
User user = userMapper.selectById(userId);
List<Order> orders = orderMapper.selectByUserId(userId);
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
vo.setOrders(orders);
return vo;
}
}
5.5 超时设置
@Service
public class ReportService {
/**
* 超时事务:如果事务执行时间超过指定秒数,自动回滚
* 用于防止长时间占用数据库连接
*/
@Transactional(timeout = 30, rollbackFor = Exception.class)
public ReportVO generateMonthlyReport(int year, int month) {
// 查询大量数据并生成报表
// 如果30秒内没完成,事务自动回滚
List<Order> orders = orderMapper.selectByMonth(year, month);
// ... 复杂计算 ...
return report;
}
}
6. 事务传播行为
传播行为定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。这是Spring事务管理中最有深度的知识点。
6.1 七种传播行为
| 传播行为 | 含义 | 有事务时 | 无事务时 |
|---|---|---|---|
| REQUIRED | 默认值,支持当前事务 | 加入当前事务 | 创建新事务 |
| REQUIRES_NEW | 创建独立事务 | 挂起当前事务,创建新事务 | 创建新事务 |
| SUPPORTS | 支持当前事务 | 加入当前事务 | 以非事务方式运行 |
| NOT_SUPPORTED | 不支持事务 | 挂起当前事务 | 以非事务方式运行 |
| MANDATORY | 必须有事务 | 加入当前事务 | 抛出异常 |
| NESTED | 嵌套事务 | 创建嵌套事务 | 创建新事务 |
| NEVER | 必须没有事务 | 抛出异常 | 以非事务方式运行 |
6.2 REQUIRED(最常用)
默认传播行为。如果当前存在事务则加入,否则创建新事务。
@Service
public class OrderService {
@Autowired
private StockService stockService;
@Autowired
private AccountService accountService;
@Transactional(rollbackFor = Exception.class) // 创建事务
public void createOrder(OrderDTO dto) {
stockService.deductStock(dto.getProductId(), dto.getQuantity()); // 加入当前事务
accountService.deductBalance(dto.getUserId(), dto.getAmount()); // 加入当前事务
orderMapper.insert(buildOrder(dto)); // 加入当前事务
// 所有操作在同一个事务中,任何一步失败全部回滚
}
}
@Service
public class StockService {
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void deductStock(Long productId, Integer quantity) {
// 当前已有事务(由OrderService创建),加入该事务
// 如果当前没有事务,则创建一个新事务
}
}
特点: 多个方法共享同一个事务,同生共死。这是最常用的方式。
6.3 REQUIRES_NEW(独立事务)
总是创建新事务。如果当前有事务,则把当前事务挂起。
@Service
public class OrderService {
@Autowired
private LogService logService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
try {
// 业务逻辑
doCreateOrder(dto);
} catch (Exception e) {
// 业务失败,但日志已经记录了(因为日志在独立事务中)
throw e;
}
}
private void doCreateOrder(OrderDTO dto) {
// 记录操作日志(独立事务,不受主事务影响)
logService.recordLog("创建订单", dto.toString());
// 业务操作...
if (somethingWrong) {
throw new BusinessException("业务失败");
// 主事务回滚,但日志记录不会回滚 ✅
}
}
}
@Service
public class LogService {
/**
* REQUIRES_NEW:总是创建新事务
* 即使主事务回滚,这里的日志记录也不会回滚
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void recordLog(String action, String detail) {
Log log = new Log();
log.setAction(action);
log.setDetail(detail);
log.setCreateTime(LocalDateTime.now());
logMapper.insert(log);
}
}
使用场景:
-
操作日志记录(无论业务成功失败都要记录)
-
调用外部接口的记录
-
独立的子业务(失败不影响主业务)
注意事项: REQUIRES_NEW会导致外层事务挂起,如果嵌套调用过多,会占用大量数据库连接。
6.4 NESTED(嵌套事务)
如果当前有事务则创建嵌套事务(savepoint),否则创建新事务。嵌套事务可以独立回滚,不影响外层事务。
@Service
public class BatchService {
@Autowired
private ItemService itemService;
/**
* 批量处理:某一条失败不影响其他条
*/
@Transactional(rollbackFor = Exception.class)
public BatchResult batchProcess(List<ItemDTO> items) {
BatchResult result = new BatchResult();
for (ItemDTO item : items) {
try {
// 每条记录在嵌套事务中处理
itemService.processItem(item);
result.addSuccess(item.getId());
} catch (Exception e) {
// 这条失败了,但不影响其他条
result.addFailure(item.getId(), e.getMessage());
}
}
return result;
}
}
@Service
public class ItemService {
/**
* NESTED:创建嵌套事务(savepoint)
* 如果这里抛出异常,只回滚到savepoint,不影响外层事务
*/
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void processItem(ItemDTO item) {
// 处理单条数据
// 如果失败,只回滚这一条的操作
}
}
NESTED vs REQUIRES_NEW 的区别:
| 对比项 | NESTED | REQUIRES_NEW |
|---|---|---|
| 事务关系 | 嵌套在外层事务中 | 完全独立的新事务 |
| 回滚范围 | 只回滚到savepoint | 整个新事务回滚 |
| 外层事务 | 外层事务可以捕获异常后继续 | 外层事务被挂起,互不影响 |
| 数据库连接 | 共用外层事务的连接 | 使用新的数据库连接 |
| 提交时机 | 随外层事务一起提交 | 独立提交 |
6.5 SUPPORTS 和 NOT_SUPPORTED
// SUPPORTS:有事务就加入,没有就以非事务方式运行
// 适合:查询方法,大多数时候不需要事务,但如果被事务方法调用也能正常工作
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public User getUserById(Long id) {
return userMapper.selectById(id);
}
// NOT_SUPPORTED:以非事务方式运行,如果有事务就挂起
// 适合:不需要事务的耗时操作,避免长时间占用数据库连接
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public List<Data> exportData() {
// 大量数据导出,不需要事务
return dataMapper.selectAll();
}
6.6 MANDATORY 和 NEVER
// MANDATORY:必须在事务中调用,否则抛异常
// 适合:必须保证数据一致性的关键操作
@Transactional(propagation = Propagation.MANDATORY)
public void criticalUpdate() {
// 如果调用方没有事务,直接抛出异常
// 强制要求调用方开启事务
}
// NEVER:必须在非事务中调用,否则抛异常
// 适合:明确不能在事务中执行的操作
@Transactional(propagation = Propagation.NEVER)
public void nonTransactionalOperation() {
// 如果调用方有事务,直接抛出异常
}
6.7 传播行为选择指南
大多数场景 → REQUIRED(默认值,不用刻意指定) 日志记录、审计 → REQUIRES_NEW(独立于主事务) 批量处理、部分失败 → NESTED(嵌套事务,可独立回滚) 纯查询方法 → SUPPORTS(有就加入,没有拉倒) 关键数据操作 → MANDATORY(强制要求调用方有事务)
7. @Transactional失效的常见场景
这是面试高频考点,也是实际开发中最容易踩的坑。
7.1 方法非public
Spring的事务是通过AOP代理实现的,AOP只能拦截public方法。
@Service
public class UserService {
// ❌ private方法,事务不生效
@Transactional(rollbackFor = Exception.class)
private void privateMethod() {
// 不会开启事务!
}
// ❌ protected方法,事务不生效
@Transactional(rollbackFor = Exception.class)
protected void protectedMethod() {
// 不会开启事务!
}
// ✅ public方法,事务生效
@Transactional(rollbackFor = Exception.class)
public void publicMethod() {
// 正常开启事务
}
}
7.2 同类内部方法调用(最常见)
通过 this 调用同类的方法,绕过了Spring的代理对象,事务不生效。
@Service
public class UserService {
// ❌ 错误:内部调用,事务不生效
public void outerMethod() {
// this.innerMethod() 直接调用,没有经过代理
this.innerMethod(); // 事务不生效!
}
@Transactional(rollbackFor = Exception.class)
public void innerMethod() {
// 事务逻辑
}
}
解决方案:
@Service
public class UserService {
@Autowired
private ApplicationContext applicationContext;
// 方案1:注入自身代理
@Autowired
private UserService self; // 注入的是代理对象
// 方案2:通过ApplicationContext获取
private UserService getSelf() {
return applicationContext.getBean(UserService.class);
}
// 方案3:使用AopContext(需要配置exposeProxy=true)
// @EnableAspectJAutoProxy(exposeProxy = true)
public void outerMethod() {
// ❌ this.innerMethod(); // 事务不生效
// ✅ 方案1:通过代理对象调用
self.innerMethod();
// ✅ 方案2:通过ApplicationContext获取
getSelf().innerMethod();
// ✅ 方案3:通过AopContext获取
// ((UserService) AopContext.currentProxy()).innerMethod();
}
@Transactional(rollbackFor = Exception.class)
public void innerMethod() {
// 事务逻辑
}
}
7.3 异常被吞掉
catch了异常但没有重新抛出,Spring感知不到异常,不会回滚。
@Service
public class OrderService {
// ❌ 错误:异常被吞掉,事务不回滚
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
try {
orderMapper.insert(buildOrder(dto));
stockMapper.deduct(dto.getProductId(), dto.getQuantity());
int i = 1 / 0; // 抛出ArithmeticException
} catch (Exception e) {
// 异常被catch了,Spring不知道发生了异常
// 事务不会回滚!
log.error("出错了", e);
}
}
// ✅ 正确做法1:不catch,让异常自然传播
@Transactional(rollbackFor = Exception.class)
public void createOrderCorrect1(OrderDTO dto) {
orderMapper.insert(buildOrder(dto));
stockMapper.deduct(dto.getProductId(), dto.getQuantity());
// 如果出异常,自然传播到Spring,触发回滚
}
// ✅ 正确做法2:catch后重新抛出
@Transactional(rollbackFor = Exception.class)
public void createOrderCorrect2(OrderDTO dto) {
try {
orderMapper.insert(buildOrder(dto));
stockMapper.deduct(dto.getProductId(), dto.getQuantity());
} catch (Exception e) {
log.error("出错了", e);
throw e; // 重新抛出,让Spring感知到异常
}
}
// ✅ 正确做法3:手动回滚
@Transactional(rollbackFor = Exception.class)
public void createOrderCorrect3(OrderDTO dto) {
try {
orderMapper.insert(buildOrder(dto));
stockMapper.deduct(dto.getProductId(), dto.getQuantity());
} catch (Exception e) {
log.error("出错了", e);
// 手动标记当前事务需要回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
7.4 异常类型不匹配
默认只对RuntimeException和Error回滚,受检异常不触发回滚。
@Service
public class FileService {
// ❌ IOException是受检异常,默认不会触发回滚
@Transactional
public void importData() throws IOException {
// 数据库操作...
throw new IOException("文件读取失败");
// 事务不会回滚!
}
// ✅ 指定rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class)
public void importDataCorrect() throws IOException {
// 数据库操作...
throw new IOException("文件读取失败");
// 事务会回滚 ✅
}
}
7.5 数据库引擎不支持事务
MySQL的MyISAM引擎不支持事务,只有InnoDB支持。
-- 查看表的引擎 SHOW TABLE STATUS WHERE Name = 'user'; -- 如果是MyISAM,改为InnoDB ALTER TABLE user ENGINE = InnoDB;
7.6 异步方法或多线程
@Service
public class AsyncService {
// ❌ @Async方法中事务不生效(在新线程中执行)
@Async
@Transactional(rollbackFor = Exception.class)
public void asyncProcess() {
// 这里的事务和调用方不在同一个线程
// 异常无法传播到调用方
}
// ❌ 新线程中的操作不在事务范围内
@Transactional(rollbackFor = Exception.class)
public void multiThread() {
orderMapper.insert(order); // 在事务中 ✅
new Thread(() -> {
// 这个操作在新线程中,不在事务中
stockMapper.deduct(productId, quantity); // 不在事务中 ❌
}).start();
}
}
7.7 失效场景速查表
| 场景 | 是否生效 | 原因 |
|---|---|---|
| 方法非public | 不生效 | AOP只能代理public方法 |
| 同类内部调用 | 不生效 | 绕过了代理对象 |
| 异常被catch没抛出 | 生效但不回滚 | Spring感知不到异常 |
| 受检异常 | 生效但不回滚 | 默认只回滚RuntimeException |
| MyISAM引擎 | 不生效 | 引擎不支持事务 |
| 多线程/异步 | 不生效 | 不同线程不共享事务上下文 |
| final方法 | 不生效 | 无法被代理重写 |
| static方法 | 不生效 | 不属于实例方法 |
8. 什么时候用事务,什么时候不用
8.1 必须使用事务的场景
// 场景1:多表写操作(原子性要求)
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
accountMapper.deduct(from, amount); // 扣款
accountMapper.add(to, amount); // 入账
transferLogMapper.insert(log); // 记录日志
// 三步必须同时成功或同时失败
}
// 场景2:批量操作(要么全部成功,要么全部回滚)
@Transactional(rollbackFor = Exception.class)
public void batchImport(List<User> users) {
for (User user : users) {
userMapper.insert(user);
accountMapper.initAccount(user.getId());
roleMapper.assignDefaultRole(user.getId());
}
// 任何一条失败,全部回滚
}
// 场景3:状态流转(步骤之间有依赖)
@Transactional(rollbackFor = Exception.class)
public void payOrder(Long orderId) {
Order order = orderMapper.selectById(orderId);
if (order.getStatus() != 0) {
throw new BusinessException("订单状态异常");
}
order.setStatus(1); // 待支付 → 已支付
orderMapper.updateById(order);
accountMapper.deduct(order.getUserId(), order.getTotalAmount());
// 两步必须原子完成
}
8.2 不需要使用事务的场景
// 场景1:纯查询
// 不需要事务,加了反而浪费资源
public User getUser(Long id) {
return userMapper.selectById(id);
}
// 场景2:单条简单写操作
// 只操作一张表的一条记录,不需要事务
public void updateNickname(Long userId, String nickname) {
userMapper.updateNickname(userId, nickname);
}
// 场景3:幂等操作
// 重复执行结果一样,不需要事务保护
public void updateProfile(UserProfile profile) {
profileMapper.updateById(profile);
}
// 场景4:对一致性要求极低的统计操作
public void incrementViewCount(Long articleId) {
articleMapper.incrementViewCount(articleId);
// 即使少计一次浏览量,影响也不大
}
8.3 场景判断速查表
| 场景 | 是否需要事务 | 理由 |
|---|---|---|
| 转账 | 必须 | 多表写操作,原子性要求 |
| 电商下单 | 必须 | 扣库存+扣余额+创建订单 |
| 用户注册(仅插入用户表) | 不需要 | 单表单条操作 |
| 用户注册(初始化账户+发优惠券) | 必须 | 多表操作 |
| 修改昵称 | 不需要 | 单表单条更新 |
| 批量导入 | 必须 | 要么全部成功,要么全部回滚 |
| 发送短信 | 不需要 | 失败可以重试补偿 |
| 查询列表 | 不需要 | 纯读操作 |
| 订单支付 | 必须 | 状态变更+扣款,原子操作 |
9. 事务与锁的关系
事务和锁是两个不同层面的概念,但它们紧密关联:数据库的隔离级别本质上是通过锁机制来实现的。
9.1 隔离级别的实现原理
READ UNCOMMITTED: 读操作不加锁,直接读取数据的最新版本 写操作加排他锁(X锁),事务结束释放 READ COMMITTED: 读操作使用MVCC(每次SELECT生成新快照),不加锁 写操作加排他锁(X锁),事务结束释放 REPEATABLE READ: 读操作使用MVCC(事务开始时创建快照,全程使用) 写操作加排他锁(X锁)+ 间隙锁(防止幻读) SERIALIZABLE: 读操作加共享锁(S锁) 写操作加排他锁(X锁) 所有操作串行执行
9.2 事务中的悲观锁
悲观锁必须在事务中使用,因为锁的生命周期就是事务的生命周期。
/**
* 悲观锁必须配合事务使用
* SELECT ... FOR UPDATE 在事务中才会生效
*/
@Service
public class StockService {
@Transactional(rollbackFor = Exception.class) // 事务必须有
public boolean deductStock(Long productId, Integer quantity) {
// FOR UPDATE 加的行锁,会在事务提交/回滚时自动释放
Stock stock = stockMapper.selectForUpdate(productId);
if (stock.getQuantity() < quantity) {
throw new BusinessException("库存不足");
}
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
return true;
// 事务提交时,行锁自动释放
}
}
9.3 事务中的乐观锁
乐观锁不依赖事务,但通常也会配合事务使用。
/**
* 乐观锁不强制要求事务
* 但配合事务使用更安全(保证查询和更新之间数据不被修改)
*/
@Service
public class StockService {
// 方案1:不用事务(简单场景可以)
public boolean deductWithoutTx(Long productId, Integer quantity) {
Stock stock = stockMapper.selectById(productId);
// 在查询和更新之间,其他线程可能修改了数据
// 但乐观锁的版本号检查会发现冲突
int rows = stockMapper.deductWithVersion(productId, quantity, stock.getVersion());
return rows > 0;
}
// 方案2:用事务(更安全)
@Transactional(rollbackFor = Exception.class)
public boolean deductWithTx(Long productId, Integer quantity) {
Stock stock = stockMapper.selectById(productId);
int rows = stockMapper.deductWithVersion(productId, quantity, stock.getVersion());
// 在事务保护下,查询和更新是一个原子操作
return rows > 0;
}
}
9.4 总结关系
事务 ≠ 锁,但事务依赖锁来实现隔离性 事务是逻辑概念:一组操作的原子性保证 锁是物理机制:控制并发访问的手段 悲观锁:必须在事务中使用(SELECT FOR UPDATE) 乐观锁:可以不用事务,但配合事务更安全 事务的隔离级别 ← 通过锁/MVCC来实现
10. 编程式事务
除了声明式事务(@Transactional),Spring还提供了编程式事务管理,适合需要更精细控制事务的场景。
10.1 TransactionTemplate
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
/**
* 编程式事务:使用TransactionTemplate
* 适合需要在代码中精细控制事务边界的场景
*/
public OrderVO createOrder(OrderDTO dto) {
return transactionTemplate.execute(status -> {
try {
// 扣减库存
stockMapper.deduct(dto.getProductId(), dto.getQuantity());
// 扣减余额
accountMapper.deductBalance(dto.getUserId(), dto.getAmount());
// 创建订单
Order order = buildOrder(dto);
orderMapper.insert(order);
return OrderVO.from(order);
} catch (Exception e) {
// 手动标记回滚
status.setRollbackOnly();
throw e;
}
});
}
/**
* 无返回值版本
*/
public void updateStock(Long productId, Integer quantity) {
transactionTemplate.executeWithoutResult(status -> {
Stock stock = stockMapper.selectById(productId);
if (stock.getQuantity() < quantity) {
status.setRollbackOnly(); // 标记回滚
return;
}
stock.setQuantity(stock.getQuantity() - quantity);
stockMapper.updateById(stock);
});
}
}
10.2 PlatformTransactionManager
@Service
public class AdvancedTransactionService {
@Autowired
private PlatformTransactionManager transactionManager;
/**
* 使用PlatformTransactionManager手动管理事务
* 最底层的事务管理方式,灵活性最高
*/
public void manualTransaction() {
// 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setTimeout(30);
// 获取事务状态
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务逻辑
doSomething();
doSomethingElse();
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
10.3 声明式 vs 编程式对比
| 对比项 | 声明式(@Transactional) | 编程式(TransactionTemplate) |
|---|---|---|
| 使用方式 | 注解,零侵入 | 代码中显式控制 |
| 灵活性 | 较低,整个方法是一个事务 | 高,可以精确控制事务边界 |
| 代码侵入 | 无 | 有,需要写事务管理代码 |
| 适用场景 | 大多数场景 | 需要精细控制事务的场景 |
| 可读性 | 好 | 一般 |
11. 分布式事务简介
当一个业务操作涉及多个数据库或多个微服务时,单机事务无法保证跨系统的数据一致性,需要使用分布式事务。
11.1 什么时候需要分布式事务
单机事务:操作同一个数据库的多张表 → Spring @Transactional 就够了 分布式事务:操作多个数据库,或跨多个微服务 → 需要分布式事务方案
场景示例: 订单服务(订单库) → 扣减库存 → 扣减余额 → 创建订单 如果订单服务、库存服务、余额服务是不同的微服务 每个服务有自己的数据库 → 需要分布式事务
11.2 常见的分布式事务方案
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 2PC(两阶段提交) | 准备阶段+提交阶段,协调者统一指挥 | 强一致性 | 性能差,协调者单点故障 |
| TCC(Try-Confirm-Cancel) | 三个阶段:预留→确认→取消 | 灵活,性能较好 | 代码侵入大,需要实现三个接口 |
| Saga | 将大事务拆分为多个小事务,失败时执行补偿操作 | 性能好,无锁 | 最终一致性,补偿逻辑复杂 |
| 本地消息表 | 利用本地事务+消息队列实现最终一致性 | 实现简单,可靠 | 最终一致性,有一定延迟 |
| Seata | 阿里开源的分布式事务框架,支持AT/TCC/Saga模式 | 开箱即用,生态好 | 需要额外部署协调服务 |
11.3 Seata AT模式示例
// 1. 引入Seata依赖
// pom.xml
/*
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
*/
// 2. 在方法上添加@GlobalTransactional
@Service
public class OrderService {
@Autowired
private StockFeignClient stockFeignClient;
@Autowired
private AccountFeignClient accountFeignClient;
/**
* 分布式事务:跨多个微服务的事务
* @GlobalTransactional 是Seata的全局事务注解
*/
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 调用库存服务(远程调用,不同的数据库)
stockFeignClient.deduct(dto.getProductId(), dto.getQuantity());
// 调用账户服务(远程调用,不同的数据库)
accountFeignClient.deduct(dto.getUserId(), dto.getAmount());
// 本地操作
orderMapper.insert(buildOrder(dto));
// 任何一个服务调用失败,所有服务的操作都会回滚
}
}
12. 实战:电商下单完整示例
把前面学到的所有事务知识综合运用到一个完整的电商下单场景中。
@Service
@Slf4j
public class OrderService {
@Autowired
private StockMapper stockMapper;
@Autowired
private AccountMapper accountMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private LogService logService;
/**
* 创建订单
*
* 涉及知识:
* 1. @Transactional 保证原子性
* 2. rollbackFor = Exception.class 确保任何异常都回滚
* 3. 悲观锁(FOR UPDATE)防止库存超卖
* 4. 传播行为:日志用REQUIRES_NEW独立记录
*/
@Transactional(rollbackFor = Exception.class)
public OrderVO createOrder(CreateOrderDTO dto) {
// ============ 第一步:扣减库存(悲观锁) ============
Stock stock = stockMapper.selectForUpdate(dto.getProductId());
if (stock == null) {
throw new BusinessException("商品不存在");
}
if (stock.getQuantity() < dto.getQuantity()) {
throw new BusinessException("库存不足");
}
stock.setQuantity(stock.getQuantity() - dto.getQuantity());
stockMapper.updateById(stock);
log.info("库存扣减成功,商品ID: {},扣减数量: {}", dto.getProductId(), dto.getQuantity());
// ============ 第二步:扣减余额 ============
Account account = accountMapper.selectForUpdateByUserId(dto.getUserId());
BigDecimal totalAmount = dto.getUnitPrice().multiply(BigDecimal.valueOf(dto.getQuantity()));
if (account.getBalance().compareTo(totalAmount) < 0) {
throw new BusinessException("余额不足");
}
account.setBalance(account.getBalance().subtract(totalAmount));
accountMapper.updateById(account);
log.info("余额扣减成功,用户ID: {},扣减金额: {}", dto.getUserId(), totalAmount);
// ============ 第三步:创建订单主表 ============
Order order = new Order();
order.setOrderNo(generateOrderNo());
order.setUserId(dto.getUserId());
order.setTotalAmount(totalAmount);
order.setStatus(OrderStatus.CREATED.getCode());
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
// ============ 第四步:创建订单明细 ============
OrderItem item = new OrderItem();
item.setOrderId(order.getId());
item.setProductId(dto.getProductId());
item.setProductName(dto.getProductName());
item.setQuantity(dto.getQuantity());
item.setUnitPrice(dto.getUnitPrice());
orderItemMapper.insert(item);
// ============ 第五步:记录操作日志(独立事务) ============
try {
logService.recordOrderLog(order.getOrderNo(), "创建订单", dto.toString());
} catch (Exception e) {
// 日志记录失败不影响订单创建
log.warn("订单日志记录失败: {}", e.getMessage());
}
log.info("订单创建成功,订单号: {}", order.getOrderNo());
// 返回结果
OrderVO vo = new OrderVO();
vo.setOrderNo(order.getOrderNo());
vo.setTotalAmount(totalAmount);
vo.setStatus(OrderStatus.CREATED.getCode());
return vo;
}
/**
* 支付订单
*
* 涉及知识:
* 乐观锁防止并发支付(同一个订单被重复支付)
*/
@Transactional(rollbackFor = Exception.class)
public void payOrder(Long orderId, Long userId) {
Order order = orderMapper.selectById(orderId);
// 校验订单归属
if (!order.getUserId().equals(userId)) {
throw new BusinessException("无权操作此订单");
}
// 校验订单状态(只能对待支付状态的订单进行支付)
if (order.getStatus() != OrderStatus.CREATED.getCode()) {
throw new BusinessException("订单状态异常,当前状态: " + order.getStatus());
}
// 乐观锁更新订单状态
order.setStatus(OrderStatus.PAID.getCode());
order.setPayTime(LocalDateTime.now());
int rows = orderMapper.updateWithVersion(order);
if (rows == 0) {
throw new BusinessException("订单状态已变更,请刷新后重试");
}
log.info("订单支付成功,订单号: {}", order.getOrderNo());
}
/**
* 取消订单
*/
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId, Long userId) {
Order order = orderMapper.selectById(orderId);
if (!order.getUserId().equals(userId)) {
throw new BusinessException("无权操作此订单");
}
if (order.getStatus() != OrderStatus.CREATED.getCode()) {
throw new BusinessException("只能取消待支付的订单");
}
// 回滚库存
stockMapper.restore(order.getProductId(), order.getQuantity());
// 回滚余额
accountMapper.restoreBalance(userId, order.getTotalAmount());
// 更新订单状态
order.setStatus(OrderStatus.CANCELLED.getCode());
order.setCancelTime(LocalDateTime.now());
orderMapper.updateById(order);
log.info("订单已取消,订单号: {}", order.getOrderNo());
}
private String generateOrderNo() {
return "ORD" + System.currentTimeMillis() +
String.format("%04d", new Random().nextInt(10000));
}
}
/**
* 日志服务:使用REQUIRES_NEW,独立于主事务
*/
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void recordOrderLog(String orderNo, String action, String detail) {
OrderLog log = new OrderLog();
log.setOrderNo(orderNo);
log.setAction(action);
log.setDetail(detail);
log.setCreateTime(LocalDateTime.now());
orderLogMapper.insert(log);
// 使用REQUIRES_NEW,即使主事务回滚,这条日志也会保留
}
}
13. 常见面试题精选
Q1:@Transactional的实现原理是什么?
Spring通过AOP(面向切面编程)实现@Transactional。 1. Spring扫描带有@Transactional注解的类/方法 2. 为这些类创建代理对象(JDK动态代理或CGLIB代理) 3. 调用方法时,代理拦截器会: a. 获取数据库连接 b. 关闭自动提交(setAutoCommit(false)) c. 执行业务方法 d. 如果没有异常,提交事务 e. 如果有异常(且满足rollbackFor条件),回滚事务 f. 释放数据库连接
Q2:事务隔离级别是数据库层面的还是Spring层面的?
事务隔离级别是数据库层面的概念,SQL标准定义了四种隔离级别。 Spring只是通过@Transactional注解的isolation参数,将隔离级别设置传递给数据库。 Spring本身不实现隔离级别,它只是一个"传话筒"。 实际的隔离控制由数据库(如MySQL的InnoDB引擎)通过锁和MVCC来实现。
Q3:REPEATABLE READ是如何解决幻读的?
MySQL的REPEATABLE READ级别下: 1. 快照读(普通SELECT): 通过MVCC实现,读取的是事务开始时的快照 即使其他事务插入了新记录,当前事务也看不到 2. 当前读(SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE): 通过Next-Key Lock(临键锁 = 行锁 + 间隙锁)实现 锁定记录及其之间的间隙,阻止其他事务在间隙中插入新记录
Q4:@Transactional方法A调用方法B,B上的事务会生效吗?
取决于调用方式: 1. 通过this调用(内部调用)→ B的事务不生效 因为绕过了Spring代理对象 2. 通过代理对象调用 → B的事务生效 例如注入自身代理或通过ApplicationContext获取 3. B的方法传播行为是REQUIRED → 加入A的事务 不管怎么调用,B都在A的事务中执行
Q5:事务的ACID中,哪个最重要?
一致性(Consistency)是最终目标,其他三个是手段。 - 原子性保证操作要么全做要么全不做 → 保证一致性 - 隔离性保证并发事务互不干扰 → 保证一致性 - 持久性保证提交后数据不丢失 → 保证一致性 没有一致性,其他三个特性都没有意义。
14. 总结
一张图看懂事务
┌─────────────────────────────────────────────────────────────┐ │ Spring事务全景图 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 声明式事务 │ │ 编程式事务 │ │ 分布式事务 │ │ │ │ @Transactional│ │TransactionT. │ │ Seata │ │ │ │ 最常用 │ │ 精细控制 │ │ 跨服务/跨库 │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └───────────────────┼───────────────────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ │ ACID特性 │ │ │ │ 原子性/一致性/ │ │ │ │ 隔离性/持久性 │ │ │ └────────┬────────┘ │ │ │ │ │ ┌──────────────┼──────────────┐ │ │ │ │ │ │ │ ┌──────┴──────┐ ┌────┴────┐ ┌───────┴───────┐ │ │ │ undo log │ │锁+MVCC │ │ redo log │ │ │ │ 原子性 │ │隔离性 │ │ 持久性 │ │ │ └─────────────┘ └─────────┘ └───────────────┘ │ │ │ │ 隔离级别:READ UNCOMMITTED → READ COMMITTED │ │ → REPEATABLE READ → SERIALIZABLE │ │ │ │ 传播行为:REQUIRED(默认) / REQUIRES_NEW / NESTED │ │ / SUPPORTS / MANDATORY / NOT_SUPPORTED / NEVER │ │ │ │ 常见失效:非public / 内部调用 / 异常被吞 / 异常类型不匹配 │ │ │ └─────────────────────────────────────────────────────────────┘
最佳实践速查
1. 永远写 rollbackFor = Exception.class 2. 事务方法必须是 public 3. 避免在事务中进行远程调用(HTTP/RPC),会延长事务持有时间 4. 避免在事务中处理大量数据,防止长事务 5. 只读查询加 readOnly = true 6. 日志记录用 REQUIRES_NEW 独立事务 7. 同类内部调用注意代理失效问题 8. 批量操作考虑用 NESTED 嵌套事务 9. 生产环境优先选择 READ COMMITTED 隔离级别 10. 涉及多服务/多库时考虑分布式事务方案
一句话总结
事务通过ACID特性保证数据的一致性,隔离级别控制并发读写的安全性与性能平衡,传播行为定义事务边界的传递规则。理解@Transactional的失效场景和底层原理,是Java后端开发的基本功。
更多推荐



所有评论(0)