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

news2025/7/27 13:34:59

文章目录

    • 引入
    • 相关资料
    • 环境准备
    • 分页查询处理,减少单次批量处理的数据量级
    • 补充亿点点日志,更易观察
    • 多线程优化查询_切数据版
    • 多线程_每个线程都分页处理

引入

都说后端开发能顶半个运维,我们经常需要对大量输出进行需求调整,很多时候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);
	}

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/394665.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Umi + React + Ant Design Pro 项目实践(一)—— 项目搭建

学习一下 Umi、 Ant Design 和 Ant Design Pro 从 0 开始创建一个简单应用。 首先&#xff0c;新建项目目录&#xff1a; 在项目目录 D:\react\demo 中&#xff0c;安装 Umi 脚手架&#xff1a; yarn create umi # npm create umi安装成功&#xff1a; 接下来&#xff0c;…

《OpenGL宝典》--纹理

文章目录创建并初始化纹理创建纹理更新纹理数据纹理目标和类型从着色器中读取纹理数据采样器类型使用texelFetch内置函数从着色器读取纹理使用texture&#xff08;&#xff09;函数从着色器读取纹理获取更多信息控制纹理数据的读取方式使用采样器对象存储采样器包装和过滤模式的…

AVL树的介绍和实现

我们知道&#xff0c;二叉搜索树是会出现单向的。单向在查找时效率是非常低的&#xff0c;时间复杂度会退化成O(N)&#xff0c;而AVL树就是解决这个问题。 文章目录1. AVL 树1.1 AVL树的概念1.2 AVL树节点的定义1.3 插入后的平衡因子1.4 AVL树的旋转1.4.1 右右&#xff1a;左单…

JavaScript 循环实例集合

文章目录JavaScript 循环实例集合For 循环循环输出 HTML 标题While 循环Do while 循环break 语句continue 语句使用 For...In 声明来遍历数组内的元素JavaScript 循环实例集合 For 循环 源码 <!DOCTYPE html> <html> <head> <meta charset"utf-8&q…

PG数据库入门知识

前言 Linux和windows的路劲分隔符是不同的&#xff0c;Linux下是斜杠/,而windows是反斜杠&#xff08;\&#xff09;。但在PG里window下也要使用linux的/作为路劲分隔符。 基础知识 为什么选择PG PostgreSQL是一款企业级关系型数据库管理系统。PostgreSQL之所以如此特别&am…

如何成为程序员中的牛人/高手?

目录 一、牛人是怎么成为牛人的&#xff1f; 二、关于牛人的一点看法 三、让程序员与业务接壤&#xff0c;在开发团队中“升级” 四、使用低代码平台 目标效果 五、最后 祝伟大的程序员们梦想成真、码到成功&#xff01; 一、牛人是怎么成为牛人的&#xff1f; 最近在某…

Android开发学习—手机开机启动的AMS流程

前言 AMS是Android中最核心的服务&#xff0c;主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作&#xff0c;其职责与操作系统中的进程管理和调度模块相类似&#xff0c;因此它在Android中非常重要。 客户端使用ActivityManager类。由于AMS是系统核心服…

浅谈ChatGPT 和 对AI 的思考

新世纪以来&#xff0c;人工智能作为一个非常热门话题&#xff0c;一直收到大众的广泛的关注。从一开始的图像的分类&#xff0c;检测&#xff0c;到人脸的识别&#xff0c;到视频分析分类&#xff0c;到事件的监测&#xff0c;到基于图片的文本生成&#xff0c;到AI自动写小说…

机器学习-卷积神经网络CNN中的单通道和多通道图片差异

背景 最近在使用CNN的场景中&#xff0c;既有单通道的图片输入需求&#xff0c;也有多通道的图片输入需求&#xff0c;因此又整理回顾了一下单通道或者多通道卷积的差别&#xff0c;这里记录一下探索过程。 结论 直接给出结论&#xff0c;单通道图片和多通道图片在经历了第一…

QUIC 多流桥接、新增 DDS 协议转换代理

驽马十驾&#xff0c;功在不舍。新春之交&#xff0c;NanoMQ 继续保持稳步更新&#xff0c;最新的 0.16 版本将于三月初发布。NanoMQ 为用户提供了 2 个重要新功能&#xff1a;MQTT over QUIC 的多流桥接和 DDS 协议转换代理&#xff0c;拓宽了 NanoMQ 的弱网桥接传输性能和在边…

