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

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

📋 目录


业务场景

背景

在物联网设备管理系统中,需要实时监控大规模设备(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 技术团队

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值