如何实现设备组缓存的正确清除?——基于心跳请求和心跳响应的解决方案

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 如何实现设备组缓存的正确清除?——基于心跳请求和心跳响应的解决方案

在设备组关闭后,如何保证缓存中的设备组信息能够正确清除?本文将介绍如何通过前端实现设备组心跳检测和缓存清除,以及通过后端实现缓存清除的逻辑来解决该问题。我们还将详细讨论如何利用心跳请求和心跳响应来实现设备组缓存的正确清除,并提供基于Vue和SpringBoot的代码示例。


一、问题描述

在开发设备管理系统时,我们经常需要保证设备组在关闭后能够从缓存中正确删除,以避免占用过多的系统资源。


示例:若依前后端分离框架,如果用户使用当前设备组,那么当前设备组会被写进缓存里,然后被占用,其他用户则不能使用该设备组;如果用户退出当前设备组,那么将从缓存里删掉该设备,但是很难保证的情况是,如果用户突然关闭浏览器,或者不正常关闭页面、退出帐号,都不能正常从缓存里删除该设备组,如何保证不管怎么样退出,都能从缓存中删掉该设备组?


二、问题分析

前端使用一个定时器,每隔5秒向后端发送请求,告知后端当前设备组是否还在使用中。后端使用一个DEVICE_GROUP_KEY + id来保存设备组是否被占用的状态,当用户加入设备组时,将该设备组的状态设置为占用,并设定过期时间为10秒;当用户退出设备组时,从DEVICE_GROUP_KEY + id中删除该设备组的状态。如果后端收到了一段时间内没有收到定时器请求的设备组,就会自动将该设备组从DEVICE_GROUP_KEY + id中删除。


当用户正常退出设备组时,前端会清除定时器并向后端发送请求,告知后端该设备组已经退出使用。如果用户异常退出设备组,则后端会在一段时间后自动删除该设备组。


三、解决方案

你的前端代码看起来已经调用了后端接口将设备组放入缓存中了。如果你想实现定时向后端发送请求,告知后端该设备组是否还在使用中,可以使用setInterval函数创建一个定时器,每隔一定时间向后端发送请求,告知后端该设备组仍在使用中。


前端

deviceInfo是预选设备组,currentDeviceInfo是当前设备组,deviceGroupKeys是缓存中的设备组,代码示例如下:

<template>
  <div>
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="60px">
      <el-form-item label="设备组" prop="group">
        <el-select v-model="deviceGroup" placeholder="请选择" @change="selectDeviceGroup">
          <!-- 选择deviceGroupList -->
          <el-option v-for="item in deviceGroupList" :key="item.deviceGroup" :label='"第" + item.deviceGroup + "组"'
            :value="item.deviceGroup" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-circle-check" size="mini" @click="joinDeviceGroup">确定</el-button>
        <el-button icon="el-icon-remove-outline" size="mini" type="info" @click="leaveDeviceGroup">退出当前设备组</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
    /** 加入设备组 */
  async joinDeviceGroup(){
  //查询预选的设备组是否被占用
  if (this.deviceGroupKeys.includes(`${this.deviceInfo.deviceGroup}`)) {
          this.$message({
            message: "该设备组已被使用",
            type: "warning",
          });
          return;
        }
        //预选设备组没有被占用,如果当前设备组被自己占用,则将其从缓存中删除
        if (this.currentDeviceInfo.deviceGroup !== "") {
          await deleteDeviceGroupKey(this.currentDeviceInfo.deviceGroup);
          clearInterval(this.timer);
        }
        //否则将deviceInfo预选设备组放入currentDeviceInfo当前设备组
        this.currentDeviceInfo = JSON.parse(JSON.stringify(this.deviceInfo)) || null;
        // 将currentDeviceInfo中的deviceGroup放入缓存,用setDeviceGroupKey方法
        await setDeviceGroupKey(this.currentDeviceInfo.deviceGroup);
        // 开启定时器
        this.setInterval();
  }
    /** 定义一个定时器,每隔5秒钟,调用一次sendDeviceHeartbeat方法 */
    setInterval() {
      // 如果currentDeviceInfo.deviceGroup为空,则停止定时器
      if (this.currentDeviceInfo.deviceGroup == "") {
        clearInterval(this.timer);
      } else {
        this.timer = setInterval(() => {
          this.sendDeviceHeartbeat();
        }, 5000);
      }
    },
    // 发送心跳请求的函数
    sendDeviceHeartbeat() {
      // 如果this.currentDeviceInfo.deviceGroup为空,则停止定时器
      if (this.currentDeviceInfo.deviceGroup == "") {
        clearInterval(this.timer);
      }
      // 发送请求deviceHeartBeat
    deviceHeartBeat(this.currentDeviceInfo.deviceGroup).then((response) => {
        // console.log(response);
        if (response === 0) {
          // 心跳成功,设备组仍在使用中
        } else {
          // 心跳失败,设备组已经退出使用
          this.$message({
            message: "设备组已经退出使用",
            type: "warning",
          });
          clearInterval(this.timer);
        }
      });
    },

