解决@Transactional遇上@synchronized的生产问题

 更新时间:2025年06月21日 16:07:08   作者:echola_mendes  
这篇文章主要介绍了解决@Transactional遇上@synchronized的生产问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

@Transactional遇上@synchronized的生产问题

近日遇到一个问题,就是一个订单被两个用户抢了问题,排查后发现是由于 @Transactional和@synchronized注解的使用问题

一、问题点:数据重复读

@Transactional注解用于开启事务,当在高并发情况下我们可能为了保证数据的安全使用悲观锁,可以在方法上使用@synchronized使用悲观锁。一个线程执行完方法并释放锁后,事务并未提交,第二个线程又获得了该锁,导致数据出问题

@Transactional注解通过AOP实现事务管理,当标注该注解的方法执行完成后才提交事务,而synchronized代码块又是在一个事务内,就会出现第一个线程释放锁后但是事务还没提交,第二个线程就进入同步代码块获取到未提交的数据库数

@Transactional控制事务的范围比sychronized 大,如图:

思路:既然事务下不能使用锁,那我们把锁和事务进行分开。使得在锁环境下包含事务,最终依然是线程安全的

  • 方法一:将锁替换成数据库的锁比如select for update或者版本号version
  • 方法二:在service下将事务代码的抽取单独使用,无事务方法调用有事务的方法

既然问题出在事务未提交,那么只要把对应事务操作的代码单独抽取出来,封装成一个单独的方法,在synchronized中调用该方法即可

@Service
public class OrderServiceImpl implements OrderServiceI {
	@Autowired
	private OrderDao orderDao;

	public sychronized Order updateOrder(int id) {  //加锁
		return updateOrderSafely(id); //调用数据库
	}

	@Transactional
	public Order updateOrderSafely(int id) {
		return orderDao.updateOrder(id);
	}
}

看起来好像是解决了事务未提交的问题,但会存在新的问题,可能会出现@Transactional事务不生效的情况

二、问题点 >》方法二引申:@Transactional事务不生效

在同一个类内部调用@Transactional标注的方法事务也不会开启,原因是:

@Transactional事务管理是基于动态代理对象的代理逻辑实现的,那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了

方法:将锁提取到controller层,不包含任何事务

  • Controller类
 @RestController
 public class TestController {
     @Resource 
     private TestService testService;
     @PostMapping("/test")
     public synchronized void testInterface() {
          testService.functionA(); // 调用数据库操作方法
     }
 }
  • Service类
 @Service("testService")
 public class TestServiceImpl implements TestService {
    @Transactional(rollbackFor = Exception.class, 
                  propagation = Propagation.REQUIRES_NEW)
     public void functionA() {
         //数据库读写操作
     }
 }

注意点:

1、调用本类加@Transactional的方法时,要使用一个AOP对象来进行代理,获取到代理对象之后@Trasactional才会正常生效,否则是不生效的。

2、synchronized要比事务的粒度要大,否则还是会出现重复读的现象。

3、被调用的functionA为什么要使用 REQUIRES_NEW的隔离级别呢? 因为这样就可以实现锁的粒度大于事务的粒度啦。无论如何,都创建新的事务,外层事务不受内层事务影响。但是有个问题,外层事务失败了,内层事务还是把记录入库了,有可能产生脏数据;

下面这段代码有什么问题呢?

Lock lock = new ReentrantLock();

@Transactional
public void save(){
    try{
        lock.lock();
        //业务代码……
    }finally{
        lock.unlock();
    }
}

1、显而易见就可以看出,Spring 会在方法开始时开启一个事务,并在方法结束时提交或回滚该事务。如果在方法内部手动加锁,可能会导致锁的持有时间超过事务的生命周期,这样可能会引发死锁或其他并发问题。

2、lock.lock()会阻塞线程,直至获取到锁为止,具体来说,如果锁已经被其他线程持有,调用 lock.lock() 的线程会被阻塞,直到持有锁的线程释放锁

