Skip to content

Redis 高并发缓存治理

Redis 用在高并发系统里,最容易出事故的不是 setget 不会写,而是这些问题没有治理好:

  1. 大 Key:一个 Key 太大,导致网络、内存、删除、迁移、持久化都变慢。
  2. 热 Key:一个 Key 请求量太高,单个 Redis 节点或单个分片被打满。
  3. 缓存穿透:大量不存在的数据绕过缓存打数据库。
  4. 缓存击穿:热点 Key 过期瞬间大量请求回源。
  5. 缓存雪崩:大量 Key 同时过期或 Redis 故障,流量压到数据库。
  6. 慢命令和阻塞:Redis 单线程执行命令,一个慢命令会影响后续请求。
  7. 连接池和超时:应用线程卡在 Redis,进一步拖垮服务。

一句话理解:

Redis 高并发治理的核心不是“让缓存更快”,而是让缓存、数据库和应用在流量异常时都不会被瞬间打垮。

学习目标

目标需要掌握什么
知道是什么分清大 Key、热 Key、穿透、击穿、雪崩、慢命令、连接池耗尽
知道为什么理解 Redis 单线程、内存模型、网络传输、Cluster 分片热点
知道怎么发现会用 --bigkeysMEMORY USAGESLOWLOGINFO、监控指标定位
知道怎么治理会拆 Key、本地缓存、逻辑过期、互斥锁、限流、降级、异步删除
会写 Demo能写热点缓存、本地缓存、逻辑过期、分片 Key、Lua 限流
会生产排查能从现象反推是数据库被打穿、Redis 被打满,还是应用连接池堵塞

高并发问题全景

Redis 在商业系统里通常站在应用和数据库之间。它不是单独变慢,而是会和应用线程、数据库连接、网络带宽、对象序列化、缓存命中率一起形成连锁反应。

mermaid
flowchart TD
    A["高并发请求"] --> B["应用线程池"]
    B --> C["Redis 连接池"]
    C --> D["Redis 节点"]
    D --> E["数据库回源"]
    D --> F["主从复制和持久化"]
    D --> G["Cluster 分片"]

常见事故可以按“压力打到哪里”来分类:

压力位置常见问题典型现象关键治理
Redis 单个 Key热 Key、大 Key单节点 CPU 高、网络高、timeout本地缓存、拆 Key、限流、异步删除
Redis 命令线程慢命令、长 Lua、全量查询slowlog 有大命令,延迟抖动禁用危险命令、分页、控制脚本时间
Redis 内存大 Key、空值缓存过多、TTL 缺失内存持续升高、频繁淘汰Key 规范、容量评估、淘汰策略
Redis 客户端连接池耗尽、超时配置不合理应用线程堆积,接口大量超时连接池监控、超时、熔断降级
数据库穿透、击穿、雪崩DB QPS 飙升,慢 SQL 变多空值缓存、布隆过滤器、互斥锁、逻辑过期
Cluster 分片slot 热点、数据倾斜某个节点很忙,其他节点空闲热点识别、分片 Key、迁移 slot、业务拆分

不要把这些问题割裂看。比如一个大 Key 可能先导致 Redis 命令变慢,进而应用连接池被占满,最后接口超时和线程池堆积;一个热点 Key 过期可能先造成缓存击穿,随后数据库慢查询变多,再反过来拖慢缓存重建。

Redis 为什么高并发下也会慢

Redis 很快,但不是无限快。

mermaid
flowchart TD
    A["客户端请求"] --> B["网络传输"]
    B --> C["Redis 单线程执行命令"]
    C --> D["读取或修改内存数据结构"]
    D --> E["返回结果"]
    C --> F["持久化、过期删除、复制、集群迁移等后台影响"]

Redis 慢通常来自这些地方:

原因解释
单个命令太重大 Key、复杂集合操作、Lua 脚本时间太长
返回结果太大网络传输慢,客户端反序列化慢
单个 Key 太热请求集中到一个节点或一个分片
持久化抖动RDB、AOF rewrite、fork 带来延迟
主从复制压力大量写入导致复制延迟
Cluster 热点槽某个 slot 所在节点压力远高于其他节点
连接池耗尽应用线程等待 Redis 连接,接口超时
数据库被打穿缓存失效后 DB 慢,反过来拖住应用线程

所以排查 Redis 问题时不能只看 Redis 本身,还要看应用、数据库、网络和客户端连接池。

单线程 多线程和 IO 多路复用

面向零基础时,很容易产生一个疑问:Redis 既然常说是单线程,为什么还能承受很高并发?

这里要分清三件事:

