@Transactional的那些事

420 阅读1分钟

当Transactional碰到锁,有个大坑,要小心。 - why技术 - 博客园 (cnblogs.com)

问题

首先看以下代码,在隔离级别是可重复读的情况下,其中有一个坑

    @Transactional(rollbackFor = Exception.class)
    public CommonResult<String> test1() {
        String lockKey = "test:lock";
        boolean tryLock = lockUtils.trylock(lockKey, 30, TimeUnit.HOURS);
        if (tryLock) {
            try {
                // 执行数据库操作——查询商品库存数量
                // 如果 库存数量 满足要求 执行数据库操作——减少库存数量——模拟卖出货物操作
            } finally {
                lockUtils.unlock(lockKey);
            }
        }
        return new CommonResult<>(CommonResultEnum.OK);
    }

按照没有问题的流程图应该是需要事务的开启与提交能完整的包裹在 lock 与 unlock之间。

如下:

首先,事务的开始肯定在需要事务的开启与提交能完整的包裹在 lock 与 unlock之间,但是如果把解锁放在提交事务之前,那么是以下情况:

  1. 假设现在库存就只有一个了。
  2. 这个时候 A,B 两个线程来请求下单。
  3. A 请求先拿到锁,然后查询出库存为一,可以下单,走了下单流程,把库存减为 0 了。
  4. 但是由于 A 先执行了 unlock 操作,释放了锁。
  5. B 线程看到后马上就冲过来拿到了锁,并执行了查询库存的操作。
  6. 注意了,这个时候 A 线程还没来得及提交事务,所以 B 读取到的库存还是 1,如果程序没有做好控制,也走了下单流程。
  7. 哦豁,超卖了。

首先,事务的开启一定是在 lock 之后的,那么,事务的提交是在 unlock 之前,还是之后呢?

看下@Transactional 的大概逻辑如下:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

{
  // 开启事务        
  TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

  Object retVal;
  try {
      // 执行业务代码
      retVal = invocation.proceedWithInvocation();
  } catch (Throwable var20) {
      // 事务回滚
      this.completeTransactionAfterThrowing(txInfo, var20);
      throw var20;
  } finally {
      this.cleanupTransactionInfo(txInfo);
  }

  // 提交事务
  this.commitTransactionAfterReturning(txInfo);
  return retVal;
}

可以看到,真正的业务代码是被 try catch 住然后执行的,也就是说提交事务是在解锁之后。所以说,可能出现超卖的情况。

那么怎么解决呢?

自己注入自己,然后调用

   @Resource
   private XXXService xxxService;
    
    public CommonResult<String> test1() {
        String lockKey = "test:lock";
        boolean tryLock = lockUtils.trylock(lockKey, 30, TimeUnit.HOURS);
        if (tryLock) {
            try {
                xxxService.查询并扣库存()
            } finally {
                lockUtils.unlock(lockKey);
            }
        }
        return new CommonResult<>(CommonResultEnum.OK);
    }
        
    @Transactional(rollbackFor = Exception.class)
    public void 查询并扣库存() {
       // 执行数据库操作——查询商品库存数量
       // 如果 库存数量 满足要求 执行数据库操作——减少库存数量——模拟卖出货物操作
    }

编程式事务编写

    @Resource
    private TransactionTemplate transactionTemplate;
    
    public CommonResult<String> test5() {
        String lockKey = "test:lock";
        boolean tryLock = lockUtils.trylock(lockKey, 30, TimeUnit.HOURS);
        if (tryLock) {
            try {
                transactionTemplate.execute(status -> {
                    查询并扣库存();
                    return null;
                });
            } catch (Exception e) {
                log.error("报错了",e);
            } finally {
                lockUtils.unlock(lockKey);
            }
        }
        return new CommonResult<>(CommonResultEnum.OK);
    }

还有一种情况,满足以下条件,会抛出 Transaction rolled back because it has been marked as rollback-only 异常

  1. 两个方法都加事务
  2. 内层方法出现异常,默认进行了回滚
  3. 外层try catch住内层方法

