开发者

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

目录
  • 1、定义
    • 1.1、多线程
    • 1.2、并发
  • 2、多线程事务
    • 2.1、定义
    • 2.2、主要区别
    • 2.3、联系
    • 2.4、DataSourceTransactionManager
    • 2.5、TransactionSynchronizationManager
  • 3、事务处理
    • 3.1、底层原理
    • 3.2、失效原因
    • 3.3、解决方案
  • 总结

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

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

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

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

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

    1、定义

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

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

    1.1、多线程

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

    如下图所示:

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

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

    1.2、并发

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

      如下图所示:

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

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

      2、多线程事务

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

      2.1、定义

      单线程事务

      • 在单线程环境中,事务在一个线程中完成,没有其他线程的干扰。
      • 所有的操作顺序执行,事务的开始、提交和回滚都在同一个线程中处理。

      多线程事务

      • 在多线程环境中,多个线程可以访问和操作同一事务。可能会有多个线程并发执行的事务,或同一事务在不同线程中交替进行。
      • 这种方式更复杂,因为需要考虑多个线程对共享数据的竞争、隔离和一致性。

      2.2、主要区别

      并发性

      • 单线程事务:操作是线性的,只有一个线程在执行事务,与其他事务完全隔离。通常更容易实现和维护。
      • 多线程事务:事务的并发处理可能导致数据竞争,多个线程可能同时读取和写入相同的数据,导致数据的不一致性。

      数据一致性

      • 单线程事务:由于没有其他线程参与,数据一致性容易维护。只要在事务中完成的操作是原子性的,就能保证一致性。
      • 多线程事务:需依赖事务隔离级别来保证一致性,例如读已提交、可重复读,以及串行化等。必须采用锁或其他机制来处理并发冲突。

      锁的使用

      • 单线程事务:通常不会涉及复杂的锁管理。
      • 多线程事务:需要使用锁或其他并发控制机制(如乐观锁、悲观锁)来处理数据竞争,提高系统的并发性,同时减少冲突。

      性能

      • 单线程事务:性能简单易测,因为没有其他操作对于事务的直接影响。
      • 多线程事务:性能会受到并发访问的影响,需要考虑锁的争用和等待时间,以及如何平衡事务的执行效率。

      2.3、联系

      事务的性质

      • 无论是单线程还是多线程,事务均有原子性、一致性、隔离性和持久性(ACID)属性。
      • 这些属性是保证数据正确性和完整性的重要原则。

      使用的技术

      • 两者都可以应用于数据库系统中,无论是在单用户环境还是多用户环境,确保数据的完整性和一致性。

      设计考虑

      • 设计事务时,单线程和多线程的事务处理都需要考虑数据的一致性和隔离性。
      • 虽然实现细节不同,但在设计原则上是相通的。

      2.4、DataSourceTransactionManager

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

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

      2.5、TransactionSynchronizationManager

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

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

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

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

      代码如下图所示:

      public void bindResource(Objecthttp://www.devze.com 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了,这样方法和嵌套方法用的就python是同一个事务了。

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

      扩展:

      编程式事务中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) {
                  tryeYPfc {
                      thread.join();  // 等待每个子线程完成
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
      
              context.close();
          }
      }

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

      总结

      以上www.devze.com为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