概念说明
命令执行线程Redis 核心命令执行主要是单线程,避免复杂锁竞争
IO 多路复用一个线程可以同时监听大量连接事件,不需要一个连接一个线程
Redis 6/7 多线程多线程主要用于网络 IO、后台释放内存、AOF 等辅助工作,不等于多个线程同时乱改同一份数据

简化流程:

mermaid
flowchart TD
    A["多个客户端连接"] --> B["IO 多路复用监听事件"]
    B --> C["读请求数据"]
    C --> D["命令队列"]
    D --> E["单线程顺序执行命令"]
    E --> F["写回响应"]

为什么单线程反而快:

  1. 不需要为每个命令加锁,少了大量锁竞争。
  2. 命令大多是内存操作,单条命令很短。
  3. IO 多路复用让一个线程处理很多连接事件。
  4. 数据结构实现针对具体场景优化。

为什么单线程也会出问题:

  1. 只要一个命令执行很久,后面的命令就要排队。
  2. 大 Key 返回结果很大时,网络写回也会拖慢链路。
  3. Lua 脚本执行时间太长,会阻塞其他命令。
  4. KEYSSMEMBERSHGETALL 这类全量命令会让延迟抖动。

所以 Redis 的高并发能力来自“单条命令短、内存快、IO 模型好”,而不是“不管怎么用都快”。

大 Key 是什么

大 Key 不是只看 Key 名字长不长,而是看这个 Key 对应的 value 是否太大。

常见大 Key:

类型示例风险
String 大对象一个 Key 存 1MB 到 10MB JSON网络慢、反序列化慢、内存浪费
Hash 大字段一个 Hash 有几十万 fieldhgetall、迁移、删除都慢
List 大列表一个 List 存几十万元素范围查询、删除、持久化压力大
Set 大集合一个 Set 存大量用户 IDsmembers 可能阻塞
ZSet 大排行一个 ZSet 存百万成员范围查询、更新、内存占用高

大 Key 没有绝对统一阈值,要结合业务和机器能力判断。常见经验:

  1. String value 超过几十 KB 就要关注,超过几百 KB 要谨慎。
  2. Hash、Set、ZSet、List 元素超过几千到几万就要关注。
  3. 单次命令返回大量数据,比“存储很大”更危险。
  4. Cluster 中大 Key 会造成分片内存和迁移不均衡。

大 Key 为什么危险

mermaid
flowchart TD
    A["大 Key"] --> B["网络传输慢"]
    A --> C["命令执行时间长"]
    A --> D["删除阻塞"]
    A --> E["内存分布不均"]
    A --> F["主从复制和 AOF 压力"]
    A --> G["Cluster 迁移慢"]

典型事故:

事故现象背后原因
Redis timeout 增多大 Key 命令占用执行线程或网络传输慢
hgetall 很慢Hash 字段太多,一次返回全量
删除 Key 后 Redis 抖动del 同步释放大量内存
Cluster 某节点内存特别高大 Key 落在某个 slot
主从同步延迟升高大对象写入和复制压力大
AOF 文件膨胀大对象频繁写入

大 Key 怎么发现

1. --bigkeys

bash
redis-cli --bigkeys

它会扫描 keyspace,输出每种数据结构里最大的 Key。优点是简单,缺点是只能给出概览,且扫描会增加 Redis 压力,生产要谨慎在低峰执行。

2. MEMORY USAGE

bash
MEMORY USAGE user:profile:1001

查看单个 Key 的内存占用,适合对可疑 Key 精确验证。

3. SCAN 分批扫描

不要在生产使用 KEYS *,它会阻塞 Redis。

bash
SCAN 0 MATCH product:detail:* COUNT 1000

SCAN 是渐进式扫描,但仍会消耗资源。生产扫描要限速,并最好走从库或离线分析。

4. 慢日志和监控

bash
SLOWLOG GET 20
INFO memory
INFO commandstats

如果慢日志里频繁出现 hgetallsmemberslrange 0 -1zrange 0 -1,通常要怀疑大 Key 或一次取太多数据。

大 Key 怎么治理

场景错误做法正确做法
大 JSON一个 Key 存完整对象和大量子列表拆成对象基础信息 + 子集合分页
大 Hashhgetall 一次取所有字段按业务维度拆 Hash,或 HSCAN 分批
大 Listlrange 0 -1只取分页范围,限制长度
大 Setsmembers 全量取SSCAN 或改为分页索引
删除大 KeyDEL big:keyUNLINK big:key 异步释放
排行榜无限增长一个 ZSet 永久追加只保留 TopN 或按时间拆分

异步删除:

bash
UNLINK big:hash:1001

UNLINK 会把释放内存的工作交给后台线程,减少主线程阻塞。它不是不消耗资源,而是把同步阻塞风险降低。

大 Key 拆分 Demo:商品评论

错误设计:

text
product:comments:1001 -> 一个 List 存所有评论

