Redis Zset Score精度问题解析与解决方案

107 篇文章 ¥59.90 ¥99.00
Redis Zset在处理分数时可能出现精度问题,导致排序和范围查询出错,甚至影响成员唯一性。解决方案包括将分数存储为字符串或扩大精度范围。通过这两种方式可以确保Redis Zset的正确性和准确性。

在软件测试和测试开发领域,Redis是一个常用的高性能键值存储系统。其中,Redis Zset(有序集合)是一种特殊的数据结构,它可以存储带有分数(Score)的成员(Member)。然而,Redis Zset在处理分数时可能存在精度问题,本文将对这一问题进行详细分析,并提供解决方案。

问题描述

Redis Zset的Score字段是一个浮点数,用于对成员进行排序。然而,由于计算机内部对浮点数的存储和计算存在精度限制,这可能导致在Redis Zset中存储的分数值出现舍入误差或精度丢失的问题。

具体而言,当我们存储一个浮点数作为Score时,Redis会将其转换为二进制进行存储。在二进制表示中,一些小数无法精确表示,这可能导致在进行计算或比较时出现意外的结果。例如,当我们存储一个分数为0.1的成员时,实际存储的值可能是0.10000000000000001或0.09999999999999999。

这种精度问题可能会在两个方面对Redis应用程序产生影响:

  1. 排序和范围查询:由于分数的精度问题,可能导致成员的排序结果不符合预期。当我们对Zset进行范围查询时,返回的成员列表可能会包含一些不应该出现在该范围内的成员。

  2. 成员唯一性:Redis Zset要求成员具有唯一性。然而,由于分数的精度问题,可能会导致相同分数值的成员被错误地视为不同的成员,从而破坏了成员的唯一性约束。

解决方案

针对Redis Zset在处理分数时可能出现的精度问题,我们可以采取以下解决方案:

  1. 使用字符串作为S
