多线程事务如何保证效率和原子性

文章探讨了在Spring开发中,如何通过多线程和批处理提高Excel数据导入数据库的效率,并介绍了如何使用自定义的事务管理和并发控制(如CountDownLatch和Volatile)来确保数据的原子性和一致性。通过逐步优化方案,从顺序插入到批量处理再到多线程并行处理,最后处理了线程间事务协调的问题,以应对可能出现的错误和回滚需求。

多线程事务

在Spring开发时,遇到一个从Excel表导入数据到数据库的需求,当然最简单的方法就是先使用EasyExcel把数据读出到集合中,然后依次插入到数据库中。

但如何保证效率,原子性呢?我们一步步优化方案。这里不会引入不必要的组件,而是自己模拟类似的思想。

方法1:依次顺序插入

void test() {
        List<User> users = getAllUsers();
        users.forEach(user -> userService.save(user));
    }
复制代码

方法2:使用批处理,一次操作中执行多条SQL

void test() {
    List<User> users = getAllUsers();
    userService.saveBatch(users);
}
复制代码

方法3:使用多线程+批处理,每个线程插入多条数据

需要注意的一点,Spring容器不允许线程注入,也就是没办法在多线程直接使用Bean操作,例如:

void testThread() {
// 下面两种方式是无效的,不会执行任何东西
Runnable runnable = () -> {
         userService.save(new User());
     };
// 方法1
new Thread(runnable).start();
// 方法2
Executors.newFixedThreadPool(1).submit(runnable);
}
复制代码

我们需要下面的方式进行执行

void testThread() {
     Runnable runnable = () -> {
         userService.save(new User());
     };
     ExecutorService executorService = Executors.newFixedThreadPool(1);
     CompletableFuture<Void> future = CompletableFuture.runAsync(runnable, executorService);
     future.join();
 }
​
复制代码
void testThread() {
    int threadSize = 5;
    ExecutorService executorService = Executors.newFixedThreadPool(threadSize);
    List<List<User>> list = new ArrayList<>();
    for (int i = 0; i < threadSize; i++) {
        // 我们假设数据拆分为五分
        list.add(getAllUsers());
    }
    for (List<User> users : list) {
        CompletableFuture.runAsync(()->{
            userService.saveBatch(users);
        },executorService).join();
    }
    System.out.println("插入成功");
}
复制代码

方法4:这时候速度已经很快了,但是如果其中一个线程插入数据时发生错误进行回滚,其他线程是无法得知的,因为事务是针对线程的,所以这里我们需要用一些方式保证每个线程之间的状态是被共享的。

// UserService#saveUserSyn()
@Override
public boolean saveUserSyn(List<User> users, CountDownLatch threadLatch, CountDownLatch mainLatch, UserError hasError) {
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        System.out.println("子线程:" + Thread.currentThread().getName());
        try {
            users.forEach(this::save);
        } catch (Throwable e) {
            hasError.setHasError(true);
        } finally {
            threadLatch.countDown(); // 切换到主线程执行
        }
        try {
            mainLatch.await();  //等待主线程执行
        } catch (Throwable e) {
            hasError.setHasError(true);
        }
        // 判断是否有错误,如有错误 就回滚事务
        if (hasError.isHasError()) {
            dataSourceTransactionManager.rollback(transactionStatus);
        } else {
            dataSourceTransactionManager.commit(transactionStatus);
        }
        return true;
    }
复制代码
// 测试方法
@Test
    void userSaveSyn() {
​
        List<User> userList = getAllUsers();
​
        // 添加一个错误数据
        User user = new User();
        user.setUserAccount(null);
        user.setUserPassword("123456");
        userList.add(user);
​
        // 线程数量
        final Integer threadCount = 4;
​
        //每个线程处理的数据量
        final Integer dataPartionLength = (userList.size() + threadCount - 1) / threadCount;
​
        // 创建多线程处理任务
        ExecutorService studentThreadPool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch threadLatchs = new CountDownLatch(threadCount); // 用于计算子线程提交数量
        CountDownLatch mainLatch = new CountDownLatch(1); // 用于判断主线程是否提交
​
        for (int i = 0; i < threadCount; i++) {
            // 每个线程处理的数据
            List<User> threadDatas = userList.stream()
                    .skip(i * dataPartionLength).limit(dataPartionLength)
                    .collect(Collectors.toList());
            studentThreadPool.execute(() -> {
                userService.saveUserSyn(threadDatas, threadLatchs, mainLatch, hasError);
            });
        }
​
        try {
            // 倒计时锁设置超时时间 30s
            boolean await = threadLatchs.await(30, TimeUnit.SECONDS);
            if (!await) { // 等待超时,事务回滚
                hasError.setHasError(true);
            }
        } catch (Throwable e) {
            e.printStackTrace();
            hasError.setHasError(true);
        }
        mainLatch.countDown(); // 切换到子线程执行
        studentThreadPool.shutdown(); //关闭线程池
        System.out.println("主线程完成");
    }
复制代码

这里我们使用CountDownLatchVolatile来解决这个问题。

CountDownLatch的语法与原理讲解

Volatile保证线程间数据的可见性

2PC(两阶段提交),这个属于分布式事务的一个理论,这里模拟了这样的业务场景,大致流程为:

  • 每个线程开启事务,插入数据,但不提交,向主线程通知说,我这里已经好了
  • 主线程等待一段时间,看是否所有的子线程都没问题了。如果超时也算是异常
  • 如果没有异常,主线程向所有子线程通知,可以提交事务
  • 如果有异常,主线程向所有子线程通知,进行回滚操作
  • 而中间使用Volatile修饰的hasError对象进行传达,是否出现异常。需要注意如果只是传递普通的boolean对象,可能会发生不一致的情况,我测试时没法通过。
  • CountDownLatch则保证子线程在主线程没有通知前,是不能提交事务的。

