之前写过一篇文章是关于多线程如何操作数据库,且控制事务的全局回滚,今天继续上一次进行扩展,上一次主要是针对单个线程操作没有返回值,而有时候我们希望进行多个线程批量操作数据库的同时,能返回每次成功插入到数据库的主键,这个时候就需要callable接口上场了,但是如果是希望线程执行结果是有返回的,还有很多地方需要注意的!特别是多个子线程和主线程协同操作,下面看一下具体的业务场景描述
场景描述:
现在有1万条数据需要插入到数据库,考虑到性能,我们会开启多个线程并发插入,来提高我们的程序插入性能
具体要求:
1 多线程插入操作
2 一个线程操作失败,需要回滚之前所有已经成功执行的线程
3 每个线程插入完以后,要返回对应的成功插入的主键
4 在插入数据之前,需要先进行删除或者更新操作,
那么要实现上面的功能,可以先简单画一个逻辑分析图,比如下面所示
首先因为需要返回值,所以我们可能需要创建一个包装线程执行结果的对象
package com.abcft.thread.transaction.common;
//包装线程执行的结果
public class CalcResult {
private boolean success;
private int result;
public boolean isSuccess() {
return success;
}
public void setResult(int result) {
this.result = result;
}
public void setSuccess(boolean success) {
this.success = success;
}
public int getResult() {
return result;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
}
封装回滚对象
package com.abcft.thread.transaction.common;
public class DataRollBack {
public DataRollBack(boolean isRollBack) {
this.isRollBack = isRollBack;
}
//事务是否回滚 true回滚 false不回滚
public boolean isRollBack() {
return isRollBack;
}
public void setRollBack(boolean rollBack) {
isRollBack = rollBack;
}
private boolean isRollBack;
}
封装模拟事务管理操作
public class TransactionManager {
/**
* 模拟事务回滚
*/
public void rollBack(){
System.out.println("线程:"+Thread.currentThread().getName()+"执行事务回滚");
}
/**
* 模拟事务提交
*/
public void commit(){
System.out.println("线程:"+Thread.currentThread().getName()+"开始提交数据");
}
}
模拟数据插入操作
package com.abcft.thread.transaction.common;
//模拟插入数据库的操作
public class InsertService {
private UpdateService updateService = new UpdateService();
// TODO: 2019-12-17 假设这里是使用spring来管理数据源以及事务
//@autowired
// JdbcTemplate jdbcTemplate
public boolean insert(String data){
// jdbcTemplate.insert(data)
updateService.update(data);
System.out.println("开始保存数据到数据库中");
if(Thread.currentThread().getName().contains("thread-4") || Thread.currentThread().getName().contains("Thread-2"))
return false;
return true;
}
}
模拟数据更新操作
package com.abcft.thread.transaction.common;
//模拟更新操作
public class UpdateService {
// TODO: 2019-12-17 假设这里是使用spring来管理数据源以及事务
//@autowired
// JdbcTemplate jdbcTemplate
public boolean update(String sql){
//jdbcTemplate.update(sql);
return true;
}
}
封装数据业务逻辑相关操作
public class DataService {
// 插入数据
private InsertService insertService = new InsertService();
// 修改数据
private UpdateService updateService = new UpdateService();
public CalcResult execute(String data){
boolean insert = insertService.insert(data);
boolean update = updateService.update(data);
CalcResult calcResult = new CalcResult();
if (insert && update){
calcResult.setSuccess(true);
}
else {
calcResult.setSuccess(false);
}
return calcResult;
}
}
封装单个线程执行数据插入的操作
package com.abcft.thread.transaction.future;
import com.abcft.thread.transaction.common.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
@SuppressWarnings("all")
public class DataCallable implements Callable<CalcResult> {
//业务方法
private DataService dataService;
//主线程
private CountDownLatch mainCountDownLatch;
//子线程
private CountDownLatch childCountDownLatch;
//保存各个子线程执行的结果
private BlockingQueue<Boolean> resultList;
//事务管理
private TransactionManager transactionManager;
//数据结果执行决定是否回滚
private DataRollBack dataRollBack;
private CalcResult calcResult;
public DataCallable(DataService dataService, CountDownLatch mainCountDownLatch, CountDownLatch childCountDownLatch, BlockingQueue<Boolean> resultList,
TransactionManager transactionManager, DataRollBack dataRollBack, CalcResult calcResult) {
this.dataService = dataService;
this.mainCountDownLatch = mainCountDownLatch;
this.childCountDownLatch = childCountDownLatch;
this.resultList = resultList;
this.transactionManager = transactionManager;
this.dataRollBack = dataRollBack;
this.calcResult = calcResult;
}
@Override
public CalcResult call() throws Exception {
childCountDownLatch.countDown();
int result1 = calcResult.getResult();
calcResult.setResult(result1*10);
System.out.println(Thread.currentThread().getName()+"子线程开始执行任务");
// TODO: 2019-12-17 这块需要注意了 有可能会出现 Lock wait timeout exceeded; try restarting transaction
// TODO: 2019-12-17 原因是因为这里出现对数据库的多中操作 比如 插入操作 修改操作 导致 出现了事务层面的锁,也就是我们说的数据库层面的死锁
// TODO: 2019-12-17 如何避免这个问题 那么就需要保证 操作和修改操作 不去抢夺事务执行资源
// TODO: 2019-12-17 比如 insertservice 使用的是原生jdbc ,updateservice 使用的是基于spring 托管的jdbctemplate 这样它们就不会出现资源上的抢夺
// TODO: 2019-12-17 当然 方法不止上面一种,主要能保证不同的数据库事务操作 对应不同的事务操作资源即可
CalcResult execute = dataService.execute("test");
calcResult.setSuccess(execute.isSuccess());
resultList.add(execute.isSuccess());
//子线程执行
//阻塞主线程 回滚事务是由主线程来执行的
try {
// TODO: 2019-12-17 这行代码会导致 如果没有执行到主线程模块的时候,当前这个子线程是永远获取不到结果的 所以一定要注意!!!!
mainCountDownLatch.await();
//在这里遍历
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"子线程执行剩下的任务");
if (dataRollBack.isRollBack()){
transactionManager.rollBack();
}
else {
transactionManager.commit();;
}
return calcResult;
}
}
模拟主线程应用
package com.abcft.thread.transaction.main;
import com.abcft.thread.transaction.common.*;
import com.abcft.thread.transaction.future.DataCallable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* @author hpjia@abcft.com
* @version 1.0
* @date
* @description
*/
@SuppressWarnings("all")
public class DataCallableMain {
public static void main(String[] args)throws Exception {
int CHILD_SUM =5;
//控制主线程执行的开关
CountDownLatch mainCountDownLathc = new CountDownLatch(1);
//控制子线程执行的开关
CountDownLatch childCountDownLathc = new CountDownLatch(CHILD_SUM);
//封装操作是否需要回滚
DataRollBack dataRollBack = new DataRollBack(false);
//实例化业务操作类
DataService dataService = new DataService();
//定义事务管理器 todo 不同的数据库操作 共用这一个事务管理器资源 会出现 Lock wait timeout exceeded; try restarting transaction 这种异常
TransactionManager transactionManager = new TransactionManager();
//存储每个线程执行结果的是否需要回滚的标识
BlockingQueue<Boolean> resultList = new LinkedBlockingDeque<>(10);
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//存储每个线程异步执行的结果
List<Future<CalcResult>> fs = new ArrayList<>();
Future<CalcResult> submit = null;
for (int i=0;i<5;i++){
CalcResult calcResult = new CalcResult();
calcResult.setResult(i);
DataCallable dataCallable = new DataCallable(dataService,mainCountDownLathc,childCountDownLathc,resultList,transactionManager,dataRollBack,calcResult);
submit = executorService.submit(dataCallable);
// TODO: 2019-12-17 这里需要注意submit方法是提交单个任务,且提交完了,不会立马返回结果
// TODO: 2019-12-17 而invokeAll 提交的是多个任务,且是等待所有执行结果返回的,所以可以根据不同的场景来使用不同的方法
fs.add(submit);
}
int sum=0;
//阻塞子线程 子线程不执行完 下面的逻辑不会被执行
try {
childCountDownLathc.await();
// TODO: 2019-12-17 如果在这个地方获取各个子线程的结果 ,会出现在执行 CalcResult calcResult = calcResultFuture.get();
// TODO: 2019-12-17 这行代码一直阻塞住,原因是因为这个线程是需要有返回结果的,而主线 mainCountDownLatch.await();这行代码会
// TODO: 2019-12-17 阻塞住 所以永远获取不到结果 而后面的其它子线程也不会被执行了 出现了互相等待的'死锁' ,所以需要放在执行主线程的代码前面
// for (Future<CalcResult> calcResultFuture:fs){
// CalcResult calcResult = calcResultFuture.get();
// int result = calcResult.getResult();
// System.out.println("res:"+result);
// sum+=result;
// }
System.out.println("主线程开始执行任务");
//通过队列获取结果
for (int i=0;i<CHILD_SUM;i++){
boolean calcResult = resultList.take();
if (!calcResult){
dataRollBack.setRollBack(true);
}
}
// 执行主线程 此时子线程应该是已经执行完了
mainCountDownLathc.countDown();
// TODO: 2019-12-17 执行主线程之前 循环拿到各个子线程的结果 然后再去执行主线程(到了要执行主线程这块代码,说明子线程的代码肯定都是执行完毕了,除非抛了异常)
//提交线程池执行
for (Future<CalcResult> calcResultFuture:fs){
CalcResult calcResult = calcResultFuture.get();
int result = calcResult.getResult();
System.out.println("res:"+result);
sum+=result;
}
// TODO: 2019-12-17 主线程 执行全局操作 事务提交或者回滚
if (dataRollBack.isRollBack()){
System.out.println("执行线程出现了异常,需要进行全局事务回滚");
}
else {
System.out.println("多线程插入数据已经全部完毕且提交事务了");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result:"+sum);
executorService.shutdown();
}
}
总结:使用的过程和注意点 具体看上面代码上的具体注释