问题:评论越来越多,分页、删除、迁移都变慢。

更稳的设计:

text
product:comment:summary:1001       -> 评论总数、好评率
product:comment:page:1001:1        -> 第 1 页评论 ID
product:comment:page:1001:2        -> 第 2 页评论 ID
product:comment:detail:commentId   -> 单条评论详情

这样页面只查需要的页,不会一次把所有评论取出来。

热 Key 是什么

热 Key 是访问频率远高于其他 Key 的 Key。

典型热 Key:

  1. 秒杀商品库存。
  2. 热门商品详情。
  3. 热门文章、热榜、首页配置。
  4. 全站字典配置。
  5. 登录态、权限、用户画像中的少数热点用户。
  6. 某个租户或机构的热点数据。

热 Key 的危险在于:Redis Cluster 是按 slot 分片的,一个 Key 只能落到一个 slot,一个 slot 属于一个主节点。某个 Key 极热,就会把单个节点打满。

mermaid
flowchart TD
    A["大量请求访问 hot:product:1001"] --> B["hash slot 固定"]
    B --> C["落到 Redis 节点 A"]
    C --> D["节点 A CPU 或网络打满"]
    C --> E["其他节点仍然空闲"]

这就是为什么“集群有很多节点,但还是被一个热点打挂”。

热 Key 怎么发现

方法说明注意
应用侧埋点统计缓存 Key 访问频率最准确,推荐长期做
网关/接口统计找到热点接口,再定位 Key粒度较粗
Redis --hotkeys基于 LFU 统计热点 Key需要 LFU 策略支持
MONITOR观察实时命令生产慎用,开销大
代理层统计Twemproxy、Codis、网关统计看架构是否支持
慢日志不直接等于热 Key,但能发现重命令适合辅助判断

--hotkeys 示例:

bash
redis-cli --hotkeys

注意:它依赖 Redis 对 Key 访问频率的统计,通常需要 LFU 相关淘汰策略才能更有价值。生产更推荐应用侧做 Key 访问统计。

热 Key 怎么治理

方案适合场景风险
本地缓存热点详情、字典、配置多实例数据短暂不一致
多级缓存本地缓存 + Redis + DB架构复杂,需要过期策略
热点预热秒杀、活动页、榜单预热数据要准确
逻辑过期允许短暂旧数据需要异步重建
拆分 Key计数、库存、读热点写入合并复杂
限流降级秒杀、活动、热门接口用户体验下降
请求合并单实例内相同 Key 只回源一次实现复杂

本地缓存治理热 Key

本地缓存适合读多写少、允许短时间旧值的热点数据,例如商品详情、字典配置、资产元数据。

mermaid
flowchart TD
    A["请求热点数据"] --> B["查本地缓存"]
    B --> C{"命中"}
    C -- "是" --> D["直接返回"]
    C -- "否" --> E["查 Redis"]
    E --> F{"命中"}
    F -- "是" --> G["写本地缓存并返回"]
    F -- "否" --> H["查数据库并重建缓存"]

本地缓存的问题是每台应用都有一份缓存,更新后不能强一致同步。因此它适合“允许短暂旧数据”的场景,不适合账户余额、库存扣减、权限强一致判断。

Java Demo:Caffeine + Redis 多级缓存

下面演示热点资产详情查询。Caffeine 作为本地一级缓存,Redis 作为二级缓存,MySQL 作为事实来源。

java
public class AssetCacheService {
    private final Cache<Long, AssetDTO> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(Duration.ofSeconds(30))
        .build();

    private final StringRedisTemplate redisTemplate;
    private final AssetRepository assetRepository;
    private final ObjectMapper objectMapper;

    public AssetCacheService(StringRedisTemplate redisTemplate,
                             AssetRepository assetRepository,
                             ObjectMapper objectMapper) {
        this.redisTemplate = redisTemplate;
        this.assetRepository = assetRepository;
        this.objectMapper = objectMapper;
    }

    public AssetDTO getAsset(Long assetId) throws JsonProcessingException {
        AssetDTO local = localCache.getIfPresent(assetId);
        if (local != null) {
            return local;
        }

        String redisKey = "asset:detail:" + assetId;
        String json = redisTemplate.opsForValue().get(redisKey);
        if (json != null) {
            AssetDTO dto = objectMapper.readValue(json, AssetDTO.class);
            localCache.put(assetId, dto);
            return dto;
        }

        AssetDTO dto = assetRepository.findDetail(assetId);
        if (dto == null) {
            redisTemplate.opsForValue().set(redisKey, "NULL", Duration.ofMinutes(2));
            return null;
        }

        redisTemplate.opsForValue().set(
            redisKey,
            objectMapper.writeValueAsString(dto),
            Duration.ofMinutes(30).plusSeconds(ThreadLocalRandom.current().nextInt(300))
        );
        localCache.put(assetId, dto);
        return dto;
    }
}

