java单元测试批处理数据模板【亿点点日志配合分页以及多线程处理】

引入

都说后端开发能顶半个运维,我们经常需要对大量输出进行需求调整,很多时候sql语句已经无法满足我们的需求,此时就需要使用我们熟悉的 java语言结合单元测试写一些脚本来进行批量处理。

相关资料

案例代码获取
视频讲解:

环境准备

可直接使用我分享的工程:
案例代码获取
我这里准备了一个10000条数据的的user表,和对应的一个springboot工程:

@Slf4j
@SpringBootTest(classes = MyWebDemoApplication.class,
		// 配置端口启动,否则获取失败
		webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BatchDemo {

	@Autowired
	private UserMapper userMapper;

}

分页查询处理,减少单次批量处理的数据量级

当我们的数据量很大,并且单个对象也很大时,如果一次查出所有待处理的数据,往往会把我们的对象给撑爆,这时我们可以利用分页的思想将数据拆分,分页去处理

  • 已知数据量总数的分页批处理模板
/**
 * 分页查询处理,减少单次批量处理的数据量级
 * 当前已知数据量总数
 */
@Test
public void test1() {
	// 预定义参数
	int page = 0;
	int pageSize = 5000;
	// 获取总数
	Integer total = userMapper.selectCount(null);
	// 计算页数
	int pages = total / pageSize;
	if (total % pageSize > 0) {
		pages++;
	}

	// 开始遍历处理数据
	for (; page < pages; page++) {
		List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().last(String.format("LIMIT %s,%s", page * pageSize, pageSize)));

		users.forEach(user -> {
			/// 进行一些数据组装操作
		});
		/// 批量 修改/插入 操作


		User lastUser = users.get(users.size() - 1);
		log.info("最后一个要处理的用户的ID为:{},名字:{}", lastUser.getId(), lastUser.getNickName());
	}

}

上面展示的是已知数据量总数的情况,有时候我们是未知总量的,此时可以采用如下写法

  • 未知数据量总数的分页批处理模板
/**
 * 未知总数的写法
 */
@Test
public void test2() {
	// 预定义参数
	int page = 0;
	int pageSize = 500;
	// 开始遍历处理数据
	for (; ; ) {
		List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().last(String.format("LIMIT %s,%s", (page++) * pageSize, pageSize)));
		users.forEach(user -> {
			/// 进行一些数据组装操作
		});
		/// 批量 修改/插入 操作

		if (CollUtil.isNotEmpty(users)) {
			User lastUser = users.get(users.size() - 1);
			log.info("最后一个要处理的用户的ID为:{},名字:{}", lastUser.getId(), lastUser.getNickName());
		}

		if (users.size() < pageSize) {
			break;
		}
	}
}

这里每次输出循环的最后一条数据,帮助我们验证结果:
在这里插入图片描述

补充亿点点日志,更易观察

良好的日志输出能够帮助我们实时了解脚本的运行情况,很多时候每次循环内部都会处理一个耗时操作,这里用已知总数的情况添加日志如下:

  • 起始展示待处理数据总量,总页数,每页条数
  • 每页开始展示当前进度,每页结束暂时,耗时,已处理条数,失败数,最后一条数据信息等
  • 循环内部,每分钟输出一次日志
  • 处理完毕输出总耗时,总条数,失败数,失败数据id集合等
/**
 * 补充亿点点日志
 */
@Test
public void test3() {
	// 预定义参数
	int page = 1;
	int pageSize = 500;
	// 获取总数
	Integer total = userMapper.selectCount(null);
	// 计算页数
	int pages = total / pageSize;
	if (total % pageSize > 0) {
		pages++;
	}

	// 总处理条数
	int count = 0;
	// 成功处理数
	int countOk = 0;
	// 处理失败记录
	List<Integer> wrongIds = new ArrayList<>();
	// 已过分钟数
	int countMinute = 1;

	long start = System.currentTimeMillis();
	// 开始遍历处理数据
	log.info("================== 开始批量处理数据 ==================");
	log.info("待处理条数:{}", total);
	log.info("总页数:{}", pages);
	log.info("每页条数:{}", pageSize);
	for (; page < pages; page++) {
		log.info("================== 当前进度{}/{} ==================", page, pages);
		List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().last(String.format("LIMIT %s,%s", (page - 1) * pageSize, pageSize)));
		for (User user : users) {
			/// 进行一些数据组装操作
			if (user.getId() % 99 == 0) {
				wrongIds.add(user.getId());
			} else {
				countOk++;
			}
			count++;

			/// 模拟耗时操作
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}

			// 每分钟输出一次日志
			if ((System.currentTimeMillis() - start) / 1000 / 60 > countMinute) {
				log.info("已耗时:{} s", (System.currentTimeMillis() - start) / 1000);
				log.info("当前总条数:{}", count);
				log.info("处理成功数:{}", countOk);
				log.info("处理失败数:{}", wrongIds.size());
				log.info("当前处理用户信息:{} : {}", user.getId(), user.getNickName());
				countMinute++;
			}
		}
		/// 批量 修改/插入 操作
		log.info("已耗时:{} s", (System.currentTimeMillis() - start) / 1000);
		log.info("当前总条数:{}", count);
		log.info("处理成功数:{}", countOk);
		log.info("处理失败数:{}", wrongIds.size());
		if (CollUtil.isNotEmpty(users)) {
			User user = users.get(users.size() - 1);
			log.info("{} : {}", user.getNickName(), user.getId());
		}
	}
	log.info("========================== 运行完毕 ==========================");
	log.info("总耗时:{} s", (System.currentTimeMillis() - start) / 1000);
	log.info("总处理条数:{}", count);
	log.info("处理成功数:{}", countOk);
	log.info("处理失败数:{}", wrongIds.size());
	log.info("处理失败数据id集合:{}", wrongIds);
}

