Redis Pub Sub:如何仅让一个订阅者处理过期事件
这个问题在多实例分布式部署场景里太常见了!我来给你几个靠谱的解决方案,都是基于Redis本身就能实现的,不用额外引入太多组件:
解决方案一:用Redis分布式锁实现排他处理
当你的实例收到map1的过期事件时,先尝试获取一个针对该过期键的分布式锁,只有成功拿到锁的实例才执行map2的递减操作。具体步骤:
- 收到过期事件后,提取出过期的
map1键名(比如user:1001) - 构造锁的唯一键名,比如
lock:map1:expire:user:1001 - 执行Redis命令:
SET lock:map1:expire:user:1001 <instance-id> EX 15 NX<instance-id>用你的应用实例唯一标识(比如容器ID、主机名+端口),方便后续排查问题EX 15设置锁的过期时间,要确保足够覆盖你的递减操作耗时,避免实例挂掉导致死锁NX表示只有当锁不存在时才设置成功,保证同一时间只有一个实例拿到锁
- 如果命令返回
OK,说明当前实例拿到了锁,执行HINCRBY map2 <对应键> -1(根据你的map2实际结构调整命令) - 操作完成后,可以主动执行
DEL lock:map1:expire:user:1001释放锁,也可以让锁自动过期,避免操作中途实例挂掉导致锁长期存在
优点:实现简单,不需要额外组件;缺点:Redis单实例故障会影响锁可用性,锁过期时间需要谨慎设置,极端情况下可能存在极短时间的重复处理(比如锁刚好过期,另一个实例拿到锁,但前一个实例还在处理)
解决方案二:用Redis Stream + 消费者组实现消息唯一分发
这个方案更可靠,利用Redis Stream的消费者组特性,让Redis自动把过期事件分发给唯一实例处理,从根源上避免重复消费:
- 修改监听逻辑:当收到
map1的过期事件时,不要直接处理,而是把事件内容(比如过期键名、处理时间)写入一个Stream,用命令:XADD stream:map1:expire * key <过期键名>(*表示自动生成唯一消息ID) - 每个应用实例启动时,加入同一个消费者组(比如
group:map1:expire),并设置自己的消费者名称(比如consumer:instance-002) - 每个实例通过
XREADGROUP GROUP group:map1:expire consumer:instance-002 COUNT 1 BLOCK 0 STREAMS stream:map1:expire >命令消费消息>表示消费组中未被消费过的消息BLOCK 0表示阻塞等待新消息,你也可以设置合理的超时时间
- 实例处理完递减操作后,执行
XACK stream:map1:expire group:map1:expire <消息ID>确认消息已处理,这样Redis就不会把这条消息再分发给其他实例
优点:Redis负责消息分发和确认,彻底避免重复处理;即使某个实例挂了,未确认的消息会被Redis标记为待处理,其他实例可以重新消费;支持负载均衡,多个实例可并行处理不同的过期事件;缺点:需要调整现有逻辑,引入Stream和消费者组的概念,学习成本略高,但长期来看更稳定
额外注意事项
- 确保Redis已正确开启键空间通知,配置
notify-keyspace-events Ex(E表示键事件通知,x表示过期事件) - 因为Redis的键空间通知是**即发即弃(Fire and Forget)**的,如果Redis发送通知时故障,事件可能丢失。如果业务要求强一致性,建议增加补偿机制(比如定期扫描
map1的过期键,或设置map1键时同步记录到待处理集合,定期检查) - 用分布式锁时,尽量让锁的粒度和过期键一一对应,不要用全局锁,避免不同键的处理互相阻塞
内容的提问来源于stack exchange,提问作者Don Corleone
相关产品推荐
相关产品推荐