@Transactional注解要指定rollbackFor

rollbackFor 默认是 RuntimeException 和 Error,别的异常不会回滚,只会进行提交

org.springframework.transaction.interceptor.TransactionAspectSupport#completeTransactionAfterThrowing

    protected void completeTransactionAfterThrowing(@Nullable TransactionAspectSupport.TransactionInfo txInfo, Throwable ex) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex);
            }

            if (txInfo.transactionAttribute != null 
                // 判断 rollbackFor,不满足执行else 提交
                && txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                } catch (TransactionSystemException var6) {
                    this.logger.error("Application exception overridden by rollback exception", ex);
                    var6.initApplicationException(ex);
                    throw var6;
                } catch (Error | RuntimeException var7) {
                    this.logger.error("Application exception overridden by rollback exception", ex);
                    throw var7;
                }
            } else {
                try {
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                } catch (TransactionSystemException var4) {
                    this.logger.error("Application exception overridden by commit exception", ex);
                    var4.initApplicationException(ex);
                    throw var4;
                } catch (Error | RuntimeException var5) {
                    this.logger.error("Application exception overridden by commit exception", ex);
                    throw var5;
                }
            }
        }

    }

注解失效的情况

  1. 抛出的异常和 rollbackFor 不对应
  2. 方法是不是public的,因为在使用 Spring AOP 代理时,Spring 在生成代理的时候会判断方法是不是public的 org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
  3. 异常被 catch 住,并且没有重新抛出异常
  4. 数据库本身不支持,MySql 的 MyISAM 引擎不支持回滚,如果需要自动回滚事务,需要将MySql的引擎设置成InnoDB;
  5. 方法内部直接调用,没加注解的方法调用了加注解的方法
  6. 如果项目是多数据源的,需要配置不同数据源的事务管理器,如下:
@Primary
@Bean(name = "db1")
public DataSource getDataSource() {
    return createDataSource();
}
@Bean(name = "db1TransactionManager")
public PlatformTransactionManager txManager(@Qualifier("db1") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}


@Bean(name = "db2")
public DataSource getDataSource() {
    return buildDataSource();
}
@Bean(name = "db2TransactionManager")
public PlatformTransactionManager txManager(@Qualifier("db2") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
   }
  1. 新开启的线程也不会进行正确的回滚,因为spring实现事务的原理是通过 ThreadLocal 把数据库连接绑定到当前线程中,而新建个线程就是另外的 ThreadLocal 了
@Transactional
public void deleteUser() throws MyException{
    userMapper.deleteUserA();
    try {
        
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(() -> {
        int i = 1/0;
        userMapper.deleteUserB();
    }).start();    
}
  1. 事务传播属性设置错误
  • PROPAGATION_SUPPORTS: 如果存在事务,则进入事务;否则,以非事务方式运行。
  • PROPAGATION_NOT_SUPPORTED: 如果存在事务,则挂起事务,并以非事务方式运行。
  • PROPAGATION_NEVER: 以非事务形式运行,如果存在事务,则抛出异常。

@transactional 的事务传播机制是怎么实现的

主要逻辑在 org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction 方法里

默认隔离级别:PROPAGATION_REQUIRED

再说一下数据库连接获取有两个地方,如下图:

在第二个框住的地方里面有一个 doBegin 方法,会做两件事:

  1. 第一次获取数据库连接
  2. 把ConnectionHolder存入threadLocal里
  protected void doBegin(Object transaction, TransactionDefinition definition) {
        DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
        Connection con = null;

        try {
            // 获取连接
            if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
                Connection newCon = this.obtainDataSource().getConnection();
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
                }

                txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
            }

            // XXXXX
            // XXXXX
            // XXXXX
            
            // 把ConnectionHolder存入threadLocal里
            if (txObject.isNewConnectionHolder()) {
                TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
            }

        }
    }

然后以后别的被 @Transactional 修饰的方法执行数据库操作的时候,就是以下的逻辑:

这样获取的事务的连接就是原来的,即加入到原来的事务里面了。