Spring多线程事务一致性+动态数据源的学习和探究
1.问题背景
当我们有以下的问题需求,并且该需求需要异步去执行提高效率时,思考如何解决
首先,这三个过程都会修改数据库中的数据,因此都需要加上事务,接着又需要启用多个线程分别执行任务,这就是多线程保证事务一致性的一般场景
public void transferMoneyBusiness (User from, User to, final double money) {
//1. 扣除from用户指定的金额 money
deduceMoney(from, money);
//2. 增加to用户账户中的金额 money
addMoney(to, money);
//3. 记录转账流水
recordTransferFlow(from, to, money);
}
思路:这三个步骤可以看作是一个整体的大的事务,使用多线程为每个步骤分发线程去执行其业务,最后根据执行结果来判断这个大的事务需要提交还是回滚。那么事务就不能交由Spring管理(Spring会自动提交事务),需要我们自己手动去控制事务的状态。
既然需要让任务异步执行,并且需要等到所有任务执行完再根据其结果判断是否需要提交或回滚,那么我们要能够获取任务执行的结果,即执行任务的线程中是否有抛出异常。如果有,则所有的事务都要回滚;如果没有出现异常,则所有的事务都提交。
根据上面的需求,选择CompletableFuture
来实现异步任务这个流程,选择该类的方法有如下两点
- 需要获取线程任务的执行结果,
CompletableFuture.get()
可以获得任务执行的结果 - 需要所有的线程任务执行结束后,主线程才能继续往下走,
CompletableFuture().allOf(CompletableFuture<?>... cfs)
可以等待列表中的所有任务执行完再往下走
2.实现过程
2.1自定义线程类
为了后面日志打印时能够更加清楚直观地看到任务的执行结果,这里自定义一个线程类为任务添加一个taskName
属性用来指代任务名称
public class NamedRunnable implemenets Runnable {
private String taskName;
private final Runnable task;
@Override
public void run() {
}
public NamedRunnable(String taskName, Runnable task) {
this.taskName = taskName;
this.task = task;
}
public String getTaskName() {
return taskName;
}
public Runnable getTask() {
return task;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
}
2.2动态数据源
动态数据源简单来说就是在程序运行的过程中,可以使用多个不同的数据源来创建连接并进行操作。例如有的代码需要切换到A scheme
,有的需要切换到B scheme
。由于我自己是实现了动态数据源
,因此之后代码中出现下面的代码时为动态数据源的操作(本文使用的数据源为slave3).
具体可以阅读这篇文章学习SpringBoot中动态数据源的实现与探究
DynamicDatasourceHolder.shift("slave3"); // 切换到"slave3"数据源
DynamicDatasourceHolder.poll(); //清除当前数据源,切换到默认的数据源
2.3自定义线程池
使用CompletableFuture
最好还需要使用自定义的线程池,能够更加灵活方便地管理任务线程。定义线程池最好用单例模式,能够有效避免代码粗心导致多次创建线程池,浪费内存。实现如下:
/**
* 自定义线程池功能方法接口
*/
interface ThreadPoolFunction {
ThreadPoolTaskExecutor getThreadPoolExecutor();
}
/**
* 自定义线程池枚举 (使用枚举实现单例模式)
*/
enum ThreadPoolTaskExecutorEnum implements ThreadPoolFunction {
ZZ_Executor {
public volatile ThreadPoolTaskExecutor threadPoolExecutor;
private final Object lock = new Object();
@Override
public ThreadPoolTaskExecutor getThreadPoolExecutor() {
if (threadPoolExecutor == null) {
synchronized (lock) {
if (threadPoolExecutor == null) {
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(0b1010);
executor.setMaxPoolSize(0b1010);
executor.setThreadFactory(new NamedThreadFactory("ZZ_ThreadFactory"));
executor.setQueueCapacity(1 << 10);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.setWaitForTasksToCompleteOnShutdown(Boolean.FALSE);
executor.setKeepAliveSeconds(30);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
this.threadPoolExecutor = executor;
}
}
}
return threadPoolExecutor;
}
}
}
//优雅停止线程池
@Component
class ThreadPoolTaskExecutorHook implements InitializingBean {
private final Logger log = LoggerFactory.getLogger(getClass().getName());
private void destroy() {
Stream.of(ThreadPoolTaskExecutorEnum.values())
.filter(Objects::nonNull)
.map(ThreadPoolFunction::getThreadPoolExecutor)
.filter(Objects::nonNull)
.forEachOrdered(ExecutorConfigurationSupport::shutdown);
log.info("hook执行完毕,所有线程池已关闭");
}
@Override
public void afterPropertiesSet() throws Exception {
log.info("线程池Hook注册");
Runtime.getRuntime().addShutdownHook(new Thread(() -> { //获取运行环境并设置钩子
try {
destroy();
} catch (Exception e) {
log.error("executeShutDownHookError {}", e.getMessage());
}
}, "ThreadPoolTaskHook"));
}
}
2.4事务管理器
因为我们需要手动控制事务,因此我们需要自己实现一个能够执行异步任务的事务管理器方便将任务和事务集中管理。
按照之前的理解,我们需要在每个任务中创建事务并进行数据库操作,如果出现异常就进行标记,最后根据该标记来判断整体事务需要提交还是回滚
public class MultiplyThreadTransactionManager {
private static final Logger log = LoggerFactory.getLogger(getClass());
/**
* 如果是多数据源的情况下,需要指定是哪个数据源
*/
@Autowired
private DataSource datasource;
/**
* @param tasks 异步执行列表
* @param executor 使用的线程池,考虑线程隔离,因此强制传该参数
*/
public void runAsyncTaskButWaitUntilAllDown(List<NamedRunnable> tasks, Executor executor) {
if (executor == null) {
throw new GlobalException("线程池不能为空");
}
final DataSourceTransactionManager transactionManager = getDatasourceTransactionManager();
final AtomicBoolean ex = new AtomicBoolean(); //标记是否出现异常
final List<CompletableFuture> taskFutureList = new ArrayList<>();
//有线程安全问题
final List<TransactionStatus> transactionStatusList = new CopyOnWriteArrayList<>();
tasks.forEach(task -> {
taskFutureList.add(CompletableFuture.runAsync(
() -> {
TransactionStatus transactionStatus = null;
try {
//子线程切换到指定数据源
DynamicDatasourceHolder.shift("slave3");
//开启事务
transactionStatus = openNewTransaction(transactionManager);
log.info("{}开始执行线程任务", task.getTaskName());
transactionStatusList.add(transactionStatus);
task.run();
} catch (Exception throwable) {
log.error(throwable.getMessage());
//标记异常
ex.set(Boolean.TRUE);
//其他未执行的任务无需执行
taskFutureList.forEach(completableFuture -> completableFuture.cancel(true));
} finally {
//这里需要将对象置空回收对象,因为线程使用完后会归还到线程池中,线程会继续持有相应 的资源和对象
if (status != null) {
status = null;
}
}
}
, executor));
});
try {
//等待所有任务执行结束,并获取结果
CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[0])).get();
} catch (Exception e) {
e.printStackTrace();
log.error("多线程事务中出现异常,进行事务回滚");
}
//如果出现异常则执行回滚逻辑,否则提交
if (ex.get()) {
for (int i = 0; i < transactionStatusList.size(); i++) {
transactionManager.rollback(transactionStatusList.get(i)); //对每个事务资源进行回滚
log.info("任务 {} 回滚成功", i + 1);
}
} else {
for (int i = 0; i < transactionStatusList.size(); i++) {
transactionManager.commit(transactionStatusList.get(i)); //对每个事务资源进行提交
log.info("任务 {} 提交成功", i + 1);
}
}
}
/**
* 获取一个事务资源
*/
private TransactionStatus openNewTransaction(DataSourceTransactionManager dataSourceTransactionManager) {
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
return dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);、
}
public DataSourceTransactionManager getDatasourceTransactionManager() {
return new DataSourceTransactionManager(datasource);
}
}
2.5异步执行任务
根据之前的需求,首先我们需要一个存放各个任务的列表List<Runnable>
,将任务添加到列表中后再去执行任务
//如果有并发的情况下还需要加锁保证账户金额正确
@Autowired
private UserMapper userMapper;
@Autowired
private TransferFlowMapper transferFlowMapper;
@Autowired
private MultiplyThreadTransactionManager multiplyThreadTransactionManager;
public void multiThreadTransaction(User from, User to, final double money) {
DynamicDatasourceHolder.shift("slave3"); //首先切换到对应数据源
List<NamedRunnable> list = new ArrayList<>(); //创建任务列表
//向任务列表添加任务
list.add(new NamedRunnable("转账A",() -> {
System.out.println("进入第一个阶段");
from.setAmount(from.getAmount() - money);
userMapper.updateById(from);
System.out.println("第一阶段结束");
}));
list.add(new NamedRunnable("收款B",() -> {
System.out.println("进入第二个阶段");
if (from.getAmount() < money) {
throw new GlobalException("账户余额为负");
}
to.setAmount(to.getAmount() + money);
userMapper.updateById(to);
System.out.println("第二阶段结束");
}));
list.add(new NamedRunnable("流水C",() -> {
System.out.println("进入第三个阶段");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
final TransferFlowPO po = new TransferFlowPO();
po.setAmount(money);
po.setCreateAt(simpleDateFormat.format(new Date()));
po.setTakeBy(from.getId());
po.setReceiveBy(to.getId());
transferFlowMapper.insert(po);
System.out.println("第三阶段结束");
}));
multiplyThreadTransactionManager.runAsyncTaskButWaitUntilAllDown(list, ThreadPoolTaskExecutorEnum.ZZ_Executor.getThreadPoolExecutor());
}
2.6流程测试
数据库中有两个用户,初始数据如下,接着A向B转账金额50
id | username | amount |
---|---|---|
1 | 小A | 100 |
2 | 小B | 100 |
控制台输出如下,任务都能够正常执行,但是却出现了异常
任务正常执行
抛出的异常信息
java.lang.IllegalStateException: No value for key [com.course.datasource.DynamicDatasourceConfig$$EnhancerBySpringCGLIB$$3b6ae611@796613b7] bound to thread [http-nio-8081-exec-1]
解释:无法找到绑定在当前线程的Key为DynamicDatasourceConfig$$EnhancerBySpringCGLIB$$3b6ae611@796613b7
的资源(这个DynamicDatasourceConfig
是动态数据源,等同于一个数据源Datasource)。简单来说就是当前线程没有找到slave3
这个数据源创建的ConnectionHolder对象
这个ConnectionHolder会持有一个Connection连接对象,也就是说主线程没有对应的Connection对象
出现这种情况的原因是我们的事务是在子线程中创建的,slave3
数据源创建的ConnectionHolder绑定在子线程中(绑定在子线程的threadLocalMap
中),而我们进行提交和回滚的操作是在主线程中的,因此在回滚或者提交的时候需要先获取连接再调用方法con.commit()/con.rollback()
,但是由于无法获取对应子线程中threadLocalMap中绑定的资源,所以才会抛出了异常
要解决该问题,需要将子线程对应的资源共享给主线程,让其能够利用该共享资源进行提交或者回滚操作!
2.7多线程资源共享问题解决
- 分析完上面的问题,同时借鉴他人的解决思路。最后决定使用——将资源在进行复制来线程之间进行共享,来解决这个问题
修改后的代码如下,添加了资源共享的逻辑
public void runAsyncTaskButWaitUntilAllDown(List<NamedRunnable> tasks, Executor executor) {
if (executor == null) {
throw new GlobalException("线程池不能为空");
}
DataSourceTransactionManager transactionManager = getDatasourceTransactionManager();
final AtomicBoolean ex = new AtomicBoolean();
final List<CompletableFuture> taskFutureList = new ArrayList<>();
final List<TransactionStatus> transactionStatusList = new CopyOnWriteArrayList<>();
//使用一个List来存放共享给主线程的资源
final List<TransactionResource> transactionResources = new CopyOnWriteArrayList<>();
tasks.forEach(task -> {
taskFutureList.add(CompletableFuture.runAsync(
() -> {
TransactionStatus transactionStatus = null;
try {
DynamicDatasourceHolder.shift("slave3");
transactionStatus = openNewTransaction(transactionManager);
log.info("{}开始执行线程任务", task.getTaskName());
transactionStatusList.add(transactionStatus);
//资源复制
transactionResources.add(TransactionResource.copyTransactionResource());
task.run();
} catch (Exception throwable) {
log.error(throwable.getMessage());
ex.set(Boolean.TRUE);
taskFutureList.forEach(completableFuture -> completableFuture.cancel(true));
} finally {
if (status != null) {
status = null;
}
}
}
, executor));
});
try {
CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[0])).get();
} catch (Exception e) {
e.printStackTrace();
log.error("多线程事务中出现异常,进行事务回滚");
}
if (ex.get()) {
for (int i = 0; i < transactionStatusList.size(); i++) {
transactionResources.get(i).autoWiredTransactionResource();
transactionManager.rollback(transactionStatusList.get(i));
transactionResources.get(i).removeTransactionResource();
log.info("任务 {} 回滚成功", i + 1);
}
} else {
for (int i = 0; i < transactionStatusList.size(); i++) {
transactionResources.get(i).autoWiredTransactionResource();
transactionManager.commit(transactionStatusList.get(i));
transactionResources.get(i).removeTransactionResource();
log.info("任务 {} 提交成功", i + 1);
}
}
}
@Builder
private static class TransactionResource {
//事务结束后默认会移除集合中的Datasource作为Key关联的资源记录
private Map<Object, Object> resources = new HashMap<>();
//下面五个属性会被自动清理,无需手动清理
private Set<TransactionSynchronization> synchronizations = new HashSet<>();
private String currentTransactionName; //事务名
private Boolean currentTransactionReadOnly; //是否只读
private Integer currentTransactionIsolationLevel; //事务隔离级别
private Boolean actualTransactionActive; //事务是否存活
public static TransactionResource copyTransactionResource() {
return TransactionResource.builder()
//返回不可变集合
.resources(TransactionSynchronizationManager.getResourceMap())
.synchronizations(new LinkedHashSet<>())
.currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
.currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
.currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
.actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive())
.build();
}
public void autoWiredTransactionResource() {
//将资源绑定到当前线程(本文中就是绑定到主线程)
resources.forEach(TransactionSynchronizationManager::bindResource);
//如果是新事务才进行初始化
if(!TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.initSynchronization();
}
TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
}
public void removeTransactionResource() {
//这里解绑除了Datasource之外的资源(后面解释)
resources.keySet().forEach(
x -> {
if (!(x instanceof DataSource)) {
//资源与当前线程解绑
TransactionSynchronizationManager.unbindResource(x);
}
}
);
}
}
这个TransactionSynchronizationManager
是一个事务同步管理器,能够处理不同线程的事务和资源
修改之后再进行测试,结果如下,事务能够正常提交,并且数据库中的数据也都正确
接着将A账户的金额修改为0,前面由前面5.异步执行任务实现代码可知,当账户金额扣款后小于0则会抛出异常,接着 再进行测试,结果如下,查询数据库发现事务全部都能够正常回滚。
3.总结与思考
1.前面使用transactionStatusList.size()
计数遍历而不直接使用任务数taskFutureList.size()
来进行遍历是因为,只有正确开启了事务的资源才要执行commit()
/rollback()
操作。如果出现connection closed
连接关闭导致事务开启失败,那么事务数组和资源数组的个数 < 任务数 此时遍历任务数的话就会出现 数组索引越界异常
-
获取事务最重要的就是前面的
getTransaction()
方法return dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);
- 获取事务对象并包装:该方法中会调用
doGetTransaction()
来创建一个新的事务对象txObject
,接着会以当前数据源为key获取一个ConnectionHolder
资源对象(有可能为空)并赋给txObject
的newConnectionHolder
属性。 - 使用事务对象启动事务:接着再调用
startTransaction()
方法来启动事务,首先会创建一个DefaultTransactionStatus
对象(在前面,我们能够使用这个对象对事务进行回滚等操作).然后判断该txObject
绑定的ConnectionHolder
是否为空,为空就使用obtainDataSource().getConnection()
根据数据源创建一个新的ConnectionHolder
。接着将ConnectionHolder
持有的Connection
对象设置事务取消自动提交,最后将该ConnectionHolder
以数据源:ConnectionHolder
的形式绑定到当前线程中。
- 获取事务对象并包装:该方法中会调用
这也就解释了为什么绑定资源和解绑资源都是以数据源为key,资源为value进行操作的
同时资源复制的也是这个ConnectionHolder,因为只有获取连接才能够操作对应的事务,才能够进行提交和回滚操作
3.移除资源为什么不移掉Datasource
前面的removeTransactionResource()
方法中移除了除Datasource
之外的资源是因为:
-
在调用该方法之前事务已经进行提交或者回滚了,而追溯提交/回滚的源码可以知道在内部的执行完操作后最终还会进行资源清除,其中就调用了
DataSourceTransactionManager
的doCleanupAfterCompletion()
方法@Override protected void doCleanupAfterCompletion(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; //这里就已经将数据源与当前线程解绑了 if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.unbindResource(obtainDataSource()); } // 重置连接 Connection con = txObject.getConnectionHolder().getConnection(); try { if (txObject.isMustRestoreAutoCommit()) { con.setAutoCommit(true); } DataSourceUtils.resetConnectionAfterTransaction( con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly()); } catch (Throwable ex) { logger.debug("Could not reset JDBC Connection after transaction", ex); } if (txObject.isNewConnectionHolder()) { if (logger.isDebugEnabled()) { logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); } DataSourceUtils.releaseConnection(con, this.dataSource); } txObject.getConnectionHolder().clear(); }
所以如果再进行一次解绑操作,就会因为找不到对应的数据而抛出异常
4.补充说明
本来以为这部分的学习到此就结束了,但是后面经过测试依旧还是出现了问题,问题如下:
-
当执行的总任务数超过设置的线程数,会出现以下的错误,并且只有一个任务执行了并进行了回滚,其余两个任务不会执行
Could not open JDBC Connection for transaction; nested exception is java.sql.SQLException: connection closed
-
当出现这个错误后我再不断地请求,抛出的异常信息又发生改变了
Already value [org.springframework.jdbc.datasource.ConnectionHolder@42bfda3b] for key [com.course.datasource.DynamicDatasourceConfig$$EnhancerBySpringCGLIB$$f729be7c@64921450] bound to thread [ZZ_ThreadFactory-4]
由线程池配置可以看出来核心线程数==最大线程数
都等于10,而在我执行了该转账流程三次时都是正常的,只有在第四次的时候出现的第一个问题的现象。这个时候打开的连接总数为10,与线程池的大小一样。
于是我想到会不会是因为线程池中的线程持有了连接对象,并且该连接对象是关闭的状态,导致有新的任务去获取线程时,利用线程绑定的处于关闭状态的Connection
对象来开启新的事务所以报错了。
经过排查,发现是因为:线程池中的线程归还后,依旧持有ConnectionHolder对象,但是该对象持有的Connection连接已经关闭了(因为执行了commit/rollback),所以导致连接不能复用,因此需要去释放线程中的ConnectionHolder资源
起初我以为直接在线程执行的finally
块加上解绑资源的代码即可,如下
public void runAsyncTaskButWaitUntilAllDown(List<NamedRunnable> tasks, Executor executor) {
if (executor == null) {
throw new GlobalException("线程池不能为空");
}
DataSourceTransactionManager transactionManager = getDatasourceTransactionManager();
final AtomicBoolean ex = new AtomicBoolean();
final List<CompletableFuture> taskFutureList = new ArrayList<>();
final List<TransactionStatus> transactionStatusList = new CopyOnWriteArrayList<>();
final List<TransactionResource> transactionResources = new CopyOnWriteArrayList<>();
tasks.forEach(task -> {
taskFutureList.add(CompletableFuture.runAsync(
() -> {
TransactionStatus status = null;
try {
DynamicDatasourceHolder.shift("slave3");
status = openNewTransaction(transactionManager);
log.info("{}开始执行线程任务", task.getTaskName());
transactionStatusList.add(status);
transactionResources.add(TransactionResource.copyTransactionResource());
task.run();
} catch (Exception throwable) {
log.error(throwable.getMessage());
ex.set(Boolean.TRUE);
taskFutureList.forEach(completableFuture -> completableFuture.cancel(true));
} finally {
if (status != null) {
status = null;
}
//解绑资源
TransactionSynchronizationManager.unbindResource(transactionManager.getDataSource());
}
}
, executor));
});
try {
CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[0])).get();
} catch (Exception e) {
e.printStackTrace();
log.error("多线程事务中出现异常,进行事务回滚");
}
if (ex.get()) {
for (int i = 0; i < transactionStatusList.size(); i++) {
transactionResources.get(i).autoWiredTransactionResource();
transactionManager.rollback(transactionStatusList.get(i));
transactionResources.get(i).removeTransactionResource();
log.info("任务 {} 回滚成功", i + 1);
}
} else {
for (int i = 0; i < transactionStatusList.size(); i++) {
transactionResources.get(i).autoWiredTransactionResource();
transactionManager.commit(transactionStatusList.get(i));
transactionResources.get(i).removeTransactionResource();
log.info("任务 {} 提交成功", i + 1);
}
}
}
结果又报错了,主线程抛出了没有绑定的资源的异常
java.lang.IllegalStateException: No value for key [com.course.datasource.DynamicDatasourceConfig$$EnhancerBySpringCGLIB$$3b6ae611@796613b7] bound to thread [http-nio-8081-exec-1]
后面找出原因是:因为TransactionResource
中的copyTransactionResource()
方复制资源时直接用this.resources = TransactionSynchronizationManager.getResourceMap()
的方法进行赋值,当finally块中解绑资源时,这个resources
中的值也会改变,因此修改赋值方式,如下
public static TransactionResource copyTransactionResource() {
final TransactionResource build = TransactionResource.builder()
//返回不可变集合
.resources(new HashMap<>())
//如果需要注册事务监听者,这里记得修改
.synchronizations(new LinkedHashSet<>())
.currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
.currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
.currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
.actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive())
.build();
build.equipData(TransactionSynchronizationManager.getResourceMap());
return build;
}
public void equipData(Map<Object,Object> from) {
this.resources.putAll(from);
}
修改完再进行测试后发现先前的问题也全部解决了!
最后感谢大家的阅读,如果上面有什么错误或者有更好的思路也可以告诉我哈
学无止境~