监听 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`)
})
这些事件看起来像是技术细节,但它们描述的是连接的真实状态。
connect 和 ready 的区别是什么?连接建立不等于就绪。TCP 连接可能成功了,但 Redis 可能还在加载数据,还在进行主从同步。ready 才意味着真正可以工作。
close 和 end 的区别是什么?close 是连接断开,但客户端会尝试重连。end 是彻底终止,不再重连。前者是暂时的失联,后者是永久的离开。
连接建立过程:
connecting→ 开始建立 TCP 连接connect→ TCP 连接成功建立ready→ Redis 握手完成,可以执行命令
连接断开过程:
close→ 连接关闭(会自动重连)reconnecting→ 开始重连尝试connect→ 重连成功ready→ 恢复就绪状态
彻底断开:
close→ 连接关闭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 服务长时间不可用,客户端会一直尝试重连。这会:
- 占用应用的连接资源
- 产生大量无效的网络请求
- 在日志中堆积大量错误信息
- 给运维团队造成噪音
设置重连上限是必要的。 当重连次数超过阈值,应该:
- 停止重连,释放资源
- 触发告警,通知人工介入
- 切换到降级模式,不再依赖 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
幂等性检查
在不确定的系统中,幂等性检查是对重复的容忍,是对稳定性的追求,也是对失败的预期与接纳
错误隔离
失败是必然的。真正的问题不是失败本身,而是失败如何蔓延。错误隔离不是为了消除失败,而是为了控制失败的范围。
单例模式管理 Redis 连接
连接不是技术细节,而是系统与外部世界的第一次握手,是可靠性的起点
监控观察期法
部署不是结束,而是验证的开始。修复代码只是假设,监控数据才是证明。48小时观察期:让错误主动暴露,让数据证明修复。
资源不会消失,只会泄露
在积分系统中,用户的钱不会凭空消失,但会因为两个时间窗口而泄露:并发请求之间的竞争,和回调永不到达的沉默。
RPC函数的原子化处理
当一个远程函数做太多事情,失败就变得难以理解
RPC函数
关于远程过程调用的本质思考:当你试图让远方看起来像眼前
指数退避超时 - 防止无限重试循环
失败后立即重试是本能。但有些失败,需要时间来消化。指数退避不是逃避失败,而是尊重失败。
管理后台需要两次设计
第一次设计回答"发生了什么",第二次设计回答"我能做什么"。在第一次就试图解决所有问题,结果是功能很多但都不够深入。
告警分级与响应时间
不是所有问题都需要立即响应。RPC失败会在凌晨3点叫醒人。安全事件每15分钟检查一次。支付成功只记录,不告警。系统的响应时间应该匹配问题的紧急程度。
文档标准是成本计算的前提
API文档不只是写给开发者看的。它定义了系统的边界、成本结构和可维护性。统一的文档标准让隐性成本变得可见。
BullMQ 队列
队列不是技术选型,而是对时间的承认,对顺序的尊重,对不确定性的应对
BullMQ Worker
Worker 的本质是对时间的重新分配,是对主线的解放,也是对专注的追求
配置不会自动同步
视频生成任务永远pending,代码完美部署,队列正确配置。问题不在代码,在于配置的独立性被低估。静默失败比错误更危险。
CRUD 操作
四个字母背后,是数据的生命周期,是权限的边界,也是系统设计的基础逻辑
数据库参数国际化:从 13 个迁移学到的设计原则
数据不该懂语言。当数据库参数嵌入中文标签时,系统的边界就被语言限制了。这篇文章从 13 个参数对齐迁移中提炼出设计原则——国际化不是功能,是系统设计的底层约束。
Stripe Webhook中的防御性编程
三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。
双重验证:Stripe生产模式的防御性切换
从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。
端到端 Postback 模拟测试
真实的测试不是模拟完美的流程,而是重现真实世界的混乱。Postback 测试的价值在于发现系统在不确定性中的表现。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
实现幂等性处理,忽略已处理的任务
在代码层面识别和忽略已处理的任务,不是简单的布尔检查,而是对时序、并发和状态的深刻理解
缺失值的级联效应
一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。
Props Drilling
数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。
队列生产者实例的工厂函数
工厂函数不是设计模式的炫技,而是对重复的拒绝,对集中管理的追求,对变化的准备
队列、可靠性与系统边界
探讨消息队列系统如何通过时间换空间,用异步换解耦,以及可靠性背后的权衡