一、 概述
如下流程图中,描述了一个基于Spring Boot,利用MySQL的二进制日志(binlog)来实时监听数据库中的数据变化,同时结合了Redis和WebSocket,模块之间相互协作,实现实时数据监听和推送功能。

二、 代码实现
首先,我们需要引入 mysql-binlog-connector-java 依赖,以便能够监听 MySQL 的二进制日志:
<dependency>
<groupId>com.github.shyiko</groupId>
<artifactId>mysql-binlog-connector-java</artifactId>
<version>0.21.0</version>
</dependency>
接下来,我们创建一个 BinlogToRedisConfig 类来配置和启动 Binlog 监听器。
该配置类用于将
MySQL的binlog事件同步到Redis中。它通过创建和配置一个BinaryLogClient来订阅和处理MySQL的binlog事件。在binaryLogClient方法中,它从数据库连接URL中提取数据库配置信息,并创建一个BinaryLogClient实例来连接到MySQL。此外,设置了一个事件监听器,用于处理不同类型的binlog事件,如表映射事件、更新行事件、插入行事件和删除行事件。在事件监听器中,根据事件类型和数据库配置信息,将相应的事件数据发布到Redis中。在startBinaryLogClient方法中,它启动了BinaryLogClient的连接线程,以异步地连接到MySQL的Binary Log。此外,还提供了一个extractDatabaseInfo方法,用于从数据库连接URL中提取数据库配置信息。
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import com.ruoyi.common.core.redis.RedisPublisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer;
import org.springframework.beans.factory.annotation.Value;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Configuration
public class BinlogToRedisConfig {
@Value("${spring.datasource.druid.master.url}")
private String datasourceUrl;
@Value("${spring.datasource.druid.master.username}")
private String username;
@Value("${spring.datasource.druid.master.password}")
private String password;
@Autowired
private RedisPublisher redisPublisher;
/**
* 创建并配置BinaryLogClient,用于订阅和处理MySQL的binlog事件。
* 注册事件监听器,对不同类型的事件进行处理,如表映射事件、更新行事件、插入行事件和删除行事件。
*
* @return 配置好的BinaryLogClient实例。
*/
@Bean
BinaryLogClient binaryLogClient() {
HashMap<String, String> config = extractDatabaseInfo(datasourceUrl);
BinaryLogClient client = new BinaryLogClient(config.get("ip"), Integer.parseInt(config.get("port")), username, password);
EventDeserializer deserializer = new EventDeserializer();
deserializer.setCompatibilityMode(EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG);
client.setEventDeserializer(deserializer);
client.registerEventListener(event -> {
EventHeader header = event.getHeader();
EventData data = event.getData();
if (data instanceof TableMapEventData tableMapEventData) {
// 数据库数据发生变化
if (config.get("databaseName").equals(tableMapEventData.getDatabase()) && EventType.TABLE_MAP == header.getEventType()) {
// 可以继续指定数据库中的具体表
System.out.println(tableMapEventData.getTable());
redisPublisher.publish("db_changes", "table_map:" + tableMapEventData.getTableId() + ":" + tableMapEventData.getDatabase() + ":" + tableMapEventData.getTable());
}
}
// 也可以监听对应的操作类型
if (data instanceof UpdateRowsEventData) {
System.out.println("Update:" + data.toString());
}
if (data instanceof WriteRowsEventData) {
System.out.println("Insert:" + data.toString());
}
if (data instanceof DeleteRowsEventData) {
System.out.println("Delete:" + data.toString());
}
});
return client;
}
/**
* 初始化并启动BinaryLogClient。
* 通过创建一个新的线程来异步地连接到MySQL的Binary Log,可以避免连接过程中的任何延迟影响到应用程序的启动时间。
*/
@Bean
public void startBinaryLogClient() {
BinaryLogClient client = binaryLogClient();
new Thread(() -> {
try {
client.connect();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
/**
* 从数据库连接URL中提取数据库配置信息。
*
* @param url 数据库的连接URL,遵循jdbc:mysql://主机:端口/数据库名的格式。
* @return 返回一个HashMap,包含提取的数据库配置信息,包括主机(ip)、端口(port)和数据库名(databaseName)。
*/
public static HashMap<String,String> extractDatabaseInfo(String url) {
HashMap<String, String> config = new HashMap<>();
// 定义正则表达式
String regex = "jdbc:mysql://([^:]+):(\\d+)/([^?#]+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
String ip = matcher.group(1);
int port = Integer.parseInt(matcher.group(2));
String databaseName = matcher.group(3);
config.put("ip", ip);
config.put("port", String.valueOf(port));
config.put("databaseName", databaseName);
}
return config;
}
}
为了在 Redis 中处理消息发布和订阅,我们需要配置 RedisMessageListenerContainer 和相关的发布者与订阅者类。
Redis配置类,用于配置缓存和消息监听。通过构造函数,赋值redisSubscriber。
redisMessageListenerContainer方法,通过@Bean注解标识为一个Bean对象。接收一个RedisConnectionFactory对象作为参数,用于创建Redis连接。在方法内部,创建了RedisMessageListenerContainer对象,并将其连接工厂设置为传入的参数。然后,向该监听容器添加了一个消息监听器redisSubscriber,用于监听名为db_changes的频道上的消息。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{
@Autowired
private final RedisSubscriber redisSubscriber;
public RedisConfig(RedisSubscriber redisSubscriber) {
this.redisSubscriber = redisSubscriber;
}
/**
* 创建Redis消息监听容器 bean。
* 并监听特定频道的消息。
*
* @param connectionFactory Redis连接工厂,用于创建Redis连接。
* @return RedisMessageListenerContainer,消息监听容器。
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 添加订阅频道
container.addMessageListener(redisSubscriber, new PatternTopic("db_changes"));
return container;
}
// 其他配置
...xxxx...
}
创建一个 RedisPublisher 类,用于向 Redis 发布消息。
提供了一个方法来向指定的
Redis频道发布消息。类的构造函数接收一个StringRedisTemplate对象作为参数,并将其赋值redisTemplate。publish方法接收两个参数,分别是Redis频道的名称和要发布的消息内容,然后通过调用redisTemplate的convertAndSend方法,将消息发送到指定的频道。
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
/**
* Redis发布者类,用于向指定的Redis频道发布消息。
*/
@Service
public class RedisPublisher {
private final StringRedisTemplate redisTemplate;
public RedisPublisher(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 向指定的Redis频道发布消息。
*
* @param channel Redis频道名称。
* @param message 要发布的消息内容。
*/
public void publish(String channel, String message) {
redisTemplate.convertAndSend(channel, message);
}
}
创建一个 RedisSubscriber 类,用于接收和处理 Redis 消息.
抽象类,实现了Redis的
MessageListener接口,用于订阅Redis消息。其中,onMessage方法会在收到消息时被调用。该方法将接收到的消息转换为字符串格式的channel和content,并调用抽象方法handleMessage来处理消息。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* Redis消息订阅者
*/
//@Component
public abstract class RedisSubscriber implements MessageListener {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(pattern);
String content = redisTemplate.getStringSerializer().deserialize(message.getBody());
// 处理接收到的消息
handleMessage(channel, content);
}
public abstract void handleMessage(String channel, String content);
}
引入websocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
创建一个 WebSocketServer 类,用于处理 WebSocket 连接和消息。
WebSocket服务器端点,用于与客户端建立WebSocket连接并进行通信。它继承了RedisSubscriber类,并实现了OnOpen、OnClose、OnMessage和OnError等WebSocket生命周期的方法。以下是该类的主要功能:
建立WebSocket连接:通过@ServerEndpoint注解指定服务器端点的路径为/ws/{sid},其中sid为保留字段。在@OnOpen方法中,与客户端的连接建立成功时执行相应的逻辑,如将当前WebSocketServer实例添加到webSocketSet集合中,增加在线数量,并启动定时任务。
关闭WebSocket连接:在@OnClose方法中,当客户端连接关闭时执行相应的逻辑,如从webSocketSet集合中移除当前WebSocketServer实例,减少在线数量。
处理客户端发送的消息:在@OnMessage方法中,接收客户端发送的消息并进行处理。如果接收到的消息是心跳包,则直接返回;否则,根据消息内容执行相应的业务逻辑,并通过sendMessage方法将处理结果发送给客户端。
异常处理:在@OnError方法中,处理WebSocket连接发生错误时的逻辑,如记录错误日志。
群发消息:提供sendInfo方法用于向所有连接的客户端发送消息。
指定会话推送:提供sendInfo方法用于向指定会话的客户端发送消息。
数据处理:根据接收到的消息内容,调用相应的业务服务,获取数据并转换为JSON格式,最后将数据发送给客户端。
该类使用了CopyOnWriteArraySet来存储所有连接的客户端的WebSocketServer实例,使用ScheduledExecutorService来定时发送心跳包和处理业务逻辑。同时,它还利用AtomicBoolean和AtomicInteger来控制数据推送的状态和在线数量的线程安全操作。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.redis.RedisSubscriber;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.lang.Object;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* WebSocket服务类,用于与客户端建立WebSocket连接并进行通信。
* 继承自RedisSubscriber,实现特定的订阅和处理逻辑。
*/
@Slf4j
@Component
@ServerEndpoint(value = "/ws/{sid}")
public class WebSocketServer extends RedisSubscriber {
/**
* ObjectMapper
*/
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 记录在线客户端的数量。
*/
private static final AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 存放每个客户端对应的 WebSocketServer 对象
*/
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 心跳报文
*/
private static final String HEARTBEAT_PACKETS = "The heartbeat packets";
/**
* 定时器
*/
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/**
* 标志位,用于控制数据推送的开关[根据具体业务]
*/
private static final AtomicBoolean flag = new AtomicBoolean(true);
/**
* 当WebSocket连接打开时调用。
*
* @param session WebSocket会话对象
* @param sid 保留字段
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
// 保留字段
String id = sid;
this.session = session;
// 加入set中
webSocketSet.add(this);
// 在线数加1
onlineCount.getAndIncrement();
// 新连接,刷新状态,首次推送
flag.set(true);
System.out.println("新连接,连接总数" + webSocketSet.size() + "---" + onlineCount.toString());
}
/**
* 当WebSocket连接关闭时调用。
*/
@OnClose
public void onClose() {
// 从客户端集合中移除当前实例
webSocketSet.remove(this);
// 在线数量减1
onlineCount.getAndDecrement();
System.out.println("断开连接,连接总数" + webSocketSet.size() + "---" + onlineCount.toString());
}
/**
* 当WebSocket连接发生错误时调用。
*
* @param session WebSocket会话对象
* @param error 错误异常
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("[历史数据回放] - WS 异常断开", session, error);
}
/**
* 当收到客户端消息时调用。
*
* @param message 客户端发送的消息
* @param session WebSocket会话对象
*/
@OnMessage
public void onMessage(String message, Session session) {
if (StringUtils.isEmpty(message)) return;
if (HEARTBEAT_PACKETS.equals(message)) {
log.debug("[消息订阅] - 心跳.");
return;
}
WebSocketServer webSocketServer = webSocketSet.stream().filter(it -> it.session.equals(session)).toList().get(0);
webSocketServer.scheduler.scheduleAtFixedRate(() -> {
// 根据具体业务实现,这里的定时器用于控制触发频率
try {
// 如果连接已断开或标志位为false,则不处理
// if (webSocketSet.stream().filter(it -> it.session.equals(session)).toList().isEmpty()) {
// log.debug("WebSocketSet is empty, stopping the scheduled task.");
// webSocketServer.scheduler.shutdownNow(); // 如果断开socket连接,关闭推送任务
// return;
// }
if (!flag.get()) return;
try {
xxxxxx...........
sendInfo(session, xxxxxxx......);
flag.set(false);
} catch (Exception e) {
log.error("[历史数据回放] - 数据推送异常, 数据: [{}].", message, e);
}
} catch (Exception e) {
log.error("Error executing scheduled task", e);
}
}, 0, 60, TimeUnit.SECONDS); // 0秒延迟,每60秒执行一次
}
/**
* 处理Redis消息,用于接收并处理来自Redis的订阅消息。
*
* @param channel Redis频道
* @param content 消息内容
*/
@Override
public void handleMessage(String channel, String content) {
flag.set(true);
}
/**
* 群发消息到所有连接的客户端。
*
* @param message 要发送的消息
*/
public static void sendInfo(String message) {
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
log.error("[NVR 数据对接] - 数据推送异常, 数据: [{}].", message, e);
continue;
}
}
}
/**
* 向指定会话的客户端发送消息。
*
* @param session 目标会话
* @param message 要发送的消息
*/
public static void sendInfo(Session session, String message) {
for (WebSocketServer item : webSocketSet) {
try {
if (null != session && item.session.equals(session)) {
item.sendMessage(message);
}
} catch (IOException e) {
log.error("[数据对接] - 数据推送异常, 数据: [{}].", message, e);
continue;
}
}
}
/**
* 发送消息到客户端。
*
* @param message 要发送的消息内容
* @throws IOException 如果发送失败
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
}

7800

被折叠的 条评论
为什么被折叠?



