前言
先说点废话吧😜,分享一下近来的工作情况。
上个月五一前就负责一个新项目的核心开发工作。同时开发过程还对所有系统的所有数据进行处理,简单理解就是需要便遍历数据库所有数据的所有字段,然后还需要调用三方API更新部分数据,总而言之 最近一个多月算是 今年最忙的一个月了吧!😁当然,大多数时候都还是到点5点就下班,偶尔发版需要远程支持一下。
今天就分享一下,近期发现的一些问题吧!
🤪开始写这篇文章的时候还是6月,结果到7月才续上,因为中途有厂家找我合做,也花了些时间吧!
💥对了,包主偶尔还会更一下抖音视频,有兴趣的也可以关注一下包主!(🤞抖音:javaxixixi)
同事的问题代码
😊有人说把同事的代码放网上,等于当众脱别人裤子!!!为了我们的革命事业,在这儿给各位同事先敬个礼🙇,然后再脱裤子吧!!!
开个玩笑咯,只是单纯的分享问题啦!鸡蛋里挑骨头而已!实际工作中满足业务场景即可!
一、MQ一直重复消费
前段时间运营反馈通知系统一直在发送短信,每隔几分钟就发送。我们看了一下这个消息的ACK一直没有被确认。 来吧!先看看代码:
配置:
properties
# 手动确认ack
spring.rabbitmq.listener.direct.acknowledge-mode= manual
生产者:
java
public void sendDelaySmsTask(String id,long actTime){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setHeader("x-delay",actTime);
Message msg = MessageBuilder.withBody(id.getBytes()).andProperties(messageProperties).build();
rabbitTemplate.convertAndSend(RabbitMqConstant.EXCHANGE,RabbitMqConstant.SMS_ROUTING_KEY,msg);
}
消费者:
java
@RabbitListener(queues = RabbitMqConstant.SMS_DELAY_QUEUE)
public void id(Message message, Channel channel ) {
String taskId = new String(message.getBody());
log.info("-------task ID-------:{}",taskId);
STask sTask = taskService.getBaseMapper().selectById(id);
if(Objects.nonNull(sTask)){
log.info("-------schedule action sTask-------:{}", sTask);
taskService.commitTaskAfterDoSend(sTask);
}
}
存在的问题
首先配置ACK的模式配置的是MANUAL
手动确认模式,但是在消费者没有显示的去确ACK,导致在执行业务逻辑异常的时候导致消息又重新被放入队列,无限循环的消费消息。
解决方式
捕捉异常信息,手动确认消息,不再重新放入队列!
java
@RabbitListener(queues = RabbitMqConstant.SMS_DELAY_QUEUE)
public void doSmsTask(Message message, Channel channel ) {
String id = new String(message.getBody());
log.info("------- ID-------:{}",id);
try {
........................
// 消息处理成功,进行确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("消息异常:处理短信任务失败,任务ID: {}", taskId, e);
try {
// 消息处理失败,拒绝消息并选择是否重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
} catch (Exception ex) {
log.error("消息异常:拒绝消息失败,任务ID: {}", id, ex);
}
}
}
后面会单独出一篇文章,通过分析源码的方式,来分析rabbitMQ 是如何处理这个ACK,以及处理ACK有什么逻辑。
一、多线程遍历数据
最近有个任务,需要把生产环境的所有数据更新一下(具体更新的规则就不说了)。简单理解我们现在就需要把每个项目的数据,查询出来然后,调用batchUpdateByIds
.
问题代码: 下面的代码核心逻辑就是 使用多线程查询数据、然后批量更新数据。
java
private void initTableSign(String className, int threadCount) throws Exception {
.......................
IService baseService = (IService) applicationContext.getBean(mapperName);
Long dataLength = countDataLength(baseService);
//每次查询出BATCH_NUM条数据,计算出查询次数
Long forNum = dataLength%BATCH_NUM != 0 ? dataLength/BATCH_NUM + 1 : dataLength/BATCH_NUM;
// 创建线程池,使用指定数量的线程处理同一个表的数据
java.util.concurrent.ExecutorService executorService = java.util.concurrent.Executors.newFixedThreadPool(threadCount);
java.util.List<java.util.concurrent.Future<?>> futures = new java.util.ArrayList<>();
try {
// 将表数据分片,每个线程处理一部分
for (int i = 1; i <= forNum; i++) {
final int batchIndex = i;
futures.add(executorService.submit(() -> {
try {
long startTime =System.currentTimeMillis();
//查询分页数据,条件 updateStatus = null
IPage data = executeSelectPage(baseService, batchIndex, BATCH_NUM, entityClass);
//批量更新,拦截器会把 updateStatus 更新成 1
baseService.updateBatchById(data.getRecords(), data.getRecords().size());
} catch (Exception e) {
log.info("表 {} 第 {} 批次处理异常", className, batchIndex, e);
}
return null;
}));
}
// 等待所有任务完成
............................
} finally {
// 关闭线程池
......................................
}
//分页查询
public static IPage executeSelectPage(IService baseService, int current, int size, Class<?> entityClass) throws Exception {
// 创建分页对象
Page page = new Page<>(current, size);
// 获取 BaseMapper 的 selectPage 方法
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("id");
queryWrapper.isNull("updateStatus");
IPage result = baseService.getBaseMapper().selectPage(page, queryWrapper);
return result;
}
存在的问题
因为查询和更新都是在多线程中进行的。因为查询的分页数据的条件updateStatus is null
,但是更新的时候有会把updateStatus is null
的数据更新成 updateStatus=1
,所以上面这种更新的方法会导致少遍历很多数据。遍历到后半部分,就会出现查询数据为空的情况。
因为我的页码是在更新前定死的,每次查询的时候数据都发生了变化。
举个例子,现在一个有1万条数据,我现在每一批次查询1千条,按照我上面的实现方式就是。分页查询(1,1000).....(10,1000),但是因为我是异步查询、更新的 ,可能执行到(10,1000)的时候,我
updateStatus is null
就只有 1000条了,只有1页数据了,那儿还能查询出10的数据呢。
解决方式
查询的时候用单线程多查询一些数据出来,遍历更新的时候用多线程。并且更新完成之后,才能查询下一批数据。并且每次查询第一页的数据。
本身这块代码主要耗时的地方是在 更新时候 拦截器会去调用三方接口处理更新数据,再插入到数据库中。所以单线程查询出数据,多线程更新,也不会有太大的效率影响。
java
private void initTableSign(String className, int threadCount) throws Exception {
...............
try {
long totalProcessed = 0;
for (int pageNum = 1; pageNum <= forNum; pageNum++) {
// 计算当前批次需要查询的数据量,最大16万
long queryLimit = Math.min(totalCount - totalProcessed, queryPageSize);
// 分页查询数据,每次查询16万(每次查询第一页数据)
IPage<Object> dataPage = executeSelectPage(baseService, 1, (int) queryLimit, entityClass);
List<Object> records = dataPage.getRecords();
// 将查询到的16万数据分批,每批2000条,使用线程池处理
int batchSize = BATCH_NUM;
for (int i = 0; i < records.size(); i += batchSize) {
final int start = i;
final int end = Math.min(i + batchSize, records.size());
final List<Object> subList = records.subList(start, end);
int finalCurrentBatchNum = pageNum;
futures.add(executorService.submit(() -> {
try {
if (!subList.isEmpty()) {
baseService.updateBatchById(subList, subList.size());
}
} catch (Exception e) {
log.error("数据初始化:表 {} 第 {} 批次处理异常", className, finalCurrentBatchNum, e);
}
return null;
}));
}
totalProcessed += records.size();
// 等待所有任务完成
} finally {
// 关闭线程池
}
}
二、代码简化:lambda 运用
案例一
对list数据分组,判断每组数据的完成状态(同组数据中只要有一组数据未完成则整组数据的状态就是未完成)
java
Map<Long, Boolean> statusMap = new HashMap<>();
for (Condition condition : ConditionList) {
Long groupId = condition.getApplyTopicId();
Boolean finish = statusMap.get(groupId);
if(Objects.isNull(finish)){
statusMap.put(groupId,condition.getIsFill());
} else if (!finish) {
topicFinishMap2.put(groupId,false);
}
}
使用lambda:
java
Map<Long, Boolean> statusMap = ConditionList.stream()
.collect(Collectors.groupingBy(
Codition::getApplyTopicId,
Collectors.reducing(true, Condition::getIsFill, (a, b) -> a && b)
));
案例二
获取入参数(0-10)到10之间的数字
java
Integer effectiveStartLevel = (starLevel != null) ? starLevel : 0;
List<Integer> levelList = new ArrayList<>();
// 从 startLevel 开始,逐步增加到 10(包含 10)
for (int i = effectiveStartLevel; i <= 5; i++) {
levelList.add(i);
}
使用lambda:
java
int effectiveStartLevel = (starLevel != null) ? starLevel : 0;
checkStarLevel = IntStream.rangeClosed(effectiveStartLevel, 10)
.boxed()
.collect(Collectors.toList());
可以看到lmbda表达式还是很强大的,能让代码变得更加简洁。不是很熟悉lambda表达的同学看到这个代码估计会..........
总结
今天分析了因为MQ在消费消息的时候,没有处理异常以及手动确认ACK导致消费进入死循环问题,同时也分享了多线程在遍历数据时候遇到的问题,最后就是分享了两个lambda表示的简化代码场景。
这些问题都是来自,实际开发中遇到到的问题,有些问题确实看上去简单,确实也是我们再开发过程中容易忽略的问题!
希望今天的内容对各位老铁有所帮助吧! 感谢大家的一键三连👏