synchronized 遇上 @Transactional

一、开篇

初始情况下,我们的表格中只有一条id2的数据,姓名为wangjie,年龄设置为0岁

事务篇(三):分享一个隐性事务失效场景

image.png

1 不添加synchronized关键字

首先,大家先来看看这个程序有没有什么问题

    @Transactional
    public  void transactionalMethod(){
        User user = userDao.findOne(2);
        user.setAge(user.getAge()+1);
        userDao.updateUser(user);

    }

说明

transactionalMethod()方法,首先通过 findOne() 方法获取到id为2的用户记录,然后重新为该条用户记录的age加1,再通过 updateUser() 方法,更新到t_user表中 即,每次方法调用之后,实现的效果就是为id为2的用户加一岁

这是一个更新操作,想想,这个方法存在什么问题呢?

对,因为没有加锁,所以在并发的情况下,很可能出现线程不安全的情况,导致执行结果与预期不一定一致的情况

我们来测试验证一下:

@GetMapping("/transactionalMethod")
    public void transactionalMethod()  {
        final CountDownLatch latch = new CountDownLatch(1000);
          try {
              for (int i = 0; i < latch.getCount() ; i++) {
                  new Thread(() -> {
                      userService.transactionalMethod();
                      latch.countDown();
                  }).start();

              }
          }catch (Exception e){
              System.out.println(e.getMessage());
          }finally {
              latch.countDown();
          }

    }
    

说明

controller 类中,创建 transactionalMethod 方法。方法中开启 1000 个线程,来模拟 1000 个并发请求

预期:年龄被更新为1000岁

启动之后,我们访问 /transactionalMethod 接口,结果如下:

结果1

事务篇(三):分享一个隐性事务失效场景

image.png

即,wangjie 用户的年龄被更新为了 93 岁,与预期时不一致的

再来看看执行日志

事务篇(三):分享一个隐性事务失效场景

image.png

发现出现多条一样的数据,验证了在执行过程中的确发生了并发问题,导致多个线程同时获取到相同的值,执行得到脏数据

那么,接下来,我们为方法 transactionalMethod 加锁,即加上 synchronized 关键字:

