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

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

📋 目录


业务场景

背景

在物联网设备管理系统中,需要实时监控大规模设备(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 过大导致性能问题。

分片策略
java 复制代码
// 使用设备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 存储每种状态的设备列表:

redis 复制代码
device:status:online  → Set {设备1, 设备2, ...}  (15万设备)
device:status:offline → Set {设备3, 设备4, ...}  (14万设备)

问题

  • ❌ Set 内存占用大(每个元素额外开销)
  • ❌ 批量更新慢(需要逐个 SADD/SREM)
改进方案:String + JSON
redis 复制代码
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 + 反序列化

实现代码
java 复制代码
// 序列化为 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

传统方式(逐个执行)

java 复制代码
// 每次命令都需要网络往返(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 方式(批量执行)

java 复制代码
// 所有命令只需要一次网络往返
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万设备状态

java 复制代码
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. 批量更新设备状态

java 复制代码
@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. 批量查询设备状态

java 复制代码
@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. 按状态查询设备

java 复制代码
@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. 状态统计

java 复制代码
@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. 完整例子实现逻辑

java 复制代码
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:定时同步任务

关键配置

yaml 复制代码
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 技术团队

相关推荐
番茄炒鸡蛋加糖2 小时前
缓存三大问题
缓存
江晓鱼未暖7 小时前
十七、Redis 核心原理与架构详解
大数据·数据库·数据仓库·redis·缓存·架构
小罗水7 小时前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
热心市民lcj7 小时前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
zx1154508 小时前
【热点 Key 探测与加长缓存 TTL 实战】
缓存
toooooop81 天前
如何用 ss + ps 精准定位本机 Redis 的“隐形”消费者?
linux·数据库·redis·缓存
软萌萌的11 天前
C#中的多级缓存架构设计与实现深度解析
缓存·c#·wpf
醉城夜风~1 天前
HTML表单域学习博客:表单标签、输入框、按钮详解
android·java·缓存
肥胖小羊1 天前
企业微信应用的 Token 缓存策略与分布式锁高可用设计
分布式·缓存·企业微信
Rooting++1 天前
redis注册为服务
数据库·redis·缓存