这个 Demo 体现三点:

  1. 热点请求优先命中本地缓存,减少 Redis 压力。
  2. Redis 命中后回填本地缓存。
  3. Redis 未命中才查数据库,并且设置随机 TTL。

缓存击穿和热 Key 的区别

对比热 Key缓存击穿
关注点某个 Key 访问量极高热点 Key 过期后并发回源
是否必须过期不一定通常发生在过期瞬间
主要压力Redis 单节点或分片数据库
治理本地缓存、拆 Key、限流互斥锁、逻辑过期、异步重建

热 Key 是流量集中问题,击穿是热点失效后的回源风暴问题。一个热点 Key 如果没有过期治理,就很容易演变成缓存击穿。

逻辑过期治理击穿

逻辑过期的思想是:Redis 里的物理 Key 不轻易过期,而是在 value 里保存一个逻辑过期时间。过期后先返回旧数据,同时异步重建缓存。

mermaid
flowchart TD
    A["请求热点 Key"] --> B["读取 Redis"]
    B --> C{"逻辑是否过期"}
    C -- "否" --> D["返回数据"]
    C -- "是" --> E["尝试获取重建锁"]
    E --> F{"是否拿到锁"}
    F -- "是" --> G["异步查 DB 重建缓存"]
    F -- "否" --> H["直接返回旧值"]
    G --> I["更新 Redis 里的数据和逻辑过期时间"]

适合场景:

  1. 热门商品详情。
  2. 首页配置。
  3. 数据字典。
  4. 热门资产元数据。
  5. 允许短时间旧数据的查询。

不适合场景:

  1. 库存扣减。
  2. 账户余额。
  3. 支付状态。
  4. 权限强一致判断。

Java Demo:逻辑过期

java
public class CacheEnvelope<T> {
    private T data;
    private LocalDateTime expireAt;

    public T getData() {
        return data;
    }

    public LocalDateTime getExpireAt() {
        return expireAt;
    }

    public boolean expired() {
        return LocalDateTime.now().isAfter(expireAt);
    }
}

public AssetDTO getHotAsset(Long assetId) throws JsonProcessingException {
    String dataKey = "asset:hot:" + assetId;
    String lockKey = "lock:asset:hot:" + assetId;

    String json = redisTemplate.opsForValue().get(dataKey);
    if (json == null) {
        return null;
    }

    CacheEnvelope<AssetDTO> envelope = readEnvelope(json);
    if (!envelope.expired()) {
        return envelope.getData();
    }

    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, UUID.randomUUID().toString(), Duration.ofSeconds(10));

    if (Boolean.TRUE.equals(locked)) {
        rebuildExecutor.submit(() -> rebuildHotAsset(assetId, dataKey, lockKey));
    }

    return envelope.getData();
}

逻辑过期的关键不是“永不过期”,而是把过期后的数据库回源从同步变成异步,保护数据库。

缓存穿透治理

缓存穿透是查询不存在的数据,Redis 和数据库都没有。如果攻击者不断构造不存在的 ID,就会绕过缓存打数据库。

mermaid
flowchart TD
    A["请求不存在 ID"] --> B["Redis 未命中"]
    B --> C["MySQL 未命中"]
    C --> D["下次请求仍然重复"]

治理方式:

方式说明风险
参数校验非法 ID、格式错误直接拒绝只能挡明显非法
缓存空值DB 无数据也缓存短 TTL 空值无效 Key 太多会占内存
布隆过滤器判断 ID 是否可能存在有误判,删除困难
限流防止恶意请求打穿需要合理阈值

布隆过滤器适合数据集合相对稳定的场景,例如商品 ID、资产 ID、机构 ID。它能判断“一定不存在”或“可能存在”。底层原理、误判率、初始化同步和 RedisBloom Demo 看:布隆过滤器原理

缓存雪崩治理

缓存雪崩是大量 Key 同时失效,或 Redis 整体不可用,导致请求同时打到数据库。

常见原因:

  1. TTL 都设置为 30 分钟,批量同时过期。
  2. 系统启动时统一预热,过期点集中。
  3. Redis 主节点故障或网络抖动。
  4. 发布活动导致大量缓存同时失效。

治理:

mermaid
flowchart TD
    A["雪崩风险"] --> B["TTL 加随机值"]
    A --> C["热点数据预热"]
    A --> D["多级缓存"]
    A --> E["限流和降级"]
    A --> F["Redis 高可用"]
    A --> G["数据库保护"]

TTL 随机值:

java
Duration ttl = Duration.ofMinutes(30)
    .plusSeconds(ThreadLocalRandom.current().nextInt(300));
