手把手教你解决Spring中的事务失效问题,高评分秘籍get!

原创 爱他就关注他---> Java分享客栈 2024年08月15日 11:20 湖北

点击关注公众号,Java干货及时送达👇

Java分享客栈

分享IT互联网主流技术,包括Java、SpringBoot、SpringCloud-alibaba、Redis缓存、MQ队列、网络编程、websocket通信、netty、docker、k8s等技术及多年工作经验分享和感悟。

164篇原创内容

公众号

图片

引言

Spring让我们能够更轻松地管理代码中的复杂性,其中一个核心功能就是声明式事务管理。

什么是声明式事务管理?

简单来说,它允许我们通过注解来控制事务的开启、提交和回滚,而不需要写很多繁琐的代码。

但有时,即使你使用了@Transactional注解,事务却并没有按预期生效。

今天咱们就来聊聊在Spring事务管理中你可能踩过的坑,并教你如何规避这些陷阱。

一、事务不生效的困惑

在一个典型的Spring服务层中,我们可能会在方法上使用@Transactional注解来管理事务。

然而,有时你会发现事务没有按预期生效。

这种情况经常发生在同一个类内部直接调用方法的时候。

错误示例
 

java

@Service
public class UserService {

// 通过 @Transactional 注解标记的方法应该在事务中运行
@Transactional
public void createUser(User user) {
// 保存用户信息到数据库
userRepository.save(user);

// 更新用户信息,但由于直接调用 this.updateUser(),事务可能不会生效
this.updateUser(user);
}

public void updateUser(User user) {
// 模拟更新用户信息,这里实际的操作可能会失败
user.setName("Updated Name");
userRepository.update(user);
}
}

问题分析

  • this.updateUser(user); 直接调用了同一个类中的方法 updateUser,而没有通过Spring的代理对象调用,导致 updateUser 方法没有在事务中运行。

  • 如果 updateUser 方法中的操作失败或抛出异常,事务将不会正确回滚。

修正后的代码
 

java

@Service
public class UserService {

@Autowired
private UserService userService; // 注入同类的代理对象

@Transactional
public void createUser(User user) {
// 保存用户信息到数据库
userRepository.save(user);

// 通过代理对象调用 updateUser 方法,确保事务生效
userService.updateUser(user);
}

public void updateUser(User user) {
// 更新用户信息
user.setName("Updated Name");
userRepository.update(user);
}
}

代码说明

  • @Autowired private UserService userService; 通过Spring容器注入代理对象,而不是直接使用 this 关键字调用方法。

  • userService.updateUser(user); 这行代码通过Spring的代理对象调用了 updateUser 方法,确保其在事务中运行。如果此方法抛出异常,整个事务将会回滚,保证数据库的一致性。

二、事务提交的陷阱

有时,事务方法在抛出异常后没有回滚,而是被提交了。

这通常是由于异常被捕获但没有显式抛出,导致Spring误以为事务正常完成。

错误示例
 

java

@Transactional
public void processTransaction() {
try {
// 执行一些数据库操作
userRepository.save(new User("John"));

// 模拟异常
if (true) {
throw new RuntimeException("模拟异常");
}
} catch (Exception e) {
// 捕获异常,但没有显式抛出,事务可能不会回滚
System.out.println("处理异常:" + e.getMessage());
}
}

问题分析

  • 异常被捕获但没有显式抛出,Spring默认情况下只会对未捕获的 RuntimeException 进行回滚。

  • 这意味着 processTransaction 方法中的数据库操作会被提交,而不是回滚。

修正后的代码
 

java

@Transactional(rollbackFor = Exception.class) // 指定回滚的异常类型
public void processTransaction() throws Exception {
// 执行一些数据库操作
userRepository.save(new User("John"));

// 模拟异常
if (true) {
throw new Exception("强制回滚");
}
}

代码说明

  • @Transactional(rollbackFor = Exception.class) 强制Spring在遇到任何 Exception 时回滚事务,而不仅仅是 RuntimeException

  • throw new Exception("强制回滚"); 显式抛出异常,确保事务回滚,避免数据不一致。

三、事务不回滚

在事务方法中处理业务逻辑时,异常可能会被捕获和处理,导致事务没有正确回滚。

错误示例
 

java

