工作笔记-双重事务提交(一)

265 阅读5分钟

工作笔记-双重事务提交(一)

记录下在上线前夕遇到一个关于事务提交的问题

首先,让我们复习下关于事务的隔离级别和传播行为。

事务隔离级别

事务的隔离级别会产生脏读、幻读、不可重复读。

脏读

一个事务读取另外一个事务尚未提交的数据。 当事务A把字段X的值从1修改为2并未提交,接着事务B读取到X的值为2,最后事务A发生回滚,这样子事务B读取到的X的值就是错的。

不可重复读

在一个事务范围内多次查询,却返回不一样的值,原因是在查询间隔时,另外一个事务修改并提交了数据。 当事务A第一次读取X字段的值为1,接着事务B修改X值为2并提交,最后事务A第二次读取到的X值为2,发生了不可重复读。

幻读

在一个事务范围内多次操作,却返回的记录数不一样,原因是在查询间隔是,另外一个事务修改并提交了数据。 事务A对表的所有行的字段X的值从1修改为2,这个时候事务B对表新增一条记录X的值为1并提交,最后用户返回查看表会发现还会有一条记录并未修改, 该记录其实是事务B提交的数据

幻读是不可重复的一个特殊场景

spring-txTransactionDefinition接口中定义了如下5个事务隔离级别。

public interface TransactionDefinition {
    // ... 
    /**
     * 使用数据库默认的隔离级别
     */
    int ISOLATION_DEFAULT = -1;
    
    /**
     * 读不提交,这是事务最低的隔离级别
     * 允许其他事务可以看到该事务未提交的数据
     * 该隔离级别会产生脏读、幻读、不可重复读
     */
    int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
    
    /**
     * 读提交
     * 保证该事务修改的数据提交后才能被其他事务读取,其他事务不能读取该事务未提交的数据
     * 该隔离级别避免了脏读,但是无法避免幻读、不可重复读
     * 
     */
    int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
    
    /**
     * 可重复读
     * 在该事务中多次读取数据的情况下能读到一样的数据,不受其他事务影响
     * 即:事务A第一次读取数据值为1,事务B修改数据为2,不管是否有提交,事务A第二次读的数据值还是1
     * 只有当事务A提交后才能读到最新的数据
     * 该事务隔离级别避免了脏读、不可重复读,但是可能出现幻读
     */
    int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
    
    /**
     * 花费代价最高但最可靠的事务隔离级别
     * 该事务隔离级别避免了脏读、幻读、不可重复读
     */
    int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
    // ...
}

事务传播行为

spring-txTransactionDefinition接口中除了定义5个事务的隔离级别,还定义了7个事务的传播行为

public interface TransactionDefinition {
    // ...
    /**
    	 * 当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务
    	 */
    	int PROPAGATION_REQUIRED = 0;
    
    	/**
    	 * 如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式运行
    	 */
    	int PROPAGATION_SUPPORTS = 1;
    
    	/**
    	 * 如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常
    	 */
    	int PROPAGATION_MANDATORY = 2;
    
    	/**
    	 * 重新创建一个事务,如果当前存在事务则挂起当前事务
    	 */
    	int PROPAGATION_REQUIRES_NEW = 3;
    
    	/**
    	 * 以非事务方式运行,如果当前存在事务则挂起当前事务
    	 */
    	int PROPAGATION_NOT_SUPPORTED = 4;
    
    	/**
    	 * 以非事务方式运行,如果当前存在事务则抛出异常
    	 */
    	int PROPAGATION_NEVER = 5;
    
    	/**
    	 * 如果当前存在事务则嵌套其他事务;如果当前不存在事务则创建一个事务
    	 * 嵌套事务如果外围事务回滚则内部事务回滚;如果内部事务回滚,则不影响外围事务
    	 */
    	int PROPAGATION_NESTED = 6;
    // ...    	
}

问题现象

在我们的业务代码中出现以下场景: ServiceA开启事务,需要在ServiceA事务提交后调用ServiceB。ServiceB同样开启事务,在ServiceB事务提交后同样需要做其他业务操作。

ServiceA和ServiceB开启的事务隔离级别都是ISOLATION_READ_COMMITTED,事务传播行为都是默认PROPAGATION_REQUIRED。 ServiceA和ServiceB在事务提交后进行业务操作的代码实现都是使用了TransactionSynchronizationManager.registerSynchronization#afterCommit方法。

在执行代码调用的时候,发现ServiceB的事务提交后进行业务操作的afterCommit不执行。

问题分析

因为两个事务都是使用了默认的事务传播行为PROPAGATION_REQUIRED

PROPAGATION_REQUIRED 的传播行为是当前存在事务则加入当前事务,如果不存在,则创建新事务

因为在ServiceA已经开启了事务,所以ServiceB的事务则是加入到ServiceA的事务当中。所以在事务提交后执行的代码只会执行ServiceA的afterCommit方法

代码示例

ServiceA的代码

public interface IServiceA {
    void execute();
}

@Service
@Slf4j
public class ServiceAImpl implements IServiceA {
    @Autowired
    private IServiceB serviceB;
    @Override
    @Transactional(rollbackFor = Throwable.class, timeout = 60, isolation = Isolation.READ_COMMITTED)
    public void execute() {
      log.info("进入service a");
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                log.info("service a 事务提交后");
                log.info("开始执行service b");
                serviceB.execute();
            }
        });
    }
}

ServiceB的代码

public interface IServiceB {
    void execute();
}

@Service
@Slf4j
public class ServiceBImpl implements IServiceB{
    @Override
    @Transactional(rollbackFor = Throwable.class, timeout = 60, isolation = Isolation.READ_COMMITTED)
    public void execute() {
      log.info("进入 service b");
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                log.info("service b 事务提交后");
            }
        });
    }
}

客户端调用代码

@RestController
@Slf4j
@RequestMapping("/home")
public class HomeController extends BaseController{
    @Autowired
    private IServiceA serviceA;
    
    @GetMapping("/test")
    public ApiBaseResponse test(){
        serviceA.execute();
        return setResponse();
    }
}

以上代码执行后ServiceB的log.info("service b 事务提交后")代码并不会执行。

解决方法

将ServiceB的事务传播行为修改为PROPAGATION_REQUIRES_NEW即可

    ...
    @Transactional(rollbackFor = Throwable.class, timeout = 60, isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
    public void execute() {
    ...

总结

通过此次问题,重新复习了一遍事务的隔离级别和传播行为。在生产代码中对于TransactionSynchronizationManager.registerSynchronization方法的调用需要确认当前的事务 隔离级别以及传播行为,避免以上问题再次发生。