一、背景介绍

   项目是公司一个未验收的智慧园区演示项目,项目大屏上之前都是demo静态数据,现在通过mqtt接收物联网设备实时传感器数据并在大屏页面上进行展示,大屏上有一个长度为10的列表动态刷新展示实时物联设备传感器数据。因为项目并没有正式验收还处在一个demo状态,并且需求简单仅作简单展示并没有统计、图表等需求,物联设备实时传感器数据量大为了不占用过多存储资源,因此物联设备传感器数据没有使用mysql或者mongo等进行存储。

  因为不需要考虑历史数据存储问题,仅需保留近10条数据即可,因此考虑使用一个定长队列来实现该需求,将新收到的传感器数据入队列,老数据出队列随即丢失。

二、实现思路

  物联设备数量较多,而且传感器数据推送频率较高约为3s/次/设备,因此传感器数据存储队列需要考虑并发问题。

  内存中维护长度为10的队列。内存中维护队列的较为简单的方式是使用ConcurrentLinkedQueue来实现。

  redis中维护长度为10的队列。redis中没有队列这种数据结构,实际通过list数据结构和操作就可以实现队列。

相关命令介绍:

LLEN key
  计算List的长度
  时间复杂度:O(1)

RPOP key [count]
    从List的右侧移除元素
    时间复杂度:O(N),N为移除元素的个数。
LPOP key [count]
    从List的左侧移除元素
    时间复杂度:O(N),N为移除元素的个数。

RPUSH key element [element ...]
    从List的右侧保存元素
    时间复杂度:O(N),N为保存元素的个数。
LPUSH key element [element ...]
    从List的左侧保存元素
    时间复杂度:O(N),N为保存元素的个数。

LTRIM key startInclusive endInclusive
  对列表进行修剪,只保留startInclusive-endInclusive区间的元素(前后都包括),不在指定区间内的元素都将被删除。

 

三、实现方案

 

方案一:lua脚本

  传感器数据入队列和出队列无法通过单条redis命令实现,因此需要考虑多条命令间的原子性,可以使用lua脚本进行操作。最新的传感器数据通过LPUSH添加到队列头部,再对list进行LTRIM操作,只保留前10个元素,其他内容丢弃。如此就实现了一个前入后弃的定长为10的队列。而且由redis执行lua脚本保证原子性。(只能保证执行lua脚本中多条命令时不会有其他命令进行执行)

  springboot项目中使用redisTemplate执行lua脚本代码示例:

@Repository
public class DeviceRepository {

    private static final Logger logger = LoggerFactory.getLogger(DeviceRepository.class);

    public static final String DEVICE_REG_INFO_KEY = "sxrjy:device_reg_infos";

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * redis lua脚本:维护一个定长队列(长度10),每次新数据加到队列头部,超过长度部分丢弃
     */
    private static final String FIXED_LENGTH_QUEUE_LUA_SCRIPT =
                    "local key = KEYS[1] " +
                    "redis.call('lpush',key,unpack(ARGV))" +
                    "redis.call('ltrim',key,0,9)";

    /**
     * 推送数据到redis
     *
     * @param regInfos 设备寄存器信息list
     * @date 2022/8/3 10:03
     */
    public void pushDeviceRegInfo(List<DeviceRegInfo> regInfos) {
        // lua脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(FIXED_LENGTH_QUEUE_LUA_SCRIPT);
        String[] regInfoJsons = regInfos.stream().map(JSON::toJSONString).toArray(String[]::new);
        // 执行lua脚本
        redisTemplate.execute(redisScript, Collections.singletonList(DEVICE_REG_INFO_KEY), regInfoJsons);
    }

    /**
     * 获取设备寄存器数据列表
     *
     * @date 2022/8/3 12:17
     */
    public List<DeviceRegInfoVO> getDeviceRegInfoList() {
        return Optional.ofNullable(redisTemplate.opsForList().range(DEVICE_REG_INFO_KEY, 0, -1))
                .orElseGet(ArrayList::new)
                .stream()
                // str转实体
                .map(s -> JSON.parseObject(s, DeviceRegInfo.class))
                // 时间倒叙排序列表
                .sorted(Comparator.comparing(DeviceRegInfo::getCreateTime).reversed())
                // 只取10条
                .limit(10)
                // po转vo
                .map(DeviceUtil::deviceRegInfoPoToVo)
                .collect(Collectors.toList());
    }
}

 

方案二:multi+exec

   redis通过MULTI、EXEC、WATCH等命令来实现事务(transaction)功能。redis的事务提供了一种将多个命令请求打包、一次性、按顺序地执行多个命令的机制。在事务执行期间,服务器不会中断事务而去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。redis事务不支持回滚机制,如果事务中的某条命令在执行期间出现了错误,事务后续其他命令都不会执行,已经执行的命令都不会收到任何影响,不进行回滚。

@Repository
public class DeviceRepository {

    private static final Logger logger = LoggerFactory.getLogger(DeviceRepository.class);

    public static final String DEVICE_REG_INFO_KEY = "sxrjy:device_reg_infos";

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * redis lua脚本:维护一个定长队列(长度10),每次新数据加到队列头部,超过长度部分丢弃
     */
    private static final String FIXED_LENGTH_QUEUE_LUA_SCRIPT =
                    "local key = KEYS[1] " +
                    "redis.call('lpush',key,unpack(ARGV))" +
                    "redis.call('ltrim',key,0,9)";

    /**
     * 推送数据到redis
     *
     * @param regInfos 设备寄存器信息list
     * @author liurong
     * @date 2022/8/3 10:03
     */
    public void pushDeviceRegInfo(List<DeviceRegInfo> regInfos) {
       SessionCallback sessionCallback = new SessionCallback() {
           @Override
           public Object execute(RedisOperations redisOperations) throws DataAccessException {
               redisOperations.multi();
               String[] regInfoJsons = regInfos.stream().map(JSON::toJSONString).toArray(String[]::new);
               ListOperations listOperations = redisOperations.opsForList();
               listOperations.leftPushAll(DEVICE_REG_INFO_KEY, regInfoJsons);
               listOperations.trim(DEVICE_REG_INFO_KEY, 0, 9);
               return redisOperations.exec();
           }
       };
       redisTemplate.execute(sessionCallback);
    }

}

 

 

参考文献:

Redis 定长队列的探索和实践 (qq.com)