然后在用户正常退出设备组时,清除定时器并向后端发送请求,告知后端该设备组已经退出使用。代码示例如下:

leaveDeviceGroup() {
      if (this.currentDeviceInfo.deviceGroup != "") {
        deleteDeviceGroupKey(this.currentDeviceInfo.deviceGroup).then((response) => {
          //清空currentDeviceInfo
          this.currentDeviceInfo = {
            deviceGroup: "",
          };
        });
      }
      // 停止定时器
      clearInterval(this.timer);
}

计时器应该在用户正常退出设备组和关闭页面时被清除。在Vue中,可以通过在beforeDestroy()生命周期钩子中清除计时器,例如:

beforeDestroy() {
  clearInterval(this.timer);
}

这里假设你的计时器是通过setInterval()创建的,并将其存储在Vue实例的timer属性中。当Vue实例被销毁时,beforeDestroy()生命周期钩子会被调用,此时可以清除计时器。

后端

获取缓存中deviceGroup所有的key

    /**
     * 获取缓存中deviceGroup所有的key
     */
    @GetMapping("/getDeviceGroupKeys")
    public List<Integer> getDeviceGroupKeys() {
        //将redis中device_group的的基本对象列表,使用redisCache.keys()方法获取
        String[] keys = redisCache.keys(CacheConstants.DEVICE_GROUP_KEY + "*").toArray(new String[0]);
        //将keys中的值,去掉前缀,只保留1,2,3,4,5
        List<Integer> list = new ArrayList<>();
        for (String key : keys) {
            list.add(Integer.parseInt(key.substring(CacheConstants.DEVICE_GROUP_KEY.length())));
        }
        //将list从小到大排序
        list.sort((o1, o2) -> o1 - o2);
        return list;
    }

实现将设备组放入缓存

    /**
     * 将设备组放入缓存
     */
    @GetMapping(value = "/setDeviceGroupKey/{id}")
    public String setDeviceGroupKey(@PathVariable("id") Integer id) {
        redisCache.setCacheObject(CacheConstants.DEVICE_GROUP_KEY + id, "true", 10, TimeUnit.SECONDS);
        return redisCache.hasKey(CacheConstants.DEVICE_GROUP_KEY + id) ? "true" : "false";
    }

在用户正常退出设备组时,你可以实现一个deleteDeviceGroupKey接口,用于从缓存中删除该设备组。代码示例如下:

    /**
     * 将设备组从缓存中删除
     */
    @GetMapping("/deleteDeviceGroupKey/{id}")
    public String deleteDeviceGroupKey(@PathVariable("id") Integer id) {
        redisCache.deleteObject(CacheConstants.DEVICE_GROUP_KEY + id);
        return redisCache.hasKey(CacheConstants.DEVICE_GROUP_KEY + id) ? "false" : "true";
    }

你可以实现一个deviceHeartBeat接口,用于更新设备组在缓存中的存活时间。代码示例如下:

    /**
     * 检查设备组是否还在使用,心跳请求处理接口
     */
    @PostMapping(value = "/deviceHeartBeat/{id}")
    public String deviceHeartBeat(@PathVariable("id") Integer id) {
        // 检查设备组是否存在于缓存中
        if (!redisCache.hasKey(CacheConstants.DEVICE_GROUP_KEY + id)) {
            // 设备组不存在,返回心跳失败
            return "Device group not found!";
        } else {
            // 更新设备组的心跳时间
            redisCache.expire(CacheConstants.DEVICE_GROUP_KEY + id, 10, TimeUnit.SECONDS);
            // 返回心跳成功
            return "Heartbeat successfully!";
        }
    }

如果用户异常退出设备组,你可以在后端实现一个定时任务,定时检查缓存中的设备组是否过期,如果过期则删除该设备组。代码示例如下:

  /**
   * 定时任务:删除过期的设备组,每隔10秒检查一次缓存中的设备组是否超时
   */
    @Scheduled(fixedDelay = 10000)
    public void checkDeviceGroupKey() {
        // 获取当前时间
        Date now = new Date();
      Set<Object> deviceGroupKeys = redisCache.keys(CacheConstants.DEVICE_GROUP_KEY + "*");
        // 遍历缓存中的设备组DEVICE_GROUP_KEY,检查是否超时
        for (Object deviceGroupKey : deviceGroupKeys) {
            // 获取缓存中的设备组
            String key = (String) deviceGroupKey;
            // 如果缓存中的设备组存在
            if (redisCache.hasKey(key)) {
                // 获取缓存中的设备组的最后一次心跳时间
                Date lastHeartBeatTime = redisCache.getCacheObject(key);
                // 计算当前时间和最后一次心跳时间的差值
                long diff = now.getTime() - lastHeartBeatTime.getTime();
                // 如果差值大于10秒,说明设备组已经超时,将设备组从缓存中删除
                if (diff > 10000) {
                    redisCache.deleteObject(key);
                }
            }
        }
    }

