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