设备在线状态缓存技术方案
📋 目录
业务场景
背景
在物联网设备管理系统中,需要实时监控大规模设备(30万+)的在线状态。设备状态包括:
- 在线(1):设备正常连接
- 离线(0):设备未连接或断开
- 在离状态(2):设备异常或不稳定
核心需求
- 高频查询:前端需要实时查询设备状态(单设备、批量、按状态筛选)
- 批量更新:定时任务每 60 秒同步 30万+ 设备状态到缓存
- 状态统计:快速统计各状态设备数量
- 分页查询:支持按状态分页查询设备列表
- 低延迟要求:查询响应时间 < 10ms,批量更新 < 1秒
业务痛点
- 数据量大:30万设备,直接查询数据库性能差
- 更新频繁:每分钟全量更新一次
- 查询多样:单个查询、批量查询、状态筛选、统计等多种场景
- 内存敏感:需要控制 Redis 内存占用
技术方案概述
方案选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 单个 Hash | 简单 | 单 Key 过大,性能瓶颈 | < 1万设备 |
| Set 集合 | 按状态分组快 | 内存占用大,更新慢 | < 10万设备 |
| 分片 Hash + String JSON ⭐ | 高性能、低内存 | 实现复杂 | 30万+ 设备 |
最终方案
Redis 分片 Hash + String JSON 索引
- Hash 分片(30个):用于单个/批量设备状态查询
- String JSON 索引(3个):用于按状态快速筛选设备
核心技术详解
Redis 分片 Hash
什么是分片(Sharding)?
将大量数据分散存储到多个 Redis Key 中,避免单 Key 过大导致性能问题。
分片策略
// 使用设备ID的 hashCode 取模,确保同一设备总是落在同一分片
private int getShardIndex(String deviceCommId) {
return Math.abs(deviceCommId.hashCode() % SHARD_COUNT);
}
分片数量设计:
- 30万设备 ÷ 30个分片 = 每个分片约 1万设备
- 每个分片大小适中,性能最优
分片 Key 结构
device:status:shard:0 → {设备1: "1", 设备2: "0", ...} (约1万设备)
device:status:shard:1 → {设备3: "1", 设备4: "0", ...} (约1万设备)
...
device:status:shard:29 → {设备N: "1", ...} (约1万设备)
优势
✅ 分散压力:避免单 Key 过大
✅ 并行查询:可同时查询多个分片
✅ 灵活扩展:可动态调整分片数量
String JSON 索引
为什么不用 Set?
传统方案使用 Redis Set 存储每种状态的设备列表:
device:status:online → Set {设备1, 设备2, ...} (15万设备)
device:status:offline → Set {设备3, 设备4, ...} (14万设备)
问题:
- ❌ Set 内存占用大(每个元素额外开销)
- ❌ 批量更新慢(需要逐个 SADD/SREM)
改进方案:String + JSON
device:status:online:json → String '["设备1","设备2",...]'
device:status:offline:json → String '["设备3","设备4",...]'
device:status:online_offline:json → String '["设备5",...]'
优势:
✅ 内存节省 30%:JSON 紧凑存储
✅ 更新快 20%:一次性 SET 整个 JSON
✅ 查询快 40%:直接 GET + 反序列化
实现代码
// 序列化为 JSON
String onlineJson = objectMapper.writeValueAsString(onlineDeviceList);
// 一次性写入 Redis
redisTemplate.opsForValue().set(ONLINE_JSON_KEY, onlineJson);
// 查询时反序列化
String json = redisTemplate.opsForValue().get(ONLINE_JSON_KEY);
List<String> devices = objectMapper.readValue(json, new TypeReference<List<String>>() {});
Redis Pipeline 管道技术
什么是 Pipeline?
Pipeline 是 Redis 提供的批量命令执行机制,可以将多个命令打包成一个请求,一次性发送到 Redis 服务器。
传统方式 vs Pipeline
传统方式(逐个执行):
// 每次命令都需要网络往返(RTT)
redisTemplate.opsForHash().put(key1, field1, value1); // RTT 1
redisTemplate.opsForHash().put(key2, field2, value2); // RTT 2
redisTemplate.opsForHash().put(key3, field3, value3); // RTT 3
// 总耗时 = 3 × RTT(假设每次 1ms,总共 3ms)
Pipeline 方式(批量执行):
// 所有命令只需要一次网络往返
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
connection.hashCommands().hSet(key1, field1, value1);
connection.hashCommands().hSet(key2, field2, value2);
connection.hashCommands().hSet(key3, field3, value3);
return null;
});
// 总耗时 = 1 × RTT(仅 1ms)
Pipeline 核心优势
| 特性 | 传统方式 | Pipeline |
|---|---|---|
| 网络往返 | N 次 | 1 次 |
| 延迟 | N × RTT | 1 × RTT |
| 吞吐量 | 低 | 高 10-100倍 |
| 适用场景 | 单个命令 | 批量操作 |
本方案中的 Pipeline 应用
批量更新 30万设备状态:
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
// 1. 批量更新 Hash 分片(30万次 HSET)
shardDataMap.forEach((shardIndex, data) -> {
String shardKey = getShardKey(shardIndex);
data.forEach((deviceId, status) -> {
connection.hashCommands().hSet(
shardKey.getBytes(),
deviceId.getBytes(),
status.getBytes()
);
});
});
// 2. 更新 JSON 索引(3次 SET)
connection.stringCommands().set(ONLINE_JSON_KEY.getBytes(), onlineJson.getBytes());
connection.stringCommands().set(OFFLINE_JSON_KEY.getBytes(), offlineJson.getBytes());
connection.stringCommands().set(ONLINE_OFFLINE_JSON_KEY.getBytes(), abnormalJson.getBytes());
return null;
});
性能提升:
- ❌ 传统方式:30万次网络往返 = 300秒(假设每次 1ms)
- ✅ Pipeline:1次网络往返 + 执行时间 = < 1秒
Pipeline 注意事项
⚠️ 不支持返回值:Pipeline 内的命令无法立即获取返回值
⚠️ 非原子性:Pipeline 不是事务,不保证原子性
⚠️ 内存占用:大批量命令会占用客户端内存(本方案 < 50MB)
⚠️ 超时风险:单次 Pipeline 不宜超过 100万条命令
Pipeline 适用场景
✅ 批量写入(HSET、SET、SADD)
✅ 批量删除(DEL、HDEL)
✅ 批量更新(HINCRBY、EXPIRE)
❌ 需要读取中间结果的操作
❌ 需要原子性的业务逻辑
性能优化
1. 分片优化
- 分片数量:根据设备规模调整(1万/分片)
- 分片算法:hashCode 取模,分布均匀
- 并行查询:按分片分组,减少单次查询数据量
2. JSON 索引优化
- 紧凑存储:比 Set 节省 30% 内存
- 一次性更新:避免逐个 SADD/SREM
- 快速统计:只需反序列化获取 size
3. Pipeline 批量优化
- 批量更新:30万设备一次性提交
- 减少网络往返:从 30万次降到 1次
- 吞吐量提升:100倍以上
4. 序列化优化
- StringRedisTemplate:统一使用字符串序列化
- Jackson ObjectMapper:高性能 JSON 处理
- 字节操作:Pipeline 内直接操作字节数组
数据结构设计
Redis Key 设计
┌─────────────────────────────────────────────────────────────┐
│ Redis 数据结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Hash 分片(30个)- 用于单个/批量查询 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ device:status:shard:0 → Hash │ │
│ │ ├─ "869300053516981-1" : "1" (在线) │ │
│ │ ├─ "869300053516982-1" : "0" (离线) │ │
│ │ └─ ... (约1万设备) │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ device:status:shard:1 → Hash │ │
│ │ └─ ... (约1万设备) │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ ... │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ device:status:shard:29 → Hash │ │
│ │ └─ ... (约1万设备) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ String JSON 索引(3个)- 用于状态筛选 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ device:status:online:json → String │ │
│ │ '["869300053516981-1","869300053516983-1",...]' │ │
│ │ (约15万在线设备) │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ device:status:offline:json → String │ │
│ │ '["869300053516982-1","869300053516984-1",...]' │ │
│ │ (约14万离线设备) │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ device:status:online_offline:json → String │ │
│ │ '["869300053516985-1",...]' │ │
│ │ (约1万在离状态设备) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
数据一致性策略
- 双写机制:批量更新时同时更新 Hash 和 JSON 索引
- 全量覆盖:每次定时任务全量更新,避免增量不一致
- 事务性:使用 Pipeline 保证批量操作的整体性
核心功能实现
1. 批量更新设备状态
@Override
public void batchSetDeviceStatus(Map<String, String> deviceStatusMap) {
// 第一步:按分片和状态分组
Map<Integer, Map<String, String>> shardDataMap = new HashMap<>();
Map<String, List<String>> statusListsMap = new HashMap<>();
deviceStatusMap.forEach((deviceCommId, status) -> {
// 分片数据
int shardIndex = getShardIndex(deviceCommId);
shardDataMap.computeIfAbsent(shardIndex, k -> new HashMap<>())
.put(deviceCommId, status);
// 状态列表
statusListsMap.get(status).add(deviceCommId);
});
// 第二步:序列化为 JSON
String onlineJson = objectMapper.writeValueAsString(statusListsMap.get(STATUS_ONLINE));
String offlineJson = objectMapper.writeValueAsString(statusListsMap.get(STATUS_OFFLINE));
String abnormalJson = objectMapper.writeValueAsString(statusListsMap.get(STATUS_ONLINE_OFFLINE));
// 第三步:Pipeline 批量更新
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
// 更新 Hash 分片
shardDataMap.forEach((shardIndex, data) -> {
String shardKey = getShardKey(shardIndex);
data.forEach((deviceId, status) -> {
connection.hashCommands().hSet(
shardKey.getBytes(),
deviceId.getBytes(),
status.getBytes()
);
});
});
// 更新 JSON 索引
connection.stringCommands().set(ONLINE_JSON_KEY.getBytes(), onlineJson.getBytes());
connection.stringCommands().set(OFFLINE_JSON_KEY.getBytes(), offlineJson.getBytes());
connection.stringCommands().set(ONLINE_OFFLINE_JSON_KEY.getBytes(), abnormalJson.getBytes());
return null;
});
}
2. 批量查询设备状态
@Override
public Map<String, String> batchGetDeviceStatus(List<String> deviceCommIds) {
// 按分片分组
Map<Integer, List<String>> shardDevicesMap = new HashMap<>();
deviceCommIds.forEach(deviceId -> {
int shardIndex = getShardIndex(deviceId);
shardDevicesMap.computeIfAbsent(shardIndex, k -> new ArrayList<>())
.add(deviceId);
});
// 批量查询每个分片
Map<String, String> result = new HashMap<>();
shardDevicesMap.forEach((shardIndex, devices) -> {
String shardKey = getShardKey(shardIndex);
List<Object> statuses = redisTemplate.opsForHash().multiGet(shardKey,
new ArrayList<>(devices));
for (int i = 0; i < devices.size(); i++) {
Object status = statuses.get(i);
if (status != null) {
result.put(devices.get(i), status.toString());
}
}
});
return result;
}
3. 按状态查询设备
@Override
public List<String> getDevicesByStatus(String status) {
String jsonKey = getJsonKeyByStatus(status);
String json = redisTemplate.opsForValue().get(jsonKey);
if (json == null || json.isEmpty()) {
return Collections.emptyList();
}
// 反序列化 JSON
return objectMapper.readValue(json, new TypeReference<List<String>>() {});
}
4. 状态统计
@Override
public Map<String, Long> getStatusStatistics() {
Map<String, Long> statistics = new HashMap<>();
// 从 JSON 获取设备数量
statistics.put(STATUS_ONLINE, getDeviceCountFromJson(ONLINE_JSON_KEY));
statistics.put(STATUS_OFFLINE, getDeviceCountFromJson(OFFLINE_JSON_KEY));
statistics.put(STATUS_ONLINE_OFFLINE, getDeviceCountFromJson(ONLINE_OFFLINE_JSON_KEY));
return statistics;
}
private Long getDeviceCountFromJson(String jsonKey) {
String json = redisTemplate.opsForValue().get(jsonKey);
if (json == null || json.isEmpty()) {
return 0L;
}
List<String> devices = objectMapper.readValue(json, new TypeReference<List<String>>() {});
return (long) devices.size();
}
性能测试数据
测试环境
- 设备规模:30万设备
- Redis 版本:6.2.6
- 服务器配置:8核 16GB
- 网络延迟:< 1ms(内网)
性能对比
| 操作类型 | 传统 Set 方案 | 分片 Hash + JSON | 性能提升 |
|---|---|---|---|
| 单设备查询 | < 1ms | < 1ms | - |
| 批量查询(1000台) | 15ms | 8ms | ⚡ 47% 提升 |
| 按状态筛选(15万台) | 8ms | 4ms | ⚡ 50% 提升 |
| 状态统计 | 5ms | 1ms | ⚡ 80% 提升 |
| 批量更新(30万台) | 1000ms | 780ms | ⚡ 22% 提升 |
| 内存占用 | 450MB | 315MB | ⚡ 30% 节省 |
实际生产数据
2025-01-15 10:30:45 INFO 批量更新设备状态(含JSON索引)完成
- 设备总数: 302,156 台
- 分片数量: 30 个
- 在线设备: 148,523 台
- 离线设备: 142,089 台
- 在离状态: 11,544 台
- 总耗时: 768ms
5. 完整例子实现逻辑
package com.tigeriot.deviceonlinemanagerserviceapiclient.model.deviceOnlineStatus.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tigeriot.deviceonlinemanagerserviceapiclient.model.deviceOnlineStatus.DeviceOnlineStatusManager;
import com.tigeriot.deviceonlinemanagerserviceapiclient.model.deviceOnlineStatus.dto.DeviceOnlineStatusPageRequest;
import com.tigeriot.deviceonlinemanagerserviceapiclient.model.deviceOnlineStatus.dto.DeviceOnlineStatusVO;
import com.tigeriot.globalcommonservice.global.vo.JPAPageVo;
import com.tigeriot.globalcommonservice.model.productmanagerservice.iot.rundev.DevConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 设备状态管理服务(极速版 - Hash分片 + String JSON索引)
*
* 数据结构设计:
* 1. Hash 分片(30个)- 用于单个/批量设备状态查询
* device:status:shard:0~29 → {deviceId: status}
*
* 2. String JSON 索引(3个)- 用于按状态快速筛选设备 ⚡
* device:status:online → String "[\"设备1\",\"设备2\",...]"
* device:status:offline → String "[\"设备1\",\"设备2\",...]"
* device:status:abnormal → String "[\"设备1\",\"设备2\",...]"
*
* 性能优化(对比Set方案):
* - 单个查询:O(1),< 1ms(Hash)
* - 批量查询:O(N),1000台 < 10ms(Hash)
* - 状态筛选:O(1),15万台 < 5ms(String+JSON,比Set快40%)⚡
* - 状态统计:O(1),< 2ms(JSON长度计算,比Set快60%)⚡
* - 批量更新:Pipeline,30万台 < 800ms(比Set快20%)⚡
* - 内存占用:减少30%(JSON更紧凑)⚡
*
* @author TigerIot
*/
@Slf4j
@Service
public class RedisDeviceOnlineStatusManager implements DeviceOnlineStatusManager {
// 使用 Spring Boot 自动配置的 StringRedisTemplate
// 所有序列化器都是 StringRedisSerializer,与 Pipeline 字节操作一致
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
// 分片数量(30万设备 ÷ 30 = 每个分片1万设备)
private static final int SHARD_COUNT = 30;
// Redis Key 前缀
private static final String STATUS_HASH_PREFIX = "device:status:shard:";
// String JSON 索引 Key(替代Set,性能更优)
private static final String ONLINE_JSON_KEY = "device:status:online:json";
private static final String OFFLINE_JSON_KEY = "device:status:offline:json";
private static final String ONLINE_OFFLINE_JSON_KEY = "device:status:online_offline:json";
// 设备状态常量
public static final String STATUS_OFFLINE = DevConst.OnlineStatus.OFFLINE.value; // 离线
public static final String STATUS_ONLINE = DevConst.OnlineStatus.ONLINE.value; // 在线
public static final String STATUS_ONLINE_OFFLINE = DevConst.OnlineStatus.ON_OFFLINE.value; // 在离
/**
* 获取设备所在的分片索引
* 使用 hashCode 取模,确保同一设备总是落在同一分片
*
* @param deviceCommId 设备通信ID(格式:imei-modbusAddress)
* @return 分片索引(0 ~ SHARD_COUNT-1)
*/
private int getShardIndex(String deviceCommId) {
return Math.abs(deviceCommId.hashCode() % SHARD_COUNT);
}
/**
* 获取分片 Key
*
* @param shardIndex 分片索引
* @return Redis Key(例如:device:status:shard:0)
*/
private String getShardKey(int shardIndex) {
return STATUS_HASH_PREFIX + shardIndex;
}
/**
* 设置单个设备状态
*
* @param deviceCommId 设备通信ID(imei-modbusAddress)
* @param status 状态值:0-离线,1-在线,2-异常
*/
@Override
public void setDeviceStatus(String deviceCommId, String status) {
int shardIndex = getShardIndex(deviceCommId);
String shardKey = getShardKey(shardIndex);
redisTemplate.opsForHash().put(shardKey, deviceCommId, status);
log.debug("设备 {} 状态设置为: {} (分片:{})", deviceCommId, status, shardIndex);
}
/**
* 批量设置设备状态(极速版 - 同时更新 Hash 和 String JSON 索引)
* 使用 Pipeline 优化性能,一次性提交所有命令
*
* 更新策略(防雪崩设计):
* 1. 按分片逐个处理:删除分片 → 立即写入该分片新数据
* 2. 将状态集合序列化为 JSON(3个 SET)
*
* 性能提升:
* - 比 Set 方案快 20%(无需逐个 SADD)
* - 内存占用减少 30%(JSON 更紧凑)
*
* 数据清理 & 防雪崩:
* - 每次同步前清空分片,避免已删除设备的状态残留
* - 删一个写一个,避免批量删除导致的缓存雪崩
* - 更新期间最多只有1个分片暂时为空,其余29个分片可正常服务
*
* @param deviceStatusMap 设备ID -> 状态值的映射
*/
@Override
public void batchSetDeviceStatus(Map<String, String> deviceStatusMap) {
if (deviceStatusMap == null || deviceStatusMap.isEmpty()) {
log.warn("批量设置设备状态:输入为空,跳过操作");
return;
}
long startTime = System.currentTimeMillis();
try {
// 第一步:按分片和状态分组
Map<Integer, Map<String, String>> shardDataMap = new HashMap<>();
Map<String, List<String>> statusListsMap = new HashMap<>();
statusListsMap.put(STATUS_ONLINE, new ArrayList<>());
statusListsMap.put(STATUS_OFFLINE, new ArrayList<>());
statusListsMap.put(STATUS_ONLINE_OFFLINE, new ArrayList<>());
deviceStatusMap.forEach((deviceCommId, status) -> {
// 分片数据
int shardIndex = getShardIndex(deviceCommId);
shardDataMap.computeIfAbsent(shardIndex, k -> new HashMap<>())
.put(deviceCommId, status);
// 状态列表
statusListsMap.get(status).add(deviceCommId);
});
// 第二步:将状态列表序列化为 JSON
String onlineJson = objectMapper.writeValueAsString(statusListsMap.get(STATUS_ONLINE));
String offlineJson = objectMapper.writeValueAsString(statusListsMap.get(STATUS_OFFLINE));
String abnormalJson = objectMapper.writeValueAsString(statusListsMap.get(STATUS_ONLINE_OFFLINE));
// 第三步:使用 Pipeline 批量更新 Redis(防雪崩策略:删一个写一个)
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
// 1. 按分片逐个处理:先删除该分片,立即写入该分片的新数据(避免缓存雪崩)
for (int i = 0; i < SHARD_COUNT; i++) {
String shardKey = getShardKey(i);
// 删除该分片(清理已删除设备的残留数据)
connection.keyCommands().del(shardKey.getBytes());
// 立即写入该分片的新数据(如果该分片有数据)
Map<String, String> shardData = shardDataMap.get(i);
if (shardData != null && !shardData.isEmpty()) {
shardData.forEach((deviceId, status) -> {
connection.hashCommands().hSet(
shardKey.getBytes(),
deviceId.getBytes(),
status.getBytes()
);
});
}
}
// 2. 更新 JSON 索引(一次性写入整个JSON)
connection.stringCommands().set(ONLINE_JSON_KEY.getBytes(), onlineJson.getBytes());
connection.stringCommands().set(OFFLINE_JSON_KEY.getBytes(), offlineJson.getBytes());
connection.stringCommands().set(ONLINE_OFFLINE_JSON_KEY.getBytes(), abnormalJson.getBytes());
return null;
});
long duration = System.currentTimeMillis() - startTime;
log.info("批量更新设备状态(含JSON索引)完成,共 {} 台设备,分布在 {} 个分片,耗时: {}ms",
deviceStatusMap.size(), shardDataMap.size(), duration);
log.debug("状态分布 - 在线:{}, 离线:{}, 异常:{}",
statusListsMap.get(STATUS_ONLINE).size(),
statusListsMap.get(STATUS_OFFLINE).size(),
statusListsMap.get(STATUS_ONLINE_OFFLINE).size());
} catch (JsonProcessingException e) {
log.error("序列化设备状态为 JSON 失败", e);
throw new RuntimeException("设备状态序列化失败", e);
}
}
/**
* 获取单个设备状态
*
* @param deviceCommId 设备通信ID
* @return "0"-离线,"1"-在线,"2"-异常,null-不存在
*/
@Override
public String getDeviceStatus(String deviceCommId) {
int shardIndex = getShardIndex(deviceCommId);
String shardKey = getShardKey(shardIndex);
Object status = redisTemplate.opsForHash().get(shardKey, deviceCommId);
return status != null ? status.toString() : null;
}
/**
* 批量查询设备状态(优化版 - 按分片分组查询)
*
* 性能:O(N),1000台设备 < 10ms
*
* @param deviceCommIds 设备ID列表
* @return 设备ID -> 状态值的映射
*/
@Override
public Map<String, String> batchGetDeviceStatus(List<String> deviceCommIds) {
if (deviceCommIds == null || deviceCommIds.isEmpty()) {
return Collections.emptyMap();
}
long startTime = System.currentTimeMillis();
// 按分片分组
Map<Integer, List<String>> shardDevicesMap = new HashMap<>();
deviceCommIds.forEach(deviceId -> {
int shardIndex = getShardIndex(deviceId);
shardDevicesMap.computeIfAbsent(shardIndex, k -> new ArrayList<>())
.add(deviceId);
});
// 批量查询每个分片
Map<String, String> result = new HashMap<>();
shardDevicesMap.forEach((shardIndex, devices) -> {
String shardKey = getShardKey(shardIndex);
List<Object> statuses = redisTemplate.opsForHash().multiGet(shardKey,
new ArrayList<>(devices));
for (int i = 0; i < devices.size(); i++) {
Object status = statuses.get(i);
if (status != null) {
result.put(devices.get(i), status.toString());
}
}
});
long duration = System.currentTimeMillis() - startTime;
log.debug("批量查询 {} 台设备状态,耗时: {}ms", deviceCommIds.size(), duration);
return result;
}
/**
* 检查设备是否在线
*
* @param deviceCommId 设备通信ID
* @return true-在线,false-离线或不存在
*/
@Override
public boolean isDeviceOnline(String deviceCommId) {
String status = getDeviceStatus(deviceCommId);
return STATUS_ONLINE.equals(status);
}
/**
* 批量检查设备是否在线
*
* @param deviceCommIds 设备ID列表
* @return 设备ID -> 是否在线的映射
*/
@Override
public Map<String, Boolean> batchCheckOnline(List<String> deviceCommIds) {
Map<String, String> statusMap = batchGetDeviceStatus(deviceCommIds);
return deviceCommIds.stream()
.collect(Collectors.toMap(
deviceId -> deviceId,
deviceId -> STATUS_ONLINE.equals(statusMap.get(deviceId))
));
}
/**
* 获取指定状态的所有设备(极速版 - 使用 String JSON 索引)
*
* 性能:O(1),15万设备 < 5ms(比Set快40%)⚡
*
* @param status 状态值:"0"-离线,"1"-在线,"2"-异常
* @return 该状态下的所有设备ID列表
*/
@Override
public List<String> getDevicesByStatus(String status) {
long startTime = System.currentTimeMillis();
try {
String jsonKey = getJsonKeyByStatus(status);
String json = redisTemplate.opsForValue().get(jsonKey);
if (json == null || json.isEmpty()) {
log.warn("状态为 {} 的设备JSON索引不存在", status);
return Collections.emptyList();
}
// 反序列化 JSON
List<String> result = objectMapper.readValue(json, new TypeReference<List<String>>() {});
long duration = System.currentTimeMillis() - startTime;
log.info("查询状态为 {} 的设备,共 {} 台,耗时: {}ms", status, result.size(), duration);
return result;
} catch (JsonProcessingException e) {
log.error("反序列化设备状态 JSON 失败,status: {}", status, e);
return Collections.emptyList();
}
}
/**
* 分页获取指定状态的设备(适用于设备数量特别多的情况)
*
* @param status 状态值
* @param page 页码(从0开始)
* @param pageSize 每页数量
* @return 设备ID列表
*/
@Override
public List<String> getDevicesByStatusPaged(String status, int page, int pageSize) {
List<String> allDevices = getDevicesByStatus(status);
int fromIndex = page * pageSize;
if (fromIndex >= allDevices.size()) {
return Collections.emptyList();
}
int toIndex = Math.min(fromIndex + pageSize, allDevices.size());
return allDevices.subList(fromIndex, toIndex);
}
/**
* 获取所有设备的状态统计(极速版 - 使用 JSON 长度)
*
* 性能:O(1),< 2ms(比Set快60%)⚡
*
* @return Map<状态, 数量>
*/
@Override
public Map<String, Long> getStatusStatistics() {
long startTime = System.currentTimeMillis();
try {
Map<String, Long> statistics = new HashMap<>();
// 从 JSON 获取设备数量(只需反序列化获取 size,不需要完整解析)
statistics.put(STATUS_ONLINE, getDeviceCountFromJson(ONLINE_JSON_KEY));
statistics.put(STATUS_OFFLINE, getDeviceCountFromJson(OFFLINE_JSON_KEY));
statistics.put(STATUS_ONLINE_OFFLINE, getDeviceCountFromJson(ONLINE_OFFLINE_JSON_KEY));
long duration = System.currentTimeMillis() - startTime;
log.debug("获取状态统计,耗时: {}ms", duration);
return statistics;
} catch (Exception e) {
log.error("获取状态统计失败", e);
return Collections.emptyMap();
}
}
/**
* 从 JSON 获取设备数量(快速方法,不完整解析)
*/
private Long getDeviceCountFromJson(String jsonKey) {
try {
String json = redisTemplate.opsForValue().get(jsonKey);
if (json == null || json.isEmpty()) {
return 0L;
}
// 快速计算 JSON 数组长度(通过反序列化)
List<String> devices = objectMapper.readValue(json, new TypeReference<List<String>>() {});
return (long) devices.size();
} catch (Exception e) {
log.error("从 JSON 获取设备数量失败,key: {}", jsonKey, e);
return 0L;
}
}
/**
* 根据状态获取对应的 JSON Key
*
* @param status 状态值
* @return JSON Key
*/
private String getJsonKeyByStatus(String status) {
DevConst.OnlineStatus onlineStatus = DevConst.OnlineStatus.create(status);
if (onlineStatus == null){
throw new IllegalArgumentException("未知的设备状态: " + status);
}
return switch (onlineStatus) {
case ONLINE -> ONLINE_JSON_KEY;
case OFFLINE -> OFFLINE_JSON_KEY;
case ON_OFFLINE -> ONLINE_OFFLINE_JSON_KEY;
};
}
/**
* 获取所有设备及其状态
* 注意:30万设备会返回大量数据,谨慎使用
*
* @return 设备ID -> 状态值的映射
*/
@Override
public Map<String, String> getAllDeviceStatus() {
Map<String, String> allStatus = new HashMap<>();
// 遍历所有分片
for (int i = 0; i < SHARD_COUNT; i++) {
String shardKey = getShardKey(i);
Map<Object, Object> shardData = redisTemplate.opsForHash().entries(shardKey);
shardData.forEach((deviceId, status) ->
allStatus.put(deviceId.toString(), status.toString())
);
}
log.info("获取所有设备状态,共 {} 台设备", allStatus.size());
return allStatus;
}
/**
* 删除设备状态
*
* @param deviceCommId 设备通信ID
*/
@Override
public void deleteDeviceStatus(String deviceCommId) {
int shardIndex = getShardIndex(deviceCommId);
String shardKey = getShardKey(shardIndex);
redisTemplate.opsForHash().delete(shardKey, deviceCommId);
log.info("设备 {} 状态已删除", deviceCommId);
}
/**
* 获取设备总数
*
* @return 所有分片中的设备总数
*/
@Override
public long getTotalDeviceCount() {
long total = 0;
for (int i = 0; i < SHARD_COUNT; i++) {
String shardKey = getShardKey(i);
total += redisTemplate.opsForHash().size(shardKey);
}
return total;
}
/**
* 清空所有设备状态(慎用!)
*/
@Override
public void clearAllDeviceStatus() {
for (int i = 0; i < SHARD_COUNT; i++) {
String shardKey = getShardKey(i);
redisTemplate.delete(shardKey);
}
log.warn("已清空所有设备状态!");
}
/**
* 分页查询设备在线状态列表(极速版)
*
* 支持:
* 1. 按状态筛选(0-离线,1-在线,2-在离状态)
* 2. 不传状态则查询全部设备
* 3. 关键字模糊搜索
* 4. 分页(页码从1开始)
*
* 性能优化策略:
* - 如果指定状态:只读取1个JSON索引(只需1次Redis读取)⚡
* - 如果查询全部:读取3个JSON索引(只需3次Redis读取)
* - 在内存中完成过滤、排序、分页
* - 无需再查询Hash分片,避免大量Redis操作
*
* 性能对比:
* - 旧方案:getAllDeviceStatus() 需要查询30个Hash分片 + 再批量查询状态
* - 新方案:只读取1-3个JSON字符串,一次性获取所有数据
*
* @param request 分页查询请求
* @return JPAPageVo<DeviceOnlineStatusVO> 设备在线状态分页列表
*/
@Override
public JPAPageVo<DeviceOnlineStatusVO> pageQueryDeviceStatus(DeviceOnlineStatusPageRequest request) {
long startTime = System.currentTimeMillis();
try {
Map<String, String> allDeviceStatusMap = new HashMap<>();
int jsonReadCount = 0;
// 1. 智能读取JSON索引:如果指定状态只读取对应的JSON,否则读取全部
if (request.getStatus() != null && !request.getStatus().isEmpty()) {
// 只读取指定状态的JSON索引(1次Redis读取)⚡
String targetStatus = request.getStatus();
String jsonKey = getJsonKeyByStatus(targetStatus);
String json = redisTemplate.opsForValue().get(jsonKey);
if (json != null && !json.isEmpty()) {
List<String> devices = objectMapper.readValue(json, new TypeReference<List<String>>() {});
devices.forEach(deviceId -> allDeviceStatusMap.put(deviceId, targetStatus));
}
jsonReadCount = 1;
} else {
// 查询全部:读取3个JSON索引(3次Redis读取)
// 读取在线设备
String onlineJson = redisTemplate.opsForValue().get(ONLINE_JSON_KEY);
if (onlineJson != null && !onlineJson.isEmpty()) {
List<String> onlineDevices = objectMapper.readValue(onlineJson, new TypeReference<List<String>>() {});
onlineDevices.forEach(deviceId -> allDeviceStatusMap.put(deviceId, STATUS_ONLINE));
}
// 读取离线设备
String offlineJson = redisTemplate.opsForValue().get(OFFLINE_JSON_KEY);
if (offlineJson != null && !offlineJson.isEmpty()) {
List<String> offlineDevices = objectMapper.readValue(offlineJson, new TypeReference<List<String>>() {});
offlineDevices.forEach(deviceId -> allDeviceStatusMap.put(deviceId, STATUS_OFFLINE));
}
// 读取在离状态设备
String onlineOfflineJson = redisTemplate.opsForValue().get(ONLINE_OFFLINE_JSON_KEY);
if (onlineOfflineJson != null && !onlineOfflineJson.isEmpty()) {
List<String> onlineOfflineDevices = objectMapper.readValue(onlineOfflineJson, new TypeReference<List<String>>() {});
onlineOfflineDevices.forEach(deviceId -> allDeviceStatusMap.put(deviceId, STATUS_ONLINE_OFFLINE));
}
jsonReadCount = 3;
}
long readJsonTime = System.currentTimeMillis() - startTime;
log.debug("读取 {} 个JSON索引完成,共 {} 台设备,耗时: {}ms", jsonReadCount, allDeviceStatusMap.size(), readJsonTime);
// 2. 获取所有设备ID列表
List<String> filteredDeviceIds = new ArrayList<>(allDeviceStatusMap.keySet());
// 3. 关键字搜索(可选)
if (request.getKeyword() != null && !request.getKeyword().isEmpty()) {
String keyword = request.getKeyword().toLowerCase();
filteredDeviceIds = filteredDeviceIds.stream()
.filter(deviceId -> deviceId.toLowerCase().contains(keyword))
.collect(Collectors.toList());
}
// 4. 排序(保证结果稳定)
Collections.sort(filteredDeviceIds);
// 5. 分页计算(页码从1开始)
int page = request.getPage() != null ? request.getPage() : 1;
int pageSize = request.getPageSize() != null ? request.getPageSize() : 100;
long total = filteredDeviceIds.size();
// 转换为从0开始的索引
int pageIndex = page - 1;
int fromIndex = pageIndex * pageSize;
// 6. 获取当前页的设备ID列表
List<String> pageDeviceIds;
if (fromIndex >= filteredDeviceIds.size()) {
// 超出范围,返回空列表
pageDeviceIds = Collections.emptyList();
} else {
int toIndex = Math.min(fromIndex + pageSize, filteredDeviceIds.size());
pageDeviceIds = filteredDeviceIds.subList(fromIndex, toIndex);
}
// 7. 从内存Map中组装VO对象(无需再查Redis)
List<DeviceOnlineStatusVO> pageDeviceStatusList = new ArrayList<>();
for (String deviceId : pageDeviceIds) {
DeviceOnlineStatusVO vo = new DeviceOnlineStatusVO();
vo.setDeviceId(deviceId);
vo.setOnlineStatus(allDeviceStatusMap.get(deviceId));
pageDeviceStatusList.add(vo);
}
// 8. 构建分页响应
JPAPageVo<DeviceOnlineStatusVO> result = new JPAPageVo<>();
result.setContent(pageDeviceStatusList);
result.setPage(JPAPageVo.Page.build(total, page, pageSize));
long duration = System.currentTimeMillis() - startTime;
log.debug("分页查询设备在线状态列表,状态: {}, 关键字: {}, 页码: {}, 每页: {}, 总数: {}, 当前页: {}, Redis读取: {}次, 总耗时: {}ms (读JSON: {}ms)",
request.getStatus() != null ? request.getStatus() : "全部",
request.getKeyword() != null ? request.getKeyword() : "无",
page, pageSize, total, pageDeviceIds.size(), jsonReadCount, duration, readJsonTime);
return result;
} catch (Exception e) {
log.error("分页查询设备在线状态列表失败", e);
JPAPageVo<DeviceOnlineStatusVO> emptyResult = new JPAPageVo<>();
emptyResult.setContent(Collections.emptyList());
emptyResult.setPage(JPAPageVo.Page.build(0L, request.getPage(), request.getPageSize()));
return emptyResult;
}
}
}
总结
技术亮点
- ⚡ 分片 Hash:避免单 Key 过大,性能提升 47%
- ⚡ JSON 索引:替代 Set,内存节省 30%,查询快 50%
- ⚡ Pipeline 管道:批量更新快 100倍,单次网络往返
- ⚡ 双索引设计:Hash 用于点查,JSON 用于范围查,各取所长
适用场景
✅ 大规模设备在线状态管理(10万+ 设备)
✅ 高频查询 + 定期批量更新
✅ 需要按状态筛选和统计
✅ 对内存和性能有较高要求
扩展性
- 支持动态调整分片数量
- 支持水平扩展(Redis Cluster)
- 支持增量更新优化
附录
相关代码文件
RedisDeviceOnlineStatusManager.java:核心实现类DeviceOnlineStatusManager.java:接口定义SchedulerSyncDeviceOnlineStatus.java:定时同步任务
关键配置
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 50
max-idle: 20
min-idle: 10
监控指标
- Pipeline 执行耗时
- 各状态设备数量
- 分片数据分布
- Redis 内存占用
文档版本:v1.0
最后更新:2025-01-15
维护人员:TigerIot 技术团队
1431

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



