大家好,我是大华。
相信很多朋友在使用@Transactional事务注解时都踩过坑,有时候代码看起来没问题,但事务就是不生效,或者出现了莫名其妙的问题。
什么是事务?
在深入代码之前,我们先理解事务的ACID特性:
1.原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个环节
2.一致性(Consistency):事务执行前后,数据库的完整性约束不被破坏
3.隔离性(Isolation):并发事务之间互不干扰,每个事务都感觉不到其他事务在并发执行
4.持久性(Durability):事务完成后,对数据的修改是永久的,即使系统故障也不会丢失
举个生活中的例子:银行转账就是典型的事务场景 – A向B转账100元需要两步:
- A账户减100元
- B账户加100元
这两步必须同时成功或同时失败,绝对不能出现A的钱扣了但B没收到的情况。这就是事务要解决的核心问题!
@Transactional 基础用法
先来看一个简单的事务使用示例:
@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注解的方法时,调用的是原始对象的方法,而不是代理对象的方法,因此事务拦截器不会生效。
问题代码示例
@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获取代理对象
@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:
/**
* 专门处理事务的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中注入并使用:
@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)不回滚。
这是因为受检异常通常表示可恢复的业务异常,而运行时异常表示不可恢复的系统异常。
问题代码示例
@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:明确指定回滚异常类型
@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(推荐)
@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:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行
问题代码示例
@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(\"订单保存失败,整个事务回滚\");
}
}
正确使用传播机制的示例
@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(\"订单保存失败,只回滚内层事务\");
}
}
根据业务需求选择合适的传播机制,这里希望内层事务不影响外层事务。
完整的最佳实践示例
下面是一个综合了所有最佳实践的完整示例:
@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;
}
事务配置建议
@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、做好参数校验和异常处理,保证代码健壮性
事务不是越多越好,也不是越大越好,合理使用才是关键!
往期精彩
《这20条SQL优化方案,让你的数据库查询速度提升10倍》
《MySQL 为什么不推荐用雪花ID 和 UUID 做主键?》
《无需UI库!50行CSS打造丝滑弹性动效导航栏,拿来即用》
《SpringBoot3+Vue3实现的数据库文档工具,自动生成Markdown/HTML》



还没有评论呢,快来抢沙发~