使用多线程批量插入数据到数据库
本文采用线程池的形式批量插入数据。
1.创建线程池以及核心参数简介:
/**
* @Author yfx
* @Date 2021/7/15
* @Description
*/
public class ThreadPoolUtils {
// 核心线程数
private static final int CORE_POOL_SIZE = 4;
// 最大线程数
private static final int MAX_NUM_POOL_SIZE = 6;
// 时间单位
private static final TimeUnit UNIT = TimeUnit.SECONDS;
// 线程存活时间 此处定义为5分钟
private static final long KEEP_ALIVE_TIME = 60*5;
// 阻塞队列,用于存储待执行的线程,队列长度根据实际情况定义
private static final BlockingQueue WORK_QUEUE = new ArrayBlockingQueue(600);
// 通过核心参数,使用new ThreadPoolExecutor()的构造函数的方式创建线程池,不建议使用Executors来创建线程,Executors中的阻塞队列没有定义长度,可以一直向阻塞队列中添加任务,可能造成内存不足
public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_NUM_POOL_SIZE,
KEEP_ALIVE_TIME,
UNIT,WORK_QUEUE);
// 执行线程,提交到线程池中,无返回值,不利于异常捕获
public static void execute(Runnable runnable) {
threadPoolExecutor.execute(runnable);
}
// 提交到线程池,有返回值,可以实现异常捕获
public static Future submit(Callable callable) {
return threadPoolExecutor.submit(callable);
}
// 设置线程池状态
// 线程池状态:RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED
public static void shutdown() {
threadPoolExecutor.shutdown();
}
2.介绍线程池控制变量ctl:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
线程池控制变量ctl: AtomicInteger类型:32位二进制,通过CAS+volitale实现,部分源码:
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
ctl在线程池中的意义:
1.线程池状态 前3位
2.线程池工作线程数 后29位
以下是源码内容以及本人的注释理解:
// 原子类AtomicInteger,保证线程安全,保证线程安全需要:原子性,可见性,有序性
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 总工作线程二进制位数
private static final int COUNT_BITS = Integer.SIZE - 3;
// 总工作线程数
// 000 11111111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// 101 00000000000000000000000000000000
private static final int RUNNING = -1 << COUNT_BITS;
// 000 00000000000000000000000000000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// SHUTDOWN不接收新的线程,执行完阻塞队列中的线程
// 001 00000000000000000000000000000000
private static final int STOP = 1 << COUNT_BITS;
// 010 00000000000000000000000000000000
private static final int TIDYING = 2 << COUNT_BITS;
// 011 00000000000000000000000000000000
private static final int TERMINATED = 3 << COUNT_BITS;
// 线程池状态:RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED
// Packing and unpacking ctl
// 与运算后值只有前3位可能为1
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 与运算后值只有后29位可能为1
private static int workerCountOf(int c) { return c & CAPACITY; }
// 得到一个完整的线程池控制变量
private static int ctlOf(int rs, int wc) { return rs | wc; }
3.介绍多线程使用实例:
controller
/**
* @Author yfx
* @Date 2021/7/15
* @Description
*/
@RestController
public class BatchInsertController {
@Autowired
private BatchInsertService batchInsertService;
@GetMapping
public String BatchInsert() throws InterruptedException {
return batchInsertService.batchInsertByThreadPool();
}
}
service
@Service
@Slf4j
public class BatchInsertServiceImpl implements BatchInsertService {
@Autowired
private BatchInsertMapper batchInsertMapper;
@Override
public String batchInsertByThreadPool() throws InterruptedException {
List<User> userList = new ArrayList<>(100*10000);
User user = new User(0, "测试", "测试", "测试", "测试", "测试");
log.info("初始化数据");
for (int i = 0; i<100*10000; i++) {
userList.add(user);
}
log.info("初始化数据完毕{}"+userList.size()+"条");
List<List<User>> partitionList = Lists.partition(userList, 2000);
log.info("数据分组完毕{}"+partitionList.size()+"条");
CountDownLatch countDownLatch = new CountDownLatch(partitionList.size());
log.info("开始执行任务");
long begin = System.currentTimeMillis();
partitionList.forEach(x->ThreadPoolUtils.execute(()->{
batchInsertMapper.batchInsert(x);
countDownLatch.countDown();
}));
countDownLatch.await();
log.info("执行完毕,耗时:{}"+(System.currentTimeMillis()-begin)+"ms");
return "多线程插入数据成功";
}
}
mapper
可能多个表都需要执行批量插入,不用反复在mapper接口中定义方法,所以创建了一个BaseMapper接口,其他要使用批量插入的接口继承该接口
/**
* @Author yfx
* @Date 2021/7/15
* @Description
*/
public interface BaseMapper {
void batchInsert(@Param("data") List<User> data);
}
/**
* @Author yfx
* @Date 2021/7/15
* @Description
*/
@Repository
public interface BatchInsertMapper extends BaseMapper {
}
mapper.xml
<insert id="batchInsert">
insert into user(
uid,
username,
password,
nickname,
gender,
photo
)
values
<foreach collection="data" item="item" index="index" separator=",">
(
#{item.uid},
#{item.username},
#{item.password},
#{item.nickname},
#{item.gender},
#{item.photo}
)
</foreach>
</insert>
执行结果:向mysql中插入100万条数据使用了30秒钟
4.补充说明:
由于本文使用的是多线程插入,所以通常我们使用的@Transactional事务控制不起作用,笔者使用的场景是向空表中添加测试数据,并且一个线程只执行官一次批量操作,所以如果出现异常是将表格式化 truncate table 表名,如果读者想要实现多线程事务控制,可采用list记录下没个线程执行结果的状态,如果有线程执行失败,则回滚所有数据。
5.介绍下计数器
// 创建时设置最大的计数器值
CountDownLatch countDownLatch = new CountDownLatch(partitionList.size());
// 常规条件下,执行完一个线程执行一次countDown()方法,使得计数器值减1,
countDownLatch.countDown();
// 使当前调用此方法的线程阻塞,当countDownLatch计数器的值减为0时被唤醒,本文条件下:即当多线程批量插入执行完毕后再唤醒主线程
countDownLatch.await();
详细实现原理读者有兴趣可参考该文章:
https://blog.csdn.net/u010517268/article/details/113405518