前言 1.1. 背景和目标 VMS Cloud Event模块作为承载海量设备事件上报的核心组件,负责事件过滤、转发、存储等复杂处理逻辑。当前依赖的公共组件EventCenter仅支持基础订阅消费能力,存在以下瓶颈: 异常事件处理缺失:失败事件直接丢弃,无重试/隔离机制; 时效性不足:缺乏延迟触发能力(如定时删除事件); 扩展性弱:无法适配新增的复杂业务场景(如设备批量操作或批处理)。 本模块在项目中处于事件处理链路的核心地位,直接影响设备事件的可靠性时效性。本需求分析文档的撰写目标是明确死信队列延迟队列的功能需求、非功能性需求及实现方案,为后续开发提供指导。 1.2. 定义 死信队列(Dead Letter Queue, DLQ):存储无法被正常消费的异常消息(如重试耗尽、消费失败),用于错误隔离问题排查。 延迟队列(Delayed Queue):支持消息在指定延迟时间后被消费,用于异步定时任务(如定时删除事件)。 EventCenter:现有公共组件,提供事件发送、处理器注册等基础能力,支持Kafka、Local实现。 指数退避原则:设置初始等待时间,发生可重试错误时重试,再次发生错误时等待时间会以指数级别增长。 1.3. 参考资料 kafka官方文档(Apache Kafka Documentation):用于死信队列自定义拦截器实现参考。 EventCenter现有设计文档(Event Center 使用说明文档 - CRD_EP_Software_Service - Confluence)及EventCenter学习报告(EventCenter组件学习报告 - CRD_EP_Software_Service - Confluence):包含模块结构、核心接口及消息处理模式说明。 kafka消息范式调研报告(kafka消息范式扩展调研报告 - CRD_EP_Software_Service - Confluence):包含kafka消息范式扩展概述,死信队列及延迟队列设计实现。 2. 调研结果 2.1 死信队列(Dead Letter Queue, DLQ)设计实现 2.1.1 死信队列核心价值 死信队列是存储“无法被正常消费的消息”的特殊队列,核心作用: 错误隔离:避免异常消息阻塞主消费流程,保障主队列吞吐量。 问题追踪:集中存储失败消息(含上下文、错误日志),便于定位根因。 数据补偿:支持人工/自动修复后重新投递,减少数据丢失风险。 2.1.2死信队列常见设计方案 方案1:基于Kafka消费者拦截器(Consumer Interceptor) 实现原理: 在Kafka消费者端实现ConsumerInterceptor接口,拦截poll()返回的消息。当消费逻辑(如EventHandler.handle())抛出异常时,拦截器捕获失败消息,通过独立生产者将其发送至死信Topic(如vms_dlq_topic),并提交原消息的偏移量(避免重复消费)。 关键流程: 消费者从Kafka拉取消息(poll())。 拦截器预处理消息(如记录元数据)。 业务逻辑消费消息(调用handleEvent())。 若消费成功,正常提交偏移量;若失败,拦截器: 记录失败原因(异常堆栈、重试次数)。 通过独立生产者将消息发送至死信Topic。 提交原消息偏移量(避免重复消费)。 worddavc4f46eb0b4de0b1c28abbc3921c9884a.png 优点: 完全基于Kafka原生API,无需引入外部中间件。 灵活控制重试策略(如最大重试次数、间隔)。 业务代码解耦(拦截器逻辑独立)。 缺点: 需开发拦截器逻辑,增加代码复杂度。 独立生产者需处理线程隔离(避免阻塞主消费线程)。 依赖消费者端配置(需为每个消费者组启用拦截器)。 方案2:基于Kafka生产者回调(Producer Callback) 实现原理: 在消息发送阶段,通过生产者的Callback接口捕获发送失败的消息(如网络异常、Broker不可用),将其直接发送至死信Topic。此方案主要处理“发送失败”的消息,而非“消费失败”的消息。 关键流程: 生产者调用send()发送消息,附加Callback。 若消息成功写入Kafka(RecordMetadata返回),流程结束。 若发送失败(Exception抛出),Callback捕获异常,将原消息+异常信息封装后发送至死信Topic。 优点: 直接捕获发送阶段的失败消息,避免未达Broker的消息丢失。 实现简单(仅需在生产者端添加回调逻辑)。 缺点: 仅覆盖“发送失败”场景,无法处理“消费失败”的消息。 死信Topic需主Topic同步扩容,增加运维成本。 方案3:Confluent平台DLQ支持(Kafka生态扩展) 实现原理: Confluent平台(Kafka商业发行版)提供内置DLQ功能,通过在消费者配置中指定dead.letter.topic.name,当消息消费失败(如反序列化异常、处理超时)时,Confluent客户端自动将消息转发至死信Topic。 优点: 零代码开发(仅需配置),集成成本低。 自动处理反序列化失败、消费超时等异常场景。 缺点: 依赖Confluent商业组件(需评估License成本)。 仅支持Confluent客户端(原生Kafka客户端不兼容)。 无法自定义重试策略(依赖默认逻辑)。 其他中间件对比 RabbitMQ DLX:通过绑定“死信交换器”实现,支持消息拒绝、超时、队列满等场景自动路由。优点是原生支持,缺点是Kafka技术栈不兼容。 RocketMQ DLQ:为每个消费者组自动创建死信队列(%DLQ%+组名),重试耗尽后自动存储。优点是无需开发,缺点是死信无法自动消费(需人工干预)。 2.1.3 死信队列方案对比总结 消费者拦截器 消费失败消息隔离 自主可控、兼容原生Kafka 需开发拦截器,线程隔离复杂 生产者回调 发送失败消息隔离 实现简单、覆盖发送阶段 不处理消费失败场景 Confluent DLQ 快速集成、低代码场景 零开发、自动转发 依赖商业组件,License成本高 2.2 延迟队列(Delayed Queue)设计实现 2.2.1 延迟队列核心价值 延迟队列支持消息在指定延迟时间后被消费,典型场景包括: 定时任务触发(如设备事件30分钟后删除)。 失败重试(如消费失败后5分钟重试)。 订单超时取消(如未支付订单30分钟后自动关闭)。 2.2.2 延迟队列常见设计方案 方案1:基于Redis有序集合(ZSet)的Kafka扩展 实现原理: 结合KafkaRedis,将延迟消息暂存于Redis ZSet(以到期时间戳为score),通过定时任务扫描ZSet,将到期消息发送至Kafka目标Topic。 关键流程: 消息生产:生产者将延迟消息(含事件内容、延迟时间)存入Redis ZSet(Key:vms_delay_queue,score=当前时间+延迟时间)。 扫描触发:定时任务(如每1秒)执行ZRANGEBYSCORE vms_delay_queue 0 <当前时间戳>,获取到期消息。 消息投递:将到期消息通过Kafka生产者发送至目标Topic(如vms_delete_event_topic)。 异常处理:若投递失败,重新设置消息的score(当前时间+10秒)并重新插入ZSet,等待下次扫描。 worddav39d7de94833eb9c9b2016e420c5fb318.png 优点: 支持任意延迟时间(毫秒级精度)。 复用现有Redis资源(如VMS缓存集群),无需引入新中间件。 分布式友好(通过Redis主从复制保障高可用)。 缺点: 需开发定时扫描逻辑(需处理并发扫描、消息去重)。 依赖Redis持久化(如AOF)保障消息不丢失。 扫描间隔精度权衡(间隔过小增加Redis压力,过大导致延迟误差)。 方案2:基于时间轮算法的Kafka内部扩展 实现原理: 时间轮(Time Wheel)是一种高效的延迟任务调度算法,通过“轮盘槽位”管理延迟任务。Kafka的KafkaDelayedMessage和Netty的HashedWheelTimer均基于此原理。在Kafka中,可扩展消费者端实现时间轮,将延迟消息按到期时间分配至不同槽位,轮盘转动时触发消息投递。 关键设计: 时间轮结构:轮盘分为多个槽位(如100个),每个槽位代表1秒。 消息入轮:计算消息到期时间当前时间的差值,分配至对应槽位(如延迟5秒的消息放入槽位5)。 轮盘转动:每秒移动一个槽位,触发当前槽位的消息投递。 优点: 时间复杂度O(1),高吞吐量下延迟低(百万级消息/秒)。 无需外部存储(依赖内存),响应速度快。 缺点: 需深度修改Kafka客户端源码(开发难度大)。 内存限制(槽位数量消息容量需平衡,大延迟消息可能跨多轮)。 消息持久化困难(内存数据易丢失,需结合日志备份)。 方案3:基于Kafka分区消费者暂停的分桶策略 实现原理: 利用Kafka的分区特性,为不同延迟时间创建独立分区(或Topic),消费者通过pause()和resume()控制分区消费时机。例如,为延迟5秒、30秒、5分钟的消息分别创建分区,消费者启动时暂停所有分区,根据当前时间计算各分区的恢复时间(如5秒后恢复延迟5秒的分区)。 关键流程: 消息生产:根据延迟时间将消息发送至对应分区(如topic-delay-5s、topic-delay-30s)。 消费者初始化:订阅所有延迟分区,调用pause()暂停消费。 定时恢复:定时任务检查当前时间,对到期的分区调用resume(),触发消息消费。 优点: 完全基于Kafka原生功能,无需外部组件。 分区隔离保障不同延迟消息的独立性。 缺点: 仅支持预设延迟等级(如5s、30s),无法动态调整。 分区数量随延迟等级增加而膨胀(如支持10种延迟需10个分区)。 消费者需维护复杂的分区恢复逻辑(易出错)。 其他中间件对比 RabbitMQ延迟交换器:通过x-delayed-message交换器实现,消息设置x-delay字段。优点是毫秒级精度,缺点是依赖插件且Kafka不兼容。 RocketMQ延迟消息:支持18级预设延迟(1s~2h),Broker暂存后转发。优点是原生支持,缺点是延迟等级固定。 2.2.3 延迟队列方案对比总结 Redis ZSet 自定义延迟、资源复用场景 灵活、复用现有资源 需开发扫描逻辑,依赖Redis 时间轮算法 高吞吐量、低延迟场景 高效、低延迟 开发难度大,内存依赖 Kafka分区分桶 预设延迟、原生依赖场景 无需外部组件 延迟等级固定,分区膨胀 3. 功能需求 3.1. 总体描述 本模块为VMS系统事件相关组件EventCenter的扩展模块,聚焦死信队列(DLQ)延迟队列的功能实现,解决现有EventCenter的异常事件处理不完善、时效性不足问题。模块通过低侵入式扩展(最小化修改EventCenter原生代码)提供配置化管理、灵活的重试策略及定时触发机制,支持开发人员通过EventCenter原生接口调用扩展功能(如发送延迟消息、启用死信队列),运维人员通过配置平台管理策略(如重试次数、延迟等级)。 扩展模块架构如下图所示: 3.1.1. 需求主体 开发人员 VMS业务系统开发者,调用EventCenter的接口开发消息队列相关功能(如设备事件上报),需通过扩展接口启用死信队列、发送延迟消息以及配置相关策略。 运维人员 VMS系统运维人员,负责监控死信堆积量。 3.1.2. 功能模块划分 配置管理模块 01 管理死信队列(开关、重试策略)的全局配置,支持开发人员通过EventCenter接口传递配置。 死信处理模块 02 基于EventCenter消费者拦截器扩展,捕获消费失败消息,执行重试逻辑,发送至DLQ并记录日志,EventCenter原生消费流程解耦。 延迟消息管理模块 03 两种延迟方案:Redis ZSet/Kafka分区分桶(目前选用redis zset方案,可调整); 支持发送延迟消息,用延迟队列时通过调用相关方法选择延迟时间 监控告警模块 04 监控死信堆积量、延迟消息触发成功率。 3.1.3. 用例图 3.2. 功能模块1:配置管理模块(模块编号01) MQE-01-0001 支持死信队列开关配置(默认关闭) 开发人员通过EventCenter的相关接口启用,避免非必要资源消耗。 开发人员 高 MQE-01-0002 支持自定义重试次数(默认3次) 开发人员通过接口设置,某些业务需修改重试次数。 开发人员 高 MQE-01-0003 支持重试间隔策略配置(默认指数退避,可选固定间隔、自定义/预设) 开发人员通过setRetryPolicy(topic, policy)接口设置,默认指数退避(如1s→2s→4s)。 开发人员 高 MQE-01-0004 支持死信名称配置(默认业务名_dlq_topic), 开发人员开启死信队列时需要设置对应业务的死信名称,未自定义业务使用默认。 开发人员 中 配置管理流程图如下图所示: 3.3. 功能模块2:死信处理模块(模块编号02) 典型场景:事件消费时,因业务逻辑异常导致消费失败,重试3次(默认值)后仍失败,消息需进入死信队列,避免阻塞主流程。 MQE-02-0001 基于EventCenter消费者拦截器扩展,无侵入式集成 拦截器实现ConsummerInceptor接口,不修改原生消费逻辑。 开发人员 高 MQE-02-0002 捕获消费失败消息 拦截器监听EventHandler.handleEvent()的异常抛出。 系统 高 MQE-02-0003 执行重试逻辑(基于配置的重试次数和间隔),重试时间通过延迟队列实现 重试期间记录重试次数,避免无限重试。 系统 高 MQE-02-0004 重试耗尽后,将消息发送至DLQ(含原消息体、异常堆栈、重试次数) 死信消息通过独立Kafka生产者发送,不阻塞主消费线程。 系统 高 MQE-02-0005 提交原消息偏移量(仅在死信发送成功后) 避免重复消费(通过KafkaConsumer.commitSync(offset)实现)。 系统 高 MQE-02-0006 记录死信日志(含消息ID、Topic、业务标识、失败时间、错误原因) 运维人员订阅死信topic,通过其中日志追溯上下文 运维人员 中 3.4. 功能模块3:延迟消息管理模块(模块编号03) 典型场景:VMS设备删除时,需先删除事件上报,然后延迟一定时间后删除设备,避免设备删除后仍有事件上报导致数据不一致。方案还未确定,将写出两个方案的需求。 3.4.1 Redis ZSet方案需求 MQE-03-0001 支持开发人员通过sendDelayedEvent(topic, event, delayS)方法发送自定义延迟消息 延迟时间单位为秒(如30分钟=1800s),兼容任意延迟需求。(如需要扩展,添加时间转换或设置不同时间单位的参数) 开发人员 高 MQE-03-0002 消息存储至Redis ZSet(Key格式:vms_delay_) 按Topic隔离数据,避免不同业务消息混淆(如设备事件订单事件)。 系统 高 MQE-03-0003 定时扫描ZSet(间隔可配置,默认1秒)获取到期消息 扫描线程独立于EventCenter主线程,避免资源竞争。 系统 高 MQE-03-0004 到期消息通过EventCenter的send()接口发送至目标Topic(默认原Topic) 开发人员可通过参数指定目标Topic(如sendDelayedEvent(topic, event, delayS, targetTopic))。 开发人员 高 MQE-03-0005 发送失败时重新插入ZSet(新到期时间=当前时间+重试间隔,默认10秒) 重试间隔可通过setDelayRetryInterval(interval)接口配置。 系统 高 3.4.2 Kafka分区延迟方案需求(备选方案) MQE-03-0011 支持开发人员通过sendDelayedEvent(topic, event, delayLevel)接口选择预设延迟等级 延迟等级对应Kafka分区(如等级1→5s分区,等级2→30s分区),需运维配置的等级匹配。 开发人员 高 MQE-03-0012 EventCenter自动根据延迟等级将消息发送至对应分区(如topic-delay-5s) 分区由运维人员提前创建(需满足Kafka分区数≥最大等级数)。 运维人员 高 MQE-03-0013 消费者订阅所有延迟分区,启动时暂停消费 消费者通过pause()暂停分区消费,避免提前拉取未到期消息。 系统 高 MQE-03-0014 定时任务根据当前时间恢复到期分区的消费(如5秒后恢复topic-delay-5s分区) 恢复逻辑通过resume()接口实现,触发消息消费。 系统 高 目前根据调研结果,我的选型是Redis Zset方案,可以提供自定义延迟时间的功能,但是主要的缺点是要引入redis,kafka分区延迟方案不用引入外部组件。(具体方案还需要考虑业务延迟队列在业务中使用次数,在业务中有无需要自定义延迟时间的需求)。 3.5 功能模块4:监控告警模块(模块编号04) MQE-04-0001 监控死信Topic堆积量(按Topic统计) 运维人员订阅死信topic查看,记录死信堆积量。 运维人员 高 MQE-04-0002 统计延迟消息触发成功率(成功数/总到期数) 支持开发人员评估方案效果。基于Redis ZSet方案统计延迟消息触发成功率,如通过Redis计数器实现 开发人员 中 4. 非功能性需求 4.1. UI需求 无独立UI需求 4.2. 模块接口需求 enableDeadLetter(topic, enable) EventCenter 开发人员调用,启用/禁用指定Topic的死信队列(低侵入,不修改原生send()逻辑)。 setRetryCount(topic, count) EventCenter 开发人员调用,重试次数设置(默认为3次) setRetryPolicy(topic, policy) EventCenter 开发人员调用,重试策略设置(默认为指数退避) sendDelayedEvent(topic, event, delayS) EventCenter 开发人员调用,发送Redis ZSet方案的延迟消息(兼容原生send()的序列化配置)。 sendDelayedEvent(topic, event, delayLevel) EventCenter 开发人员调用,发送kafka分区方案的延迟消息(自动路由至对应分区)。 4.3. 性能需求 死信消息处理 并发处理能力≥1000条/秒(单消费者组),响应时间≤200ms(不影响主消费流程)。 Redis ZSet延迟消息方案 扫描间隔误差≤1秒(1秒间隔场景),单线程扫描最大处理1000条/次(可配置)。 kafka分区延迟消息方案(备选方案) 分区恢复延迟≤500ms(确保到期消息及时消费),支持10个预设等级(分区数≥10)。 4.4. 用户体验需求 开发人员调用扩展接口 :接口文档完整率100%,示例代码覆盖90%以上常用场景(如死信启用、延迟发送)。 4.5. 用户技术支持需求 死信消息追溯 运维人员支持通过消息ID查询原消息内容、异常堆栈、重试记录。 延迟消息状态查询 开发人员可通过queryDelayedEventStatus(eventId)接口查看消息状态(待触发/已发送)。 告警日志导出 支持导出一定时间内的告警记录(含触发时间、处理人、解决方案),用于复盘优化。 4.6 单元测试覆盖率 配置管理模块 80% 覆盖配置解析、接口调用校验逻辑。 死信处理模块 80% 覆盖拦截器逻辑、重试策略、死信发送等核心流程。模拟消费失败场景,验证重试次数、死信消息是否包含完整上下文。 延迟消息管理模块 80% 覆盖ZSet操作/分区分桶路由、扫描触发等关键逻辑。测试不同延迟时间的消息是否准时触发,Redis扫描间隔配置是否生效。 5. 可行性分析 配置管理模块开发 MQE-01-0001~0004 中 高 EventCenter接口层。 死信处理模块开发 MQE-02-0001~0006 高 高 Kafka消费者拦截器、EventCenter事件上下文。 延迟消息管理模块开发 MQE-03-0001~0005、MQE-03-0011~0014(备选) 高 高 Redis客户端、Kafka分区路由逻辑(备选)、EventCentersend()接口扩展。 监控告警模块开发 MQE-04-0001~0002 低 中 死信队列消费者。 说明: 核心功能(死信处理、延迟消息管理)需优先实现,确保解决设备删除数据不一致、消费失败丢失问题。 低侵入性设计通过接口扩展实现(如拦截器、sendDelayedEvent()),避免修改EventCenter原生代码(如send()、registerBroadcast()的核心逻辑)。 附录 无 已补充内容:根据现有redis实现代码分析的弃用原因: 单线程消费模型:每个Topic的消费者任务(RedisUnicastConsumerTask/RedisBroadcastConsumerTask)由独立线程池(ThreadPoolExecutor(1,1,...))驱动; 资源消耗高:每个Topic需独立维护线程池(topicExecutorServiceMap)和消费者任务(topicTaskMap),随着Topic数量增长(如百个业务Topic),线程资源和内存占用将显著增加。 监控运维工具缺失:Kafka有成熟的监控工具,而Redis Stream的消息堆积、消费延迟等指标需自定义采集。 而本方案使用Redis将延迟消息存入Redis Zset,一个定时任务线程对集合进行扫描,会避免起过多线程的问题;请根据以下评审意见对以上需求分析报告进行修改:1.eventcenter中redis实现消息队列的方案为何弃用,延迟队列目前选用redis zset方案需要写清楚这部分;2.评判延迟队列redis方案和kafka分区方案吞吐量上大概的值,在何种吞吐量下,redis方案需要扩容,扩容的方案,带来的延时误差;kafka分区方案能实现多大的吞吐量,超过后的策略;3.修改延迟队列redis方案失败逻辑,如何判断成功/失败,为什么会出现从redis拉出来可能会失败的情况,应该消费成功后删除吗,(现有的redis zset方案重新插入redis机制重新设置时间后已经不满足该队列想要的延迟,所以是不是考虑直接使用死信队列处理,重试策略及逻辑均通过死信队列处实现)4.延迟队列kafka分区方案消费者是转发,有专门的消费者订阅专门的延迟队列,利用定时任务唤醒,然后转发至真实消费者,评估一下redis方案中每秒轮询和kafka方案中定时任务的资源消耗,(kafka是队列,先进先出所以订阅相应延时分区的消费者每次只需要pause队列最前面的事件需要等待的时间,到达目标时间后resume后在进行转发),最前同时评估两个方案整体的资源消耗5.请不要使用enableDeadletter接口,可以通过配置或者重载方法来实现是否开启死信队列,优化使用。
最新发布
09-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值