后端与系统架构1397 字

某读书会Redis 大Key引发的线上事故分析总结

·全库 394 / 611

《某读书会Redis 大Key引发的线上事故分析总结》共 0.1 万字,通篇围绕 Redis、监控、数据库、延迟 展开,归在「后端与系统架构」这一层。

覆盖Redis监控数据库延迟缓存MySQL

适合给 AI 系统搭底座、要管得住的人

目录骨架(8 节)
  1. 1. 事故现象
  2. 2. 根本原因
  3. 1.核心思路
  4. 2.实现步骤
  5. 性能提升:
  6. 可维护性增强:
  7. 经验教训
  8. 技术要点
目录 · 7 节

背景

某读书会运营一个在线阅读平台,用户可以创建自己的读书记录并分享书评。为了提高性能,系统使用Redis作为存储层,存储热门用户的读书记录数据。然而,运营期间,系统在某天晚上突然出现严重的延迟和关闭超时,直接影响了用户体验。


一、事故分析

1. 事故现象

  • 平台API响应时间从正常的50ms激增至5s。
  • 数据库负载异常增加,读写次数激增。
  • Redis 的INFO输出显示内存使用率异常高,且响应时间大大延长。

2. 根本原因

经检查,问题源于某个Redis服务器键(大key)。该键存储了一个用户的所有读书排记录,采用hash类型,记录了数万条数据,大小超过100 MB。由于以下原因,大键导致了性能问题:

  • 获取大Key:读取整个哈希类型数据导致阻塞。
  • 中间逐出问题:大Key被Redis淘汰时,导致主线程阻塞,其他请求被延迟。
  • 网络延迟:关键数据量过大,传输时间过长。
  • 重建存储:每次重新计算大Key,数据库查询压力激增。

二、解决方案

1.核心思路

  • 定位大键:快速找到导致问题的大key。
  • 拆分优化:将大Key操作拆分为多个小Key,减少每次的体积。
  • 实时监控:构建Redis大关键监控机制,预防类似问题。

2.实现步骤

(1)定位大键 可以通过以下Lua脚本快速扫描Redis并定位大键:

-- scan_big_key.lua
local cursor = "0"
local big_keys = {}
repeat
    local result = redis.call("SCAN", cursor, "COUNT", 100)
    cursor = result[1]
    local keys = result[2]
    for _, key in ipairs(keys) do
        local key_type = redis.call("TYPE", key).ok
        local key_size = 0
        if key_type == "string" then
            key_size = redis.call("STRLEN", key)
        elseif key_type == "hash" then
            key_size = redis.call("HLEN", key)
        elseif key_type == "list" then
            key_size = redis.call("LLEN", key)
        elseif key_type == "set" then
            key_size = redis.call("SCARD", key)
        elseif key_type == "zset" then
            key_size = redis.call("ZCARD", key)
        end
        if key_size > 10000 then -- 自定义大Key阈值
            table.insert(big_keys, {key, key_type, key_size})
        end
    end
until cursor == "0"
return big_keys

运行结果可快速定位占用内存大的键。


(2)优化大关键 发现问题后,可以采用以下方式拆分:

  1. 按用户分片:大键中的hash按用户ID分片,例如user:read_logs:1000。
  2. 分页存储:限制每个关键存储的边界数,例如user:read_logs:1000:page1。

以下是示例代码:

import redis.clients.jedis.Jedis;

import java.util.List;

public class RedisKeyOptimizer {
    public static void main(String[] args) {
        try (Jedis jedis = new Jedis("localhost", 6379)) {
            String largeHashKey = "user:profile:1002";
            int pageSize = 1000; // 每页存储 1000 条记录

            // 拆分大Key
            List<String> fields = jedis.hkeys(largeHashKey).stream().toList();
            for (int i = 0; i < fields.size(); i += pageSize) {
                int end = Math.min(i + pageSize, fields.size());
                List<String> subFields = fields.subList(i, end);
                String newKey = largeHashKey + ":page" + (i / pageSize);
                subFields.forEach(field -> {
                    String value = jedis.hget(largeHashKey, field);
                    jedis.hset(newKey, field, value);
                });
                jedis.expire(newKey, 3600); // 设置过期时间为1小时
            }

            // 删除原始大Key
            jedis.del(largeHashKey);

资料里没有你的场景?

把你手上的数据和想解决的那个环节发上来,我们按具体情况回,不复述方法论。

发布需求