线上 Redis 突然"爆"了,怎么办?
作为一名后端开发或运维工程师,你一定遇到过这样的场景:线上某个服务的响应突然变慢,日志中频繁出现 Redis 超时或连接拒绝的错误,CPU 飙升到 100%,甚至直接触发报警。这时候,你的心跳可能也跟着"爆"了。别慌!本文将从基础概念到高级排查技巧,循序渐进地带你掌握 Redis 故障的应对方法。## 一、Redis 为什么会"爆"?------ 先理解基础Redis 是一个基于内存的键值存储系统,它之所以快,是因为数据都放在内存里。但"快"不代表不会出问题。常见的"爆"的原因包括:- 内存爆炸 :数据量超过物理内存,导致 OOM(内存溢出)或被 Linux 内核 Kill。- CPU 飙升 :慢查询、高并发命令(如 KEYS *)或复杂数据结构操作。- 连接数打满 :应用未及时释放连接,或连接池配置过小。- 主从复制延迟 :主库写入过快,从库同步跟不上。- 持久化阻塞 :RDB 快照或 AOF 重写占用大量 CPU 和磁盘 IO。下面我们先从一个最简单的模拟场景开始,看看如何用代码复现 Redis 压力,然后逐步深入排查。---## 二、从基础排查开始:监控和慢查询### 2.1 使用 Redis 自带的监控命令当 Redis 开始"报警"时,第一步是登录服务器,执行以下命令获取实时状态:bash# 查看 Redis 整体状态redis-cli info# 重点关注:# used_memory_human:当前内存使用量# connected_clients:当前连接数# instantaneous_ops_per_sec:每秒操作数# latest_fork_usec:最近一次 fork 耗时(持久化相关)# 查看慢查询(默认记录执行时间超过 10 毫秒的命令)redis-cli slowlog get 20如果发现 slowlog 中有大量 KEYS * 或 HGETALL 等命令,说明业务代码有问题。### 2.2 用 Python 复现一个"爆"的场景下面这段代码模拟了一个常见的错误:在循环中使用 KEYS * 扫描所有键,导致阻塞其他请求。pythonimport redisimport time# 连接 Redisr = redis.Redis(host='localhost', port=6379, db=0)# 模拟写入大量数据for i in range(10000): r.set(f"user:{i}", f"data_{i}")# 错误做法:每次请求都扫描全量键(导致 CPU 飙升)def bad_search(pattern): # 危险!KEYS 会遍历所有键,时间复杂度 O(N) # 当键数量很大时,会阻塞 Redis 主线程 keys = r.keys(pattern) # 等价于 KEYS * return [r.get(k) for k in keys]# 模拟并发请求(实际线上可能是高并发)start = time.time()for _ in range(100): result = bad_search("user:*")print(f"耗时:{time.time() - start:.2f} 秒")运行这段代码,你会看到 Redis 的 CPU 瞬间飙升,其他正常请求都会被卡住。---## 三、中级技巧:优化代码和数据结构### 3.1 用 SCAN 替代 KEYSKEYS 命令是 Redis 性能的"头号杀手"。正确的做法是使用 SCAN 命令进行增量迭代,避免阻塞。pythonimport redisr = redis.Redis(host='localhost', port=6379, db=0)def safe_search(pattern): """使用 SCAN 替代 KEYS,分批获取键""" cursor = 0 keys = [] while True: # SCAN 每次返回一小部分数据,不会阻塞 cursor, batch = r.scan(cursor=cursor, match=pattern, count=100) keys.extend(batch) if cursor == 0: break # 批量获取值(使用 pipeline 减少网络往返) with r.pipeline() as pipe: for key in keys: pipe.get(key) values = pipe.execute() return dict(zip(keys, values))# 测试安全版本import timestart = time.time()result = safe_search("user:*")print(f"安全耗时:{time.time() - start:.2f} 秒")print(f"获取到 {len(result)} 个键")关键优化点 :- SCAN 命令是游标式的,每次返回少量数据,不会阻塞主线程。- 使用 pipeline 批量获取值,减少 TCP 往返次数。- 设置 count=100 控制每次扫描的粒度。### 3.2 检查并优化大 Key大 Key(如包含百万元素的 List 或 Hash)是另一个常见"爆点"。可以使用以下命令排查:bash# 使用 redis-cli 的 bigkeys 分析工具redis-cli --bigkeys# 或者手动检查某个键的大小redis-cli debug object user:1000如果发现大 Key,建议拆分或使用更合适的数据结构(如将 Hash 拆分为多个小 Hash)。---## 四、高级排查:内存和持久化调优### 4.1 内存爆满怎么办?当 used_memory 接近 maxmemory 时,Redis 会触发淘汰策略(如 LRU)。检查配置:bash# 查看当前淘汰策略redis-cli config get maxmemory-policy# 常见策略:# volatile-lru:对设置了过期时间的键使用 LRU# allkeys-lru:对所有键使用 LRU(推荐)# noeviction:不淘汰,写操作直接报错如果内存持续增长,需要分析哪些键占用了空间。可以使用 redis-rdb-tools 分析 RDB 文件,或编写脚本遍历键:pythonimport redisr = redis.Redis(host='localhost', port=6379, db=0)# 检查每个键的大小(注意:debug object 会影响性能,建议低峰期使用)cursor = 0large_keys = []while True: cursor, keys = r.scan(cursor=cursor, count=1000) for key in keys: # 获取键内部编码和大小 info = r.debug_object(key) size = info.get('serializedlength', 0) if size > 1024 * 10: # 大于 10KB 视为大 Key large_keys.append((key, size)) if cursor == 0: break# 按大小排序并输出 Top 10large_keys.sort(key=lambda x: x[1], reverse=True)for key, size in large_keys[:10]: print(f"Key: {key}, 大小: {size} 字节")### 4.2 持久化导致阻塞Redis 的 RDB 和 AOF 重写都是 fork 子进程完成的,但 fork 本身会阻塞主线程。如果内存很大(比如 10GB),fork 可能耗时数秒。解决方案:- 开启 lazyfree :Redis 4.0+ 支持异步删除大 Key(UNLINK 替代 DEL)。- 调整持久化策略 :降低 RDB 保存频率,或改用 AOF 持久化。- 使用混合持久化 :Redis 4.0+ 的 AOF+ RDB 混合模式。配置示例:bash# redis.conf 中开启 lazyfreelazyfree-lazy-eviction yeslazyfree-lazy-expire yeslazyfree-lazy-server-del yes# 使用 UNLINK 命令替代 DEL# redis-cli UNLINK big_key---## 五、终极方案:架构层面的预防### 5.1 限流和降级在应用层使用 Redis 客户端连接池,并配置合理的超时和重试策略:pythonimport redisfrom redis.connection import ConnectionPool# 配置连接池:最大连接数、超时时间pool = ConnectionPool( max_connections=50, timeout=5, # 连接超时 5 秒 socket_connect_timeout=3, socket_timeout=3, retry_on_timeout=True, max_retries=3)r = redis.Redis(connection_pool=pool)# 使用装饰器实现降级def redis_fallback(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except (redis.exceptions.TimeoutError, redis.exceptions.ConnectionError): # 降级:返回默认值或从本地缓存读取 return None return wrapper@redis_fallbackdef get_user_data(user_id): return r.get(f"user:{user_id}")### 5.2 使用 Redis Cluster 或读写分离当单机无法承载时,考虑以下架构:- Redis Cluster :自动分片,支持水平扩展。- 主从复制 + 读写分离 :读操作走从库,写操作走主库。- 本地缓存 + Redis :如使用 Caffeine(Java)或 lru_cache(Python)作为一级缓存。---## 总结线上 Redis 突然"爆"了,不要慌张。按照以下步骤排查:1. 立即监控 :使用 info、slowlog、bigkeys 快速定位问题。2. 代码优化 :用 SCAN 替代 KEYS,避免大 Key,使用 pipeline 批量操作。3. 配置调优 :调整内存淘汰策略、持久化参数,开启 lazyfree。4. 架构升级 :引入连接池、限流降级,必要时使用集群或读写分离。记住:Redis 的"爆"不是灾难,而是系统成长的信号。每一次故障排查,都是对技术栈的深入理解。最好的修复,是预防------在代码上线前,用压力测试和慢查询分析提前发现隐患。希望这篇文章能帮你从"爆"中快速恢复,甚至避免下一次"爆"。