也可以限制哪几个设备组需要被清除

    /**
     * 定时任务:删除过期的设备组,每隔10秒检查一次缓存中的设备组是否超时
     */
    @Scheduled(fixedDelay = 10000)
    public void checkDeviceGroupKey() {
        // 获取当前时间
        Date now = new Date();
        // 遍历缓存中的设备组DEVICE_GROUP_KEY,检查是否超时
        for (int i = 1; i <= 16; i++) {
            // 获取缓存中的设备组
            String key = CacheConstants.DEVICE_GROUP_KEY + i;
            // 如果缓存中的设备组存在
            if (redisCache.hasKey(key)) {
                // 获取缓存中的设备组的最后一次心跳时间
                Date lastHeartBeatTime = redisCache.getCacheObject(key);
                // 计算当前时间和最后一次心跳时间的差值
                long diff = now.getTime() - lastHeartBeatTime.getTime();
                // 如果差值大于10秒,说明设备组已经超时,将设备组从缓存中删除
                if (diff > 10000) {
                    redisCache.deleteObject(key);
                }
            }
        }
    }

后端缓存时间设置为10秒钟,前端每隔5秒向后端发送请求,那么在正常情况下,如果前端正常关闭,后端会在10秒钟后自动清除该设备组的缓存。


如果前端异常关闭,那么后端会在10秒钟后检测到该设备组的心跳信号已经停止,然后自动清除该设备组的缓存。


因此,这种方法可以保证在大多数情况下能够及时清除缓存,但是仍然可能存在一些极端情况导致缓存无法及时清除,比如网络故障等。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
7月前
|
存储 缓存 安全
第二章 HTTP请求方法、状态码详解与缓存机制解析
第二章 HTTP请求方法、状态码详解与缓存机制解析
137 0
|
23天前
|
缓存 NoSQL 数据库
缓存穿透、缓存击穿和缓存雪崩及其解决方案
在现代应用中,缓存是提升性能的关键技术之一。然而,缓存系统也可能遇到一系列问题,如缓存穿透、缓存击穿和缓存雪崩。这些问题可能导致数据库压力过大,甚至系统崩溃。本文将探讨这些问题及其解决方案。
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
40 5
|
2月前
|
缓存 弹性计算 NoSQL
新一期陪跑班开课啦!阿里云专家手把手带你体验高并发下利用云数据库缓存实现极速响应
新一期陪跑班开课啦!阿里云专家手把手带你体验高并发下利用云数据库缓存实现极速响应
|
6月前
|
缓存 NoSQL Java
避免缓存失效的三大杀手:缓存击穿、穿透与雪崩的解决方案
避免缓存失效的三大杀手:缓存击穿、穿透与雪崩的解决方案
132 0
|
2月前
|
缓存 JavaScript CDN
一次js请求一般情况下有哪些地方会有缓存处理?
一次js请求一般情况下有哪些地方会有缓存处理?
42 4
|
3月前
|
缓存 JavaScript 中间件
优化Express.js应用程序性能:缓存策略、请求压缩和路由匹配
在开发Express.js应用时,采用合理的缓存策略、请求压缩及优化路由匹配可大幅提升性能。本文介绍如何利用`express.static`实现缓存、`compression`中间件压缩响应数据,并通过精确匹配、模块化路由及参数化路由提高路由处理效率,从而打造高效应用。
192 14
|
2月前
|
缓存 NoSQL 关系型数据库
缓存穿透以及解决方案
缓存穿透以及解决方案
39 0
|
4月前
|
存储 缓存 运维
平稳扩展:可支持RevenueCat每日12亿次API请求的缓存
平稳扩展:可支持RevenueCat每日12亿次API请求的缓存
50 1
|
4月前
|
存储 缓存 NoSQL
【性能飙升的秘密】FastAPI应用如何借助缓存技术实现极速响应?揭秘高效Web开发的制胜法宝!
【8月更文挑战第31天】FastAPI是一个高性能Web框架,利用Starlette和Pydantic实现高效API构建。本文介绍如何通过缓存提升FastAPI应用性能,包括使用`starlette-cache[redis]`实现Redis缓存,以及缓存一致性和缓存策略的注意事项。通过具体示例展示了缓存的配置与应用,帮助开发者构建更高效的Web应用。
285 0