redisTemplate.opsForValue().set(key, value, ttl);

Redis 慢命令和阻塞

Redis 单线程执行命令。一个慢命令会阻塞后续命令处理。

高风险命令:

命令风险替代
KEYS *扫全库阻塞SCAN
HGETALL big_hash返回大量字段HSCAN、按字段查询
SMEMBERS big_set返回全量集合SSCAN
LRANGE key 0 -1返回整个列表分页范围
ZRANGE key 0 -1返回整个排行只取 TopN
DEL big_key同步释放大内存UNLINK
长 Lua阻塞主线程控制脚本复杂度

慢日志:

bash
CONFIG GET slowlog-log-slower-than
SLOWLOG GET 20
SLOWLOG LEN

slowlog-log-slower-than 单位是微秒。生产要根据业务延迟要求设置合适阈值。

连接池和超时

很多 Redis 事故表现为应用报 timeout,但根因可能不是 Redis 慢,而是连接池被耗尽。

mermaid
flowchart TD
    A["请求进入应用"] --> B["获取 Redis 连接"]
    B --> C{"连接池是否有空闲连接"}
    C -- "有" --> D["执行 Redis 命令"]
    C -- "没有" --> E["线程等待连接"]
    E --> F["接口超时和线程堆积"]

常见原因:

  1. Redis 命令慢,连接长时间不释放。
  2. 连接池太小。
  3. 应用并发突然升高。
  4. 网络抖动。
  5. 没有设置命令超时。
  6. 大 Key 返回导致连接占用时间长。

Spring Boot Lettuce 示例:

properties
spring.data.redis.timeout=2s
spring.data.redis.lettuce.pool.max-active=32
spring.data.redis.lettuce.pool.max-idle=16
spring.data.redis.lettuce.pool.min-idle=4
spring.data.redis.lettuce.pool.max-wait=500ms

参数不是越大越好。连接池过大可能把 Redis 打得更重,也会放大下游压力。

过期删除和内存淘汰

高并发缓存不是只设置 TTL 就完事,还要理解 Key 到期以后 Redis 怎么处理,以及内存满了以后会发生什么。

Redis 过期删除可以简单理解为两类机制一起工作:

mermaid
flowchart TD
    A["Key 设置 TTL"] --> B["惰性删除"]
    A --> C["定期删除"]
    B --> D["访问 Key 时发现过期再删除"]
    C --> E["后台周期抽样清理过期 Key"]

为什么不是到期瞬间立刻删除所有 Key:如果 Redis 为每个 Key 都维护一个精确删除定时器,Key 数量很大时定时器管理成本会很高。惰性删除和定期删除是性能与内存回收之间的折中。

如果大量 Key 在同一时间过期,会出现两个问题:

  1. 请求同时未命中,数据库被回源流量打满。
  2. Redis 需要清理大量过期 Key,带来额外 CPU 开销。

所以生产里要给 TTL 加随机值,避免过期点集中。

内存达到 maxmemory 后,Redis 会按淘汰策略处理:

策略含义适合场景
noeviction不淘汰,写入报错不希望缓存静默丢数据
allkeys-lru从所有 Key 中淘汰最近最少使用通用缓存场景常用
allkeys-lfu从所有 Key 中淘汰低频访问热点更稳定的场景
volatile-lru只从有 TTL 的 Key 中按 LRU 淘汰混合存储但只允许淘汰缓存
volatile-ttl优先淘汰更快过期的 Key临时数据较多
allkeys-random随机淘汰对命中率要求不高

配置示例:

conf
maxmemory 4gb
maxmemory-policy allkeys-lru

如果淘汰策略设计不当,可能出现“Redis 没挂,但命中率突然下降”的事故:热数据被淘汰后,大量请求回源数据库,数据库慢了又拖慢缓存重建,最终表现成全链路超时。

Pipeline 批量命令和输出缓冲区

高并发接口经常会一次查很多缓存。很多人会本能地循环 get

java
for (Long id : ids) {
    redisTemplate.opsForValue().get("asset:" + id);
}

这样每个 get 都要一次网络往返,接口耗时会被网络 RTT 放大。更好的方式是使用 MGET 或 Pipeline 减少网络往返。

java
List<String> keys = ids.stream()
    .map(id -> "asset:" + id)
    .collect(Collectors.toList());

List<String> values = redisTemplate.opsForValue().multiGet(keys);

Pipeline 的意义是把多个命令一次发给 Redis,再批量读取响应:

mermaid
flowchart TD
    A["客户端准备多条命令"] --> B["一次发送到 Redis"]
    B --> C["Redis 顺序执行"]
    C --> D["批量返回结果"]

