Springboot中多线程数据库操作下的事务一致性问题的解决方案
背景
在某些特定的场景下,我们需要进行功能(数据)迁移,在数据迁移的过程中涉及到多表的数据的 crud 操作,如果这些 crud 操作通过主线程进行流式执行,那么当数据量上来之后,必然会造成程序的卡顿。
此时,我们想到的是使用JUC进行程序的性能优化,通过 JUC 实现程序的并发执行确实会显著地提高性能,但是也会带来新的问题——多线程数据库操作下的事务一致性问题。
Springboot中的事务
在使用 springboot 框架时,我们提到数据库事务,必然会想到声明时事务 @Transactional,@Transactional 就像拥有魔法一样,当我们给需要一致性操作的方法打上 @Transactional 注解时,那么此方法中的数据库事务的提交与回滚,就无需我们去关注了。
其实则是在 spring 的 bean 实例化时判断当前对象事务需要代理对象(遍历对象寻找 @Transactional 注解),以便进行前置后置事件的执行,当 spring 为我们生成代理对象后,后序开发人员进行 bean 的注入以及方法的调用,都会通过代理对象进行完成。
代理对象的作用就是某个方法的执行前后做 Intercept,以便完成 spring 的 “魔法” 操作。
Springboot中的魔法事务在多线程下的不可用性
在多线程的情况下,每个线程都会存在自己的 connect 连接,我们知道数据库的事务是通过 connect 的 rollback() 和 commit() 实现的,所以对于多线程来说,主线程的事务管理器是无法控制子线程中的事务状态的。
另一方面,因为多线程的事务一致性问题一般要求,当一个线程出现异常,那么所有的线程中的数据库写操作都需要进行回滚的,显然在 springboot 中使用 @Transactional 是无法实现的(因为线程见的不可见性或者线程隔离)。
Springboot中实现多线程下的数据库事务一致思路
上述讲到了多线程之间的事务隔离(孤岛)的问题,所以多线程下实现事务一致性的问题,其实就转变成了如何解决多线程之间的事务孤岛的问题,解决事务孤岛的问题有演变成如何实现多线程下的数据库事务的统一提交与统一回滚。
要想实现多线程下的数据库事务的统一提交与统一回滚,我们需要解决如下的几个问题。
问题一:完成统一提交与回滚操作所在的线程。
问题二:多线程中的异常可见性问题。
问题三:多线程中的事务状态和事务资源的保存问题——为统一提交/回滚提供资源。
问题四:多线程的阻塞等待问题——只有所有线程都完成后,我们再可知道事务存在异常。
问题五:多线程出现异常时的其他线程中断——只要出现异常,其他线程无需再执行了,等待统一回滚即可。
回答(问题一):主线程。
回答(问题二):可以通过 AtomicBoolean 做异常标识。
回答(问题三):我们需要为每个task保存其自己的事务状态和事务资源,我们可以通过类似 Map<Integer, Map<TransactionStatus, TransactionResource>> 的结构保存,其 key 为 task 的hashCode,value 为 task 的事务状态和事务资源。
回答(问题四):我们可以通过 CompletableFuture 启动多线程,然后通过 CompletableFuture.allOf(...).get(); 阻塞多线程。
回答(问题五):在子线程中可以中断其他线程,所以我们需要将 CompletableFuture 保存到 taskFutureList 中,以便在其他线程中遍历 taskFutureLis,调用 completableFuture.cancel(true) 终止线程。
完整的多线程的事务管理器代码实现
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 描述:混合线程的事务管理器
*
* @author huxuehao
**/
@Component
public class MultiplyThreadManager {
private final Logger log = LoggerFactory.getLogger(getClass());
// 如果是多数据源的情况下,需要指定具体是哪一个数据源
private final DataSource dataSource;
public MultiplyThreadManager(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 执行的是无返回值的任务
*
* @param tasks 异步执行的任务列表
* @param threadPool 异步执行任务需要用到的线程池,考虑到线程池需要隔离,这里强制要求传
*/
public boolean runAsyncWithTransaction(List<Runnable> tasks, ExecutorService threadPool) {
try {
// 参数有效性校验
if (tasks == null || tasks.size() == 0) {
return false;
}
if (threadPool == null) {
throw new IllegalArgumentException("线程池不能为空");
}
// 用于标记是否存在线程发生了异常
AtomicBoolean ex = new AtomicBoolean();
// 为当前线程声明事务管理器
DataSourceTransactionManager transactionManager = genTransactionManager();
// 混合事务定义信息map,用于存储 TransactionStatus & TransactionResource
Map<Integer, Map<TransactionStatus, TransactionResource>> multiplyTransactionDefinition = new ConcurrentHashMap<>();
// 开辟多线程,将CompletableFuture存储到taskFutureList,方便后面的管理
List<CompletableFuture<?>> taskFutureList = new ArrayList<>(tasks.size());
tasks.forEach(task ->
taskFutureList.add(CompletableFuture.runAsync(() -> {
try {
// 1.填充值(事务状态 & 事务资源)到混合事务定义map中
multiplyTransactionDefinition.put(
task.hashCode(),
new HashMap<TransactionStatus, TransactionResource>(1){
{
put(openNewTransaction(transactionManager), TransactionResource.copyTransactionResource());
}
}
);
// 2.异步任务执行
task.run();
} catch (Throwable throwable) {
// 打印异常
throwable.printStackTrace();
// 异常标记
ex.set(Boolean.TRUE);
// 其他任务还没执行的不需要执行了
taskFutureList.forEach(completableFuture -> completableFuture.cancel(true));
}
}, threadPool)
));
try {
// 阻塞直到所有任务全部执行结束---如果有任务被取消,这里会抛出异常,需要捕获
CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[]{})).get();
} catch (Exception ignored) {}
// 遍历所有的task做事务的提交或回滚
for (int i = 0; i < tasks.size(); i++) {
// 根据线程task的hashCode获取对应线程事务的状态和资源
Map<TransactionStatus, TransactionResource> transactionStatusAndResource = multiplyTransactionDefinition.get(tasks.get(i).hashCode());
// 根据事务状态和事务资源回滚事务
for (Map.Entry<TransactionStatus, TransactionResource> entry : transactionStatusAndResource.entrySet()) {
// 载入事务资源
entry.getValue().loadTransactionResource();
// 发生了异常则进行回滚操作,否则提交
if(ex.get()) {
// 回滚
transactionManager.rollback(entry.getKey());
log.warn("已完成第 {} 个事务的回滚...", (i + 1));
} else {
// 提交
transactionManager.commit(entry.getKey());
log.info("已完成第 {} 个事务的提交...", (i + 1));
}
// 卸载事务资源
entry.getValue().unLoadTransactionResource();
}
}
// 返回多线程执行结果(true表示成功,false表示失败)
return !ex.get();
} finally {
if (threadPool != null) {
// 关闭线程池
threadPool.shutdown();
}
}
}
/**
* 开启一个新事务
* @param transactionManager 事务管理器
*/
private TransactionStatus openNewTransaction(DataSourceTransactionManager transactionManager) {
// JdbcTransactionManager根据TransactionDefinition信息来进行一些连接属性的设置(包括隔离级别和传播行为等)
DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition();
// 开启一个新事务---此时autocommit已经被设置为了false,并且当前没有事务,这里创建的是一个新事务
return transactionManager.getTransaction(transactionDef);
}
/**
* 生成一个事务管理器
*/
private DataSourceTransactionManager genTransactionManager() {
return new DataSourceTransactionManager(dataSource);
}
/**
* 保存当前事务资源,用于线程间的事务资源COPY操作
*/
private static class TransactionResource {
// 保存当前事务关联的资源--默认只会在新建事务的时候保存当前获取到的DataSource和当前事务对应Connection的映射关系--当然这里Connection被包装为了ConnectionHolder
// 事务结束后默认会移除集合中的DataSource作为key关联的资源记录
private final Map<Object, Object> resources;
// 下面五个属性会在事务结束后被自动清理,无需我们手动清理
// 事务监听者--在事务执行到某个阶段的过程中,会去回调监听者对应的回调接口(典型观察者模式的应用)---默认为空集合
private Set<TransactionSynchronization> synchronizations;
// 存放当前事务名字
private final String currentTransactionName;
// 存放当前事务是否是只读事务
private final Boolean currentTransactionReadOnly;
// 存放当前事务的隔离级别
private final Integer currentTransactionIsolationLevel;
// 存放当前事务是否处于激活状态
private final Boolean actualTransactionActive;
public TransactionResource(Map<Object, Object> resources, Set<TransactionSynchronization> synchronizations, String currentTransactionName, Boolean currentTransactionReadOnly, Integer currentTransactionIsolationLevel, Boolean actualTransactionActive) {
this.resources = resources;
this.synchronizations = synchronizations;
this.currentTransactionName = currentTransactionName;
this.currentTransactionReadOnly = currentTransactionReadOnly;
this.currentTransactionIsolationLevel = currentTransactionIsolationLevel;
this.actualTransactionActive = actualTransactionActive;
}
/**
* 复制事务资源
*/
public static TransactionResource copyTransactionResource() {
return new TransactionResource(
// 返回的是不可变集合
TransactionSynchronizationManager.getResourceMap(),
// 如果需要注册事务监听者,这里记得修改--我们这里不需要,就采用默认负责--spring事务内部默认也是这个值
new LinkedHashSet<>(),
TransactionSynchronizationManager.getCurrentTransactionName(),
TransactionSynchronizationManager.isCurrentTransactionReadOnly(),
TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(),
TransactionSynchronizationManager.isActualTransactionActive()
);
}
/**
* 载入事务所需资源
*/
public void loadTransactionResource() {
resources.forEach(TransactionSynchronizationManager::bindResource);
// 如果需要注册事务监听者,这里记得修改--我们这里不需要,就采用默认负责--spring事务内部默认也是这个值
TransactionSynchronizationManager.initSynchronization();
TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
}
/**
* 卸载事务资源
*/
public void unLoadTransactionResource() {
// 事务结束后默认会移除集合中的DataSource作为key关联的资源记录
// DataSource如果重复移除,unbindResource时会因为不存在此key关联的事务资源而报错
resources.keySet().forEach(key -> {
if (!(key instanceof DataSource)) {
TransactionSynchronizationManager.unbindResource(key);
}
});
}
}
}
使用多线程的事务管理器
@Service
public class TransactionalService {
private final UserMapper userMapper;
private final MultiplyThreadTransactionManager multiplyThreadTransactionManager;
public TransactionalServiceImpl3(UserMapper userMapper,MultiplyThreadTransactionManager multiplyThreadTransactionManager) {
this.userMapper = userMapper;
this.multiplyThreadTransactionManager = multiplyThreadTransactionManager;
}
@Override
public String doTest(Integer num) {
List<Runnable> tasks = new ArrayList<>();
// 线程1
tasks.add(() -> {
User user = new User("胡学好01", 19, "000001", "男");
userMapper.insert(user);
throw new RuntimeException("回滚测试");
});
// 线程2
tasks.add(() -> {
User user = new User("胡学好02", 19, "000001", "男");
userMapper.insert(user);
});
// 使用多线程事务管理器
boolean isSuccess = multiplyThreadTransactionManager.runAsyncWithTransaction(tasks, Executors.newFixedThreadPool(2));
return isSuccess? "SUCCESS" : "ERROR";
}
}
答疑
1. TransactionResource 内部类的作用?
因为多线程事务的统一提交和统一回滚是在主线程中完成的,我们知道每个线程会维护器自己的事务状态与资源。
我们可以在先线程中通过如下操作,获取到当期线程的TransactionStatus。
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition())
事务的手动提交与回滚看似只是通过 transactionManager.commit(transactionStatus); 和 transactionManager.rollback(transactionStatus); 来实现的,但是还需要用到TransactionSynchronizationManager,TransactionSynchronizationManager 是 Spring 中的事务同步机制,它用于协调和管理事务的执行。在多线程环境下,它能够确保每个线程都有其自己独立的事务状态,避免出现数据一致性问题。除此之外,TransactionSynchronizationManager 中resources还保存了 当前线程使用的 ConnectionHolder。
所在在主线程中进行多线程的事务操作时,除了需要子线程的 TransactionStatus,当时还需要子线程当时的 TransactionSynchronizationManager,也就是我所定义的内部类 TransactionResource。
Map<Integer, Map<TransactionStatus, TransactionResource>> multiplyTransactionDefinition 中除了存储子线程的事务状态外,还存储了子线程的 TransactionSynchronizationManager 信息。
所以在进行统一提交或者回滚的前后需要载入和卸载当时线程的事务同步管理器资源(TransactionSynchronizationManager)。
2. 使用DataSourceTransactionManager管理事务时,是如何设置autoCommit为false?
从前面的代码,我们全程没有设置 autoCommit 为 false,但是我们可以肯定的是,事务中的 connect 的 autoCommit 必然已经被设置成了 false,不然也不能手动进行提交和回滚。
要想知道 connect 的 autoCommit 何时被设置成 false,就是在创建事务的时候是如何设置 connect?
DataSourceTransactionManager extends AbstractPlatformTransactionManager
---> getTransaction(@Nullable TransactionDefinition definition)
--> handleExistingTransaction(...)
---> startTransaction(...)
---> doBegin(...)
DataSourceTransactionManager.doBegin(Object transaction, TransactionDefinition definition)
最终就是在 DataSourceTransactionManager.doBegin(...) 方法中设置 autoCommit 为 false。
更多推荐
所有评论(0)