这里细心些就会发现,即便是主线程通知子线程可以提交了,子线程依然有可能出现提交失败的可能,那其他线程提交事务是无法得知这边的失败的消息的。这就是我们其实无法在一个Java进程中保证多线程的原子性。

作者:啵啵肠
原文链接:https://juejin.cn/post/7195078084830167097
 

<think>好的,我现在需要回答用户关于“多线程保证事务强一致性”的问题。首先,我得理解这两个概念:多线程事务的强一致性。多线程是指程序中同时运行多个线程,提高效率,但会带来并发问题。事务的强一致性则要求事务执行后,系统从一个一致状态变到另一个一致状态,所有操作要么全部完成,要么全部不完成。 接下来,我需要考虑多线程环境下如何保证事务的强一致性。常见的挑战包括竞态条件、死锁、数据可见性等问题。可能的解决方案包括锁机制、事务隔离级别、原子操作、CAS(比较并交换)等。 然后,我需要将这些技术具体化,说明它们如何应用在多线程事务中。比如,使用互斥锁或同步块来保证同一时间只有一个线程访问共享资源;数据库中的事务隔离级别如可串行化(Serializable)如何防止脏读、不可重复读等问题;原子类如AtomicInteger如何确保操作的原子性。 另外,可能需要提到分布式系统中的两阶段提交(2PC)或三阶段提交(3PC),但用户的问题可能更偏向于单机多线程而非分布式系统,所以需要区分清楚。如果涉及分布式,可能需要扩展,但先专注于单机多线程的情况。 还要考虑例子是否合适,比如银行转账操作,如何通过锁事务保证一致性。同时,要指出实现强一致性的代价,比如性能下降、复杂度增加,以及可能影响系统的吞吐量。 最后,需要结构化回答,分点说明挑战、解决方案、实现方式、示例以及注意事项,确保用户能够逐步理解。需要检查技术术语的准确性,比如是否正确区分了事务的ACID特性中的一致性,以及多线程中的并发控制机制。 可能存在的误区是混淆了数据库事务编程中的线程同步机制,需要明确两者在不同层面的应用。确保回答中提到的技术如锁、CAS、原子操作属于编程层面的多线程控制,而事务隔离级别属于数据库层面,但两者都是为了实现一致性。 总结步骤:解释基本概念,分析挑战,列举解决方案,给出例子,讨论优缺点,最后总结应用场景注意事项。需要确保语言通俗易懂,结构清晰,符合用户要求的中文回答。</think>多线程环境下保证事务强一致性是一个复杂但关键的问题,需结合并发控制与事务管理技术。以下从原理到实践逐步解析: ### 一、核心概念 1. **强一致性** 事务完成后,系统必须处于一致状态,所有操作要么全部生效,要么全部回滚(ACID中的C)。 2. **多线程挑战** 并行执行可能导致: - 竞态条件(Race Conditions) - 脏读/幻读(Dirty Reads/Phantom Reads) - 死锁(Deadlocks) ### 二、关键技术方案 #### 1. 锁机制 - **互斥锁(Mutex)** 确保临界区代码串行执行: ```java synchronized(lock) { // 事务操作:如账户A向账户B转账 accountA.debit(amount); accountB.credit(amount); } ``` - **读写锁(ReadWriteLock)** 提升读多写少场景的性能,允许并发读但互斥写。 #### 2. 原子操作 - **CAS(Compare-And-Swap)** 硬件级指令实现无锁同步,例如Java的`AtomicInteger`: ```java AtomicInteger balance = new AtomicInteger(100); balance.compareAndSet(100, 90); // 线程安全的更新 ``` #### 3. 事务隔离级别(数据库层面) - **可串行化(Serializable)** 最高隔离级别,通过锁或MVCC强制事务串行执行。 - 代码示例(SQL): ```sql SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; ``` #### 4. 两阶段提交(2PC,分布式场景) - 协调者协调多个节点预提交(Phase 1)最终提交(Phase 2)。 - 缺点:存在单点故障风险。 ### 三、典型场景示例 **银行转账操作实现强一致性:** ```java public void transfer(Account from, Account to, int amount) { Object firstLock = from.getId() < to.getId() ? from : to; Object secondLock = from.getId() < to.getId() ? to : from; synchronized(firstLock) { synchronized(secondLock) { if (from.getBalance() >= amount) { from.debit(amount); to.credit(amount); } } } } ``` *关键点:按固定顺序加锁避免死锁,双锁保护共享资源。* ### 四、实现代价与权衡 | 方案 | 一致性强度 | 性能代价 | 适用场景 | |---------------|------------|----------|------------------------| | 悲观锁 | 高 | 高 | 写密集型操作 | | 乐观锁(CAS) | 中 | 低 | 读多写少 | | 可串行化隔离 | 最高 | 最高 | 金融交易等强一致需求 | ### 五、注意事项 1. **粒度控制** 锁范围过大会降低并发度,过小可能引发数据竞争。 2. **死锁预防** 使用超时机制或按固定顺序获取锁。 3. **性能监控** 通过线程转储(Thread Dump)分析锁竞争情况。 ### 总结 多线程事务强一致性的核心在于**精细化并发控制**与**合理选择隔离机制**。需根据业务场景在一致性与性能之间权衡,通常需结合数据库事务(如MySQL的InnoDB引擎)与应用层锁(如Java的`ReentrantLock`)共同实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值