同事的代码问题第六期(MQ与多线程处理数据)

前言

先说点废话吧😜,分享一下近来的工作情况。

上个月五一前就负责一个新项目的核心开发工作。同时开发过程还对所有系统的所有数据进行处理,简单理解就是需要便遍历数据库所有数据的所有字段,然后还需要调用三方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表示的简化代码场景。

这些问题都是来自,实际开发中遇到到的问题,有些问题确实看上去简单,确实也是我们再开发过程中容易忽略的问题!

希望今天的内容对各位老铁有所帮助吧! 感谢大家的一键三连👏

相关推荐
MarkGosling1 小时前
【开源项目】网络诊断告别命令行!NetSonar:开源多协议网络诊断利器
运维·后端·自动化运维
Codebee1 小时前
OneCode3.0 VFS分布式文件管理API速查手册
后端·架构·开源
_新一1 小时前
Go 调度器(二):一个线程的执行流程
后端
estarlee2 小时前
腾讯云轻量服务器创建镜像免费API接口教程
后端
风流 少年2 小时前
Cursor创建Spring Boot项目
java·spring boot·后端
毕设源码_钟学姐3 小时前
计算机毕业设计springboot宿舍管理信息系统 基于Spring Boot的高校宿舍管理平台设计与实现 Spring Boot框架下的宿舍管理系统开发
spring boot·后端·课程设计
方圆想当图灵3 小时前
ScheduledFutureTask 踩坑实录
后端
全栈凯哥3 小时前
16.Spring Boot 国际化完全指南
java·spring boot·后端
M1A14 小时前
Java集合框架深度解析:LinkedList vs ArrayList 的对决
java·后端
31535669135 小时前
Springboot实现一个接口加密
后端