但是 Pipeline 不是越大越好:

  1. Redis 仍然是顺序执行这些命令,不能把重命令变轻。
  2. 一次返回结果太大,会增加网络和客户端反序列化压力。
  3. 客户端来不及读取响应时,输出缓冲区可能增大。
  4. Cluster 下批量 key 如果跨 slot,要看客户端是否能自动拆分。

经验上要控制批量大小,例如每批 100 到 1000 个,具体根据 value 大小、网络、Redis 延迟和业务 P99 来压测。

降级熔断和数据库保护

Redis 是用来保护数据库的,但 Redis 自己异常时,也必须防止所有请求瞬间打到数据库。

mermaid
flowchart TD
    A["Redis 异常或命中率下降"] --> B["应用快速失败或降级"]
    B --> C["限制数据库回源并发"]
    C --> D["返回兜底数据"]
    D --> E["异步重建缓存"]

商业项目常见策略:

场景处理
字典、配置缓存不可用返回本地兜底缓存或默认配置
热门详情缓存不可用限制回源并发,返回旧数据
排行榜缓存不可用返回上一周期快照
非核心统计缓存不可用临时关闭统计展示
数据库压力过高熔断部分非核心接口

真正的高并发缓存设计,必须明确“缓存挂了能不能查 DB、能查多少、查不到返回什么、多久恢复、如何告警”。

Lua 限流 Demo

高并发热点接口要能限流。下面是固定窗口限流示例。

lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])

local current = tonumber(redis.call('GET', key) or '0')
if current >= limit then
  return 0
end

current = redis.call('INCR', key)
if current == 1 then
  redis.call('EXPIRE', key, ttl)
end

return 1

Java 调用时,按接口、用户、租户等维度设计 key:

text
rate:asset:detail:user:1001
rate:login:ip:192.168.1.10
rate:tenant:2001

限流的目标不是让所有请求成功,而是在异常流量下保护 Redis、数据库和业务服务。

Cluster 下的热点问题

Redis Cluster 通过 16384 个 slot 分片。普通 Key 会根据 hash 结果落到某个 slot。

text
slot = CRC16(key) % 16384

热 Key 的问题是:一个 Key 只能属于一个 slot,一个 slot 只能由一个主节点负责。即使集群有很多节点,单个 Key 热点也不能天然被多个主节点分担。

治理思路:

  1. 热点读:本地缓存、多级缓存。
  2. 热点计数:分片计数,最后汇总。
  3. 热点库存:预扣库存、队列削峰、Lua 控制。
  4. 热点排行榜:只缓存 TopN,按时间拆分。
  5. 热点租户:按业务拆 Key,避免所有数据堆到一个 Key。

分片计数示例:

text
article:read:1001:shard:0
article:read:1001:shard:1
article:read:1001:shard:2
article:read:1001:shard:3

写入时随机选择一个 shard 自增,读取时汇总多个 shard。这样可以分散写压力,但读取和一致性更复杂。

高并发缓存整体架构

mermaid
flowchart TD
    A["用户请求"] --> B["网关限流"]
    B --> C["应用本地缓存"]
    C --> D{"本地命中"}
    D -- "是" --> E["返回"]
    D -- "否" --> F["Redis"]
    F --> G{"Redis 命中"}
    G -- "是" --> H["回填本地缓存"]
    G -- "否" --> I["互斥锁或逻辑过期"]
    I --> J["查询数据库"]
    J --> K["写 Redis,TTL 随机"]
    K --> E
    H --> E

这个链路里,每一层都在保护下一层:

  1. 网关限流保护应用。
  2. 本地缓存保护 Redis。
  3. Redis 保护数据库。
  4. 互斥锁和逻辑过期保护数据库。
  5. 降级保护整体可用性。

生产排查流程

mermaid
flowchart TD
    A["接口 Redis timeout"] --> B["看应用连接池"]
    B --> C["看 Redis slowlog"]
    C --> D{"是否有慢命令"}
    D -- "是" --> E["查大 Key 或复杂命令"]
    D -- "否" --> F["看 QPS、CPU、网络、内存"]
    F --> G{"是否单 Key 或单节点热点"}
    G -- "是" --> H["本地缓存、拆 Key、限流"]
    G -- "否" --> I["查持久化、主从、网络、数据库回源"]

生产排查清单:

现象优先排查
Redis timeout连接池、慢命令、大 Key、网络、Redis CPU
DB QPS 飙升穿透、击穿、雪崩、Redis 故障、缓存命中率
Redis 单节点 CPU 高热 Key、复杂命令、Lua、Cluster 热点
Redis 内存高大 Key、TTL 缺失、缓存空值过多、淘汰策略
删除后卡顿是否 DEL 大 Key,改用 UNLINK
主从延迟高大对象写入、网络、磁盘、复制积压
Cluster 节点不均衡大 Key、slot 分布、热点业务
应用线程堆积连接池耗尽、命令超时过长、回源 DB 慢