@Transactional
public void processData() {
try {
// 进行数据库操作
userRepository.updateUser(new User("Jane"));

// 模拟一个业务异常
throw new CustomBusinessException("业务错误");
} catch (CustomBusinessException e) {
// 捕获异常,事务未回滚
System.out.println("捕获业务异常:" + e.getMessage());
}
}

问题分析

  • 在捕获异常后,事务没有被标记为回滚,因此 processData 方法中的数据库操作仍然会提交,导致数据状态不一致。

修正后的代码
 

java

@Transactional(rollbackFor = CustomBusinessException.class) // 指定业务异常时回滚
public void processData() throws CustomBusinessException {
// 进行数据库操作
userRepository.updateUser(new User("Jane"));

// 模拟一个业务异常
throw new CustomBusinessException("业务错误");
}

代码说明

  • @Transactional(rollbackFor = CustomBusinessException.class) 明确指定当 CustomBusinessException 被抛出时回滚事务,即使该异常被捕获。

  • throw new CustomBusinessException("业务错误"); 直接抛出业务异常,确保事务回滚,维护数据的一致性。

四、死锁的困境与破解

死锁现象

死锁是数据库操作中常见的问题,尤其是在多个事务相互依赖资源时。

通常表现在两个或多个事务相互等待对方释放锁,结果大家都无法完成。

示例代码

以下是一个可能发生死锁的简单示例:

 

java

@Transactional
public void updateUserBalances(Long user1Id, Long user2Id) {
User user1 = userRepository.findById(user1Id);
User user2 = userRepository.findById(user2Id);

user1.setBalance(user1.getBalance() - 100);
user2.setBalance(user2.getBalance() + 100);

userRepository.save(user1);
userRepository.save(user2);
}

问题分析

  • 如果两个线程同时执行此方法,并且顺序不同,例如第一个线程先锁定user1,而第二个线程先锁定user2,那么它们将陷入死锁,导致事务无法完成。

解决策略

为了避免死锁,我们需要确保事务方法锁定资源的顺序一致,或者通过短小的事务方法减少锁持有时间。

此外,合理设计事务逻辑,避免循环依赖也很重要。

 

java

@Transactional
public void updateUserBalancesConsistently(Long user1Id, Long user2Id) {
User user1 = userRepository.findById(user1Id);
User user2 = userRepository.findById(user2Id);

// 按照固定的顺序处理用户更新,避免死锁
if (user1Id < user2Id) {
updateBalances(user1, user2);
} else {
updateBalances(user2, user1);
}
}

private void updateBalances(User user1, User user2) {
user1.setBalance(user1.getBalance() - 100);
user2.setBalance(user2.getBalance() + 100);

userRepository.save(user1);
userRepository.save(user2);
}

代码说明

  • if (user1Id < user2Id):通过确保固定顺序锁定资源,避免死锁的发生。

  • updateBalances(User user1, User user2):将更新逻辑提取到一个单独的方法中,简化事务的复杂性,减少锁持有时间。

五、事务提交后的回调陷阱

事务提交后的问题

有时候,我们会在事务提交后触发一些回调事件,比如发送消息或者更新缓存。

但如果在这些回调中重新开启事务,可能会导致Spring的事务状态和数据库的事务状态不一致,进而引发意想不到的错误。

示例代码
 

java

@Transactional
public void processOrder() {
// 一些订单处理逻辑
transactionSynchronizationRegistry.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 回调事件中开启新事务
processNotification();
}
});
}

public void processNotification() {
// 这里我们可能再次使用事务来处理通知逻辑
// 但如果事务状态管理不当,可能导致问题
notificationService.sendNotification();
}

问题分析

  • 在事务提交后的回调中重新开启事务,可能会导致Spring管理的事务状态和数据库的实际事务状态不一致。

  • 这会引发诸如数据未及时刷新、缓存不一致等问题,甚至可能导致业务逻辑的错误执行。

修正后的代码

为了避免这种问题,可以使用 Propagation.REQUIRES_NEW 来确保在回调中开启的新事务与原事务分离,或在回调前清除事务状态。

 

java

@Transactional
public void processOrder() {
// 订单处理逻辑
transactionSynchronizationRegistry.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 使用新的事务传播行为
notificationService.sendNotificationInNewTransaction();
}
});
}

