多线程环境下队列访问的问题

问题的由来:

最近在做一个springboot后端项目,涉及到mqtt服务器接收消息后的回调处理,然后使用websocket通知前端更新数据(websocket的功能已经实现,可以看我上一篇文章)。
如下代码的业务是:当mqtt服务器收到消息后,执行回调函数messageArrived,然后根据主题信息分别进行数据处理(涉及到数据库操作),如下的数据处理设计模式,在短时间内有非常大的数据量时,弊端就显现出来了:

1. 性能问题:

大量的消息会导致CPU和内存资源的高消耗,因为每条消息都会触发messageArrived回调,并在其中执行一系列的操作(如JSON解析、数据库查询与更新、WebSocket通知等)。

2. 数据库压力:

对于每条消息,都会进行数据库查询和更新操作,这可能会导致数据库连接池耗尽,查询和更新操作延迟,甚至出现数据库锁争用问题。

3. WebSocket通知延迟:

频繁地向前端发送WebSocket通知可能会导致WebSocket服务器的负载增加,通知的延迟,甚至可能导致WebSocket连接断开。

4. 线程饱和:

Spring Boot中的默认线程池可能会因为过多的并发任务而饱和,导致任务排队延迟,线程切换频繁,甚至出现OOM(Out of Memory)问题。

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.zhengjie.device.domain.DeviceInfo;
import me.zhengjie.device.domain.NetworkTopology;
import me.zhengjie.device.service.DeviceInfoService;
import me.zhengjie.device.service.NetworkTopologyService;
import me.zhengjie.modules.mnt.websocket.MsgType;
import me.zhengjie.modules.mnt.websocket.SocketMsg;
import me.zhengjie.modules.mnt.websocket.WebSocketServer;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.List;
import java.util.Objects;

@Component
public class MqttConsumerCallBack implements MqttCallback {

    private DeviceInfoService deviceInfoService;

    private NetworkTopologyService networkTopologyService;

    private final  ObjectMapper objectMapper = new ObjectMapper();

    public MqttConsumerCallBack(DeviceInfoService deviceInfoService, NetworkTopologyService networkTopologyService) {
        this.deviceInfoService = deviceInfoService;
        this.networkTopologyService = networkTopologyService;
    }


    @Override
    public void connectionLost(Throwable throwable) {
        System.out.println(throwable);
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        String payload = new String(message.getPayload());
        if (topic.matches("mesh/.+/toCloud")) {
            handleHeartbeatMessage(payload);
            // 通知前端更新拓扑结构
            notifyFrontendTopologyUpdate();
        } else if (topic.matches("mesh/.+/topo")) {
            handleTopoMessage(payload);
        }
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
        System.out.println("消息发布成功");
    }

