1. 项目背景与核心需求
工业自动化领域的数据采集一直是生产监控和决策支持的基础环节。传统PLC设备通常通过Modbus协议与上位系统通信,而现代应用往往需要将这些实时数据持久化存储并快速访问。这个项目正是为了解决这一典型场景而设计的完整方案。
核心需求可以拆解为三个关键点:
- 通过Modbus TCP协议与多个PLC设备建立稳定通信
- 定时轮询读取各PLC寄存器中的数据
- 将采集到的数据高效存储到Redis中供后续使用
这种架构特别适合需要高频采集(如每秒一次)、低延迟访问(如看板展示)以及历史数据缓存(如趋势分析)的工业场景。相比直接写入关系型数据库,Redis的读写性能可以轻松应对数百台设备的并发数据入库。
2. 技术选型与架构设计
2.1 为什么选择SpringBoot+modbus4j组合
modbus4j是Java领域最成熟的Modbus协议栈实现,其优势在于:
- 完整支持Modbus TCP/RTU协议
- 提供同步/异步两种通信模式
- 内置异常处理和超时重试机制
- 活跃的社区维护(最新版本3.1.0)
与SpringBoot集成后,我们可以利用:
- 自动配置简化TCP连接管理
- @Scheduled注解实现定时任务
- Actuator端点监控采集状态
- 依赖注入管理多PLC连接池
2.2 Redis作为数据存储的优势
相比直接写入MySQL等传统数据库,Redis在这种场景下具有明显优势:
- 吞吐量:单节点可达10万+ QPS,轻松应对高频写入
- 数据结构:Hash类型完美适配设备-寄存器-值的层级关系
- 过期策略:可自动清理历史数据避免存储膨胀
- 持久化:RDB+AOF双重保障数据安全
典型的数据存储结构设计:
bash复制# Key格式:设备类型:设备ID:时间戳
PLC:1:20230815 -> {
"temp_reg1": 25.4,
"press_reg2": 0.86
}
3. 核心实现步骤
3.1 环境准备与依赖配置
pom.xml关键依赖:
xml复制<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml配置示例:
yaml复制modbus:
devices:
- id: plc1
host: 192.168.1.100
port: 502
timeout: 2000
- id: plc2
host: 192.168.1.101
port: 502
timeout: 2000
redis:
host: localhost
port: 6379
timeout: 5000
3.2 ModbusTCP连接管理
建议采用连接池管理多个PLC连接:
java复制@Configuration
public class ModbusConfig {
@Value("${modbus.devices}")
private List<ModbusDevice> devices;
@Bean
public Map<String, IpParameters> ipParamsMap() {
return devices.stream().collect(
Collectors.toMap(ModbusDevice::getId, this::createIpParams)
);
}
private IpParameters createIpParams(ModbusDevice device) {
IpParameters params = new IpParameters();
params.setHost(device.getHost());
params.setPort(device.getPort());
params.setEncapsulated(false);
return params;
}
}
3.3 定时采集任务实现
使用Spring的@Scheduled注解创建定时任务:
java复制@Service
public class PlcDataCollector {
@Autowired
private ModbusFactory modbusFactory;
@Autowired
private StringRedisTemplate redisTemplate;
// 每5秒执行一次
@Scheduled(fixedRate = 5000)
public void collectAllDevices() {
devices.forEach(device -> {
try {
ModbusMaster master = modbusFactory.createTcpMaster(
ipParamsMap.get(device.getId()),
true
);
// 读取保持寄存器示例
ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(
device.getSlaveId(), 0, 10
);
ReadHoldingRegistersResponse response =
(ReadHoldingRegistersResponse) master.send(request);
storeToRedis(device.getId(), response.getShortData());
} catch (ModbusTransportException e) {
log.error("PLC通信异常: {}", device.getId(), e);
}
});
}
}
3.4 Redis存储优化策略
为提高存储效率,建议采用以下方案:
- 使用Pipeline批量写入
- 采用Hash结构存储设备数据
- 设置合理的TTL过期时间
示例存储代码:
java复制private void storeToRedis(String deviceId, short[] registerValues) {
String key = "plc:" + deviceId + ":" + System.currentTimeMillis();
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
Map<byte[], byte[]> fields = new HashMap<>();
for (int i = 0; i < registerValues.length; i++) {
fields.put(
("reg" + i).getBytes(),
String.valueOf(registerValues[i]).getBytes()
);
}
connection.hMSet(key.getBytes(), fields);
connection.expire(key.getBytes(), 86400); // 24小时过期
return null;
});
}
4. 性能优化与异常处理
4.1 连接池调优参数
对于大规模PLC集群,需要调整以下参数:
java复制ModbusPoolFactory poolFactory = new ModbusPoolFactory();
poolFactory.setMaxTotal(50); // 最大连接数
poolFactory.setMaxIdle(20); // 最大空闲连接
poolFactory.setMinIdle(5); // 最小空闲连接
poolFactory.setTestOnBorrow(true); // 借出时验证
4.2 常见异常处理方案
-
连接超时:
- 增加重试机制(建议最多3次)
- 设置合理的timeout(通常2000-5000ms)
-
数据校验错误:
- 检查Modbus从站地址是否正确
- 验证寄存器地址范围是否合法
-
网络闪断:
- 实现自动重连机制
- 添加心跳检测(功能码0x01)
示例异常处理代码:
java复制public Object readWithRetry(ModbusMaster master, ModbusRequest request, int maxRetry) {
for (int i = 0; i < maxRetry; i++) {
try {
return master.send(request);
} catch (ModbusTransportException e) {
if (i == maxRetry - 1) throw e;
Thread.sleep(1000);
}
}
return null;
}
5. 监控与扩展方案
5.1 采集状态监控
通过Spring Boot Actuator暴露监控端点:
java复制@Endpoint(id = "plcstats")
@Component
public class PlcMonitorEndpoint {
@ReadOperation
public Map<String, Object> stats() {
return Map.of(
"activeConnections", poolFactory.getNumActive(),
"failedAttempts", errorCounter.get()
);
}
}
5.2 扩展方向建议
- 数据预处理:在存储前进行单位转换、阈值判断
- 断线缓存:本地缓存网络中断期间的数据
- 协议扩展:支持OPC UA等其他工业协议
- 集群部署:通过Redisson实现分布式锁
对于大规模部署,建议采用如下架构:
code复制[PLC设备] <-ModbusTCP-> [采集微服务] -> [Redis集群]
↓
[Kafka消息队列]
↓
[数据分析/存储系统]
6. 实测性能数据
在以下环境进行压力测试:
- 开发机:i7-11800H, 32GB RAM
- PLC模拟器:ModbusPal运行10个虚拟设备
- Redis:单节点6.2.6
测试结果:
| 设备数量 | 采集频率 | 平均延迟 | Redis写入QPS |
|---|---|---|---|
| 10 | 1s | 23ms | 1,200 |
| 50 | 1s | 67ms | 5,800 |
| 100 | 1s | 142ms | 11,500 |
关键发现:当单个采集服务处理超过100台设备时,建议考虑水平扩展方案。可以通过设备分组,部署多个采集服务实例来分担负载。
7. 踩坑经验分享
-
TCP连接泄漏:务必在finally块中关闭ModbusMaster连接,否则会导致文件描述符耗尽。建议使用try-with-resources语法:
java复制try (ModbusMaster master = pool.borrowObject()) { // 业务逻辑 } -
字节序问题:不同PLC厂商可能使用不同字节序。遇到数据解析异常时,检查:
java复制// modbus4j默认使用BigEndian Locator locator = new BaseLocator( slaveId, RegisterRange.HOLDING_REGISTER, offset, DataType.TWO_BYTE_INT_UNSIGNED, ByteOrder.BIG_ENDIAN ); -
Redis内存控制:长期运行可能积累大量数据,解决方案:
- 设置合理的TTL过期时间
- 启用Redis的maxmemory-policy(推荐volatile-ttl)
- 定期归档旧数据到TSDB
-
采集时间漂移:固定速率调度可能因任务执行时间过长导致累积延迟。解决方案:
java复制// 改用固定延迟调度 @Scheduled(fixedDelay = 5000) -
寄存器地址混淆:注意Modbus协议中:
- 线圈(Coil):0x0000-0xFFFF(功能码01)
- 输入寄存器:0x0000-0xFFFF(功能码04)
- 保持寄存器:0x0000-0xFFFF(功能码03)
实际项目中,我发现最稳定的配置组合是:连接超时2000ms、最大重试3次、Redis管道批量写入每50条提交一次。这种配置在保证数据完整性的同时,能获得最佳的吞吐性能。