ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

分布式锁在梯控集群调度中的高并发实践

分布式锁在梯控集群调度中的高并发实践 1. 场景与难点机器人梯控到底在控什么去年做楼宇机器人物流调度项目从立项到落地用了一年。电梯控制这块不算最起眼但绝对是最磨人的。今天把核心部分也就是基于分布式锁的梯控集群调度拿出来详细聊一聊。这套系统要解决的问题很直接多个机器人同时请求电梯资源如何保证每一部电梯在同一时刻只被一个任务合法占用同时又不让高并发请求把调度集群冲垮。先说一下背景。园区里有几十部电梯机器人要在不同楼层之间送快递、送餐、做巡检。以前单机调度几台机器人还能靠“先到先得”勉强撑住机器人数量一上来问题就全爆了。最典型的就是两台机器人同时走到电梯口电梯门开了两台都想往里进结果卡在门口死锁后台还不知道。这个场景本质上是多台机器人竞争一个独占资源。电梯不是为机器人设计的它一次只能响应一个任务序列响铃、开门、等乘客进入、关门、按楼层、运行、开门、等人出去。对机器人来说这个序列必须被完整、独占执行中间任何一步被打断整个任务就废了。从系统设计角度看梯控调度要解决的核心问题有三个。第一个是资源竞争。同一部电梯在同一时刻只能分配给一个机器人任务两台机器人同时拿到同一部电梯的授权轻则等待超时重则两台机器人在轿厢门口互相等待最后双双罢工。第二个是状态一致性。电梯的状态包括空闲、已响应、轿厢内、到达、门开、门关等这些状态会被多个服务节点并发读取和更新如果没有互斥机制就会出现状态错乱。比如电梯明明已经分配给A任务B任务也能看到“空闲”状态接着把电梯抢走。第三个是故障恢复。电梯控制链路很长任何一环失败比如网络超时、梯控控制器没响应、机器人卡在轿厢里系统都要能兜住不能让锁一直不下发也不能让业务永久挂起。这三个问题单靠一台调度服务器里的本地锁解决不了。调度服务本身要上集群多个节点同时处理请求必须用分布式锁把所有节点的判断和决策统一起来。这也是我把分布式锁放在整个系统设计核心位置的原因。1.1 一次乘梯任务要经过哪些环节先拆解一次机器人乘梯的完整链路你会发现这里面能出问题的地方太多了。机器人发起乘梯请求携带任务ID、所在楼层、目标楼层、期望到达时间。接下来调度系统要做选电梯、抢锁、下呼梯指令、等待电梯到位、开门、确认进入、按楼层、关门、运行到目标楼层、开门、确认出舱、释放电梯。我把每一个环节都定义为状态机中的一个节点任何一个节点超时或者收到异常结果都要有对应的恢复策略。比如呼梯指令下发了但电梯没响应就不能立刻把锁释放掉否则下一个任务拿到锁后下发同样的指令电梯可能重复响应。这里我踩过最深的坑是把电梯看作一个响应式接口忽略了它真实的物理约束。电梯没有CPU不能像分布式服务那样接收请求后返回ACK。很多梯控接口返回了“指令已下发”但电梯实际操作会被后面的指令覆盖。所以分配一台电梯后必须用Redis加数据库双重记录当前任务占用后续所有指令只能由持有锁的节点下发。1.2 并发场景下最容易出现的三种故障项目上线前我们做了多轮并发仿真故障最多的时候就这三个。一是双机抢梯。两台机器人同时请求两个不同的调度节点各自决策都选中了同一部电梯。因为没有抢锁两个节点都往梯控控制器下发呼梯指令。最后电梯来了两台机器人从不同方向走向电梯口门一开场面直接失控。二是指令覆盖。A机器人拿到电梯后下发了“去6楼”的指令。B机器人因为状态同步延迟以为电梯空闲也下发了一条“去3楼”的指令。梯控控制器执行了最后一条A机器人带着错误路径运行到了3楼一看不对任务直接失败。三是持锁节点宕机。A节点抢到了电梯锁但在下发指令前进程崩溃。如果没有锁自动过期机制这部电梯会被永久锁住所有后续任务都在它门口排队。调度中心看起来一切正常但实际业务已经死了一半。这三个故障本质上都是对资源持有权的管理问题。分布式锁要解决的就是谁能在某个时间窗口内合法地控制这台电梯以及节点挂了之后控制权如何平滑转移。1.3 为什么必须用分布式锁而不是本地锁或数据库锁有人问调度节点加个本地锁不就行了不行。本地锁只对当前进程内的线程有效。集群环境下请求会负载均衡到不同节点A节点加了锁B节点的线程照样可以进入临界区。所以必须用所有节点都能看到的公共组件来做互斥这就是分布式锁的由来。那为什么不用数据库锁呢数据库锁有类似能力但有两个问题。一是性能梯控调度对锁获取的延迟非常敏感高并发下对电梯资源行的锁竞争会变成数据库热点QPS上限直接被压得很惨。二是锁的释放语义太笨重连接断开、事务回滚、长事务持锁等场景都要额外处理。Redis实现分布式锁性能高、容量大、过期机制天然支持配合Lua脚本还能保证操作的原子性是目前最成熟的方案。我这里的实现并不是教科书里那种极端对立的RedLock场景而是基于Redis SET NX和Redisson的常规生产实践。常规业务规模下Redis分布式锁完全够用关键是把它用对。接下来第二、三章就把整体架构和锁的具体落地展开讲。2. 集群调度系统整体架构设计讲完场景再往上走一层看整体架构。梯控集群调度系统本质上是一个有状态资源管理平台只不过状态分散在多个角色里机器人的位置和任务状态、电梯的真实物理状态、调度系统的分配决策、梯控控制器的执行结果。架构设计要解决的是这么多状态如何安全、高效地流转。2.1 模块划分与职责边界整个系统我分成五个模块。机器人端Agent运行在机器人上的客户端负责任务上报、位置感知、状态采集、乘梯执行器。接入网关统一接收机器人端的长连接使用MQTT或WebSocket网关无状态可以水平扩展。调度中心核心决策模块收到乘梯请求后执行选梯、抢锁、下发指令的流程多节点部署节点间不共享内存状态。资源管理器管理电梯资源池保存电梯的实时状态和锁占用情况这一层是Redis的主要使用方。梯控适配层屏蔽不同电梯厂商的协议差异往上是统一的指令接口往下是RS485、CAN、HTTP或私有物联协议。这五个模块之间是单向依赖关系机器人端面向网关网关面向调度中心调度中心同时使用资源管理器和梯控适配层。资源管理器不直接对接梯控梯控层也不持有调度状态双方通调度中心串联。为什么这样分层最直接的原因是换电梯厂商时不用改调度逻辑。我们项目里接了三家电梯品牌协议完全不一样但调度中心和机器人端只认统一接口。梯控适配层把“呼梯”指令翻译成厂商A的RS485帧或厂商B的HTTP POST各厂商的逻辑隔离在适配层内部。2.2 调度集群的高可用方案调度中心是Java服务部署了三个节点前面挂负载均衡。这里有个关键设计调度节点必须无状态。所有与电梯相关的状态都放Redis和数据库节点自身只保存一些本地缓存和连接池信息。无状态的好处是节点挂了可以直接摘除请求会转移到其他节点。但无状态也意味着任何节点在处理乘梯请求时不能只靠本地内存判断“电梯是否空闲”必须去Redis里做一次权威确认。这个权威确认就是分布式锁的加锁操作。Redis部署上用的是集群模式三主三从数据自动分片。电梯资源锁的Key带有电梯编号比如elevator:lock:EL001这些Key会按哈希槽分布在不同主节点上天然分散了锁的压力。数据库用PostgreSQL主要保存电梯任务的历史记录、任务日志、对账数据。实时调度不读数据库数据库只做审计和报表避免数据库成为整个链路的瓶颈。调度集群内部节点之间还有一个轻量的心跳通道节点定期广播自己的存活状态和负载情况。一旦某个节点心跳超时负载均衡器自动把它摘除存活节点的锁看门狗会接管它持有的电梯任务。因为所有锁都在Redis里接管动作不需要节点间直接通信只需要在发现锁对应的业务心跳消失后按照预置策略重新分配。2.3 电梯资源池的数据模型电梯资源池的核心数据结构就三块。首先是电梯基础信息包括电梯编号、物理位置、所在楼栋、服务楼层范围、当前所在楼层、当前开关门状态、是否故障。这些信息会实时更新更新动作集中在资源管理器里。然后是任务占用信息记录当前电梯被哪个任务占用占用开始时间预期完成时间占用状态。最后是分布式锁本身锁的Key是电梯编号锁的Value是任务编号加节点标识用来区分不同持有者释放时校验。三块信息中分布式锁是实时权威数据数据库是异步落盘。为什么要这样设计因为锁和状态在Redis里有了调度判断就快数据库保留完整轨迹出了问题可以回溯。实时链路和审计链路分离互不干扰。实际操作中我还会存一份独立的任务上下文。任务上下文里不只有电梯和楼层信息还包括机器人的实时坐标、当前剩余电量、最快到达电梯口的预估时间。这些数据对分配决策非常重要但不会每时每刻都写库Redis里存一份热数据定期异步刷新到数据库。2.4 消息协议与接口定义调度中心和机器人端、梯控适配层的通信全部走统一的JSON消息体。所有交互消息都带这几个公共字段消息ID、消息类型、产生时间、来源节点标识、电梯编号、任务编号。核心接口设计为五个乘梯请求、呼梯指令、开门指令、楼层指令、关门指令。每个接口都定义了明确的超时时间和重试规则。比如乘梯请求的超时时间设为15秒若15秒内没收到分配结果机器人端会主动重新上报重试时带上原消息ID调度中心根据消息ID做幂等判断。梯控适配层的指令回调也要走统一格式。适配层收到电梯的真实状态变化后会发送状态回调消息包括电梯到位、门已开、门已关、轿厢到达目标楼层等。这个统一消息格式换来的是上层调度逻辑无需感知厂商差异也方便在压测时用模拟器批量灌入回调消息。3. 分布式锁在梯控调度中的落地实现这一章是全文的核心。光知道“用Redis做分布式锁”不够真正落地时锁的粒度、Key设计、加锁解锁流程、续期策略每一处都要抠细节。我在项目里踩的坑基本都是这些细节里踩出来的。3.1 锁粒度与Key设计分布式锁的粒度决定了系统的并发上限。粒度越粗越安全但并发能力越低粒度越细越高效但边界处理越复杂。在梯控场景里我定义了三种粒度的锁。电梯资源锁粒度最粗以电梯为单位一个任务拿到后其他任务不能碰这台电梯这是最核心的锁。轿厢操作锁电梯资源锁成功后在操作轿厢内部状态机开门、按楼层、关门期间防止同电梯的其他状态更新线程干扰。这个锁由持有电梯锁的任务在内部使用。全局分配锁执行选电梯算法时防止多个节点同时扫描候选电梯集合造成重复分配这个锁放在固定Key上比如schedule:assign:global持锁时间极短算法执行完立刻释放。锁的Key设计上统一前缀加对象标识。以电梯资源锁为例elevator:lock:EL001。任务锁用elevator:task:LOCK-EL001-20250101T103000这样的格式带任务编号方便日志检索。这里我把三种锁的关系整理成一张表锁名称Key示例持锁时长用途全局分配锁schedule:assign:global几十毫秒选梯算法互斥电梯资源锁elevator:lock:EL021整个乘梯周期独占电梯资源轿厢操作锁elevator:cabin:opt:EL021单次轿厢操作操作时序保护三种锁的获取顺序也有讲究。先获取全局分配锁执行选梯算法确定候选电梯后释放全局锁。然后获取候选电梯的资源锁成功后才进入电梯任务流程。如果资源锁失败立刻尝试下一个候选电梯不用重新执行完整分配算法。3.2 加锁、解锁的标准流程基于Redis的分布式锁标准写法是SET NX PX加Lua脚本解锁。生产环境加锁时不要用SETNX加EXPIRE两条命令。非原子操作存在设置过期时间前进程崩溃、锁永久不过期的风险必须用单条SET命令复合参数实现原子性SET elevator:lock:EL001 task_20250101_103000 NX PX 30000返回OK表示抢锁成功返回空表示锁已被持有要做失败处理。这里PX 30000是锁的过期时间单位毫秒我一开始设的就是30秒配合看门狗续期。解锁也不是简单的DEL。要先用GET比较锁的Value是否等于自己持有者的标识确认是自己的锁才能删。这个“比较后删除”是两个操作必须用Lua脚本保证原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end为什么要校验持有者因为锁可能已经过期被另一个任务获取了。如果直接DEL等于把别人的锁释放了会造成两个任务同时操作一台电梯。这个坑很隐秘灰度测试时就出过一次A任务锁过期后B任务获取了同一电梯锁A任务执行完释放锁时用了简单DEL直接把B的锁删掉了。好在测试环境发现得早否则线上就是大事故。生产环境我强烈建议直接用Redisson的RLock它封装了加锁、续期、释放的完整语义Lua脚本保证原子性自带看门狗续期比手写Redis命令靠谱得多。手写SET NX只是理解原理时用生产环境别硬造轮子。3.3 锁续期和看门狗机制锁超时时间要设置多少取决于一次乘梯任务的最大合理时长。我们实测过从获取电梯到机器人出舱正常情况一百多秒最坏情况要五分钟。如果把锁超时设成30秒任务还没执行一半锁就过期了。如果把锁超时设成10分钟节点崩溃后锁要10分钟才能被清理电梯资源会长时间浪费。权衡下来我选择了“短超时自动续期”方案。Redisson的看门狗默认每10秒检查一次锁如果业务还在执行会自动把锁的过期时间续到30秒。这样只要业务线程活着锁就不会断业务线程死了锁在30秒内自动释放不会造成永久占用。我这里手动实现了一个类似的看门狗机制。用独立的后台任务每隔10秒对当前活跃的电梯任务做一次续期操作。业务侧每次续期前检查任务是否仍在状态机里活跃如果任务已经结束就不续期并主动释放锁。这个机制要格外注意和业务超时时间配合。机器人进入轿厢后如果一直没检测到关门信号任务就进入超时保护状态。此时续期任务应该停止锁应该被释放转入人工或救援流程。我这边拉了一个阈值电梯门关闭信号超过15秒没收到就判定为异常锁自动释放并通知运维。3.4 锁竞争失败后的排队与重试策略分布式锁有一道必考题拿不到锁怎么办答案不是简单报错而是队列加指数退避。最简单的做法是拿不到锁就抛出异常让上层等待。但高并发下多个任务同时抢一部电梯失败任务如果立刻重试只会加剧竞争形成惊群效应。正确做法是让每个失败任务计算一个随机退避时间比如200到500毫秒退避后再重新尝试。退避时间带随机值是为了避免多个任务同时醒来同时重试。实际项目中我用了双重策略。先是短时间级别的快速重试任务对候选电梯列表轮询尝试获取每一部的锁。单一电梯锁获取失败退避200到500毫秒再试下一个候选。所有候选都失败则进入等待队列。等待队列由调度中心管理每当有电梯任务释放锁就从队列头部取出等待任务重新发起抢锁。这个队列还要支持优先级。早期的排队设计是严格FIFO结果出现过“电梯任务就绪但机器人还没走到电梯口”的尴尬局面。后来把任务排队时的状态作为优先级参数机器人已在电梯口等待的高优先级还在前往电梯途中的低优先级。优先级的计算在入队时生成带着时间戳防止老任务永远排不上。这个方案上线后整体调度成功率从92%提升到了97.5%左右效果非常明显。4. 调度核心流程与关键实现锁的实现稳定后整条调度链路才能有价值。调度流程是串行加异步的组合串行处理一个任务内部的时序节点异步处理多个任务之间的资源竞争。下面详细拆解每一步。4.1 电梯分配决策流程电梯分配是整个系统的决策入口。一个乘梯请求进来后调度中心要回答三个问题哪些电梯可用哪个电梯最合适如何保证分配不冲突先看哪些电梯可用。可用性判断依据电梯基础信息服务楼层包含起点和终点、当前状态不是故障、没有正在执行的任务。这一步不查锁只做粗筛目的是缩小候选范围。再看哪个电梯最合适。我用的评分函数综合考虑了以下因素电梯当前楼层距离起点楼层的差值差值越小越可能快速响应电梯当前是否为空闲状态空闲电梯优先电梯是否顺路比如机器人在6楼要去10楼电梯当前在8楼且方向向上这种最理想排队任务对电梯的历史等待时长等待越久的任务越应该优先拿到资源。评分值越高优先级越高。候选电梯按评分排序后进入抢锁步骤。抢锁时要执行双重确认先再次从Redis读取电梯的占用状态确认空闲后再执行SET NX加锁。加锁操作本身是原子的因此不会出现“确认空闲了但加锁失败”之外的超卖问题。加锁成功后立刻更新电梯资源池里的任务占用信息和电梯状态然后下发梯控指令。这里有一个非常重要的经验加锁和更新状态必须在同一个操作边界内完成。我通过Redis的Lua脚本做到这一点把“加锁写占用信息更新电梯状态”合并成一个原子操作避免中间环节崩溃导致状态与锁不一致。4.2 轿厢操作时序控制电梯资源锁获取成功只是拿到了“使用电梯”的资格。真正执行乘梯动作时还有一个更细的时序问题机器人进轿厢、按楼层、离开轿厢这些动作必须严格按顺序执行而且每一步都要等待电梯真实状态到位。在这一步我设计了一个乘梯状态机状态迁移如下WAITING_HALL_CALL已下发呼梯指令等待电梯到达起点楼层。ELEVATOR_ARRIVED电梯到达开门等待机器人进入轿厢。ROBOT_ENTERED机器人进入轿厢准备下发楼层指令。RUNNING轿厢运行中等待到达目标楼层。TARGET_ARRIVED目标楼层到达开门等待机器人出舱。ROBOT_EXITED机器人出舱乘梯任务完成释放资源。每个状态都有一个超时阈值。比如WAITING_HALL_CALL超过60秒没收到电梯到位信号就认为呼梯失败主动释放锁并上报任务异常。状态机由持有电梯锁的节点负责驱动其他节点收到该电梯的请求时因为锁被持有直接返回“电梯忙碌”不会干扰状态机。机器人进轿厢的确认我用的是“轿厢内视觉加地磁传感器”双通道。机器人进入轿厢后通过Agent上报确认消息同时梯控层上报的门磁信号也到位两个信号一致才推进到ROBOT_ENTERED状态。只有一个信号时进入重试等待1秒再次确认最多重试3次。双通道确认是为了防止视觉识别出错导致的“假进入”最终避免机器人还没进轿厢就按了楼层电梯带着一个空轿厢走了。驱动状态机的线程在任务整个生命周期内持有电梯锁任务完成时放锁。这一段时间锁一直被占用符合设计预期。看门狗会持续续期直到任务状态机走到终态。4.3 集群节点间的指令唯一性保证集群环境下最怕多个节点对同一部电梯同时下发指令。我在这里做了三层保证。第一层调度中心入口的幂等校验。每个乘梯请求都带有全局唯一的RequestID调度中心收到后先去Redis查询这个RequestID是否已处理过处理过就直接返回上次的结果不重新分配。第二层电梯资源锁。真正能下发指令的节点必须持有电梯锁。没有锁的节点连Redis里的电梯状态都不允许更新更不会走到梯控适配层去。这一层把指令下发权和锁持有权强绑定是最关键的一层。第三层梯控控制器的指令序列化。即使前面的逻辑因为极端情况同时下发了多条指令梯控适配层内部还有一个指令队列把同一电梯的指令串行化执行。适配层确保一次只向电梯控制器发送一条指令收到执行完成信号后才发送下一条。这样做牺牲了一点响应速度但能保证物理电梯不会被并发指令搞乱。这三层保证每一层都解决不同层面的并发风险第一层防重复请求第二层防资源竞争第三层防控制器指令乱序。缺任何一层都有可能在特定条件下出问题。4.4 故障转移与锁的自动清理故障转移是分布式系统的必修课。梯控场景里故障主体主要是三类调度节点挂了、锁持有者死了、梯控控制器失联。调度节点挂了因为我们设计了无状态节点负载均衡会把请求转发到健康节点。健康节点接手后如果发现某个电梯锁存在但锁对应的业务心跳已消失就会触发锁清理逻辑。清理逻辑先检查任务记录里的最后心跳时间超过一定时长就判定为孤儿任务把锁强制释放电梯状态恢复为空闲。锁持有者死了锁本身的超时机制会兜底。即使看门狗还没来得及续期锁过期后自动释放电梯资源不会永久丢失。这里要注意的是过期时间绝对不能被一个长时间任务硬耗必须配合续期把过期时间压缩到故障可容忍的最小值。梯控控制器失联是最麻烦的。电梯有真实的物理运动不能简单把状态清掉。我的做法是保留锁和任务占用但把电梯状态标记为UNKNOWN禁止新任务获取该电梯的锁同时通知运维去排查梯控链路。只有运维确认电梯恢复正常手动把状态改为空闲后电梯才能重新进入候选池。这个人工介入的设计看起来粗暴但处理物理设备故障时宁慢勿错。4.5 定时巡检与自动补偿机制状态机和故障转移都是事件驱动的但事件驱动有个盲区如果某个事件一直没发生系统不会自动发现问题。我为此加了一套定时巡检任务。巡检任务每5秒执行一次扫描所有正在执行中的乘梯任务。超过状态超时阈值的任务会被标记为疑似卡死进入自动补偿流程。补偿动作按业务类型区分还在呼梯阶段的重新下发一次呼梯指令已经进入轿厢的检查电梯当前楼层和门状态如果电梯根本没动就触发救援流程先释放锁再通知运维。这套机制上线后彻底解决了压测时遇到的“任务卡在中间状态无人处理”的问题。现在的经验是Kubernetes部署时把巡检任务做成独立的CronJob既能保证频率又不会与调度主流程抢占资源。巡检任务本身要幂等因为同一任务可能被多个巡检周期扫到但任何一次补偿成功就要在任务上下文里打上已处理标记。5. 高并发压测与性能调优实录系统设计得再好不压测等于白设计。梯控场景的压测不同于普通接口压测因为它涉及真实的资源竞争模型多机器人抢多部电梯。下面分享压测时设定的场景、遇到的瓶颈以及调优结果。5.1 压测场景设计与指标设定压测环境调度中心三个节点Redis Cluster三主三从模拟100台机器人并发发任务场景里电梯20部单次乘梯任务平均时长60秒。压测工具我用了两类。一类是JMeter用来打调度中心HTTP接口主要看QPS和时延另一类是自研的机器人模拟器部署在独立服务器上按真实机器人Agent的行为模式批量上报乘梯请求、模拟状态回调。只用JMeter测不出真实场景因为真实场景里机器人端的状态流转和反馈是异步的必须有模拟器配合。主要指标我定了四个调度接口QPS乘梯请求的处理能力目标值500。锁获取成功率任务成功拿到电梯锁的比例目标值99%以上。平均调度时延从请求进来到完成电梯分配的耗时目标值300毫秒以下。任务完成率整个乘梯链路完成的成功率目标值97%以上。第一轮压测结果很不理想调度接口在300 QPS时锁获取成功率掉到了94%平均调度时延500毫秒大量请求超时。压测一上去Redis连接数直接飙升到几千CPU和内存都吃紧。5.2 典型性能瓶颈分析瓶颈首先出在Redis连接池上。调度中心每个节点默认连接池只有几十个连接但压测时所有请求都通过连接池访问Redis连接被瞬间耗尽后续请求排队等待连接超时率飙升。把连接池调整到200以后连接不足的问题缓解但新的瓶颈又出现了锁竞争太激烈大量请求在“抢锁失败-重试”循环里反复消耗Redis资源。这里做一个定量分析。20部电梯100台机器人同时发起任务每部电梯同一时刻平均只能服务一个任务剩余99个任务即使全部到达也都在电梯门口排队。调度接口的瓶颈不在Redis本身而在锁竞争过程中反复的读和写。每次抢锁失败后原来的代码会立即重试形成对Redis的无效访问风暴。查明原因后我在调度中心增加了本地锁冲突计数当连续失败超过3次时开始指数退避。退避时间分别取1秒、2秒、4秒最多退避到8秒。这个策略大大减少了无效请求压测数据立刻好转。5.3 最终调优手段与实测效果调优主要做了三件事。第一Redis连接池扩容并进行连接预热把连接数从几十个提升到300个。这个操作简单但效果明显。实际调整时还要注意连接池的等待队列长度单独的maxTotal上调不够maxWaitMillis也要相应调整否则请求还是会因为等待连接而超时。第二锁竞争重试策略从“立即重试”改为“指数退避加随机抖动”。压测数据显示锁等待时间从平均450毫秒降到了180毫秒。第三把部分实时查询从Redis同步读取改为“本地缓存加异步失效”。电梯的基础信息比如楼层位置、服务范围这些数据变化不频繁放在本地缓存里可以大幅减少Redis读压力。电梯状态变更时通过Redis Pub/Sub推送失效消息让各节点本地缓存及时更新。最终压测数据如下指标调优前调优后调度接口QPS300600锁获取成功率94%99.6%平均调度时延500ms220ms任务完成率92%98.1%数据达到这个程度才敢往真实环境部署。真实环境上线后又有几个新问题暴露出来都是压测环境不容易模拟的放到下一章详细说。6. 常见问题与避坑速查表最后整理一份避坑清单。这些坑分布在压测、灰度、正式运行三个阶段每一个都是真金白银买来的教训。6.1 Redis主从切换导致锁短暂失效Redis Cluster中有一个主节点因为磁盘抖动触发了故障转移期间该主节点上的电梯锁短暂不可读。恰好有任务在抢锁读到了空值以为电梯空闲下发了指令。等主从切换完成后实际锁还存在任务状态就错乱了。解决思路有三层。一是通过业务侧“抢锁成功后二次确认电梯真实状态”来降低影响。二是释放锁时校验持有者。三是对关键电梯任务启用独立的Redis进程不让它和普通缓存混用。第三个方案成本高只在核心链路上启用梯控场景我用了前两层已经够用。这个问题的本质是CAP理论在分布式锁场景下的体现。Redis主从切换期间可用性优先导致了一致性窗口。理解了这一点就知道为什么业务侧不能只依赖锁状态还要在抢锁成功后做一次真实电梯状态的读取确认。6.2 锁超时时间设置不当导致任务中断我曾把锁超时设置为“平均任务时长加10秒”结果某个周六园区电梯排队严重机器人等待时间超过锁过期时间任务还没开始执行锁就没了。后续任务拿到锁后两个任务在电梯门口撞上。现在的经验是锁超时时间要设置成“允许的最大等待时间”而不是“平均任务时长”。同时必须配看门狗续期。如果业务环境不允许用Redisson至少要在业务线程中启动一个续期调度器让锁在业务活跃期间永远不超时。实际操作中我还遇到过Redisson看门狗和自定义锁混用导致的续期失效问题。同一个电梯资源有的节点用Redisson获取锁有的节点手写SET NX两者的续期机制互不识别。最终我统一了获取锁的实现方式全部走封装好的LockService不允许其他代码路径直接读写锁Key。6.3 电梯门到位信号延迟导致状态机卡死梯控控制器的信号延迟是物理世界不给面子。我们的梯控接口里开门完成信号偶尔会延迟几百毫秒而状态机里设的等待阈值是1秒信号一慢状态机直接判定超时并释放了锁。这个问题没法通过单纯调大超时时间解决因为阈值拉太大会让异常任务长期占锁。我的解法是增加一个信号缓冲期收到“指令下发成功”后不立即等待完成信号而是先等一个短时间的稳定期再进入状态等待。另外在状态机里增加了信号重试动作信号未到位但任务时间未超时就周期性重新查询设备状态直到超时阈值触发。这样的设计让门到位信号偶尔延迟几百毫秒不再导致任务失败。压测时这个问题从占比8%降到了0.3%以下。不过新增的重试查询也带来一个副作用对梯控控制器的查询频率上升。为了不压垮控制器查询间隔设得比较保守默认2秒一次最多重试3次。6.4 优先级倒置与队列饥饿前面提到队列带了优先级但优先级策略上线后有副作用高优先级任务反复插队低优先级任务长时间得不到电梯资源。最极端的一个巡检任务在队列里等了25分钟才被调度。解决优先级倒置我用的是“优先级加等待时间加权”混合排序。任务得分等于优先级权重乘基础分加等待时间加成。等待时间越长加成越高就算优先级低的早晚也能排到前面。同时给每个队列里的任务设置最大等待阈值超过阈值直接升级为高优先级并为它预留一部电梯的调度份额。这套机制上线后最长等待时间从25分钟降到了4分钟以内任务完成率进一步提高了0.8个百分点。这里有一点要说明给低优先级任务预留电梯份额意味着高峰期会让部分高优先级任务等待更久。实际业务中楼宇巡检、清洁这类任务对实时性要求没有那么高做这种取舍是可以接受的。关键是把配额参数做成可配置上线后通过监控报表动态调整。做分布式锁梯控集群调度最大的体会是把分布式锁当资源所有权来理解而不是一个简单的Redis命令。锁的设计要跟着电梯这个物理资源的特性走粒度怎么切、超时怎么设、释放时机怎么定都是围绕着“电梯在同一时刻只能干一件事”来推敲的。系统上线这几个月机器人乘梯的成功率和稳定性都达到了业务预期。踩过的这些坑可能场景不一定完全相同但思路是通用的希望能给正在做类似高并发资源调度系统的朋友一些参考。
返回列表