设备在线状态缓存便捷的技术方案

设备在线状态缓存技术方案

目录


业务场景

背景

在物联网设备管理系统中,需要实时监控大规模设备(30万+)的在线状态。设备状态包括:

  • 在线(1):设备正常连接
  • 离线(0):设备未连接或断开
  • 在离状态(2):设备异常或不稳定

核心需求

  1. 高频查询:前端需要实时查询设备状态(单设备、批量、按状态筛选)
  2. 批量更新:定时任务每 60 秒同步 30万+ 设备状态到缓存
  3. 状态统计:快速统计各状态设备数量
  4. 分页查询:支持按状态分页查询设备列表
  5. 低延迟要求:查询响应时间 < 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 × RTT1 × 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台)15ms8ms47% 提升
按状态筛选(15万台)8ms4ms50% 提升
状态统计5ms1ms80% 提升
批量更新(30万台)1000ms780ms22% 提升
内存占用450MB315MB30% 节省

实际生产数据

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;
                                                                                }
                                                                                }
                                                                                }

总结

技术亮点

  1. 分片 Hash:避免单 Key 过大,性能提升 47%
  2. JSON 索引:替代 Set,内存节省 30%,查询快 50%
  3. Pipeline 管道:批量更新快 100倍,单次网络往返
  4. 双索引设计: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 技术团队

posted @ 2025-12-19 12:31  clnchanpin  阅读(20)  评论(0)    收藏  举报