redis实现定长队列
一、背景介绍
项目是公司一个未验收的智慧园区演示项目,项目大屏上之前都是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); } }
参考文献:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)