大家好,我是大华。 相信很多朋友在使用@Transactional事务注解时都踩过坑,有时候代码看起来没问题,但事务就是不生效,或者出现了莫名其妙的问题。
什么是事务?
在深入代码之前,我们先理解事务的ACID特性:
1.原子性(Atomicity) :事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个环节 2.一致性(Consistency) :事务执行前后,数据库的完整性约束不被破坏 3.隔离性(Isolation) :并发事务之间互不干扰,每个事务都感觉不到其他事务在并发执行 4.持久性(Durability):事务完成后,对数据的修改是永久的,即使系统故障也不会丢失
举个生活中的例子:银行转账就是典型的事务场景 - A向B转账100元需要两步:
- A账户减100元
- B账户加100元
这两步必须同时成功 或同时失败,绝对不能出现A的钱扣了但B没收到的情况。这就是事务要解决的核心问题!
@Transactional 基础用法
先来看一个简单的事务使用示例:
java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 最简单的事务用法
* 方法内所有数据库操作要么全部成功,要么全部回滚
*/
@Transactional
public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) {
// 第一步:从转出方扣款
User fromUser = userRepository.findById(fromUserId)
.orElseThrow(() -> new RuntimeException("转出用户不存在"));
if (fromUser.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
fromUser.setBalance(fromUser.getBalance().subtract(amount));
userRepository.save(fromUser);
// 第二步:向接收方转账
User toUser = userRepository.findById(toUserId)
.orElseThrow(() -> new RuntimeException("接收用户不存在"));
toUser.setBalance(toUser.getBalance().add(amount));
userRepository.save(toUser);
// 如果任何一步出现异常,整个操作都会回滚
log.info("转账成功:{} 向 {} 转账 {}", fromUserId, toUserId, amount);
}
}
这个例子中,如果扣款成功但转账失败,Spring会自动回滚整个操作,确保数据一致性。
坑1:事务不生效的经典场景
Spring的事务管理是通过AOP代理实现的。当在同一个类中调用被@Transactional注解的方法时,调用的是原始对象的方法,而不是代理对象的方法,因此事务拦截器不会生效。
问题代码示例
java
@Service
public class ProblematicUserService {
@Autowired
private UserRepository userRepository;
public void updateUserWithProblem(User user) {
// 这里直接调用同类方法,事务注解不会生效
updateUser(user); // 事务失效!
}
@Transactional
public void updateUser(User user) {
userRepository.save(user);
// 即使这里抛出异常,数据也不会回滚
if (user.getAge() < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
}
在同一个类内部调用,事务不生效!这是因为Spring AOP代理的机制导致的。
解决方案1:通过ApplicationContext获取代理对象
java
@Service
public class Solution1UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private ApplicationContext applicationContext;
public void updateUserCorrectly(User user) {
// 关键:从Spring容器中获取代理对象
Solution1UserService proxy = applicationContext.getBean(Solution1UserService.class);
proxy.updateUser(user); // 现在事务生效了
}
@Transactional
public void updateUser(User user) {
userRepository.save(user);
if (user.getAge() < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
}
通过ApplicationContext获取代理对象,从Spring容器中获取代理对象,确保事务生效。
解决方案2:将事务方法拆分到不同Service中(推荐)
首先创建专门处理事务的Service:
java
/**
* 专门处理事务的Service
* 推荐使用这种方式,代码结构更清晰
*/
@Service
public class TransactionalService {
@Autowired
private UserRepository userRepository;
/**
* 事务方法放在独立的Service中
*/
@Transactional
public void updateUserInTransaction(User user) {
userRepository.save(user);
if (user.getAge() < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
}
然后在原Service中注入并使用:
java
@Service
public class Solution2UserService {
@Autowired
private TransactionalService transactionalService;
public void updateUserByOtherService(User user) {
// 调用专门的事务Service,确保事务生效
transactionalService.updateUserInTransaction(user);
}
}
调用其他Service的事务方法,这种方式代码更清晰,也符合单一职责原则。
为什么解决方案2更推荐?
1. 代码清晰 :事务方法集中在专门的Service中,职责明确 2. 易于维护 :事务相关的修改只影响一个Service 3. 避免循环依赖 :拆分Service可以减少复杂的依赖关系 4. 符合设计原则:单一职责原则,每个类都有明确的职责
坑2:异常类型不对导致不回滚
Spring默认只对RuntimeException及其子类进行回滚,对受检异常(Exception)不回滚。 这是因为受检异常通常表示可恢复的业务异常,而运行时异常表示不可恢复的系统异常。
问题代码示例
java
@Service
public class ExceptionProblemService {
@Autowired
private UserRepository userRepository;
/**
* 问题:默认只对RuntimeException回滚,Exception不会回滚
*/
@Transactional
public void updateUserWithExceptionProblem(User user) throws Exception {
userRepository.save(user);
if (user.getName() == null) {
// 这里抛出的是Exception,不是RuntimeException,默认不会回滚!
throw new Exception("用户名不能为空"); // 不会触发回滚!
}
}
}
解决方案1:明确指定回滚异常类型
java
@Service
public class ExceptionSolution1Service {
@Autowired
private UserRepository userRepository;
@Transactional(rollbackFor = Exception.class)
public void updateUserWithCorrectExceptionHandling(User user) throws Exception {
userRepository.save(user);
if (user.getName() == null) {
// 现在这个异常也会触发回滚了
throw new Exception("用户名不能为空");
}
}
}
明确指定回滚的异常类型,使用rollbackFor指定所有Exception都回滚
解决方案2:使用RuntimeException(推荐)
java
@Service
public class ExceptionSolution2Service {
@Autowired
private UserRepository userRepository;
/**
* 方案2:使用RuntimeException(推荐)
* 业务异常可以继承RuntimeException
*/
@Transactional
public void updateUserWithRuntimeException(User user) {
userRepository.save(user);
if (user.getName() == null) {
// RuntimeException默认会回滚
throw new BusinessException("用户名不能为空");
}
}
}
/**
* 自定义业务异常基类,继承RuntimeException
*/
class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
业务异常可以继承RuntimeException实现。
坑3:事务传播机制理解不清
Spring定义了7种事务传播行为,最常用的是: 1. REQUIRED (默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 2. REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起 3. NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行
问题代码示例
java
@Service
public class PropagationProblemService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
/**
* 外层方法:开启一个事务
*/
@Transactional
public void outerMethod(User user, Order order) {
// 这个保存操作在外层事务中
userRepository.save(user);
try {
// 调用内层方法
innerMethod(order);
} catch (Exception e) {
// 由于内层方法使用默认的REQUIRED传播机制,
// 它加入了外层事务,所以内层异常会导致外层事务也回滚
// user的保存会被回滚!
log.error("内层方法异常,外层事务也会回滚", e);
}
}
/**
* 内层方法:默认使用REQUIRED传播机制
* 加入外层事务,同一个事务
*/
@Transactional(propagation = Propagation.REQUIRED)
public void innerMethod(Order order) {
orderRepository.save(order);
throw new RuntimeException("订单保存失败,整个事务回滚");
}
}
正确使用传播机制的示例
java
@Service
public class PropagationSolutionService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void correctOuterMethod(User user, Order order) {
// 用户保存操作 - 这个在外层事务中
userRepository.save(user);
try {
// 使用REQUIRES_NEW:新建独立事务,不影响外层事务
correctInnerMethod(order);
} catch (Exception e) {
// 内层事务回滚,但外层事务继续执行
log.error("内层事务回滚,但外层事务不受影响", e);
}
// 这里可以继续其他操作,不受内层事务失败影响
log.info("用户保存成功,继续执行其他业务逻辑");
}
/**
* 内层方法:使用REQUIRES_NEW传播机制
* 创建新事务,独立于外层事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void correctInnerMethod(Order order) {
orderRepository.save(order);
// 这个异常只会回滚内层事务
throw new RuntimeException("订单保存失败,只回滚内层事务");
}
}
根据业务需求选择合适的传播机制,这里希望内层事务不影响外层事务。
完整的最佳实践示例
下面是一个综合了所有最佳实践的完整示例:
java
@Service
@Slf4j
public class BestPracticeService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
@Autowired
private OperationLogRepository operationLogRepository;
/**
* 完整的事务最佳实践示例
* @param request 订单请求
* @return 处理结果
*/
@Transactional(
rollbackFor = Exception.class, // 所有异常都回滚
timeout = 30, // 设置30秒超时,防止长时间占用连接
isolation = Isolation.DEFAULT, // 使用数据库默认隔离级别
propagation = Propagation.REQUIRED, // 默认传播机制
readOnly = false // 读写事务
)
public CompleteResult processUserOrder(OrderRequest request) {
log.info("开始处理用户订单事务");
// 1. 验证用户信息
User user = validateUser(request.getUserId());
// 2. 扣减账户余额
deductUserBalance(user, request.getAmount());
// 3. 创建订单记录
Order order = createOrder(user, request);
// 4. 记录操作日志(这个方法应该不在事务中)
logOperation(user, order);
log.info("用户订单处理完成");
return new CompleteResult(user, order);
}
/**
* 验证用户信息
*/
private User validateUser(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new BusinessException("用户不存在"));
}
/**
* 扣减用户余额
*/
private void deductUserBalance(User user, BigDecimal amount) {
if (user.getBalance().compareTo(amount) < 0) {
throw new BusinessException("用户余额不足");
}
user.setBalance(user.getBalance().subtract(amount));
userRepository.save(user);
log.info("用户 {} 余额扣减 {}", user.getId(), amount);
}
/**
* 创建订单
*/
private Order createOrder(User user, OrderRequest request) {
Order order = new Order();
order.setAmount(request.getAmount());
order.setStatus("COMPLETED");
Order savedOrder = orderRepository.save(order);
log.info("创建订单成功,订单ID: {}", savedOrder.getId());
return savedOrder;
}
/**
* 日志记录通常不需要事务,使用NOT_SUPPORTED传播机制
* 这样即使日志记录失败,也不会影响主业务流程
*/
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void logOperation(User user, Order order) {
try {
OperationLog log = new OperationLog();
log.setOperationType("CREATE_ORDER");
operationLogRepository.save(log);
log.info("操作日志记录成功");
} catch (Exception e) {
// 日志记录失败不应该影响主业务流程
log.error("记录操作日志失败,但不影响主流程", e);
}
}
}
/**
* 自定义业务异常
*/
class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
/**
* 请求DTO
*/
@Data
class OrderRequest {
private Long userId;
private BigDecimal amount;
private String productId;
}
/**
* 返回结果DTO
*/
@Data
@AllArgsConstructor
class CompleteResult {
private User user;
private Order order;
}
事务配置建议
java
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
/**
* 事务管理最佳实践配置
*/
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.setTimeout(30); // 设置合理的超时时间
template.setReadOnly(false);
template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return template;
}
}
总结
不适合使用事务的场景:
查询操作 :纯查询不需要事务,可以添加@Transactional(readOnly = true) 耗时较长的业务 :长时间占用数据库连接会影响性能 非数据库操作:如调用外部接口、文件操作等(这些操作无法被数据库事务管理)
事务选择建议:
简单查询 :不加事务或使用readOnly = true 单表修改 :可以使用事务,但要根据业务重要性决定 多表关联修改 :必须使用事务 重要业务操作:必须使用事务,并做好异常处理
关键要点:
1、方法必须是public的,事务才生效 2、避免同类方法调用,使用代理对象调用 3、异常要正确处理,不要随意捕获异常 4、了解传播机制,根据业务需求选择合适的传播行为 5、做好参数校验和异常处理,保证代码健壮性
事务不是越多越好,也不是越大越好,合理使用才是关键!
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《MySQL 为什么不推荐用雪花ID 和 UUID 做主键?》