效果如下
在这里插入图片描述

多线程优化查询_切数据版

多核CPU才能真正意义上的并行,不然就是宏观并行,微观串行 o(╥﹏╥)o,大家得看下自己的cpu,当然,如果有很多阻塞IO,单核进行切换线程也是能够提高性能的

这里开5个线程,将数据按线程数进行拆分,代码如下:

/**
 * 多线程优化查询,【切数据版 ,按线程数量切割数据,直接处理】
 * + 需要程序进行大量计算
 * + 数据库能承受较大并发
 * + 多核CPU才能真正意义上的并行,不然就是宏观并行,微观串行 o(╥﹏╥)o
 */
@Test
public void test4() {
	// 预定义参数
	int threadNum = 5;

	long start = System.currentTimeMillis();
	// 获取总数
	Integer total = userMapper.selectCount(null);
	// 创建线程池,这里为了简便操作直接用Executors创建,推荐自行集成配置线程池
	ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
	// 设置信号标,用于等待所有线程执行完
	CountDownLatch countDownLatch = new CountDownLatch(threadNum);

	// 计算线程需要处理的数据量的递增步长
	int threadTotalStep = total / threadNum;
	// 判断是否有余数,如果有多出的数据,补给最后一个线程
	int more = total % threadNum;

	// 开启 threadNum 个线程处理数据
	for (int i = 0; i < threadNum; i++) {
		int finalI = i;
		executorService.execute(() -> {
			int current = threadTotalStep * finalI;
			/// 如果有余数,最后一次计算得补充余数
			if (more > 0 && finalI == threadNum - 1) {
				current += more;
			}
			List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().last(String.format("LIMIT %s,%s", current, threadTotalStep)));

			users.forEach(user -> {
				/// 进行一些数据组装操作

				/// 进行一些耗时操作
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			});
			/// 批量 修改/插入 操作

			User user = users.get(users.size() - 1);
			log.info("线程-{} 处理的最后一个数据的id为:{}", finalI + 1, user.getId());
			countDownLatch.countDown();
		});

	}

	try {
		countDownLatch.await();
		executorService.shutdown();
		log.info("总耗时:{} s", (System.currentTimeMillis() - start) / 1000);
	} catch (InterruptedException e) {
		throw new RuntimeException(e);
	}
}

执行结果如下:
在这里插入图片描述

多线程_每个线程都分页处理

如果单个线程处理数据量也很大,此时每个线程都可补充分页进行处理,如下

/**
 * 多线程优化查询,【分页版,先按数量切数据,再在每个线程中分页处理数据】
 * + 需要程序进行大量计算
 * + 数据库能承受较大并发
 * + 多核CPU才能真正意义上的并行,不然就是宏观并行,微观串行 o(╥﹏╥)o
 */
@Test
public void test5() {
	// 预定义参数
	int threadNum = 5; // 线程数
	int pageSize = 500; // 每页处理条数

	long start = System.currentTimeMillis();
	// 获取总数
	Integer total = userMapper.selectCount(null);
	// 创建线程池,这里为了简便操作直接用Executors创建,推荐自行集成配置线程池
	ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
	// 设置信号标,用于等待所有线程执行完
	CountDownLatch countDownLatch = new CountDownLatch(threadNum);

	// 计算线程需要处理的数据量的递增步长
	int threadTotalStep = total / threadNum;
	// 判断是否有余数,如果有多出的数据,补给最后一个线程
	int more = total % threadNum;

	// 开启 threadNum 个线程处理数据
	for (int i = 0; i < threadNum; i++) {
		int finalI = i;
		executorService.execute(() -> {
			/// 数据总数就是 数据总数步长
			int threadTotal = threadTotalStep;
			// 获取上一个线程最终行数
			int oldThreadCurrent = threadTotalStep * finalI;
			/// 如果有余数,最后一次计算得补充余数
			if (more > 0 && finalI == threadNum - 1) {
				threadTotal += more;
			}
			log.info("线程-{} 要处理的数据总数为:{}", finalI + 1, threadTotal);
			// 计算页数
			int pages = threadTotal / pageSize;
			if (threadTotal % pageSize > 0) {
				pages++;
			}
			// 统计数量,当等于线程总总数时退出循环,避免重复计数
			int handleCount = 0;
			// 获取最后一个user
			User lastUser = new User();
			// 开始遍历处理数据
			for (int page = 0; page < pages; page++) {
				List<User> users = userMapper.selectList(
						Wrappers.<User>lambdaQuery()
								.last(String.format("LIMIT %s,%s", page * pageSize + oldThreadCurrent, pageSize)));

				for (User user : users) {
					handleCount++;
					if (handleCount == threadTotal) {
						break;
					}

					/// 模拟真正的逻辑处理,耗时操作
					try {
						Thread.sleep(1);
					} catch (InterruptedException e) {
						throw new RuntimeException(e);
					}
				}

				/// 批量 修改/插入 操作

				if (CollUtil.isNotEmpty(users)) {
					lastUser = users.get(users.size() - 1);
				}
			}
			log.info("线程-{} 处理的最后一个数据的id为:{}", finalI + 1, lastUser.getId());
			countDownLatch.countDown();
		});

	}

	try {
		countDownLatch.await();
		executorService.shutdown();
		log.info("总耗时:{} s", (System.currentTimeMillis() - start) / 1000);
	} catch (InterruptedException e) {
		throw new RuntimeException(e);
	}

}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值