Redis timeout 全链路排查

Redis timeout 不是一个原因,而是一个结果。它可能发生在应用拿连接、网络传输、Redis 排队执行、返回大响应、客户端反序列化、回源数据库等多个位置。

零基础可以先把一次缓存读取拆成这条链:

mermaid
flowchart TD
    A["业务线程进入缓存方法"] --> B["从连接池借 Redis 连接"]
    B --> C["发送命令到 Redis"]
    C --> D["Redis 排队等待执行"]
    D --> E["执行命令"]
    E --> F["返回响应数据"]
    F --> G["客户端反序列化"]
    G --> H{"是否命中"}
    H -- "命中" --> I["返回业务结果"]
    H -- "未命中" --> J["回源数据库"]
    J --> K["写回 Redis"]
    K --> I

每一步的证据不同:

位置典型现象证据
借连接慢应用线程卡在连接池等待Lettuce/Jedis 连接池指标、线程栈
网络慢Redis 自身不慢,但客户端耗时高同机房网络延迟、丢包、跨机房访问
Redis 排队单节点延迟升高,后续命令都慢SLOWLOGINFO commandstats、CPU
命令执行慢某些命令耗时高SLOWLOG GET、大 Key、复杂 Lua
返回数据大Redis 执行不算慢,但客户端耗时高响应体大小、MEMORY USAGE、网卡流量
反序列化慢Java CPU 高,GC 增加应用 CPU、火焰图、对象大小
回源慢Redis 命中率下降后 DB 被打满缓存命中率、DB QPS、慢 SQL

所以排查 Redis timeout 不要只盯 Redis 服务端。很多线上事故是“Redis 未命中 -> 大量回源 DB -> DB 慢 -> 业务线程堆积 -> Redis 连接还不及时归还 -> Redis timeout 更多”,这是连锁反应。

mermaid
flowchart TD
    A["缓存命中率下降"] --> B["大量请求回源数据库"]
    B --> C["DB 慢 SQL 和连接池等待"]
    C --> D["业务线程占用时间变长"]
    D --> E["Redis 连接归还变慢"]
    E --> F["应用侧 Redis 连接池等待"]
    F --> G["更多请求 timeout"]

第一步:先判断 timeout 发生在哪

问题判断方式
是拿连接超时吗看连接池等待时间、活跃连接数、最大连接数、等待队列
是命令执行超时吗看 Redis SLOWLOG、节点 CPU、慢命令分布
是网络超时吗看 Redis 节点延迟、网卡、客户端到 Redis 的 RTT
是大响应拖慢吗看 key 大小、响应字节数、客户端反序列化耗时
是回源拖慢导致连锁吗看缓存命中率、DB QPS、慢 SQL、应用线程池

常见误判:

误判为什么错
Redis timeout 一定是 Redis 挂了应用连接池、网络、DB 回源都可能让调用超时
SLOWLOG 没慢命令就没问题热 Key 单次命令不慢,但频率太高也能打满 CPU 或网卡
只扩 Redis 连接池就能解决如果 Redis 或 DB 已经慢,扩连接池只会放大压力
只看平均耗时高并发事故要看 P95/P99、等待队列和突刺

第二步:看应用连接池

如果应用使用 Lettuce、Jedis 或 Redisson,要先看连接池和线程池。连接池满时,业务线程可能不是卡在 Redis 命令执行,而是卡在“借连接”。

排查指标:

指标意义
active connections活跃连接数是否接近上限
idle connections空闲连接是否长期为 0
wait queue是否有线程等待连接
borrow latency借连接耗时是否升高
command timeoutRedis 命令超时是否集中在某些接口

治理原则:

  1. 不要无脑把连接池调很大。Redis 单节点承载能力有限,连接太多会让排队更严重。
  2. 先降低单请求 Redis 次数,例如批量 MGET、Pipeline、合并查询。
  3. 对低价值接口做限流,避免把连接池占满。
  4. 对热点数据加本地缓存,减少 Redis 调用次数。
  5. 缩短不必要的大事务或慢业务链路,及时归还连接。

第三步:看 Redis 服务端

常用命令:

bash
redis-cli slowlog get 20
redis-cli info commandstats
redis-cli info clients
redis-cli info stats
redis-cli info memory
redis-cli memory usage your:key

判断逻辑:

证据可能原因下一步
slowlogHGETALLSMEMBERS大 Key 或全量命令拆 Key、改 HSCAN、禁止全量
cmdstat_get 调用极高热 Key 或高频接口应用侧 TopN、加本地缓存
used_memory 接近上限大 Key、TTL 缺失、空值缓存过多查 bigkeys、淘汰策略、Key 生命周期
connected_clients 异常高连接泄漏或池配置过大查应用连接池和客户端
instantaneous_ops_per_sec 突刺流量峰值或缓存雪崩限流、随机 TTL、预热

