问题的由来:
最近在做一个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();
}
}
}
解决方案建议:
-
批量处理消息:
将消息批量处理,而不是逐条处理。可以使用队列将消息暂存,并定期批量处理这些消息。 -
异步处理:
使用异步任务处理消息,减少对主线程的阻塞。可以使用Spring的@Async注解或者ExecutorService来实现异步处理。 -
数据库优化:
对数据库操作进行优化,减少不必要的查询和更新。可以使用批量更新的方式来提高数据库操作的效率。 -
限流和降级:
对MQTT消息进行限流处理,控制单位时间内处理的消息数量。当流量过大时,可以采取降级措施,比如丢弃部分不重要的消息。 -
缓存使用:
使用缓存来减少对数据库的频繁访问。例如,使用一个内存缓存(如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 设置为静态变量,通过类级别的共享特性,确保了它在多个线程之间的可见性和共享性,解决了多线程环境下队列访问的问题。