还有值得注意的一点,很多人看到@Transactional和try……在一起使用,就以为@Transactional会失效,其实并不是

①try-catch结构:

  • 如果在try-catch块中捕获异常而不重新抛出,可能导致事务不按预期回滚。
  • 解决方法:在@Transactional注解中指定rollbackFor属性,或者在catch块中重新抛出异常

②try-finally结构:

  • 使用try-finally结构来确保资源释放(如锁的解锁)不会影响事务的正常行为,只要异常最终被抛出。
  • 而上述代码用的是try-finally结构来确保锁的释放,这通常不会影响事务的正常行为。只要在业务代码中抛出的异常没有被捕获,事务就会按照预期回滚

因此应该修改成:

private final ReentrantLock lock = new ReentrantLock();

@Transactional(rollbackFor = Exception.class)
public void save() {
     if (!lock.tryLock()) {
          // 如果未能获取锁,可以记录日志或采取其他措施
          System.out.println("Failed to acquire the lock.");
          return; // 或者抛出异常
      }

      try {
        // 业务代码……
      } catch (Exception e) {
        // 处理异常
        throw e;
      } finally {
          lock.unlock();
      }
}
     

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 都9102年了,你还用for循环操作集合吗

    都9102年了,你还用for循环操作集合吗

    这篇文章主要给大家介绍了关于java中for循环操作集合使用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者使用java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-03-03
  • Spring学习笔记1之IOC详解尽量使用注解以及java代码

    Spring学习笔记1之IOC详解尽量使用注解以及java代码

    这篇文章主要介绍了Spring学习笔记1之IOC详解尽量使用注解以及java代码 的相关资料,需要的朋友可以参考下
    2016-07-07
  • java后端http接口流式输出到前端的方法示例

    java后端http接口流式输出到前端的方法示例

    这篇文章主要介绍了java后端http接口流式输出到前端的相关资料,对比SseEmitter与WebFlux两种实现方式,通过实例代码详细分析了其适用场景及优劣,需要的朋友可以参考下
    2025-09-09
  • java中匿名内部类详解

    java中匿名内部类详解

    这篇文章主要对java中的匿名内部类的详细总结,需要的朋友可以参考下
    2017-04-04
  • 浅谈Java之Map 按值排序 (Map sort by value)

    浅谈Java之Map 按值排序 (Map sort by value)

    下面小编就为大家带来一篇浅谈Java之Map 按值排序 (Map sort by value)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-08-08
  • java实现变更文件查询的方法

    java实现变更文件查询的方法

    这篇文章主要介绍了java实现变更文件查询的方法,可通过java查询文件最后修改时间的方法实现查找变更文件的功能,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • Java面试题冲刺第十四天--PRC框架

    Java面试题冲刺第十四天--PRC框架

    这篇文章主要为大家分享了最有价值的三道关于PRC框架的面试题,涵盖内容全面,包括数据结构和算法相关的题目、经典面试编程题等,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • java基于servlet实现文件上传功能解析

    java基于servlet实现文件上传功能解析

    这篇文章主要为大家详细介绍了java基于servlet实现上传功能,后台使用java实现,前端主要是js的ajax实现,感兴趣的小伙伴们可以参考一下
    2016-05-05
  • 使用SpringCache操作Redis缓存数据的示例代码

    使用SpringCache操作Redis缓存数据的示例代码

    SpringCache是一个框架,实现了基于注解的缓存功能,只需要简单的加一个注解,就能实现缓存功能,本文给大家介绍了如何使用SpringCache操作Redis缓存数据,文中有相关的代码示例供大家参考,需要的朋友可以参考下
    2024-01-01
  • mybatisplus根据条件只更新一个字段的实现

    mybatisplus根据条件只更新一个字段的实现

    MyBatis-Plus提供使用update方法结合Wrapper来指定更新条件和要更新的字段,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-12-12

最新评论