
产生背景
场景:消息服务重构上线之后Redis的内存飙升
时间:2025年4月10日,Redis告警飙升后的会议室
角色:程序员(小李):负责消息中心重构后的的开发和优化。
运维(老张):负责系统的部署和监控。业务方
(李总):负责业务需求和上线进度,强调快速扩张。
老张(运维):
“李总,小李,今天早上新消息中心服务上线美国后,Redis的内存突然飙升,告警直接触发了。我查了一下,占比最多的key都来自消息服务。小李,这是怎么回事?”
李总(业务方):
“老张,这个问题很严重啊!美国市场是我们业务扩张的重点,Redis告警会影响整个系统的稳定性。小李,你这边有没有排查出原因?”
小李(程序员):
“李总,老张,我刚刚看了一下日志,发现新消息中心服务上线美国后,发送的消息量突然增加了好几倍。这些消息的key没有设置合理的过期时间,导致Redis内存迅速被占满。”
老张(运维):
“小李,你确定是key的过期时间问题吗?我看了监控,Redis的内存使用率在短时间内从30%飙升到了90%,这可不是小问题。如果消息量继续增加,Redis可能会直接崩溃。尤其是当前系统的所有服务共有的一个redis集群,一死全死”
李总(业务方):
“小李,老张,你们别互相推诿了!我们现在时间很紧,业务正在快速扩张,新消息中心服务上线美国必须尽快上线。小李,你能尽快解决这个问题吗?”
小李(程序员):
“李总,我理解你的担忧。我已经找到了临时解决方案:首先,我们可以暂时关闭美国的流量开关,走老的消息服务,减少消息发送量;其次,老张,你能帮忙删除Redis中占内存较大的key吗?这样可以快速缓解内存压力。”
老张(运维):
“好的,小李,我马上安排删除这些key。另外,我建议我们在生产环境启用Redis的内存监控和告警策略,防止类似问题再次发生。”
李总(业务方):
“老张,你先按照小李的方案操作,确保Redis稳定。小李,你这边有没有长期解决方案?我们不能每次都靠临时措施解决问题。”
小李(程序员):
“李总,我已经在后续版本中全量排查并修改了Redis.set值的过期时间,确保每个key都有合理的过期时间。另外,我会优化消息发送的逻辑,避免消息量突然激增。”
老张(运维):
“小李,我建议我们在测试环境模拟高并发场景,确保优化后的方案能够应对消息量的波动。另外,我们可以考虑引入Redis集群,分担单节点的压力。”
李总(业务方):
“好,老张,你先安排测试环境的模拟。小李,你这边抓紧时间优化代码,我们明天再开一次会,必须有一个明确的长期解决方案。记住,公司业务正在快速扩张,技术问题不能成为瓶颈!”
分析过程
1.找DBA帮忙拿出占比较大的key就可以看出是哪个系统的问题
因为aws的redis分析能力有限,写了一段python脚本找出排名前50的key ;
import redis
from collections import defaultdict
def scan_keys(redis_client):
cursor = 0
while True:
cursor, keys = redis_client.scan(cursor, count=1000) # 每次扫描 1000 个 keys
yield from keys # 使用生成器返回 keys
if cursor == 0:
break # 如果游标为 0,则表示扫描完成
def main(redis_host, redis_port, redis_db, redis_password, use_ssl):
# 连接 Redis,支持 SSL 连接
redis_client = redis.StrictRedis(
host=redis_host,
port=redis_port,
db=redis_db,
password=redis_password,
ssl=use_ssl, # 根据配置是否使用 SSL
decode_responses=True # 自动解码字节为字符串
)
# 要统计的 key 前缀和数量
prefix_count = defaultdict(int)
# 使用 SCAN 统计前缀
for key in scan_keys(redis_client):
prefix = key[:6] # 取前 6 个字符作为前缀
prefix_count[prefix] += 1 # 计数
# 输出统计结果并取前50个
sorted_prefix_count = sorted(prefix_count.items(), key=lambda item: item[1], reverse=True)[:50]
print("前缀统计结果(前 50 个):")
for prefix, count in sorted_prefix_count:
print(f"前缀: {prefix}, 数量: {count}")
if __name__ == "__main__":
# 输入 Redis 配置信息
# redis_host = input("请输入 Redis 的 IP 地址: ")
# redis_port = int(input("请输入 Redis 的端口: ")) # 端口一般是 6379
# redis_db = int(input("请输入 Redis 的数据库索引 (0-15): ")) # 默认数据库索引是 0
# redis_password = input("请输入 Redis 的密码 (如果没有,请直接回车): ") or None
redis_host = '10.100.1.182'
redis_port = 63100 # 端口一般是 6379
redis_db = 3 # 默认数据库索引是 0
redis_password ='xxxxxxxx'
# use_ssl = False
use_ssl = True
main(redis_host, redis_port, redis_db, redis_password,use_ssl)结论: messag前缀的key占比最多,167W条;
2.分析这些key,因为设置过期时间太长,且业务发送量大,导致Redis内存飙升
发送消息的时候做幂等控制,过期时间太长了。60个小时变为30分钟;
这个需要更新程序。
3. 清理掉这些已知的不影响业务的密集key 。
import redis
import time
def delete_keys_with_prefix(redis_client, prefix, batch_size=1000):
"""
批量删除指定前缀的Redis key
:param redis_client: Redis客户端实例
:param prefix: 要删除的key的前缀
:param batch_size: 每次删除的key数量,默认1000
"""
cursor = 0
total_deleted = 0
while True:
# 使用SCAN命令获取一批key
cursor, keys = redis_client.scan(cursor=cursor, match=f"{prefix}*", count=batch_size)
if not keys:
print("没有找到更多符合条件的key,删除完成。")
break
# 删除这批key
deleted_count = redis_client.delete(*keys)
total_deleted += deleted_count
print(f"已删除 {deleted_count} 个key,总共删除 {total_deleted} 个key。")
# 如果SCAN返回的cursor为0,表示遍历完成
if cursor == 0:
print("所有符合条件的key已删除完毕。")
break
# 为了避免对Redis造成过大压力,可以适当休眠
time.sleep(0.1)
if __name__ == "__main__":
# Redis连接配置
redis_host = "localhost" # Redis主机地址
redis_port = 6379 # Redis端口
redis_db = 0 # Redis数据库编号
redis_password = "your_password_here" # Redis密码
redis_ssl = False # 是否启用SSL认证
# 创建Redis客户端
redis_client = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db,
password=redis_password,
ssl=redis_ssl, # 是否启用SSL认证
decode_responses=True
)
# 要删除的key的前缀
prefix = "messag"
# 批量删除
delete_keys_with_prefix(redis_client, prefix)解决之后的效果:
解决方案
临时方案:
1、切换开关,发消息通道切回旧msg发送
2、DBA帮忙删除占内存较大的key
最终方案:
后续版本全量排查修改Redis.set值的过期时间
经验总结
系统重构上线,要考虑到各个中间件性能问题,不光是Redis,还有MySQL、MQ性能问题,从系统自身业务量出发设计合理技术方案,以及兜底方案,尽量对业务零影响。