@Service
public class NotificationService {

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotificationInNewTransaction() {
// 在新的事务中处理通知发送逻辑
sendNotification();
}

public void sendNotification() {
// 实际的通知发送逻辑
System.out.println("Notification sent.");
}
}

代码说明

  • @Transactional(propagation = Propagation.REQUIRES_NEW):确保在回调事件中开启一个新的事务,独立于原有事务,这样可以避免Spring的事务管理状态和数据库事务状态不一致的问题。

  • sendNotificationInNewTransaction():使用新的事务传播行为确保通知逻辑在一个独立的事务中执行,减少潜在的错误风险。

六、事务状态管理工具类

在某些复杂的业务场景中,我们可能需要手动管理事务的状态。

此时,创建一个事务管理工具类可以帮助我们更加灵活地控制事务的开启、提交和回滚。

工具类实现

以下是一个简单的事务状态管理工具类的实现示例,在需要时手动管理事务的生命周期。

 

java

@Component
public class TransactionManager {

@Autowired
private PlatformTransactionManager transactionManager;

/**
* 在新事务中执行任务
* @param task 要执行的任务
*/
public void executeInNewTransaction(Runnable task) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
// 设置事务传播行为为 PROPAGATION_REQUIRES_NEW
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// 执行任务并在新事务中提交
transactionTemplate.execute(status -> {
task.run();
return null;
});
}
}

使用示例
 

java

@Service
public class OrderService {

@Autowired
private TransactionManager transactionManager;

public void placeOrder(Order order) {
// 在主事务中处理订单保存
saveOrder(order);

// 使用事务管理工具类在新事务中发送通知
transactionManager.executeInNewTransaction(() -> {
notificationService.sendOrderConfirmation(order);
});
}

@Transactional
public void saveOrder(Order order) {
// 保存订单逻辑
orderRepository.save(order);
}
}

代码说明

  • executeInNewTransaction(Runnable task):通过 TransactionTemplate 手动管理事务,确保任务在独立的事务中执行。

  • setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW):设置事务传播行为,确保新的事务与当前事务相互独立。

  • transactionManager.executeInNewTransaction():在新的事务中执行任务,避免事务状态混乱。

总结

怎么样,是不是细节还挺多的,平时没怎么注意过吧。

Spring事务管理是我们在开发过程中必不可少的一部分,事务不生效、提交陷阱、事务回滚,死锁、回调陷阱等等,都存在着你可能会遇到的坑点。

希望通过这些分享,大家能在以后的开发中更好地利用Spring的事务管理功能,避免踩坑。


好了,今天的知识你学会了吗?

加油!加油!加油!

图片

图片

关注公众号,回复关键词【面试】,或选择 菜单 - 学习资源 - 面试题,都可获取精心收集的Java面试题。

包含:【Java面试八股文10万字总结】【Java进阶架构核心手册】

后续会不断收集更多资源及干货放在里面

图片

你的点赞+在看,是我持续分享干货的动力哦!

END

图片

分享

图片

收藏

图片

点赞

图片

在看

往期推荐

未来迈向Java高级工程师绕不过的技能:JMeter压测

SpringBoot线程池参数搜一堆资料还是不会配,我花一天测试换你此生明白。

一文搞定CompletableFuture并行处理,成倍缩短查询时间。

我曾经的两个Java老师一个找不到工作了一个被迫转行了

从线上环境摘取了四个代码优化记录分享给大家

SpringBoot整合WebSocket+Stomp搭建群聊项目

一个包装过简历的新同事写完微信支付引起事故后果断离职了

我为什么极力推荐XXL-JOB作为中小厂的分布式任务调度平台

Springboot+Redisson自定义注解一次解决重复提交问题(含源码)

记一次最近生产环境项目中发生的两个事故及处理方法

SpringBoot+Minio搭建不再爆肝秃头的分布式文件服务器

 

图片

最近整理了一批这些年来学习和工作手记的PDF文档目录,覆盖了Java核心技术、SpringBoot、SpringCloud微服务、数据库、ElasticSearch、MQ消息队列等主流技术,相信对渴望成长的人会有所帮助。

获取方式:点“在看”,关注公众号并回复“资源” 领取,更多内容陆续奉上。

图片

点个在看,证明你还爱我

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值