Springboot の章の全体的なコラム:
[1] springboot は Swagger を統合します (非常に詳細な
[2] springboot は swagger (カスタム) を統合します (非常に詳細)
[4] springboot は mybatis-plus を統合します (非常に詳細) (オン)
[5] springboot は mybatis-plus を統合します (非常に詳細) (下記)
[6] springboot はカスタムのグローバル例外処理を統合します
[7] springboot は redis を統合します (非常に詳細)
[8] springbootはAOPを統合してログ操作を実現します(超詳細)
[9] springboot 統合タイミング タスク (超詳細)
[10] springboot は redis を統合してスタートアップ サービスを実現します。つまり、ホットスポット データをグローバルと redis に保存します (超詳細)
[イレブン] スプリングブートはクォーツを統合してタイミングタスクの最適化を実現します(超詳細)
[12] springboot はスレッド プールを統合して高い同時実行性を解決します (非常に詳細で、理解が容易です)
【その13】springbootは非同期呼び出しを統合して戻り値を取得する(超詳細)
[14] springboot は WebService を統合します (超詳細)
[15] springboot は WebService を統合します (パラメーターの受け渡しについて) (超詳細)
[16] springboot は WebSocket を統合します (超詳細)
[Seventeen] springboot が WebSocket を統合してチャット ルームを実現 (超詳細)
[18] springboot はカスタムのグローバル例外処理を実装します
[Nineteen] springboot は ElasticSearch の実戦を統合します (1 万文字)
[21] springboot は実戦でのインターセプターを統合し、フィルターを比較します
[22] springboot統合活動7 (1) 実践的なデモンストレーション
【23】springboot統合スプリングビジネスの詳細説明と実戦
[24] springbootはEasyExcelとスレッドプールを使用してExcelデータのマルチスレッドインポートを実現します
[25] springboot は、キャッシュの侵入を処理するために jedis および redisson Bloom フィルターを統合します
[26] springboot はマルチスレッド トランザクション処理を実装します_springboot マルチスレッド トランザクション
[27] springbootはthreadLocal+パラメータパーサーを通じて現在のログイン情報をセッションと同様に保存する機能を実現します
前回までの24章ではExcelデータのマルチスレッドインポートを実現するSpringbootのデモをEasyExcelとスレッドプールを使って作成しましたが、書いているときにトランザクション処理をするのを忘れていました、コメント欄の偉い人が提案してくれました。は約 24 章のコードは、マルチスレッドのトランザクション処理を改善するために変更されています。
springbootのトランザクション処理については、第23章でspringboot統合のスプリングトランザクションと実戦学習について詳しく説明しましたが、マルチスレッドの場合はこのことが当てはまらず、本章では手書きのトランザクション処理(プログラムによるトランザクション処理)を使用します。
この章は、第 24 章の一括インポート機能を拡張したものであるため、トランザクション処理 (第 24 章の内容) に関係のない紹介は書きません。
QQ 交換グループ ナビゲーション ——> 231378628
目次
1. 目的と実施方法を説明する
前章で実装したマルチスレッド処理Excelインポート機能では、サブスレッドにエラーが発生すると、そのサブスレッドのデータは処理できなくなりますが、他のサブスレッドのデータは処理され続けます。コードを変更してトランザクション処理を実装すると、すべてのスレッドは通常に実行した場合にのみデータを保存し、それ以外の場合はロールバックします。おおよそ次のとおりです。
2. 子スレッドに手動でエラーを報告させる
後でトランザクションのロールバックをテストするには、次のように、手動でサブスレッド (スレッド 3 という名前のサブスレッドなど) にエラーを報告させます。
3. メインスレッドを変換する
上図の考え方に従って、まずメインスレッドのコードを変更します。全体のコードは次のようになります。
package com.swagger.demo.service;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.swagger.demo.config.SpringJobBeanFactory;
import com.swagger.demo.mapper.DeadManMapper;
import com.swagger.demo.model.entity.DeadManExcelData;
import com.swagger.demo.thread.DeadManThread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author zrc
* @version 1.0
* @description: TODO 最新入狱名单导入监听器
*
* @date 2022/5/30 15:56
*/
@Service
@Slf4j
@Component
public class DeadManExcelListener extends AnalysisEventListener<DeadManExcelData> {
/**
* 多线程保存集合,使用线程安全集合
*/
private List<DeadManExcelData> list = Collections.synchronizedList(new ArrayList<>());
/**
* 创建线程池必要参数
*/
private static final int CORE_POOL_SIZE = 10; // 核心线程数
private static final int MAX_POOL_SIZE = 100; // 最大线程数
private static final int QUEUE_CAPACITY = 100; // 队列大小
private static final Long KEEP_ALIVE_TIME = 1L; // 存活时间
public List<DeadManExcelData> getData(){
return list;
}
public DeadManExcelListener(){
}
public void setData(List<DeadManExcelData> deadManExcelDataList){
this.list = deadManExcelDataList;
}
@Override
public void invoke(DeadManExcelData deadManExcelData, AnalysisContext analysisContext) {
if(deadManExcelData!=null){
list.add(deadManExcelData);
}
}
/**
* 多线程方式保存
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("解析结束,开始插入数据");
// 创建线程池
ExecutorService executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
// 指定每个线程需要处理的导入数量,假设每个线程处理15000条,注意配合上面线程池的大小
int singleThreadDealCount = 15000;
// 根据假设每个线程需要处理的数量以及总数,计算需要提交到线程池的线程数量
int threadSize=(list.size()/singleThreadDealCount)+1;
// 计算需要导入的数据总数,用于拆分时线程需要处理数据时使用
int rowSize = list.size() + 1;
// 测试开始时间
long startTime = System.currentTimeMillis();
// 申明该线程需要处理数据的开始位置
int startPosition = 0;
// 申明该线程需要处理数据的结束位置
int endPosition = 0;
// 为了让每个线程执行完后回到当前线程,使用CountDownLatch,值为线程数,每次线程执行完就会执行countDown方法减1,为0后回到主线程,也就是当前线程执行后续的代码
CountDownLatch count = new CountDownLatch(threadSize);
// 用来控制主线程回到子线程
CountDownLatch mainCount = new CountDownLatch(1);
// 用来控制最终回到主线程
CountDownLatch endCount = new CountDownLatch(threadSize);
// 用来存放子线程的处理结果,若出错就保存一个false
CopyOnWriteArrayList<Boolean> sonResult = new CopyOnWriteArrayList<Boolean>();
// 使用线程安全的对象存储,保存主线程最后总的判断结果,是提交还是回滚
AtomicBoolean ifSubmit = new AtomicBoolean(true);
// 计算每个线程要处理的数据
for(int i=0;i<threadSize;i++){
// 如果是最后一个线程,为保证程序不发生空指针异常,特殊判断结束位置
if((i+1)==threadSize){
// 计算开始位置
startPosition = (i * singleThreadDealCount);
// 当前线程为划分的最后一个线程,则取总数据的最后为此线程的结束位置
endPosition = rowSize-1;
}else{
// 计算开始位置
startPosition = (i * singleThreadDealCount);
// 计算结束位置
endPosition = (i + 1) * singleThreadDealCount;
}
DeadManMapper deadManMapper = SpringJobBeanFactory.getBean(DeadManMapper.class);
DeadManThread thread = new DeadManThread(count,deadManMapper,list,startPosition,endPosition
,sonResult,mainCount,ifSubmit,endCount);
executor.execute(thread);
}
try {
count.await();
for (Boolean resp : sonResult) {
if (!resp) {
// 只要有一个子线程出异常,就设置最终结果为回滚
log.info("主线程:有线程执行失败,所有线程需要回滚");
ifSubmit.set(false);
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 回到子线程处理回滚或者提交事务
mainCount.countDown();
}
try {
endCount.await();
// 逻辑处理完,关闭线程池
executor.shutdown();
long endTime = System.currentTimeMillis();
log.info("总耗时:"+(endTime-startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
次の 4 つのパラメータを追加します (count は前の章の既存のものです)。
PS: CountDownLatch クラスで前述したように、複数のスレッド間の切り替えは、await メソッドと countDown メソッドを使用して簡単に実現できます。
CopyOnWriteArrayList と AtomicBoolean は、複数のスレッドで共有されるデータをスレッドセーフに保存するためのものです。
次に、DeadManThread スレッド クラスの構築メソッドを書き換え、構築メソッドを通じて上記の 4 つの新しいパラメータを子スレッドに渡します。次に、サブスレッドの最初の cout を記録するawaitメソッドを呼び出し、サブスレッドが初めて実行を終了するのを待ち、メインスレッドに戻って実行を継続します。メインスレッドに戻った後、メインスレッドはサブスレッドの最初の実行後に保存されたリターンセットを判定し、falseがあるかどうかを判定します(サブスレッドがエラーを報告した場合はfalseを保存し、それ以外の場合はtrueを保存します)。false がある場合は、idsubmit を false に設定します。これは、データをロールバックする必要があることを意味します。その後、メインスレッドの実行を記録する mainCount の countDown メソッドを呼び出し、メインスレッドの実行を終了させ、位置に戻ります。ここで、サブスレッドはmainCount.countDown を呼び出して、サブスレッドの実行を継続します。
子スレッドは ifSubmit に従ってトランザクション操作をロールバックするかサブミットするかを判断した後、メインスレッドに戻り、メインスレッドはスレッド プールを閉じます。
4. サブスレッドの変換
次に、子スレッドを変換します。全体のコードは次のようになります。
package com.swagger.demo.thread;
import com.swagger.demo.config.SpringJobBeanFactory;
import com.swagger.demo.mapper.DeadManMapper;
import com.swagger.demo.model.entity.DeadMan;
import com.swagger.demo.model.entity.DeadManExcelData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author zrc
* @version 1.0
* @description TODO
* @date 2022/7/22 15:40
*/
@Component
@Slf4j
public class DeadManThread implements Runnable{
/**
* 当前线程需要处理的总数据中的开始位置
*/
private int startPosition;
/**
* 当前线程需要处理的总数据中的结束位置
*/
private int endPosition;
/**
* 需要处理的未拆分之前的全部数据
*/
private List<DeadManExcelData> list = Collections.synchronizedList(new ArrayList<>());
/**
* 记录子线程第一次执行是否完成
*/
private CountDownLatch count;
private DeadManMapper deadManMapper;
/**
* 保存每个线程的执行结果
*/
private CopyOnWriteArrayList<Boolean> sonResult;
/**
* 记录主线程是否执行过判断每个线程的执行结果这个操作
*/
private CountDownLatch mainCount;
/**
* 记录主线程对每个线程的执行结果的判断
*/
private AtomicBoolean ifSubmit;
/**
* 声明该子线程的事务管理器
*/
private DataSourceTransactionManager dataSourceTransactionManager;
/**
* 声明该线程事务的状态
*/
private TransactionStatus status;
/**
* 记录子线程第二次执行是否完成
*/
private CountDownLatch endCount;
public DeadManThread() {
}
public DeadManThread(CountDownLatch count, DeadManMapper deadManMapper, List<DeadManExcelData> list
, int startPosition, int endPosition, CopyOnWriteArrayList<Boolean> sonResult,CountDownLatch mainCount
,AtomicBoolean ifSubmit,CountDownLatch endCount) {
this.startPosition = startPosition;
this.endPosition = endPosition;
this.deadManMapper = deadManMapper;
this.list = list;
this.count = count;
this.sonResult = sonResult;
this.mainCount = mainCount;
this.ifSubmit = ifSubmit;
this.endCount = endCount;
}
@Override
public void run() {
try{
dataSourceTransactionManager = SpringJobBeanFactory.getBean(DataSourceTransactionManager.class);
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
status = dataSourceTransactionManager.getTransaction(def);
if(Thread.currentThread().getName().contains("3")){
throw new RuntimeException("线程3出问题了");
}
List<DeadMan> deadManList = new ArrayList<>();
List<DeadManExcelData> newList = list.subList(startPosition, endPosition);
// 将EasyExcel对象和实体类对象进行一个转换
for (DeadManExcelData deadManExcelData : newList) {
DeadMan deadMan = new DeadMan();
BeanUtils.copyProperties(deadManExcelData, deadMan);
deadManList.add(deadMan);
}
// 批量新增
deadManMapper.insertBatchSomeColumn(deadManList);
sonResult.add(true);
} catch (Exception e) {
e.printStackTrace();
sonResult.add(false);
} finally {
// 当一个线程执行完了计数要减一不然这个线程会被一直挂起
count.countDown();
try {
log.info(Thread.currentThread().getName() + ":准备就绪,等待其他线程结果,判断是否事务提交");
mainCount.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ifSubmit.get()) {
dataSourceTransactionManager.commit(status);
log.info(Thread.currentThread().getName() + ":事务提交");
} else {
dataSourceTransactionManager.rollback(status);
log.info(Thread.currentThread().getName() + ":事务回滚");
}
// 执行完所有逻辑,等待主线程执行
endCount.countDown();
}
}
}
まず、メインスレッドによって渡されたパラメータを受け入れ、それらを独自の内部プライベートに設定するように構築メソッドを変換します。
次に、実行メソッドを変更します。
最初に独自のトランザクションを開始し、後続のコミットまたはロールバック操作のためにトランザクションの状態を保存します。データの処理後、正常に終了した場合はスレッドセーフな戻り値セット変数を true として保存し、それ以外の場合は false を保存し、最初のスレッドによって実行されたカウントを記録する countDown メソッドを実行し、すべてのサブスレッドが終了するのを待ちます。実行し、メインスレッドに戻って先ほど実行する 上記のsonResult判定のコードは、メインスレッドが判定を実行してifSubmitの値を設定した後、子スレッドに戻ってmain.await以降のコードを実行します。
ifSubmitがfalseの場合はロールバック、そうでない場合はsubmit 実行後、2回目のサブスレッドで実行されたendCountを記録するcountDownメソッドを実行し、すべてのサブスレッドの実行が終了したらメインスレッドに戻り、メインスレッドは、スレッドプールを 閉じるロジックを実行します。
5. テスト
コードを書いたら、テストします。テスト前のデータ:
ここで、スレッド 3 がエラーを報告し、インターフェイス テストを呼び出します。
トランザクションは正常にロールバックされましたが、データはコミットされませんでした。例外をスローするコードを手動で削除した場合は、次のようにプログラムを通常どおり実行させます。
データは正常に送信され、トランザクションは正常に処理されます。
補足:デッドロックの問題(サブスレッド数が10を超える場合に発生)については、springbootのデフォルトのデータソースである可能性があります:hikari:
maximum
-pool-sizeこの属性の結果として、maximum-pool-size: 接続の最大数が 0 以下の場合、デフォルト値の 10 にリセットされます。
hikari は、デフォルトで springboot によって使用されるデータ ソース接続プールです。
サブスレッドが 10 個を超えているにもかかわらず、接続の最大数が 10 個のみの場合、後続のサブスレッドはデータベースに接続できず、データベースに接続されている 10 個のスレッドは解放されず、その結果、行き詰まり。