监听 Redis 连接事件 - 让不可见的脆弱变得可见

4 min read
Zekari
系统设计Redis可靠性工程分布式系统

连接应该是透明的。

你写代码,调用 Redis,取数据,存数据。连接在后台悄悄工作,不应该成为你关心的事情。

但这是理想状态。现实中,连接会断开,会超时,会变慢。当这些发生时,如果你没有监听,你不会知道发生了什么,也不知道如何应对。

连接不是永恒的

Redis 连接看起来很简单:建立连接,发送命令,接收响应。但这个过程充满了不确定性。

网络可能中断。Redis 服务可能重启。连接池可能耗尽。防火墙可能关闭长时间空闲的连接。

这些都不是异常情况。这些是分布式系统的常态。

如果你假设连接永远可用,你的系统会在第一次网络抖动时陷入混乱。命令失败了,你不知道为什么。用户看到错误,你不知道从何查起。

监听连接事件,是把不可见的脆弱变得可见。

💡 Click the maximize icon to view in fullscreen

事件是状态的语言

Redis 客户端通过事件来描述连接的生命周期。

import Redis from 'ioredis'

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  retryStrategy: (times) => {
    // 重试延迟随次数增加
    return Math.min(times * 50, 2000)
  }
})

// 连接建立成功
redis.on('connect', () => {
  console.log('Redis 连接已建立')
})

// 准备好接收命令
redis.on('ready', () => {
  console.log('Redis 已就绪,可以执行命令')
})

// 连接发生错误
redis.on('error', (err) => {
  console.error('Redis 连接错误:', err.message)
})

// 连接关闭
redis.on('close', () => {
  console.log('Redis 连接已关闭')
})

// 连接完全断开
redis.on('end', () => {
  console.log('Redis 连接已终止,不会重连')
})

// 开始重连
redis.on('reconnecting', (delay) => {
  console.log(`Redis 正在重连,延迟 ${delay}ms`)
})

这些事件看起来像是技术细节,但它们描述的是连接的真实状态。

connectready 的区别是什么?连接建立不等于就绪。TCP 连接可能成功了,但 Redis 可能还在加载数据,还在进行主从同步。ready 才意味着真正可以工作。

closeend 的区别是什么?close 是连接断开,但客户端会尝试重连。end 是彻底终止,不再重连。前者是暂时的失联,后者是永久的离开。

连接建立过程:

  1. connecting → 开始建立 TCP 连接
  2. connect → TCP 连接成功建立
  3. ready → Redis 握手完成,可以执行命令

连接断开过程:

  1. close → 连接关闭(会自动重连)
  2. reconnecting → 开始重连尝试
  3. connect → 重连成功
  4. ready → 恢复就绪状态

彻底断开:

  1. close → 连接关闭
  2. end → 不再重连,连接生命周期结束

理解这些事件的时序,才能正确处理不同的状态转换。

监听不是被动的

监听事件不只是记录日志。监听是为了应对。

连接断开时,你可能需要:

  • 停止接收新的请求,避免失败堆积
  • 切换到备用的 Redis 实例
  • 使用本地缓存降级服务
  • 通知运维团队,触发告警

重连成功时,你可能需要:

  • 清空错误状态,恢复服务
  • 重新初始化某些数据
  • 通知依赖系统,连接已恢复
let isRedisAvailable = false
let failedCommandCount = 0

redis.on('ready', () => {
  isRedisAvailable = true
  failedCommandCount = 0
  console.log('Redis 服务恢复')
})

redis.on('error', (err) => {
  isRedisAvailable = false
  failedCommandCount++

  // 错误累积到一定程度,触发告警
  if (failedCommandCount > 10) {
    sendAlert('Redis 连接持续失败')
  }
})

// 在执行命令前检查状态
async function safeGet(key) {
  if (!isRedisAvailable) {
    // 降级到本地缓存或直接返回 null
    return null
  }

  try {
    return await redis.get(key)
  } catch (err) {
    console.error('Redis 命令执行失败:', err)
    return null
  }
}

这里的 isRedisAvailable 不是技术上的精确状态,而是业务层面的可用性判断。

它告诉你的应用:现在是否应该依赖 Redis。如果不应该,那么降级、跳过、或者用其他方式处理。

