工作笔记-双重事务提交(一)
记录下在上线前夕遇到一个关于事务提交的问题
首先,让我们复习下关于事务的隔离级别和传播行为。
事务隔离级别
事务的隔离级别会产生脏读、幻读、不可重复读。
脏读
一个事务读取另外一个事务尚未提交的数据。 当事务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-tx的TransactionDefinition接口中定义了如下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-tx的TransactionDefinition接口中除了定义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方法的调用需要确认当前的事务
隔离级别以及传播行为,避免以上问题再次发生。