第四步:看数据库回源

Redis timeout 有时是数据库先被打满导致的。比如热点 key 过期,所有请求未命中,业务线程都在查 DB;查 DB 期间 Redis 连接、应用线程、HTTP 线程都被占住,最后表现成 Redis timeout 和接口 timeout 一起出现。

证据:

证据说明
Redis 命中率突然下降缓存失效或雪崩
DB QPS 突然升高大量回源
DB 慢 SQL 增加回源查询本身慢
应用线程栈大量卡在 DBtimeout 根因可能不在 Redis
Redis 连接池等待升高业务线程长时间不释放资源

治理:

  1. 热点 key 用逻辑过期或互斥锁,避免同时回源。
  2. 普通缓存 TTL 加随机值,避免同一时间大面积失效。
  3. 不存在数据用参数校验、空值缓存和布隆过滤器。
  4. DB 回源加限流和降级,保护事实源。
  5. 对关键热点提前预热。

连接池、线程池和 Redis 的关系

高并发下,应用线程池、Redis 连接池、DB 连接池是连在一起的。任何一个池耗尽,都会让其他池被拖住。

mermaid
flowchart TD
    A["HTTP 线程池"] --> B["业务逻辑"]
    B --> C["Redis 连接池"]
    B --> D["DB 连接池"]
    C --> E["Redis 服务端"]
    D --> F["MySQL 服务端"]
    E --> G{"Redis 慢或未命中"}
    G -- "未命中" --> D
    F --> H{"DB 慢"}
    H -- "是" --> A

典型事故过程:

  1. 热点缓存失效。
  2. 大量请求同时未命中 Redis。
  3. 请求回源数据库。
  4. DB 连接池满,SQL 变慢。
  5. HTTP 线程被占住。
  6. Redis 连接归还变慢,连接池也开始等待。
  7. 接口整体 timeout。

所以调参时要看整体容量,不是哪个报错就只调哪个:

调大什么可能副作用
HTTP 线程池放更多请求进来,可能打爆 Redis/DB
Redis 连接池Redis 服务端排队更长,网络更拥塞
DB 连接池DB 并发更高,慢 SQL 更严重
timeout 时间用户等待更久,线程占用更久

正确思路是:限流入口,减少无效请求,降低单请求 Redis 次数,保护 DB 回源,热点本地化,慢命令治理,而不是只把池子和超时时间调大。

常用命令清单

bash
# 慢命令
redis-cli slowlog get 20

# 大 Key 概览
redis-cli --bigkeys

# 热 Key,依赖访问频率统计
redis-cli --hotkeys

# 单 Key 内存
redis-cli memory usage your:key

# Redis 状态
redis-cli info memory
redis-cli info stats
redis-cli info commandstats
redis-cli info clients
redis-cli info replication

# 渐进扫描
redis-cli scan 0 match 'asset:*' count 1000

不要在生产高峰随意使用 MONITORKEYS * 或大范围扫描。

商业场景:医疗数据采集平台

在医疗数据采集与资产平台里,Redis 可能用于:

  1. 采集任务状态缓存。
  2. 字典、机构、表结构、字段元数据缓存。
  3. 数据资产详情缓存。
  4. 限流和防重复提交。
  5. 分布式锁保护任务调度。

风险示例:

场景风险治理
资产详情热点查询某个资产被大量访问形成热 Key本地缓存、逻辑过期
字段元数据一次缓存全量大 JSON 形成大 Key按表、字段、分页拆分
任务状态列表过大List/Hash 过大按批次、机构、分页拆 Key
采集任务开始时缓存同时过期雪崩TTL 随机、预热
异常 ID 查询很多穿透参数校验、缓存空值、布隆过滤器
Redis 故障DB 被打满限流、降级、只读兜底

关联知识点

知识点继续学习什么
缓存问题穿透、击穿、雪崩基础
Redis 高级持久化、高可用、慢命令、生产排查总览
主从、哨兵与集群Cluster、slot、MOVED、ASK、主从复制
分布式锁互斥锁、Lua 释放、Redisson 看门狗
缓存一致性数据库和缓存如何保持最终一致
消息堆积与背压缓存重建、异步补偿、削峰思路

小结

Redis 高并发问题的本质是流量、数据结构和资源保护。大 Key 让单次命令变重,热 Key 让单点流量集中,穿透/击穿/雪崩会把压力打到数据库,慢命令和连接池耗尽会拖垮应用。成熟方案一定会组合使用 Key 拆分、本地缓存、逻辑过期、互斥锁、随机 TTL、布隆过滤器、限流降级、异步删除、监控告警和补偿任务。