重连的策略

Redis 客户端通常会自动重连。但自动不意味着无脑。

重连需要策略。立即重连可能导致雪崩。永远重连可能浪费资源。

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  retryStrategy: (times) => {
    // 重试超过 10 次,放弃重连
    if (times > 10) {
      console.error('Redis 重连次数超限,停止重连')
      return null // 返回 null 停止重连
    }

    // 指数退避,最长等待 2 秒
    const delay = Math.min(times * 50, 2000)
    console.log(`第 ${times} 次重连,延迟 ${delay}ms`)
    return delay
  },

  // 连接超时设置
  connectTimeout: 10000,

  // 最大重连次数
  maxRetriesPerRequest: 3
})

这里的 retryStrategy 是你对不确定性的权衡。

重试多少次算够?延迟多久算合理?这没有标准答案。你需要根据系统特点决定。

如果你的 Redis 是本地部署,网络稳定,重连可以激进一些。如果是跨地域的云服务,重连要保守一些。

重连策略反映了你对系统脆弱性的预期。

关于重连的深入思考,可以参考 指数退避超时

自动重连看起来很方便,但它也有风险。

如果 Redis 服务长时间不可用,客户端会一直尝试重连。这会:

  • 占用应用的连接资源
  • 产生大量无效的网络请求
  • 在日志中堆积大量错误信息
  • 给运维团队造成噪音

设置重连上限是必要的。 当重连次数超过阈值,应该:

  1. 停止重连,释放资源
  2. 触发告警,通知人工介入
  3. 切换到降级模式,不再依赖 Redis

重连是为了应对短暂的故障,不是为了掩盖持续的问题。

何时需要监听

不是所有场景都需要监听连接事件。

如果你的应用只是偶尔调用 Redis,失败了就失败了,那么监听意义不大。错误会被抛出,你在调用层处理就够了。

但如果你的应用严重依赖 Redis,那么监听是必要的:

  • 缓存服务:Redis 不可用时,需要降级到数据库查询
  • 会话存储:连接断开时,需要阻止新的登录请求
  • 消息队列:连接不稳定时,需要暂停消息消费
  • 分布式锁:连接异常时,需要释放锁避免死锁

监听的本质,是把连接状态从隐式变成显式。

你不再假设连接永远可用,而是根据连接状态来调整应用行为。

日志与告警

监听事件最直接的用途是记录日志。

redis.on('connect', () => {
  logger.info('Redis 连接建立', {
    host: redis.options.host,
    port: redis.options.port
  })
})

redis.on('error', (err) => {
  logger.error('Redis 连接错误', {
    error: err.message,
    stack: err.stack,
    host: redis.options.host
  })
})

redis.on('close', () => {
  logger.warn('Redis 连接关闭', {
    host: redis.options.host,
    time: new Date().toISOString()
  })
})

但日志只是第一步。更重要的是告警。

连接频繁断开、重连失败超过次数、错误持续发生,这些都应该触发告警。告警不是为了让你知道问题,而是为了让你及时处理问题。

let reconnectCount = 0
let lastReconnectTime = null

redis.on('reconnecting', () => {
  reconnectCount++
  lastReconnectTime = Date.now()

  // 1 分钟内重连超过 5 次,触发告警
  if (reconnectCount > 5) {
    sendAlert({
      level: 'critical',
      message: 'Redis 连接不稳定,频繁重连',
      count: reconnectCount
    })
  }
})

redis.on('ready', () => {
  // 连接恢复,重置计数
  if (reconnectCount > 0) {
    logger.info(`Redis 连接恢复,之前重连了 ${reconnectCount} 次`)
    reconnectCount = 0
  }
})

告警的阈值需要根据实际情况调整。过于敏感会产生噪音,过于迟钝会错过问题。

应用关闭时,也需要正确关闭 Redis 连接。

// 监听应用退出信号
process.on('SIGTERM', async () => {
  console.log('收到 SIGTERM 信号,开始优雅关闭')

  // 停止接收新请求
  server.close()

  // 关闭 Redis 连接
  try {
    await redis.quit() // quit() 会等待所有命令执行完
    console.log('Redis 连接已关闭')
  } catch (err) {
    console.error('关闭 Redis 连接失败:', err)
    redis.disconnect() // 强制断开
  }

  process.exit(0)
})

