文章目录
- 一、开篇:从生活中的转账说起
- 二、什么是事务?
-
- [2.1 ACID特性通俗解释](#2.1 ACID特性通俗解释)
- 三、Spring事务的核心作用
- 四、声明式事务实现方式
-
- [4.1 @Transactional注解详解](#4.1 @Transactional注解详解)
- [4.2 @Transactional核心属性](#4.2 @Transactional核心属性)
- 五、事务传播行为(7种传播机制)
-
- [5.1 REQUIRED(默认值)](#5.1 REQUIRED(默认值))
- [5.2 REQUIRES_NEW](#5.2 REQUIRES_NEW)
- [5.3 SUPPORTS](#5.3 SUPPORTS)
- [5.4 NOT_SUPPORTED](#5.4 NOT_SUPPORTED)
- [5.5 MANDATORY](#5.5 MANDATORY)
- [5.6 NEVER](#5.6 NEVER)
- [5.7 NESTED](#5.7 NESTED)
- 六、@Transactional失效的12种可能性
-
- [6.1 方法访问权限非public](#6.1 方法访问权限非public)
- [6.2 方法被final修饰](#6.2 方法被final修饰)
- [6.3 方法被static修饰](#6.3 方法被static修饰)
- [6.4 同一个类中的自调用](#6.4 同一个类中的自调用)
- [6.5 异常被捕获未重新抛出](#6.5 异常被捕获未重新抛出)
- [6.6 异常类型不匹配](#6.6 异常类型不匹配)
- [6.7 数据库引擎不支持事务](#6.7 数据库引擎不支持事务)
- [6.8 事务传播机制设置不当](#6.8 事务传播机制设置不当)
- [6.9 多线程调用](#6.9 多线程调用)
- [6.10 事务管理器配置错误](#6.10 事务管理器配置错误)
- [6.11 类未被Spring管理](#6.11 类未被Spring管理)
- [6.12 代理机制限制](#6.12 代理机制限制)
- 七、事务失效问题排查清单
- 八、面试常考:核心要点总结
-
- [8.1 Spring事务核心问题](#8.1 Spring事务核心问题)
- [8.2 事务传播行为记忆口诀](#8.2 事务传播行为记忆口诀)
- [8.3 最佳实践建议](#8.3 最佳实践建议)
- [8.4 高级面试题](#8.4 高级面试题)
一、开篇:从生活中的转账说起
想象这样一个场景:你要给朋友转账100元,整个转账过程其实包含两个步骤:
- 从你的账户扣减100元
- 给朋友的账户增加100元
如果第一步成功了,但第二步因为网络异常失败了,会发生什么?你的钱少了,朋友没收到,这笔钱就"消失"了!这就是没有事务保护带来的严重问题。
事务就是为了解决这类问题而生的。它就像一个"保险箱",把多个操作打包在一起,要么全部成功,要么全部失败,保证数据的一致性。
二、什么是事务?
在数据库领域,事务(Transaction)是一个最小的不可分割的工作单位。事务必须具备ACID四大特性:
2.1 ACID特性通俗解释
| 特性 | 含义 | 生活比喻 |
|---|---|---|
| 原子性(Atomicity) | 事务中的操作要么全部成功,要么全部失败 | 就像买彩票,要么中奖全拿走,要么不中奖什么都不拿 |
| 一致性(Consistency) | 事务执行前后,数据库从一个一致性状态转变到另一个一致性状态 | 就像天平,两边始终保持平衡 |
| 隔离性(Isolation) | 多个事务并发执行时,互不干扰 | 就像在银行办理业务的独立窗口 |
| 持久性(Durability) | 事务一旦提交,对数据的修改就是永久的 | 就像刻在石头上的字,风吹雨打都不会消失 |
三、Spring事务的核心作用
Spring事务管理框架是对数据库事务的封装和增强,主要提供以下核心价值:
- 简化开发:通过声明式事务,无需手动编写复杂的事务管理代码
- 统一管理:将事务逻辑从业务代码中分离,实现关注点分离
- 灵活配置:支持编程式和声明式两种事务管理方式
- 跨数据源支持:可以管理多个数据源的事务操作
核心优势:让开发者专注于业务逻辑,而将事务管理的复杂性交给Spring框架处理。
四、声明式事务实现方式
4.1 @Transactional注解详解
@Transactional是Spring最常用的事务注解,它可以作用在类或方法上。
java
// 基本用法示例
@Service
public class AccountService {
@Autowired
private AccountDao accountDao;
// 方法级别事务
@Transactional
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// 扣款
accountDao.decrease(fromId, amount);
// 模拟异常,测试事务回滚
if (amount.compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("单次转账金额不能超过10000元");
}
// 收款
accountDao.increase(toId, amount);
}
}
4.2 @Transactional核心属性
java
@Transactional(
propagation = Propagation.REQUIRED, // 传播行为
isolation = Isolation.READ_COMMITTED, // 隔离级别
timeout = 30, // 超时时间(秒)
readOnly = false, // 是否只读
rollbackFor = Exception.class, // 指定回滚异常
noRollbackFor = NullPointerException.class // 指定不回滚异常
)
属性详解:
- propagation:事务传播行为,决定多个事务方法相互调用时如何执行
- isolation:事务隔离级别,控制并发事务之间的干扰程度
- timeout:事务超时时间,超过指定时间自动回滚
- readOnly:设置为true时表示只读事务,可优化查询性能
- rollbackFor:指定哪些异常触发回滚
- noRollbackFor:指定哪些异常不触发回滚
五、事务传播行为(7种传播机制)
事务传播行为定义了多个事务方法相互调用时,事务如何传播。Spring提供了7种传播机制,我们通过具体场景来理解:
5.1 REQUIRED(默认值)
场景:方法A调用方法B,如果A有事务,B就加入A的事务;如果A没有事务,B就创建新事务。
java
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
// 创建订单
orderDao.save(order);
// 调用支付服务,会加入当前事务
paymentService.processPayment(order);
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRED)
public void processPayment(Order order) {
// 支付处理,与createOrder在同一个事务中
paymentDao.deduct(order.getUserId(), order.getAmount());
}
}
5.2 REQUIRES_NEW
场景:方法B总是创建新事务,如果A有事务,A的事务会被挂起。
java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation(String operation) {
// 总是创建新事务,独立记录日志
logDao.save(operation);
}
使用场景:日志记录、审计信息等需要独立事务的场景。
5.3 SUPPORTS
场景:如果调用者有事务就加入,没有就以非事务方式执行。
java
@Transactional(propagation = Propagation.SUPPORTS)
public User findUserById(Long userId) {
return userDao.findById(userId);
}
使用场景:查询方法,无论是否有事务都能正常执行。
5.4 NOT_SUPPORTED
场景:总是以非事务方式执行,如果调用者有事务,调用者事务会被挂起。
java
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendEmail(String to, String content) {
// 发送邮件,不使用事务
emailService.send(to, content);
}
5.5 MANDATORY
场景:必须在事务中执行,如果调用者没有事务就抛出异常。
java
@Transactional(propagation = Propagation.MANDATORY)
public void criticalOperation() {
// 必须在事务中执行,否则抛出异常
businessDao.update();
}
5.6 NEVER
场景:总是以非事务方式执行,如果调用者有事务就抛出异常。
java
@Transactional(propagation = Propagation.NEVER)
public void cacheRefresh() {
// 刷新缓存,不能在事务中执行
cacheManager.refresh();
}
5.7 NESTED
场景:如果调用者有事务,就嵌套事务执行;如果没有,就创建新事务。
java
@Transactional(propagation = Propagation.NESTED)
public void batchInsert(List<User> users) {
// 嵌套事务,可以独立回滚
for (User user : users) {
userDao.save(user);
}
}
关键区别:嵌套事务可以独立回滚,但不影响外部事务。
六、@Transactional失效的12种可能性
6.1 方法访问权限非public
问题代码:
java
@Service
public class UserService {
// private方法,事务失效
@Transactional
private void saveUser(User user) {
userDao.save(user);
}
// protected方法,事务失效
@Transactional
protected void updateUser(User user) {
userDao.update(user);
}
// package-private方法,事务失效
@Transactional
void deleteUser(Long id) {
userDao.delete(id);
}
}
原因:Spring的AOP代理机制要求目标方法必须是public的,因为代理类需要重写该方法。private、protected、package-private方法无法被代理,所以事务失效。
解决方案:将方法访问权限改为public。
6.2 方法被final修饰
问题代码:
java
@Service
public class UserService {
// final方法,事务失效
@Transactional
public final void saveUser(User user) {
userDao.save(user);
}
}
原因:final修饰的方法不能被重写,而Spring的AOP代理是通过动态代理(JDK动态代理或CGLIB)生成代理类并重写方法来实现的。final方法无法被重写,导致代理无法生成,事务失效。
解决方案:不要用final修饰需要事务的方法。
6.3 方法被static修饰
问题代码:
java
@Service
public class UserService {
// static方法,事务失效
@Transactional
public static void saveUser(User user) {
userDao.save(user);
}
}
原因:static方法属于类而不是对象,无法被代理。Spring的事务管理是基于对象级别的代理,static方法无法通过代理增强,所以事务失效。
解决方案:不要用static修饰需要事务的方法。
6.4 同一个类中的自调用
问题代码:
java
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 创建订单
orderDao.save(order);
// 直接调用同类方法,事务失效
this.saveOrderItems(order.getItems());
}
@Transactional
public void saveOrderItems(List<OrderItem> items) {
for (OrderItem item : items) {
orderItemDao.save(item);
}
}
}
原因 :Spring的事务是基于AOP代理实现的,当在同一个类中通过this直接调用方法时,调用的是原始对象的方法,而不是经过代理增强的方法,所以事务失效。
解决方案:
java
// 方案1:注入自身调用
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身
@Transactional
public void createOrder(Order order) {
orderDao.save(order);
// 通过代理对象调用,事务生效
self.saveOrderItems(order.getItems());
}
@Transactional
public void saveOrderItems(List<OrderItem> items) {
for (OrderItem item : items) {
orderItemDao.save(item);
}
}
}
// 方案2:提取到不同Service
@Service
public class OrderService {
@Autowired
private OrderItemService orderItemService;
@Transactional
public void createOrder(Order order) {
orderDao.save(order);
// 调用不同Service的方法,事务生效
orderItemService.saveOrderItems(order.getItems());
}
}
6.5 异常被捕获未重新抛出
问题代码:
java
@Service
public class PaymentService {
@Transactional
public void processPayment(Payment payment) {
try {
// 支付处理
accountDao.deduct(payment.getUserId(), payment.getAmount());
// 模拟异常
if (payment.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("金额超限");
}
accountDao.credit(payment.getToUserId(), payment.getAmount());
} catch (Exception e) {
// 异常被捕获,事务不会回滚
log.error("支付失败", e);
}
}
}
原因 :Spring默认只在抛出RuntimeException或Error时才回滚事务。当异常被catch块捕获后,事务管理器无法感知异常的发生,所以不会触发回滚。
解决方案:
java
// 方案1:重新抛出异常
@Transactional
public void processPayment(Payment payment) {
try {
accountDao.deduct(payment.getUserId(), payment.getAmount());
if (payment.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("金额超限");
}
accountDao.credit(payment.getToUserId(), payment.getAmount());
} catch (Exception e) {
log.error("支付失败", e);
throw e; // 重新抛出异常,触发回滚
}
}
// 方案2:指定回滚异常类型
@Transactional(rollbackFor = Exception.class)
public void processPayment(Payment payment) {
try {
accountDao.deduct(payment.getUserId(), payment.getAmount());
if (payment.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("金额超限");
}
accountDao.credit(payment.getToUserId(), payment.getAmount());
} catch (Exception e) {
log.error("支付失败", e);
// 即使捕获异常,也会回滚
}
}
6.6 异常类型不匹配
问题代码:
java
@Service
public class UserService {
// 默认只回滚RuntimeException
@Transactional
public void updateUser(User user) throws Exception {
userDao.update(user);
// 抛出受检异常,默认不会回滚
throw new Exception("用户更新失败");
}
}
原因 :Spring默认只在抛出RuntimeException(非受检异常)和Error时回滚事务。对于受检异常(checked exception,如Exception、IOException等),默认不会触发回滚。
解决方案:
java
// 方案1:指定回滚异常类型
@Transactional(rollbackFor = Exception.class)
public void updateUser(User user) throws Exception {
userDao.update(user);
throw new Exception("用户更新失败"); // 现在会回滚
}
// 方案2:抛出RuntimeException
@Transactional
public void updateUser(User user) {
userDao.update(user);
throw new RuntimeException("用户更新失败"); // 默认会回滚
}
6.7 数据库引擎不支持事务
问题代码:
java
// MySQL表使用MyISAM引擎
@Transactional
public void saveUser(User user) {
userDao.save(user); // 事务不会生效
}
原因:事务的最终支持是由数据库提供的。如果数据库表使用的是不支持事务的存储引擎(如MySQL的MyISAM),那么即使Spring配置了事务,也无法实现事务的回滚功能。
解决方案:确保数据库表使用支持事务的存储引擎(如MySQL的InnoDB)。
sql
-- 修改表引擎为InnoDB
ALTER TABLE user ENGINE = InnoDB;
6.8 事务传播机制设置不当
问题代码:
java
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(Order order) {
orderDao.save(order);
// 即使这里抛出异常,paymentService也不会回滚
paymentService.processPayment(order);
}
}
@Service
public class PaymentService {
// NOT_SUPPORTED传播机制,暂停当前事务
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void processPayment(Order order) {
accountDao.deduct(order.getUserId(), order.getAmount());
// 这里抛出异常,不会影响createOrder的事务
if (order.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("金额超限");
}
}
}
原因 :某些传播机制会导致事务行为不符合预期。如NOT_SUPPORTED、NEVER、SUPPORTS(无事务时)等传播机制会导致方法在非事务环境下执行,事务自然失效。
解决方案:根据业务需求选择合适的事务传播机制。
java
// 使用REQUIRED传播机制,确保加入当前事务
@Transactional(propagation = Propagation.REQUIRED)
public void processPayment(Order order) {
accountDao.deduct(order.getUserId(), order.getAmount());
// 现在抛出异常会触发回滚
if (order.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("金额超限");
}
}
6.9 多线程调用
问题代码:
java
@Service
public class UserService {
@Transactional
public void batchSaveUsers(List<User> users) {
// 创建新线程
new Thread(() -> {
// 在新线程中执行,事务失效
for (User user : users) {
userDao.save(user);
}
}).start();
}
}
原因:Spring的事务是基于ThreadLocal实现的,事务上下文绑定在当前线程。当在新线程中执行数据库操作时,新线程无法获取到主线程的事务上下文,所以事务失效。
解决方案:避免在事务方法中创建新线程,如果必须使用多线程,需要在新线程中手动管理事务。
java
// 方案1:不使用多线程
@Transactional
public void batchSaveUsers(List<User> users) {
for (User user : users) {
userDao.save(user); // 在当前线程执行,事务生效
}
}
// 方案2:使用编程式事务在新线程中管理事务
public void batchSaveUsers(List<User> users) {
new Thread(() -> {
// 在新线程中手动开启事务
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
for (User user : users) {
userDao.save(user);
}
return null;
});
}).start();
}
6.10 事务管理器配置错误
问题代码:
java
@Configuration
public class TransactionConfig {
// 事务管理器配置错误
@Bean
public PlatformTransactionManager transactionManager() {
// 使用了错误的数据源
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(wrongDataSource); // 错误的数据源
return transactionManager;
}
}
@Service
public class UserService {
@Autowired
private UserDao userDao; // userDao使用的是正确的数据源
@Transactional
public void saveUser(User user) {
userDao.save(user); // 事务失效
}
}
原因:如果事务管理器配置的数据源与实际使用的Dao层数据源不一致,事务管理器无法正确管理事务。
解决方案:确保事务管理器使用正确的数据源。
java
@Configuration
public class TransactionConfig {
@Autowired
private DataSource dataSource; // 正确的数据源
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource); // 使用正确的数据源
return transactionManager;
}
}
6.11 类未被Spring管理
问题代码:
java
// 没有添加@Service注解
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void saveUser(User user) {
userDao.save(user); // 事务失效
}
}
// 或者手动创建对象
@Service
public class OrderController {
public void createOrder() {
// 手动创建对象,不是Spring管理的Bean
UserService userService = new UserService();
userService.saveUser(new User()); // 事务失效
}
}
原因:Spring的事务是通过AOP代理实现的,只有被Spring容器管理的Bean才会被代理。如果类没有被Spring管理(没有添加@Service等注解,或者手动创建对象),Spring无法为其创建代理,事务失效。
解决方案:确保类被Spring容器管理。
java
@Service // 添加@Service注解
public class UserService {
@Autowired
private UserDao userDao;
@Transactional
public void saveUser(User user) {
userDao.save(user); // 事务生效
}
}
// 或者通过@Autowired注入
@Service
public class OrderController {
@Autowired
private UserService userService; // 注入Spring管理的Bean
public void createOrder() {
userService.saveUser(new User()); // 事务生效
}
}
6.12 代理机制限制
问题代码:
java
// 使用JDK动态代理,但实现了接口
@Service
public class UserServiceImpl implements UserService {
@Transactional
@Override
public void saveUser(User user) {
userDao.save(user);
}
}
// 或者使用了错误的代理设置
@Configuration
@EnableTransactionManagement(proxyTargetClass = false) // 强制使用JDK动态代理
public class TransactionConfig {
// ...
}
// 类没有实现接口
@Service
public class UserService { // 没有实现接口
@Transactional
public void saveUser(User user) {
userDao.save(user); // 可能代理失败
}
}
原因:Spring默认使用JDK动态代理(需要实现接口)或CGLIB代理(不需要接口)。如果代理机制配置不当,可能导致代理创建失败,事务失效。
解决方案:
java
// 方案1:使用CGLIB代理
@Configuration
@EnableTransactionManagement(proxyTargetClass = true) // 强制使用CGLIB
public class TransactionConfig {
// ...
}
// 方案2:确保接口实现正确
public interface UserService {
void saveUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Transactional
@Override
public void saveUser(User user) {
userDao.save(user);
}
}
七、事务失效问题排查清单
当遇到事务失效问题时,可以按照以下清单逐一排查:
java
// 事务失效排查清单
1. 检查方法是否为public
2. 检查方法是否被final修饰
3. 检查方法是否被static修饰
4. 检查是否存在同类自调用
5. 检查异常是否被正确抛出
6. 检查异常类型是否匹配
7. 检查数据库引擎是否支持事务
8. 检查事务传播机制设置
9. 检查是否使用了多线程
10. 检查事务管理器配置是否正确
11. 检查类是否被Spring管理
12. 检查代理机制配置是否正确
八、面试常考:核心要点总结
8.1 Spring事务核心问题
- Spring事务的本质:基于AOP和数据库事务的封装管理
- @Transactional工作原理:通过AOP代理,在方法执行前后开启/提交/回滚事务
- 事务失效的12种原因 :
- 方法访问权限非public
- 方法被final修饰
- 方法被static修饰
- 同一个类中的自调用
- 异常被捕获未重新抛出
- 异常类型不匹配
- 数据库引擎不支持事务
- 事务传播机制设置不当
- 多线程调用
- 事务管理器配置错误
- 类未被Spring管理
- 代理机制限制
8.2 事务传播行为记忆口诀
REQUIRED: 有就加入,没有就创建
REQUIRES_NEW: 总是创建新的
SUPPORTS: 有就加入,没有就不需要
NOT_SUPPORTED: 总是非事务执行
MANDATORY: 必须有事务,否则报错
NEVER: 绝对不能有事务,否则报错
NESTED: 有就嵌套,没有就创建
8.3 最佳实践建议
- 类级别配置 :在Service类上添加
@Transactional,方法级别根据需要覆盖 - 明确异常 :使用
rollbackFor = Exception.class明确指定回滚异常类型 - 合理隔离:根据业务需求选择合适的隔离级别
- 避免长事务:长事务会占用数据库资源,影响系统性能
- 测试验证:重要业务逻辑需要测试事务的回滚和提交
- 方法设计:确保事务方法为public、非final、非static
- 避免自调用:同类方法调用需要通过代理对象进行
- 异常处理:不要在事务方法中吞掉异常,要么重新抛出,要么指定rollbackFor
8.4 高级面试题
Q1:Spring事务和编程式事务有什么区别?
A:声明式事务通过AOP实现,侵入性小,适合大多数场景;编程式事务需要手动管理事务,灵活性高但代码复杂,适用于需要细粒度控制的场景。
Q2:嵌套事务和加入现有事务有什么区别?
A:嵌套事务(NESTED)可以独立回滚,不影响外部事务;加入现有事务(REQUIRED)与外部事务是一个整体,回滚时一起回滚。
Q3:如何监控Spring事务的执行情况?
A:可以通过Spring的事务事件机制、日志配置、或者AOP切面来监控事务的开始、提交、回滚等事件。
Q4:为什么private方法不能使用@Transactional?
A:因为Spring的事务是基于AOP代理实现的,代理类需要重写目标方法。private方法无法被外部类访问,也就无法被重写,所以代理无法生成,事务自然失效。
Q5:在同一个Service中,方法A调用方法B,方法B有@Transactional注解,为什么事务会失效?
A :因为方法A调用方法B时,使用的是this引用,调用的是原始对象的方法,而不是经过代理增强的方法。Spring的AOP代理是通过代理对象来拦截方法调用的,内部调用绕过了代理,所以事务失效。
Q6:如何解决同一个Service中方法调用的事务失效问题?
A :可以通过注入自身(@Autowired private SelfService self;)然后通过self.methodB()调用,或者将方法B提取到另一个Service中,通过调用不同Service的方法来实现事务传播。
Q7:Spring事务的隔离级别有哪些?
A:
- DEFAULT:使用数据库默认隔离级别
- READ_UNCOMMITTED:读未提交(可能脏读)
- READ_COMMITTED:读已提交(避免脏读,可能不可重复读)
- REPEATABLE_READ:可重复读(避免脏读、不可重复读,可能幻读)
- SERIALIZABLE:串行化(最高隔离级别,避免所有并发问题)
Q8:什么是脏读、不可重复读、幻读?
A:
- 脏读:一个事务读取了另一个未提交事务的数据
- 不可重复读:在同一个事务中,多次读取同一数据得到不同结果
- 幻读:在同一个事务中,多次查询返回的结果集不同
总结:Spring事务管理是Java开发中的必备技能,理解其工作原理和失效场景对于构建可靠的企业级应用至关重要。掌握本文的12种事务失效场景,你就能在面试和实际开发中快速定位和解决事务问题!
最后提示:纸上得来终觉浅,绝知此事要躬行。建议读者在实际项目中多实践、多踩坑、多总结,才能真正掌握Spring事务的精髓!