08 CSS05

目标&#xff1a; 1、定位 2、装饰 3、选择器拓展 一、定位 1、定位的基本介绍 2、定位的基本使用 3、静态定位 4、相对定位 5、绝对定位 6、子绝父相 7、固定定位 8、元素的层级关系1、定位的基本介绍 1.1 网页常见布局方式 &#xff08;1&#xff09;标准流 块级元素独…

CVTE前端面经(2023)

CVTE前端面经项目介绍&#xff08;重点&#xff09;在数据B中找到数组A对应的值&#xff0c;并把数组B对应的值放在数据最前面css1 定位2 外边距3 css高级应用3.1. 过渡3.2. 变形2. 浮动2.1 浮动元素特点2. 2 清除浮动3. html5语义标签4. 实现圣杯布局的两种方式4.1 定位浮动4.…

不会吧,难道真的有程序员不知道怎么接单赚钱吗?

随着大环境逐渐转好&#xff0c;跳槽、新工作、兼职等等机会都浮出水面。抛开跳槽、新工作不谈&#xff0c;今天就专门来说说程序员接单赚钱有哪些靠谱的平台。 首先分享一波关于接私活有哪些注意事项&#xff0c;给大家提个醒&#xff0c;避免盲目入坑。 一、程序员接单须知…

搬得进来,搬得出去!快来过一把数据迁移的“瘾”

欢迎访问OceanBase官网获取更多信息&#xff1a;https://www.oceanbase.com/ 经过前几次“剧透”&#xff0c;我们知道了 OceanBase 开发者大会有嘉宾、有演讲&#xff0c;有开源生态专场&#xff0c;也知道我们还会有 3 场 Hands-on Workshop 动手实验营&#xff0c;从部署到…

从LiveData迁移到Kotlin的 Flow,才发现是真的香!

LiveData 对于 Java 开发者、初学者或是一些简单场景而言仍是可行的解决方案。而对于一些其他的场景&#xff0c;更好的选择是使用 Kotlin 数据流 (Kotlin Flow)。虽说数据流 (相较 LiveData) 有更陡峭的学习曲线&#xff0c;但由于它是 JetBrains 力挺的 Kotlin 语言的一部分&…

一文搞定!postman接口自动化测试【附项目实战详解】

目录&#xff1a;导读 | 接口结果判断 功能区 脚本相关 代码模板 | 集合(批量)测试 变化的参数数据 定期任务 接口执行顺序 数据传递 | 解决依赖问题 假设场景 Postman 中的操作 运行 写在最后 附带项目实战教程地址&#xff1a;postman接口自动化测试使用教程项…

[计算机组成原理(唐朔飞 第2版)]第一章 计算机系统概论 第二章 计算机的发展及应用(学习复习笔记)

第1章 计算机系统概论 1.1 计算机系统简介 1.1.1 计算机的软硬件概念 计算机系统由“硬件”和“软件”两大部分组成。 硬件 是指计算机的实体部分&#xff0c;它由看得见摸得着的各种电子元器件&#xff0c;各类光、电、机设备的实物组成如主机、外部设备等 软件 软件看不见…

【protoc自定义插件】「go语言」实现rpc的服务映射成http的服务,protoc生成gin的插件,(详解实现原理及过程)

文章目录前言一、工程实践中如何更好的使用proto文件&#xff1f;二、protoc命令如何查询依赖的proto文件以及执行原理1. protoc命令如何查询依赖的proto文件2. protoc执行的插件加载原理是什么&#xff1f;3. proto文件中的package和go_package的作用三、protoc插件开发原理体…

春招冲刺(十): Vue2 技术复盘

vue2 技术复盘 Q1&#xff1a;MVVM框架的理解&#xff1f; MVVM模型&#xff1a; M&#xff1a;模型&#xff08;Model&#xff09;&#xff0c;data中的数据V&#xff1a;视图&#xff08;View&#xff09;&#xff0c;模板代码VM&#xff1a;视图模型&#xff08;ViewModel…

Me-and-My-Girlfriend-1靶场通关

Me-and-My-Girlfriend-1靶场通关 靶机ip:192.168.112.135 信息收集 端口&#xff1a;22、80 还是从80WEB服务器端口入手 对服务器目录进行扫描&#xff0c;扫出以下目录 访问80端口WEB服务&#xff0c;显示一段文字只允许本地用户访问。 一眼伪造ip&#xff0c;查看页面…