    private void handleHeartbeatMessage(String payload) throws IOException {
        // 解析 JSON 数据
        
        if ("heartbeat".equals(type)) {
            // 更新设备信息表
            
            } else {
               
            }
        }
    }

    private void handleTopoMessage(String payload) throws IOException {
        // 解析设备地址列表
        List<String> deviceAddressList = parseDeviceAddressList(payload);

        // 更新设备状态
        updateDeviceStatus(deviceAddressList);
    }

    private List<String> parseDeviceAddressList(String payload) {
        List<String> deviceAddressList = null;
        try {
            ObjectMapper mapper = new ObjectMapper();
            // 解析 JSON 字符串为 List<String> 类型
            deviceAddressList = mapper.readValue(payload, new TypeReference<List<String>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        return deviceAddressList;
    }

    private void updateDeviceStatus(List<String> deviceAddressList) {
        // 获取所有设备信息
        List<DeviceInfo> allDevices = deviceInfoService.getAllDevices();

        // 更新不在设备地址列表中的设备状态为离线
        for (DeviceInfo device : allDevices) {
            if (!deviceAddressList.contains(device.getDeviceAddr())) {
                device.setStatus("offline");
                // 更新设备信息
                deviceInfoService.update(device);
            }
        }
    }

    private void notifyFrontendTopologyUpdate() {
        try {
            SocketMsg socketMsg = new SocketMsg("拓扑结构已更新", MsgType.INFO);
            WebSocketServer.sendInfo(socketMsg, null); // null 表示通知所有客户端
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

解决方案建议:

  1. 批量处理消息:
    将消息批量处理,而不是逐条处理。可以使用队列将消息暂存,并定期批量处理这些消息。

  2. 异步处理:
    使用异步任务处理消息,减少对主线程的阻塞。可以使用Spring的@Async注解或者ExecutorService来实现异步处理。

  3. 数据库优化:
    对数据库操作进行优化,减少不必要的查询和更新。可以使用批量更新的方式来提高数据库操作的效率。

  4. 限流和降级:
    对MQTT消息进行限流处理,控制单位时间内处理的消息数量。当流量过大时,可以采取降级措施,比如丢弃部分不重要的消息。

  5. 缓存使用:
    使用缓存来减少对数据库的频繁访问。例如,使用一个内存缓存(如Guava Cache或EhCache)来缓存设备信息,减少数据库查询次数。

我采用队列和异步任务进行批量处理的解决方案

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.zhengjie.device.domain.DeviceInfo;
import me.zhengjie.device.service.DeviceInfoService;
import me.zhengjie.modules.datareciver.domain.MqttMessageWrapper;
import me.zhengjie.modules.mnt.websocket.MsgType;
import me.zhengjie.modules.mnt.websocket.SocketMsg;
import me.zhengjie.modules.mnt.websocket.WebSocketServer;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

@Component
@EnableAsync
public class MqttConsumerCallBack implements MqttCallback {

    private DeviceInfoService deviceInfoService;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final BlockingQueue<MqttMessageWrapper> messageQueue = new LinkedBlockingQueue<>();
    private final int BATCH_SIZE = 100;
    private final int POLL_TIMEOUT = 100; // 100 milliseconds

    public MqttConsumerCallBack(DeviceInfoService deviceInfoService) {
        this.deviceInfoService = deviceInfoService;
    }

    @Override
    public void connectionLost(Throwable throwable) {
        System.out.println("Connection lost: " + throwable);
    }

    @PostConstruct
    public void init() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // Increase core pool size
        executor.setMaxPoolSize(20); // Increase max pool size
        executor.setQueueCapacity(1000); // Increase queue capacity
        executor.initialize();
        executor.execute(this::processMessages);
    }

    @Override
    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
        System.out.println("Message delivery complete");
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) {
        messageQueue.offer(new MqttMessageWrapper(topic, message));
    }

    @Async
    public void processMessages() {
        while (true) {
            try {
                List<MqttMessageWrapper> messages = pollMessages();
                for (MqttMessageWrapper messageWrapper : messages) {
                    String topic = messageWrapper.getTopic();
                    String payload = new String(messageWrapper.getMessage().getPayload());
                    System.out.println("Processing message from topic: " + topic + ", payload: " + payload);
                    if (topic.matches("mesh/.+/toCloud")) {
                        handleHeartbeatMessageAsync(payload);
                        notifyFrontendTopologyUpdate();
                    } else if (topic.matches("mesh/.+/topo")) {
                        handleTopoMessageAsync(payload);
                        notifyFrontendOfflineDeviceListUpdate();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private List<MqttMessageWrapper> pollMessages() throws InterruptedException {
        List<MqttMessageWrapper> messages = new ArrayList<>();
        for (int i = 0; i < BATCH_SIZE; i++) {
            MqttMessageWrapper messageWrapper = messageQueue.poll(POLL_TIMEOUT, TimeUnit.MILLISECONDS);
            if (messageWrapper != null) {
                messages.add(messageWrapper);
            } else {
                break;
            }
        }
        return messages;
    }

    @Async
    public void handleHeartbeatMessageAsync(String payload) throws IOException {
        handleHeartbeatMessage(payload);
    }

    @Async
    public void handleTopoMessageAsync(String payload) throws IOException {
        handleTopoMessage(payload);
    }

    private void handleHeartbeatMessage(String payload) throws IOException {

    }

    private void handleTopoMessage(String payload) throws IOException {

    }

    private List<String> parseDeviceAddressList(String payload) {

    }

    private void updateDeviceStatus(List<String> deviceAddressList) {
        
    }

    private void notifyFrontendTopologyUpdate() {
        try {
            SocketMsg socketMsg = new SocketMsg("拓扑结构已更新", MsgType.INFO);
            WebSocketServer.sendInfo(socketMsg, "update"); // null means notify all clients
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void notifyFrontendOfflineDeviceListUpdate() {
        try {
            SocketMsg socketMsg = new SocketMsg("离线设备列表已更新", MsgType.INFO);
            WebSocketServer.sendInfo(socketMsg, "update"); // null means notify all clients
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用上述代码,遇到了问题:

	@Override
    public void messageArrived(String topic, MqttMessage message) {
        messageQueue.offer(new MqttMessageWrapper(topic, message));
    }

线程初始化正常,messageArrived回调函数有被执行,processMessages的处理也有被执行,但是就是前端的数据没有被更新。通过断点调试发现pollMessages()返回的数据是null,即返回了一个空的队列列表。

查阅资料发现:

像我这种业务,消息队列的内容只存取,不修改,又需要多线程共享的,需要给队列设置为“静态变量”,只有这样,所有 MqttConsumerCallBack 实例共享同一个 messageQueue,确保了在任何线程中对 messageQueue 的访问都是对同一个队列的访问。

public class MqttConsumerCallBack implements MqttCallback {

    private static final BlockingQueue<MqttMessageWrapper> messageQueue = new LinkedBlockingQueue<>();

    // ... other code ...
}

由此引申出一个问题:基础知识不扎实

做笔记:

静态变量的特性

类级别的共享: 静态变量属于类,而不是类的实例。无论创建了多少个类的实例,静态变量只有一个拷贝,并且由所有实例共享。
全局访问: 静态变量可以通过类名直接访问,无需创建类的实例。这意味着在任何地方,只要可以访问到类,就可以访问静态变量。
生命周期: 静态变量在类加载时初始化,并在类卸载时销毁,生命周期与类相同。这个特性确保了静态变量在整个程序运行期间都可用。

线程共享

在多线程环境中,确保共享变量的正确性至关重要。静态变量由于其类级别的特性,天然适合在不同线程中共享:

单一实例: 因为静态变量在类中只有一个实例,所有线程访问的都是同一个变量。
可见性: 所有线程对静态变量的修改对其他线程立即可见。虽然需要注意并发访问时的同步问题,但静态变量的可见性确保了修改能够及时传播。

修改为静态变量后:

实际效果

将 messageQueue 设置为静态变量后:

  • messageArrived 方法添加消息到队列中,所有线程都能看到这个变化。
  • processMessages 方法轮询队列中的消息,无论在哪个线程中调用,访问的都是同一个队列。

线程安全

尽管静态变量可以在多个线程之间共享,但在并发环境下访问和修改静态变量时仍需注意线程安全。BlockingQueue 接口的实现如 LinkedBlockingQueue 提供了内置的线程安全机制,确保在多个线程并发访问队列时不会出现竞态条件。

结论

将 messageQueue 设置为静态变量,通过类级别的共享特性,确保了它在多个线程之间的可见性和共享性,解决了多线程环境下队列访问的问题。

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值