quit()disconnect() 的区别:

  • quit() 发送 QUIT 命令,等待所有命令执行完后优雅关闭
  • disconnect() 立即断开连接,可能会丢失未完成的命令

优雅关闭保证了正在执行的命令不会被中断,避免数据不一致。

监听是对脆弱性的承认

监听 Redis 连接事件,本质上是承认系统是脆弱的。

连接会断,网络会抖,服务会挂。这些不是边缘情况,而是必然发生的事情。

监听不是多余的,监听是对现实的尊重。

你不假设连接永远可用,你为断开做准备。你不期待网络永远稳定,你为抖动设计策略。

这种思维方式不只适用于 Redis,也适用于所有外部依赖。数据库、消息队列、第三方 API,任何依赖都可能失败。

对脆弱性的承认,是可靠性的起点。

最后

连接看起来应该是透明的。理想情况下,你不需要关心它的存在。

但现实不是理想。连接会断,会慢,会失败。如果你没有监听,这些问题会在你不知情的情况下累积,直到彻底崩溃。

监听事件,是让不可见的脆弱变得可见。你知道连接的状态,你能及时应对变化,你为失败做好准备。

这不是防御性编程的过度谨慎,而是对分布式系统本质的理解。

在一个充满不确定性的系统中,能看见问题,本身就是一种能力。


相关阅读:

Related Posts

Articles you might also find interesting

幂等性检查

1 min read

在不确定的系统中,幂等性检查是对重复的容忍,是对稳定性的追求,也是对失败的预期与接纳

系统设计分布式系统
Read More

错误隔离

3 min read

失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。

系统设计可靠性工程
Read More

单例模式管理 Redis 连接

5 min read

连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点

系统设计后端架构
Read More

监控观察期法

3 min read

部署不是结束,而是验证的开始。修复代码只是假设,监控数据才是证明。48小时观察期:让错误主动暴露,让数据证明修复。

系统设计监控
Read More

资源不会消失,只会泄露

2 min read

在积分系统中,用户的钱不会凭空消失,但会因为两个时间窗口而泄露:并发请求之间的竞争,和回调永不到达的沉默。

系统设计并发控制
Read More

RPC函数的原子化处理

1 min read

当一个远程函数做太多事情,失败就变得难以理解

分布式系统RPC
Read More

RPC函数

2 min read

关于远程过程调用的本质思考:当你试图让远方看起来像眼前

分布式系统RPC
Read More

指数退避超时 - 防止无限重试循环

3 min read

失败后立即重试是本能。但有些失败,需要时间来消化。指数退避不是逃避失败,而是尊重失败。

系统设计可靠性工程
Read More

管理后台需要两次设计

3 min read

第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。

系统设计API 设计
Read More

告警分级与响应时间

2 min read

不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。

系统设计监控
Read More

文档标准是成本计算的前提

3 min read

API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。

API文档
Read More

BullMQ 队列

3 min read

队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对

系统设计异步处理
Read More

BullMQ Worker

2 min read

Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求

系统设计异步处理
Read More

配置不会自动同步

2 min read

视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。

部署配置管理
Read More

CRUD 操作

2 min read

四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑

系统设计软件工程
Read More

数据库参数国际化:从 13 个迁移学到的设计原则

3 min read

数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。

数据库国际化
Read More

Stripe Webhook中的防御性编程

2 min read

三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。

Web开发系统设计
Read More

双重验证:Stripe生产模式的防御性切换

7 min read

从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。

系统设计Stripe
Read More

端到端 Postback 模拟测试

2 min read

真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。

测试API
Read More

在运行的系统上生长新功能

3 min read

扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。

系统设计架构
Read More

实现幂等性处理,忽略已处理的任务

3 min read

在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解

系统设计并发控制
Read More

缺失值的级联效应

3 min read

一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。

系统设计防御性编程
Read More

Props Drilling

3 min read

数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。

React组件设计
Read More

队列生产者实例的工厂函数

4 min read

工厂函数不是设计模式的炫技,而是对重复的拒绝,对集中管理的追求,对变化的准备

系统设计设计模式
Read More

队列、可靠性与系统边界

1 min read

探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡

架构可靠性
Read More