2 添加 synchronized 关键字

    @Transactional
    public  synchronized void transactionalWithSynchronized(){
        User user = userDao.findOne(2);
        user.setAge(user.getAge()+1);
        userDao.updateUser(user);
        

说明

在原方法的基础上添加 synchronized 关键字

上层调用中,改为调用 transactionalWithSynchronized 方法

@GetMapping("/transactionalMethod")
    public void transactionalMethod()  {
        final CountDownLatch latch = new CountDownLatch(1000);
          try {
              for (int i = 0; i < latch.getCount() ; i++) {
                  new Thread(() -> {
                      userService.transactionalWithSynchronized();
                      latch.countDown();
                  }).start();

              }
          }catch (Exception e){
              System.out.println(e.getMessage());
          }finally {
              latch.countDown();
          }

    }
    

依旧是 1000 个线程的并发执行,结果如下:

结果2

事务篇(三):分享一个隐性事务失效场景

image.png

咦?结果为 817,依旧不是 1000 ?这又是为何呢?

我们可以看到sql执行过程中,存在重复的 age,说明产生了并发现象

事务篇(三):分享一个隐性事务失效场景

这不科学呀,明明加了 synchronized 关键字,居然还出现了并发冲突?这岂不是属于一个很奇怪的 bug ? 当时一个同事说他起初都有点怀疑人生了

那怎么破?遇到了问题,就得想办法解决呀,不符合常理的 bug ,很多人往往会通过与其他正常使用场景进行对比来寻找答案,这种方法虽然没有太高的技术含量,但个人认为是效率比较高的一种方式。先找出不同的地方在哪里,再考虑问题原因,然后入手解决就行

于是我们也是先跟其他正常使用 synchronized 的方法进行对比,发现唯一不同的一点就是:transactionalWithSynchronized 方法上有 @Transactional 注解

那我们去掉 @Transactional 注解试试

3 添加 synchronized(去掉 @Transactional 注解)

    public synchronized void transactionalWithSynchronized(){
        User user = userDao.findOne(2);
        user.setAge(user.getAge()+1);
        userDao.updateUser(user);

    }

结果3

事务篇(三):分享一个隐性事务失效场景

image.png

这样,就正常了,就符合常理啦。对于当前这个场景的话呢,我们的问题就算是解决啦!

也就是说,问题现象是,同时加上 @Transactional 注解和 synchronized 关键字,并发问题依旧存在

但是,你肯定会想,这究竟是为什么呢?

二、原理解析

1 原因分析

Spring 声明式事务,其实是采用 Spring 的 AOP 思想,在目标方法执行之前开启事务,在目标方法执行之后提交或者回滚事务

由于 Spring 的 AOP 事务机制,添加了 @Transactional 注解的方法的事务是由 spring 生成的一个代理类来处理的,当一个线程执行完该方法并释放锁后,代理类还并没有提交事务。也就是说,线程在进入 synchronized 之前会开启事务,然后再使用 synchronized 为方法加锁

我们来分析一下,带有添加 @Transactional 注解 的 synchronized 方法的请求过程

事务篇(三):分享一个隐性事务失效场景

image.png

那么,对于图中的线程A来说,它执行完代码还未提交事务时,在并发请求的情况下,很容易出现线程 B 也过来请求。那么这个时候就会出现 线程 A 和线程 B 在同一个事务中的情况,也就发生了 mysql 重复读的问题

2 解决方案

问题的原因找到了,那么,我们应该如何解决这个问题呢?

上面说到,针对当前这个问题,我们只需要去掉我们可以在 方法上添加的 @Transactional 注解即可,因为对于当前这个方法来说,它去掉事务之后,效果也是一样的

但是,当然不能取巧,我们需要考虑一种通用的解决方案,来解决一定需要添加事务,并且需要控制并发的场景

问题的原因在于,线程在提交事务之前,便释放了锁,其它线程进来后 还是读到之前的数据(可重复读隔离级别)

那么,我们只需要在 transactionalWithSynchronized() 方法之前,即调用该方法的方法上加上 synchronized 关键字。也就是说,在还没有开事务之间就加锁,那么就可以保证线程同步

比如,在controller 类中的 testTransactionalWithSynchronized()方法上添加 synchronized 关键字

 private synchronized  void testTransactionalWithSynchronized() {
        userService.transactionalWithSynchronized();
    }

    @GetMapping("/testTransactionalWithSynchronized")
    public  void invokeMethod()  {
        final CountDownLatch latch = new CountDownLatch(1000);
        try {
            for (int i = 0; i < latch.getCount() ; i++) {
                new Thread(() -> {
                    testTransactionalWithSynchronized();
                    latch.countDown();
                }).start();

            }
        }catch (Exception e){
            System.out.println(e.getMessage());
        }finally {
            latch.countDown();
        }
    }

然后在 invokeMethod()中并发调用 testTransactionalWithSynchronized()方法,观察结果:

结果4

事务篇(三):分享一个隐性事务失效场景

image.png

正如预期的那样,年龄变成了 1000 ,并且日志中也并未出现 各个线程获取到的 age 的值相同的情况

三、总结

今天,为大家演示了 方法中同时加上 @Transactional 注解和 synchronized 关键字,并发问题依旧存在的问题。我们发现原因在于:线程在提交事务之前,便释放了锁,导致其他线程与自己处在同一事务的情况

可以通过在调用这个方法的方法 (可以理解为父级方法)上添加 synchronized 关键字,使得线程在释放锁之前,就提交事务来解决即可

之前的文章中其实也有提到过,Spring 中使用 @Transactional 注解的原理

若大家真正了解了这一点的话,这个问题应该是很快就会发现的,要是相反,那类似这样的问题肯定就会存在并且变为难题咯

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
@Transactional注解是Spring框架中用于管理事务的注解,它可以将一个方法标记为事务性方法,该方法被调用时,Spring会自动开启一个事务,当方法执行完毕后,Spring会根据方法的执行情况决定是提交事务还是回滚事务。在避免死锁方面,@Transactional注解可以通过以下两种方式来实现: 1.调整事务隔离级别 在@Transactional注解中,可以通过设置isolation属性来调整事务的隔离级别。事务隔离级别是指多个事务同时操作同一个数据时,事务之间的隔离程度。Spring支持的事务隔离级别包括READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。其中,READ_COMMITTED是默认的隔离级别。如果应用程序中存在死锁问题,可以尝试将事务隔离级别调整为REPEATABLE_READ或SERIALIZABLE,这样可以减少死锁的发生。 2.使用悲观锁 在@Transactional注解中,可以通过设置propagation属性来指定事务的传播行为。如果将propagation属性设置为REQUIRES_NEW,表示该方法需要开启一个新的事务,并且在该事务中使用悲观锁来避免死锁。悲观锁是指在操作数据时,先将数据锁定,然后再进行操作。这样可以避免多个事务同时对同一数据进行操作,从而减少死锁的发生。 下面是一个使用@Transactional注解避免死锁的示例: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE) public void transfer(int fromUserId, int toUserId, double amount) { User fromUser = userDao.getUserById(fromUserId); User toUser = userDao.getUserById(toUserId); synchronized (fromUser) { synchronized (toUser) { if (fromUser.getBalance() >= amount) { fromUser.setBalance(fromUser.getBalance() - amount); toUser.setBalance(toUser.getBalance() + amount); userDao.updateUser(fromUser); userDao.updateUser(toUser); } else { throw new RuntimeException("Insufficient balance"); } } } } } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值