java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > 多线程下的事务失效及解决

多线程下的事务失效及解决方案

作者:找不到、了

这篇文章主要介绍了多线程下的事务失效及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

使用 DataSourceTransactionManager 是在 Spring 框架中进行数据库事务管理的一种常见方式。

它提供了一种简便的方式来处理事务,并能够有效地解决多线程环境下的事务隔离和一致性问题。

单核cpu的多线程模型图如下所示:

从上述模型可以知道,线程1、线程2会因为cpu的来回切换,导致当前事务的上下文不同,因此处于失效状态。

1、定义

多线程和并发是计算机科学中与执行多个任务相关的两个重要概念。

尽管它们在某些方面密切相关,但它们代表了不同的概念,下面是它们之间的主要区别和联系。

1.1、多线程

多线程是指在一个进程中同时运行多个线程。每个线程有自己的执行路径,可以在共享的内存空间中执行,也可以共享进程的资源。

如下图所示:

多线程的一个主要目标是利用多核 CPU 的能力,以提高程序的并行性和响应速度。

1.2、并发

并发是指两个或多个任务或进程在同一时间段内进行处理,通常是通过多个线程、进程或任务的交替执行来实现。

如下图所示:

并发不一定指真正的同时运行,它可能是通过快速切换来模拟同时运行的效果。并发可以在单线程或多线程环境中实现,只要在时间上有重叠即可。

2、多线程事务

多线程事务和单线程事务是两种对事务处理的不同实现方式,特别是在涉及数据库和并发操作的场景中。

2.1、定义

单线程事务

多线程事务

2.2、主要区别

并发性

数据一致性

锁的使用

性能

2.3、联系

事务的性质

使用的技术

设计考虑

2.4、DataSourceTransactionManager

实现了 PlatformTransactionManager 接口,主要用于基于 JDBC 的数据源事务管理。

通过它可以控制事务的边界,例如开始事务、提交事务和回滚事务。这种方式非常适合于多线程环境下的数据库操作。

2.5、TransactionSynchronizationManager

是 Spring 框架的一部分,专门用于管理与事务相关的同步任务。

它通常用于将事务资源与当前线程绑定,使得在当前线程中的所有操作都可以共享相同的事务上下文。

在使用时,TransactionSynchronizationManager 经常与数据源管理器(如 DataSourceTransactionManager)结合使用。使用 TransactionSynchronizationManager 绑定当前线程

TransactionSynchronizationManager 的主要用途是确保在当前线程中执行的所有数据库操作都可以利用同一个事务。利用它可以将事务上下文与当前线程捆绑在一起。

代码如下图所示:

public void bindResource(Object key, Object value) throws IllegalStateException {
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Assert.notNull(value, "Value must not be null");
        Map<Object, Object> map = this.transactionContext.getResources();
        Object oldValue = map.put(actualKey, value);
        if (oldValue != null) {
            throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to context [" + this.transactionContext.getName() + "]");
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to context [" + this.transactionContext.getName() + "]");
            }

        }
    }

3、事务处理

事务处理分为编程式和声明式两种。

3.1、底层原理

当目标类被spring管理时(即@Component,@Service,@controller,@Repository注解时),spring会为目标对象创建一个代理对象。代理对象负责拦截目标方法的调用,并在必要时应用事务管理(AOP思想)。

代理对象内部包含一个事务拦截器TransactionInterceptor,负责处理事务相关的逻辑。

事务拦截器会检查方法上是否添加了@Transactional注解,来决定是否应用事务(JDBC的connection调用setAutoCommit(false),然后会将connection存到ThreadLocal(本地线程)中,

(为了嵌套方法可以拿到外层事务的connection对象,只有使用同一个connection对象才能保证使用同一个事物)

然后去执行方法,从而执行数据库操作,然后再执行嵌套方法,然后再嵌套方法的代理对象,就可以拿到外层事务的Thread Local中connection对象,从而执行方法,嵌套方法发现ThreadLocal中有值,他不会提交事务,而是统一的交给外层事务进行提交)

事务拦截器在目标方法执行前后应用事务通知,(前置通知和后置通知)在方法执行前,事务拦截器启动事务;

在方法执行后,根据方法的执行结果决定事务的提交或回滚。

关于更多详细了解,可参考:如何合理使用Spring的事务

如下图所示:

3.2、失效原因

如果在方法中开启新线程去调用嵌套方法时,这时嵌套线程就拿不到外层事务的ThreadLocal中的connection对象。

关于ThreadLocal是绑定在主线程上的。

所以在新的线程中去调用嵌套方法时,就拿不到外层事务的connection对象,然后就会自己新建一个事务,自己存一个connection到自己的ThreadLocal中,

这样的话,方法和嵌套方法就各开启了一个事务,且是同级事务,相互不影响。

但是这样在外层事务失败回滚的时候,内层事务不受外层事务的影响,不进行回滚的话,就会造成事务回滚但还是写入数据库中的现象,我们要保证数据库数据的一致性!

3.3、解决方案

1、使用TransactionSynchronizationManager

在创建一个异步线程之后,可以手动的往嵌套方法的Tread Local中将外层事务connection对象存入,那么嵌套方法就可以从自己的Thread Local中拿到外层事务的connection了,这样方法和嵌套方法用的就是同一个事务了。

这样就不会出现多线程事务失效的情况了。

扩展:

编程式事务中DataSourceTransactionManager类org.springframework.jdbc.datasource.DataSourceTransactionManager中的doBegin方法就是spring底层在开启事务的时候进行调用的,在方法中获取数据库连接,并开启事务。

因此如果想要获取对应的connection,可以调用事务同步管理器的getResource()方法,

将spring容器当中的datasource传进来,就可以拿到,

然后再调用BindResource方法就可以将异步线程中的ThreadLocal中存入外层事务的connection。

解决方案代码展示:

 private DataSource datasource;
 ConnectionHolder connectionHolder = (ConnectionHolder)TransactionSynchronizedManager.getResource(dataSource);
 new Thread(()->{
 //绑定主线程的connection到子线程中
 TransactionSynchronizedManager.bindResource(dataSource,connectionHolder);
 //调用方法
 })

2、join() 方法

核心代码示例:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TransferExample {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        TransferService transferService = context.getBean(TransferService.class);

        // 使用线程数组来存储线程
        Thread[] threads = new Thread[3];

        // 创建多个线程来执行转账
        for (int i = 0; i < threads.length; i++) {
            String fromAccount = "accountA";  // 设定转账来源账户
            String toAccount = "accountB";      // 设定转账目标账户
            double amount = 100.0 * (i + 1);   // 每个线程按顺序转账不同金额

            threads[i] = new Thread(() -> {
                try {
                    transferService.transfer(fromAccount, toAccount, amount);
                    System.out.println("Transfer of " + amount + " from " + fromAccount + " to " + toAccount + " completed.");
                } catch (Exception e) {
                    System.out.println("Transfer failed: " + e.getMessage());
                }
            });

            threads[i].start();  // 启动线程
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            try {
                thread.join();  // 等待每个子线程完成
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        context.close();
    }
}

通过适当的事务管理和使用 join() 方法等待子线程,然后将所有操作放在各自的事务中,这样能够确保数据的一致性和完整性。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文