同事的代码问题第六期(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表示的简化代码场景。

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

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

相关推荐
bearpping29 分钟前
SpringBoot最佳实践之 - 使用AOP记录操作日志
java·spring boot·后端
一叶飘零_sweeeet31 分钟前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·后端·spring
开心就好20252 小时前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默2 小时前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦2 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl2 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6863 小时前
Spring Boot文件上传
java·spring boot·后端
世界哪有真情3 小时前
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
java·后端
RMB Player3 小时前
Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定
java·网络·spring boot·后端·spring·飞书
重庆小透明3 小时前
【搞定面试之mysql】第三篇 mysql的锁
java·后端·mysql·面试·职场和发展