跳转至

Redis 核心基础与持久化

⚡ 1. Redis 为什么快?单线程模型为什么能支撑高并发?

Redis 快,是因为它把所有数据都放在内存里,并且用单线程扫清了并发控制的一切障碍。

一个常见的误解是“单线程 = 慢”。在 Redis 的场景里恰恰相反:单线程让它避免了多线程的锁竞争、上下文切换、以及 CPU 缓存失效的开销。当一个命令执行时间在微秒级时,把这些时间花在调度线程上反而是最大的浪费。

image.png

单线程能支撑高并发的三个支柱:

  1. 纯内存操作:一次 SET 或 GET 只需要几微秒。瓶颈不在 CPU 计算,而在网络 IO。只要单线程能在微秒级处理完一个命令,它就能在 1 秒内处理几十万次请求。

  2. IO 多路复用(epoll):Redis 使用 epoll(Linux 下)同时监听成千上万个客户端 socket。哪个 socket 有数据到达,就处理哪个;没有数据时,主线程就阻塞在 epoll_wait 上,不浪费任何 CPU。这使得单线程可以轻松管理数万并发连接。

  3. 避免锁和上下文切换:多线程程序在访问共享数据结构时必须加锁,锁竞争和线程调度会吃掉大量 CPU。Redis 单线程直接操作内存中的数据结构,零锁开销,CPU 缓存永远热乎。这就是为什么 Redis 的“单线程”反而比很多“多线程”存储系统更快。

关于 Redis 6.0 引入的多线程 IO:它只用于网络读写,把数据从 socket 搬进搬出的工作分给了 IO 线程,但命令执行仍然是单线程的。这就像在餐厅里,服务员帮你端菜、收碗筷(IO 线程),但厨房里仍然只有一个大厨(主线程)在炒菜。大厨的速度没变,但顾客的等待时间却因为并行上菜而缩短了。

一个简单的代码示例:感受一下 Redis 的单线程命令有多快。

import redis, time

r = redis.Redis(host='localhost', port=6379)

# 测试单线程吞吐量
start = time.time()
for i in range(100000):
    r.set(f"key{i}", f"value{i}")
elapsed = time.time() - start
print(f"10 万次 SET 耗时: {elapsed:.2f} 秒, QPS: {100000/elapsed:.0f}")
# 典型输出: 10 万次 SET 耗时: 0.78 秒, QPS: 128000

在 Agent 场景里,我们经常用 Redis 来存储对话状态、缓存 Embedding 结果,每一次读写都是微秒级完成的,这正是 Agent 能做到毫秒级响应的基础。


💾 2. RDB 和 AOF 持久化有什么区别?混合持久化怎么工作?

Redis 数据在内存里,断电就丢。持久化就是给内存数据拍“快照”或记“日记”,让重启时能恢复。RDB 和 AOF 是两种完全不同的哲学。

RDB (快照)                        AOF (追加日志)
───────────                       ──────────────
  Redis 数据 → 二进制 dump.rdb      每一条写命令 → 追加到 appendonly.aof
  周期性保存 (如每 5 分钟)           实时记录 (每 1 秒或每条命令 fsync)
  恢复时直接加载二进制,极快         恢复时逐条重放命令,较慢
  可能丢失最近几分钟的数据           数据安全性极高,最多丢 1 秒
  文件小,适合备份                  文件大,但可重写压缩

RDB(Redis Database Backup)

  • 原理:Redis 通过 fork() 创建子进程,子进程将当前内存数据写入一个临时的 .rdb 文件,写完后原子地替换旧的 RDB 文件。主进程在子进程工作时完全不阻塞。

  • 优点:紧凑的二进制格式,恢复速度极快,适合灾难恢复和全量备份。

  • 缺点:两次 RDB 之间如果宕机,这段时间的数据就丢了。不适合对数据安全性要求极高的场景。

AOF(Append Only File)

  • 原理:将每一次写命令追加到 AOF 文件末尾,就像数据库的 WAL 日志。Redis 会定期对 AOF 文件进行重写(Rewrite),去除冗余命令,压缩体积。

  • 优点:数据安全性极高,可以配置 appendfsync everysec(每秒刷盘一次),最多丢失 1 秒的数据。

  • 缺点:AOF 文件通常比 RDB 大,恢复时需要逐条执行命令,速度比 RDB 慢。

混合持久化(Redis 4.0+ 引入)

混合持久化结合了 RDB 的恢复速度与 AOF 的数据安全性。它的 AOF 文件不再是一堆纯命令,而是前半部分为 RDB 格式的二进制快照,后半部分为快照之后的增量 AOF 命令。

image.png

工作流程:当 Redis 触发 AOF 重写时,子进程不再像以前那样逐条扫描 AOF 文件,而是直接把当前内存数据写成 RDB 格式,写入新的 AOF 文件开头;然后主进程把重写期间的新增命令以 AOF 格式追加到文件末尾。最终文件即具备了 RDB 的快速恢复能力,又保留了 AOF 的秒级数据安全。

代码示例:在配置中启用混合持久化

# redis.conf
save 900 1             # 仍然可以保留 RDB 作为备份
save 300 10
appendonly yes         # 开启 AOF
appendfsync everysec   # 每秒刷盘
aof-use-rdb-preamble yes   # 开启混合持久化 (关键配置)

在 Agent 场景的选择:如果 Redis 只是用来做缓存(比如 Embedding 缓存、对话上下文缓存),那么可以完全关闭持久化,因为数据丢了也无所谓。如果 Redis 用来存储 Agent 的任务状态、关键记忆,那么混合持久化是最佳选择——重启后秒级恢复,又不会丢失关键状态。


🗑️ 3. Redis 内存淘汰策略有哪些?在 Agent 场景如何选择?

Redis 作为内存数据库,内存是有限且昂贵的。当写入数据超过 maxmemory 上限时,Redis 必须按照某种策略淘汰一些旧数据,为新数据腾出空间。这就产生了八种策略,按“会不会淘汰”和“淘汰谁”两个维度分类。

image.png

查看内嵌表格

在 Agent 场景下如何选择?

Agent 系统通常用 Redis 存储三类数据,它们的淘汰策略应该区别对待:

  1. 对话上下文 / 任务状态:这是 Agent 的“短期记忆”,丢失意味着任务中断。这类数据绝不希望被淘汰。应该使用 noeviction,并在应用层做好过期清理(比如设置过期时间 EXPIRE session_id 3600)。当内存不足时,宁可拒绝新任务,也不要把正在进行的任务状态删掉。

  2. LLM 调用结果缓存 / Embedding 缓存:这是典型的“热缓存”,用 Redis 就是为了加速。推荐 allkeys-lruallkeys-lfu。LRU 适合访问模式均匀的场景,LFU 适合有明显冷热之分的场景(比如高频 FAQ 的缓存,不希望被一次性的大流量查询冲掉)。

  3. 工具调用结果缓存(如网页搜索、API 结果):这类数据通常有过期时间(因为外部数据会更新),所以可以全部设置过期时间,然后使用 volatile-lru 或 volatile-ttl。如果希望快到期的数据优先被淘汰,volatile-ttl 很合适。

示例代码:设置 maxmemory-policy 和过期时间

import redis

r = redis.Redis(host='localhost', port=6379)

# 1. 运行时设置内存上限 (100MB)
r.config_set('maxmemory', '100mb')

# 2. 设置全局淘汰策略为 allkeys-lru(针对缓存类数据)
r.config_set('maxmemory-policy', 'allkeys-lru')

# 3. 对会话状态类数据,使用独立的 Redis 实例或 DB,策略设为 noeviction
r_state = redis.Redis(host='localhost', port=6380)
r_state.config_set('maxmemory-policy', 'noeviction')

# 4. 对缓存数据设置过期时间
r.setex(f"llm_cache:{query_hash}", 3600, response_text)
r.setex(f"session:{session_id}:context", 1800, context_json)

精细的做法——多实例隔离:如果同一个 Redis 实例里既有“绝不能丢”的会话数据,又有“可随意淘汰”的缓存数据,用同一种策略会互相伤害。更好的方式是用两个 Redis 实例,或至少两个 DB,分别配置不同的 maxmemory-policy,从物理上隔离它们的淘汰行为。

一句话选型指南:

  • 数据丢了会出生产事故(如任务状态、消息队列)→ noeviction + 监控告警。

  • 数据丢了只是多调一次 LLM / 多算一次 Embedding(如各类缓存)→ allkeys-lru 或 allkeys-lfu。

  • 数据都有明确的 TTL,且希望快到期的先被淘汰 → volatile-ttl。


缓存穿透、击穿与雪崩

🧊 1. 什么是缓存穿透、击穿、雪崩?

这三个词描述了缓存层在不同“受力面”上被击溃的过程。可以把 Redis 想象成一座大坝,数据请求就是洪水,而它们分别对应着大坝的 “渗漏”、“管涌”和“溃堤”。

image.png

缓存穿透 现象:客户端疯狂查询一个数据库和缓存中都不存在的 key(如 product_-1)。由于缓存里永远没有,所有请求直接穿透到数据库,把数据库瞬间压垮。这往往是由恶意攻击或代码逻辑错误引起的。

  • 本质:查的是“不存在”的数据,缓存形同虚设。

缓存击穿

现象:某个热点 key(如秒杀商品、爆款新闻),在它缓存过期的瞬间,恰巧有海量并发请求同时涌入。因为缓存失效,这些请求全部打到数据库上,像一把锥子击穿了缓存层。

  • 本质:热点数据在失效的瞬间,发生了并发穿透。

缓存雪崩

现象:大量不同的 key 在同一时间窗口内集中过期,或者 Redis 集群自身宕机。导致接下来所有的请求都直接落到了数据库上,引发连锁式的服务崩溃。

  • 本质:缓存层整体失效,失去了对数据库的保护能力。

一张对比表看清三者的区别:

查看内嵌表格


🛡️ 2. 在 Agent 高并发场景下,如何系统性防护缓存穿透、击穿和雪崩?

在 Agent 系统里,缓存穿透可能发生在“恶意高频查询不存在的知识库条目”;击穿可能发生在“某个爆款 Agent 工具的调用结果频繁过期”;雪崩则容易出现在“所有会话状态 Key 使用了同一个过期时间”。

我们需要建立一套主动防御 + 被动容错的立体防护体系。

2.1 缓存穿透防护

策略一:布隆过滤器

在缓存和数据库之前,加一个布隆过滤器。它可以用极小的内存判断“一个 key 一定不存在或可能存在”。将所有合法的 Key 预先加入过滤器。当请求到来时,先问过滤器:这个 Key 在集合里吗?如果返回“不在”,直接拒绝,根本不走到缓存和数据库层。这能拦截 99% 的非法 Key 穿透。

import redis
from pybloom_live import BloomFilter

r = redis.Redis()
# 创建布隆过滤器,假设有 100 万个合法 Key
bf = BloomFilter(capacity=1000000, error_rate=0.001)

# 初始化:将数据库中所有合法 Key 加入过滤器(定时任务同步)
for key in fetch_all_valid_keys_from_db():
    bf.add(key)

def get_data(key):
    # 1. 布隆过滤器拦截
    if key not in bf:
        return None  # 直接拒绝,不走缓存和DB
    # 2. 查缓存
    data = r.get(key)
    if data:
        return data
    # 3. 查数据库
    data = db.query(key)
    if data:
        r.setex(key, 3600, data)
        return data
    # 4. 如果数据库也没有,缓存空对象防止短时间重复穿透
    r.setex(key, 60, "null")
    return None

策略二:缓存空对象

如果布隆过滤器漏过了一些请求,且数据库确实也没有这个 Key,我们可以将这个 Key 缓存一个“空对象”(或特殊标记),并设置较短的过期时间(如 30 秒)。这样后续同一 Key 的请求就能被缓存拦截,不会持续打到数据库。

策略三:请求入口校验 在业务层直接过滤掉非法参数。例如对于 id 参数,必须是正整数,长度不超过限制等。这是最简单也是最有效的第一道防线。

2.2 缓存击穿防护

策略一:互斥锁 当缓存失效时,只允许第一个线程去加载数据库并重建缓存,其他线程等待锁释放。这能保证数据库不会被同一 Key 的并发请求打垮。在 Redis 中可以用 SETNX 实现。

import redis, time

r = redis.Redis()

def get_data_with_lock(key):
    data = r.get(key)
    if data:
        return data

    lock_key = f"lock:{key}"
    # 尝试获取锁,设置过期时间 10 秒防止死锁
    if r.setnx(lock_key, "1"):
        r.expire(lock_key, 10)
        try:
            # 双重检查,防止锁内重复查库
            data = r.get(key)
            if data:
                return data
            data = db.query(key)
            if data:
                r.setex(key, 3600, data)
            else:
                r.setex(key, 60, "null")
            return data
        finally:
            r.delete(lock_key)
    else:
        # 没抢到锁,等一会儿重试
        time.sleep(0.1)
        return get_data_with_lock(key)  # 递归重试

策略二:永不过期 + 异步刷新

对极端热点的 Key,我们不设置过期时间,而是在逻辑上设计“逻辑过期”。启动一个后台线程,定期检查这些热点 Key 的逻辑过期时间,一旦过期,用后台线程去更新缓存,而主线程直接返回旧值(物理上永不过期)。这样热点 Key 永远不会真正失效,也就不会有击穿。

# 逻辑过期时间存 hash
r.hset("hotkey", "data", "value123")
r.hset("hotkey", "expire_at", time.time() + 60)  # 逻辑过期时间

def get_hot_data(key):
    meta = r.hgetall(key)
    if not meta:
        return None
    if time.time() < float(meta[b'expire_at']):
        return meta[b'data']
    # 已逻辑过期:异步刷新缓存,仍返回旧值
    asyncio.create_task(refresh_cache(key))
    return meta[b'data']

2.3 缓存雪崩防护

策略一:过期时间加随机偏移

避免大量 Key 在同一时刻过期。为每个 Key 的过期时间增加一个随机值,将集中失效的风险打散。

import random
# 基础过期时间 1 小时,随机偏移 0-600 秒
ttl = 3600 + random.randint(0, 600)
r.setex(key, ttl, value)

策略二:多级缓存 + 降级熔断

构建 Redis 主从/哨兵/集群架构,避免单点故障。同时,在应用层引入断路器。当 Redis 不可用时,自动降级为读取本地内存缓存、或直接返回友好提示,避免流量全部打到数据库。Sentinel 会自动进行故障转移。

策略三:流量削峰与限流

在网关层或 Agent 入口处进行流量整形(令牌桶/漏桶),保证到达 Redis 的请求量不会瞬间爆发。这能显著降低雪崩的冲击力。

策略四:服务降级

当数据库压力过大时,有选择地关闭非核心功能,例如暂时关闭复杂的 Agent 推理功能,只返回预设的友好提示,优先保证核心链路存活。

在 Agent 场景中的针对性设计:

  • Embedding 缓存:计算 Embedding 非常昂贵,属于典型的热点数据。使用“永不过期 + 异步刷新”策略,并在 Key 过期时间上打散。

  • LLM 调用结果缓存:对高频 Query 的最终回答进行缓存,使用“互斥锁”防止击穿。

  • 会话状态缓存:使用 Redis Cluster 分散存储,避免单点压力;并设置合理的过期时间,过期后引导用户重新开始会话。


Redis 高级数据结构

🧰 1. Redis 除了基础数据结构,还有哪些高级数据结构?

除了基础的五种(String、Hash、List、Set、ZSet),Redis 还有三种高级数据结构,它们专门解决一些特定场景下的复杂问题。在 AI Agent 系统中,它们往往能以极低的复杂度实现精巧的功能。

查看内嵌表格

3.1 HyperLogLog:亿级 UV 的统计神器

它能在 12KB 的固定内存里,统计超过 2^64 个独立元素的近似基数(标准误差 0.81%)。它不需要存储每个元素,而是通过哈希概率算法估算。在 Agent 场景中,可以极低成本地统计每天有多少独立用户使用了某个工具。

# 记录每天访问 Agent 的用户 UV
r = redis.Redis()
r.pfadd("uv:agent:20260704", "user_001", "user_002", "user_003")
count = r.pfcount("uv:agent:20260704")
print(f"今天 Agent 独立访客数: {count}")

3.2 Geo:让 Agent 懂得“附近”的智能

它可以将经纬度作为坐标存入,并直接计算两点之间的距离、查找指定半径内的所有点。

# 存入 Agent 服务的线下门店位置
r.geoadd("stores", (116.404, 39.915, "store_A"), (116.418, 39.921, "store_B"))

# 查找 (116.405, 39.910) 附近 5 公里内的门店
stores = r.georadius("stores", 116.405, 39.910, 5, unit="km")
print(stores)  # [b'store_A']

3.3 Bitmaps:用比特位做极简标记

它用一个 String 的每一位来记录一个布尔值,非常适合记录海量用户的二值状态(如签到、是否已读),内存占用极低。

# 记录用户 ID=1001 在 7 月 4 日是否签到
r.setbit("checkin:20260704", 1001, 1)
is_signed = r.getbit("checkin:20260704", 1001)
print(f"用户 1001 今天签到: {bool(is_signed)}")

这些高级数据结构的共同价值:它们用极小的内存和极高的性能,解决了传统关系型数据库需要庞大索引和复杂 SQL 才能(甚至无法)完成的任务。在 AI Agent 处理海量日志、实时统计和地理位置服务时,它们是让系统保持轻快的核心力量。

📊 2. HyperLogLog 和 Bitmap 分别适合什么场景?精度和内存占用如何?

这两者都是用极小内存处理海量数据的“极端利器”,但它们解决的根本不是一类问题。

  • HyperLogLog 解决的是“有多少”,且允许模糊。

  • Bitmap 解决的是“谁是什么状态”,要求精确。

HyperLogLog:概率统计的巅峰之作

它基于概率算法,能够在固定 12KB 的内存里,统计超过 2^64 个不同元素的近似基数,标准误差仅为 0.81%。它不需要存储具体元素,只存储哈希后的概率标记。在 Agent 系统中,它是统计海量日志、用户行为的最佳选择。

Bitmap:极致的精确布尔索引

它本质上是一个巨大的二进制位数组。每一位代表一个个体(如用户 ID)的某个布尔状态(0 或 1)。它占用的内存取决于最大 ID 值,与活跃用户数无关。只要你的 ID 是连续的数字,Bitmap 就能在极低内存下提供 100% 精确的布尔统计。

场景决策指南:

查看内嵌表格

内存占用估算对比:

# HyperLogLog 内存:永远固定
print("HLL 固定内存: 12KB")

# Bitmap 内存:取决于最大 ID 值
def bitmap_memory(max_user_id):
    return max_user_id / 8 / 1024 / 1024  # 转为 MB

print(f"最大 ID 100万: {bitmap_memory(1000000):.2f} MB")
print(f"最大 ID 1亿: {bitmap_memory(100000000):.2f} MB")

在 Agent 场景的实战应用:

比如你做一个智能客服 Agent,想统计每天有多少独立用户触发了“退款”意图。用 HyperLogLog 一行命令就能搞定,且无需存储任何用户隐私数据。但如果你需要精确知道用户 ID 为 9527 的这个人今天是否触发过,就必须用 Bitmap,因为它能精确到每一个 bit。


🚰 3. Redis Stream 如何实现消息队列?和 Kafka 有什么区别?

Redis Stream 是 Redis 5.0 引入的一种持久化、可回溯、支持消费者组的日志型数据结构。它彻底弥补了旧有 Pub/Sub 不能持久化和重复消费的短板,成为轻量级、高性能消息队列的极佳选择。

Stream 的核心概念:

  • 消息:包含 ID 和键值对。ID 自动生成,格式为 时间戳-序列号

  • 消费者组:多个消费者共享一个组,同一条消息组内只投递给一个消费者,实现负载均衡。

  • 游标:每个消费者维护自己的消费位置,崩溃重启不会丢消息。

  • ACK 机制:消费者处理完消息后显式确认,保证至少一次投递。

代码示例:实现 Agent 的任务队列

import redis

r = redis.Redis(host='localhost', port=6379)

# 1. 生产者:Agent 接收用户任务,写入 Stream
def create_task(user_id, task_description):
    task_id = r.xadd("agent_tasks", {
        "user_id": user_id,
        "description": task_description,
        "timestamp": time.time()
    })
    return task_id

# 2. 消费者组初始化(只执行一次)
try:
    r.xgroup_create("agent_tasks", "worker_group", id="0", mkstream=True)
except redis.exceptions.ResponseError as e:
    if "already exists" not in str(e):
        raise

# 3. 消费者:Worker Agent 监听并处理任务
def process_tasks(worker_name):
    while True:
        # 一次读取 5 条未消费的消息,阻塞 2 秒
        tasks = r.xreadgroup(
            groupname="worker_group",
            consumername=worker_name,
            streams={"agent_tasks": ">"},
            count=5,
            block=2000
        )
        if not tasks:
            continue
        for msg_id, msg_data in tasks[0][1]:
            try:
                # 实际调用 Agent 处理任务
                result = agent_process(msg_data)
                # 处理成功,ACK 确认
                r.xack("agent_tasks", "worker_group", msg_id)
            except Exception as e:
                # 失败不 ACK,消息会被重新投递
                print(f"任务 {msg_id} 失败: {e}")

为什么 Redis Stream 适合 Agent 场景?

  • 任务分发:Orchestrator Agent 将复杂任务拆解后放入 Stream,多个 Worker Agent 通过消费者组自动抢占执行,天然支持并发和负载均衡。

  • 状态持久:任务处理中断不丢消息,重启 Worker 后可以继续消费。

  • 轻量级:无需像 Kafka 那样部署重型集群,对于日均十万到百万级消息的 Agent 系统,Redis Stream 足够稳定且运维成本极低。

Redis Stream 与 Kafka 的核心区别:

查看内嵌表格

选择判断树:

你需要严格的消息顺序和百万级消息保留?
  ├─ 是,还需要多团队共享? → Kafka
  └─ 否 → 你的团队正在用 Redis,且消息量 < 百万 QPS?
          ├─ 是 → Redis Stream (开箱即用,零运维负担)
          └─ 否 → 需要超大规模分析? → Kafka。需要简单任务队列? → Redis Stream / BullMQ

在 Agent 微服务里,Redis Stream 往往是那个“刚刚好”的选择——它不要求你精通复杂的分布式理论,也能让多个 Agent Worker 井然有序地协同工作。而一旦数据量跨入每秒数十万、需要对消息做持久化分析,Kafka 则是不可替代的工业级标准。


Redis 集群与高可用

1、基础题:Redis 主从复制、哨兵、Cluster 三种模式有什么区别?

难度级别:⭐⭐(主从同步、故障转移、数据分片)

Redis 主从复制、哨兵和 Cluster 是三种不同级别的可用性解决方案:

  • 主从复制:数据复制方案,主节点负责写,从节点负责读。主从之间异步复制,提供读写分离和数据冗余,但主节点故障需要手动切换。

  • 哨兵:高可用监控方案,在主从复制基础上增加 Sentinel 节点监控主从状态。主节点故障时自动选举新的主节点,实现故障自动转移,但仍存在单点写入瓶颈。

  • Cluster:分布式集群方案,通过数据分片将数据分散到多个主节点,每个主节点可配置从节点。提供数据分片、故障转移、自动水平扩展能力,适合大规模数据场景。


2、进阶题:Redis Cluster 的数据分片和故障转移机制是怎样的?

难度级别:⭐⭐⭐(16384槽位、一致性哈希、Gossip协议、故障检测、脑裂问题)

1️⃣ Common Answer

Redis Cluster 是分片架构,将数据通过哈希分散到多个节点。每个节点负责一部分数据,节点间通过 Gossip 协议通信。当主节点故障时,从节点会自动升为主节点,保证高可用。客户端连接任意节点,如果数据不在该节点,会收到重定向指令。

2️⃣ Impressive Answer

Redis Cluster 通过槽位机制实现数据分片和故障转移,核心机制包括:

  1. 数据分片机制
  2. 16384 槽位:Cluster 将整个键空间划分为 16384 个 slot,每个主节点负责一部分槽位
  3. 槽位计算CRC16(key) % 16384 确定键对应的 slot,保证相同 key 路由到同一节点
  4. 槽位迁移:支持在线迁移槽位,使用 ASKING/MOVED 重定向机制处理迁移期间的数据访问
  5. Hash Tag:使用 {} 包裹部分 key(如 user:{1001}:profile)强制相关数据落到同一节点,支持多 key 操作

  6. 故障检测与转移

  7. Gossip 协议:节点间定期交换 PING/PONG 消息,维护集群拓扑状态
  8. 故障检测:节点超时未响应标记为 PFAIL(可能故障),半数以上主节点确认后升级为 FAIL(确认故障)
  9. 从节点选举:故障主节点的从节点发起选举,通过 epoch 机制保证选举唯一性,复制偏移量最大的从节点优先
  10. 故障转移:选举出的新主节点广播新配置,其他节点更新槽位映射

  11. 脑裂防护

  12. min-slaves-to-write:主节点至少需要 N 个从节点连接才接受写操作
  13. min-slaves-max-lag:从节点复制延迟超过指定秒数时拒绝写操作
  14. 防止网络分区时旧主节点继续写入导致数据不一致

  15. Agent 场景应用

  16. 多租户 Agent 平台:按租户 ID 使用 Hash Tag 将同一租户的会话数据路由到同一节点,避免跨节点事务
  17. 会话数据优化:热点 Agent 的会话数据可通过槽位迁移均衡负载
  18. 高可用保障:关键 Agent 状态数据配置主从热备,故障转移时间控制在秒级

3️⃣ Key Differences

查看内嵌表格


3、进阶题:Redis 主从复制的全量同步和增量同步机制是怎样的?

难度级别:⭐⭐⭐(runid、offset、repl_backlog、PSYNC、全量RDB传输、增量命令传播)

1️⃣ Common Answer

Redis 主从复制就是从节点连接主节点,主节点把数据发给从节点。全量同步就是主节点生成 RDB 文件发给从节点,增量同步就是主节点把写命令发给从节点。如果从节点断开重连,会根据 offset 继续同步。

2️⃣ Impressive Answer

Redis 主从复制通过PSYNC 命令实现,核心机制包括 runid、offset、repl_backlog 三个关键要素:

  1. 全量同步(首次连接或复制偏移量过大)
  2. 触发条件:从节点首次连接主节点,或复制偏移量差距超过 repl_backlog 大小
  3. 同步流程
    1. 从节点发送 PSYNC ? -1 请求全量同步
    2. 主节点执行 BGSAVE 生成 RDB 文件(fork 子进程)
    3. 主节点将 RDB 文件发送给从节点
    4. RDB 传输期间,主节点将新写命令缓存到 replication buffer
    5. RDB 传输完成后,主节点将缓存命令发送给从节点
    6. 从节点加载 RDB 并执行缓存命令,完成数据同步
  4. 缺点:fork 子进程导致主节点内存翻倍,RDB 传输占用网络带宽

  5. 增量同步(短时间断开重连)

  6. 触发条件:从节点断开后短时间内重连,且复制偏移量在 repl_backlog 范围内
  7. 核心机制
    • runid:主节点唯一标识,从节点记录上次同步的主节点 runid
    • offset:复制偏移量,记录从节点同步到主节点的哪个位置
    • repl_backlog:主节点的复制积压缓冲区(默认 1MB),环形队列存储最近写命令
  8. 同步流程

    1. 从节点发送 PSYNC <runid> <offset> 请求增量同步
    2. 主节点检查 runid 是否匹配(不匹配则触发全量同步)
    3. 主节点检查 offset 是否在 repl_backlog 范围内
    4. 如果在范围内,从 repl_backlog 获取 offset 之后的所有命令发送给从节点
    5. 从节点执行命令完成增量同步
  9. 命令传播(持续同步)

  10. 全量/增量同步完成后,主节点持续将写命令发送给从节点
  11. 从节点执行命令并更新 offset
  12. 使用心跳机制(PING/PONG)检测连接状态

  13. Agent 场景应用

  14. 会话数据同步:主节点处理 Agent 会话写操作,从节点处理读操作,实现读写分离
  15. repl_backlog 优化:根据 Agent 写入频率调整 repl_backlog 大小,避免频繁全量同步
  16. 故障恢复:从节点重启后优先尝试增量同步,减少恢复时间

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


Redis 分布式锁

1、基础题:Redis 分布式锁怎么实现?

难度级别:⭐⭐(SETNX + EXPIRE、原子性、超时释放)

Redis 分布式锁的基本实现是使用 SETNX 命令加锁,同时设置过期时间防止死锁。现代 Redis 使用 SET key value NX EX seconds 原子命令保证加锁和设置过期时间的原子性。释放锁时需要先判断锁是否属于自己,避免误删其他客户端的锁,通常使用 Lua 脚本保证"检查+删除"的原子性。


2、进阶题:Redisson 看门狗机制和 RedLock 算法的原理与争议?

难度级别:⭐⭐⭐⭐(看门狗续期、RedLock多节点、Martin Kleppmann争议、Lua脚本原子性)

1️⃣ Common Answer

Redis 分布式锁用 SETNX 加锁,设置过期时间。Redisson 实现了看门狗机制,自动续期锁的过期时间,防止业务执行时间过长导致锁过期。RedLock 是在多个 Redis 节点上同时加锁,提高可用性。释放锁时用 Lua 脚本保证原子性。

2️⃣ Impressive Answer

Redis 分布式锁的核心是原子性保证容错机制,Redisson 看门狗和 RedLock 是两个重要演进:

  1. 基础实现与陷阱
  2. 原子加锁SET lock_key unique_value NX EX 30unique_value 通常是 UUID + 线程 ID,用于识别锁持有者
  3. 非原子释放风险:先 GETDEL 存在竞态条件,可能误删其他客户端的锁
  4. Lua 脚本释放if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end 保证"检查+删除"原子性
  5. 超时困境:过期时间设置过短导致业务未完成锁过期,设置过长导致故障后无法快速恢复

  6. Redisson 看门狗机制

  7. 租约模式:默认 30 秒锁租约,看门狗线程每 10 秒检测并续期
  8. 自动续期:业务执行期间自动续期,业务完成或客户端宕机后锁自动释放
  9. 续期条件:只有锁持有者才能续期,基于 unique_value 校验
  10. 适用场景:执行时间不确定但可控的任务,如 Agent 工具调用、知识库索引构建

  11. RedLock 算法与争议

  12. 算法原理:在 N 个独立 Redis 实例上同时加锁,获取到 N/2+1 个锁才算成功,释放时向所有节点解锁
  13. 时钟漂移问题:不同 Redis 节点时钟不同步可能导致锁过期时间计算偏差
  14. Martin Kleppmann 批评
    • RedLock 在网络分区时可能违反安全性,多个客户端同时持有锁
    • 提出使用 fencing token(递增令牌)保证操作顺序,依赖存储系统检查令牌
  15. Antirez 回应:RedLock 的目标是"尽力而为"的可用性,适用于性能要求高于强一致性的场景
  16. 实际选择:强一致性场景选 Zookeeper/Etcd,高性能场景选 Redis 单节点锁,极端高可用场景选 RedLock

  17. Agent 场景应用

  18. 工具调用幂等控制:Agent 调用外部 API 时使用分布式锁保证同一请求不重复执行
  19. 并发任务互斥:多 Agent 实例处理同一任务时加锁,避免重复消费消息队列
  20. 知识库索引构建锁:向量数据库索引构建期间加锁,防止并发写入导致索引损坏
  21. 锁粒度设计:租户级锁 vs 全局锁,根据业务隔离需求选择合适粒度

3️⃣ Key Differences

查看内嵌表格


3、进阶题:Redisson 可重入锁的实现原理?锁粒度如何设计?

难度级别:⭐⭐⭐(Hash结构+计数器、Lua脚本、粗粒度vs细粒度、锁竞争优化)

1️⃣ Common Answer

Redisson 的可重入锁用 Hash 结构存储,key 是锁名,field 是线程 ID,value 是计数器。加锁时计数器加 1,解锁时减 1,减到 0 才删除锁。锁粒度就是锁的范围,粗粒度锁锁的范围大,细粒度锁锁的范围小。根据业务需求选择合适的粒度。

2️⃣ Impressive Answer

Redisson 可重入锁的核心是Hash 结构 + 计数器 + Lua 脚本,锁粒度设计需要平衡并发控制和性能:

  1. 可重入锁实现原理
  2. 数据结构:使用 Redis Hash 存储锁信息\HSET lock:resource ``
    • lock:resource:锁名称
    • <threadId>:线程唯一标识(UUID + 线程 ID)
    • <count>:重入次数计数器
  3. 加锁流程(Lua 脚本保证原子性)\luaif redis.call('exists', KEYS[1]) ==0 or redis.call('hexists', KEYS[1], ARGV[2])== 1 thenredis.call('hincrby', KEYS[1], ARGV[2], 1)redis.call('pexpire', KEYS[1], ARGV[1])return 1elsereturn 0end``
    • 如果锁不存在或当前线程已持有锁,计数器 +1 并续期
    • 否则加锁失败
  4. 解锁流程(Lua 脚本保证原子性)\luaif redis.call('hexists', KEYS[1], ARGV[2]) == 1 thenlocal count = redis.call('hincrby', KEYS[1], ARGV[2], -1)if count == 0 thenredis.call('del', KEYS[1])endreturn 1elsereturn 0end``

    • 计数器 -1,减到 0 时删除锁
  5. 锁粒度设计原则

  6. 粗粒度锁:锁的范围大,如用户级锁、订单级锁
    • 优点:实现简单,锁竞争少
    • 缺点:并发度低,容易阻塞
    • 适用场景:低并发、业务逻辑简单的场景
  7. 细粒度锁:锁的范围小,如订单项锁、库存 SKU 锁
    • 优点:并发度高,性能好
    • 缺点:实现复杂,容易死锁,锁竞争多
    • 适用场景:高并发、需要精细控制的场景
  8. 锁粒度选择矩阵: | 维度 | 粗粒度锁 | 细粒度锁 || --- | --- | --- || 并发度 | 低 | 高 || 实现复杂度 | 简单 | 复杂 || 死锁风险 | 低 | 高 || 适用场景 | 低并发、简单业务 | 高并发、复杂业务 |

  9. Agent 场景锁粒度设计

  10. 用户会话锁(粗粒度)lock:session:<userId>,同一用户的会话操作串行化
    • 原因:用户操作频率低,粗粒度锁足够
  11. 知识库文档锁(中粒度)lock:doc:<docId>,同一文档的索引/更新操作互斥
    • 原因:文档操作中等频率,需要避免并发冲突
  12. 工具调用锁(细粒度)lock:tool:<toolName>:<userId>,同一用户调用同一工具时互斥
    • 原因:工具调用高频,细粒度锁提升并发度
  13. 库存扣减锁(超细粒度)lock:stock:<skuId>,SKU 级别锁,避免超卖
    • 原因:电商场景高并发,必须精细控制

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


X.X Redis 基础数据结构底层实现

1、基础题:Redis 的 5 种基础数据类型底层编码分别是什么?

难度:⭐⭐(String/List/Hash/Set/ZSet的底层编码,ziplist/listpack/skiplist/hashtable/intset) 直接给答案:

  • String:int(整数)、embstr(≤44字节)、raw(>44字节)

  • List:listpack(元素少且小)、quicklist(元素多,双向链表+listpack节点)

  • Hash:listpack(元素≤128且值≤64字节)、hashtable(超出阈值)

  • Set:intset(全整数且≤512)、listpack(元素≤128且值≤64字节)、hashtable(超出阈值)

  • ZSet:listpack(元素≤128且值≤64字节)、skiplist+hashtable(超出阈值)

2、进阶题:ZSet 为什么同时用 ziplist/listpack 和 skiplist?跳表的查询复杂度如何保证?

难度:⭐⭐⭐⭐(ziplist省内存、skiplist支持范围查询、hashtable O(1)查分、跳表层级概率)

1️⃣ Common Answer ZSet用跳表是因为跳表支持范围查询,比红黑树实现简单

2️⃣ Impressive Answer

  1. ZSet双结构设计原因:
  2. skiplist:支持按score范围查询(ZRANGEBYSCORE),O(logN)复杂度
  3. hashtable:支持按member查score(ZSCORE),O(1)复杂度
  4. 两者共享member对象(指针引用),内存开销可控

  5. 小数据量用listpack(ziplist):元素少时,listpack连续内存布局,CPU缓存友好,内存占用远小于skiplist

  6. 跳表复杂度保证:

  7. 层级随机生成:每个节点以50%概率晋升到上一层,期望层数O(logN)
  8. 查询路径:从最高层开始,逐层向右向下,期望比较次数O(logN)
  9. 最大层数:Redis默认32层,支持2^32个元素

  10. 跳表 vs 红黑树:跳表实现简单、支持范围查询(红黑树需要中序遍历)、并发友好(锁粒度更细)

  11. Agent场景:模型调用排行榜用ZSet,ZADD O(logN)更新分数,ZREVRANGE O(logN+M)获取Top N

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 排行榜(模型调用次数 Top N)如何用 ZSet 高效实现?

难度:⭐⭐⭐(ZADD/ZINCRBY/ZREVRANGE、分页、实时更新)

1️⃣ Common Answer 用ZADD存模型和调用次数,ZREVRANGE取前N个

2️⃣ Impressive Answer

  1. 数据结构设计:
  2. Key:model:call:rank:daily:{date}(按天分榜)
  3. Member:model_id
  4. Score:调用次数

  5. 核心操作:

  6. 调用时:ZINCRBY model:call:rank:daily:20240101 1 gpt-4(原子自增)
  7. 查Top N:ZREVRANGEBYSCORE model:call:rank:daily:20240101 +inf -inf WITHSCORES LIMIT 0 10
  8. 查某模型排名:ZREVRANK model:call:rank:daily:20240101 gpt-4

  9. 性能优化:

  10. 写入:批量调用用Pipeline,减少网络RTT
  11. 过期:设置TTL自动清理历史榜单(EXPIRE key 86400)
  12. 分页:ZREVRANGE key start stop WITHSCORES,游标分页

  13. 多维度排行:按天/周/月分别维护ZSet,定时任务汇总周榜月榜

  14. 防刷:结合Lua脚本实现调用频率限制,防止单个用户刷高排名

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


X.X Redis 过期策略与内存管理

1、基础题:Redis 的 key 过期删除策略有哪些?

难度:⭐⭐(惰性删除、定期删除、主动扫描) 直接给答案:

  • 惰性删除:访问key时检查是否过期,过期则删除。CPU友好但内存不友好(过期key不访问就不删)

  • 定期删除:每隔100ms随机抽取部分设置了过期时间的key检查删除。平衡CPU和内存

  • Redis同时使用两种策略:定期删除兜底,惰性删除精确清理

2、进阶题:大量 key 同时过期会有什么问题?如何避免过期扫描阻塞主线程?

难度:⭐⭐⭐(过期扫描时间限制、随机过期时间、lazy-free异步删除)

1️⃣ Common Answer 大量key同时过期会导致Redis变慢,可以给过期时间加随机数

2️⃣ Impressive Answer

  1. 问题分析:
  2. 定期删除每次最多扫描25%的过期key,如果过期key比例超过25%,会持续扫描直到比例降低
  3. 大量key同时过期时,定期删除占用大量CPU时间,导致其他命令延迟升高
  4. 大key过期时,同步删除耗时长(如删除一个有10万元素的Hash),阻塞主线程

  5. 解决方案:

  6. 随机过期时间:TTL = base_ttl + random(0, jitter),分散过期时间,避免集中过期
  7. lazy-free异步删除:配置lazyfree-lazy-expire yes,过期key异步删除,不阻塞主线程
  8. 大key拆分:将大key拆分为多个小key,单个key删除耗时可控
  9. 主动扫描限速:hz参数控制定期删除频率(默认10次/秒),避免过度占用CPU

  10. 监控:通过INFO stats的expired_keys指标监控过期删除速率,异常时告警

  11. Agent场景:Agent会话TTL统一设置为1小时,但加±5分钟随机抖动,避免整点大量会话同时过期

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 会话 TTL 管理中,如何设计过期策略避免内存突增?

难度:⭐⭐⭐(TTL设计、内存预算、过期通知)

1️⃣ Common Answer 给会话设置TTL,过期自动删除就行了

2️⃣ Impressive Answer

  1. TTL分层设计:
  2. 活跃会话:TTL=2小时,每次交互时EXPIRE续期(滑动过期)
  3. 非活跃会话:TTL=24小时,超过24小时无交互自动清理
  4. 历史会话摘要:TTL=7天,保留最近会话的摘要信息

  5. 内存预算控制:

  6. 预估单会话内存:消息列表(平均10条×500字节)+ 元数据 ≈ 10KB
  7. 设置maxmemory和maxmemory-policy(allkeys-lru),超出内存上限时淘汰最久未使用的会话

  8. 过期通知:开启notify-keyspace-events Ex,监听会话过期事件,触发会话数据归档到MySQL

  9. 随机抖动:TTL = 7200 + random(-300, 300),避免大量会话同时过期

  10. 内存监控:定期执行MEMORY USAGE key检查大会话,超过阈值告警并触发压缩

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


X.X Redis 6.0 多线程模型

1、基础题:Redis 6.0 引入多线程解决了什么问题?哪些操作是多线程的?

难度:⭐⭐(网络IO瓶颈、多线程IO、命令执行仍单线程) 直接给答案:

  • 解决的问题:Redis单线程在高并发下,网络IO(读取请求、发送响应)成为瓶颈,CPU利用率低

  • 多线程的操作:网络数据读取(read)、请求解析、响应数据写入(write)

  • 仍是单线程的:命令执行(保证原子性和数据一致性)

2、进阶题:Redis 多线程 IO 模型和单线程命令执行如何协作?为什么命令执行仍是单线程?

难度:⭐⭐⭐⭐(IO线程池、主线程协调、无锁设计、原子性保证)

1️⃣ Common Answer Redis 6.0用多线程处理网络IO,命令执行还是单线程,这样既快又安全

2️⃣ Impressive Answer

  1. 多线程IO协作流程:
  2. 主线程accept新连接,分配给IO线程
  3. IO线程并发读取socket数据,解析请求
  4. 主线程统一执行所有命令(单线程,保证原子性)
  5. IO线程并发将响应数据写回socket

  6. 为什么命令执行仍是单线程:

  7. 避免锁竞争:多线程访问共享数据结构需要加锁,锁竞争开销可能超过多线程收益
  8. 原子性保证:单线程天然保证命令原子执行,无需额外同步机制
  9. 简化实现:Redis的数据结构(dict、skiplist等)均非线程安全,改造成本极高
  10. 瓶颈不在CPU:Redis的性能瓶颈主要在网络IO,而非命令执行的CPU计算

  11. 性能提升:多线程IO在高并发小包场景下,吞吐量可提升2-3倍

  12. 配置:io-threads 4(IO线程数,建议=CPU核数/2),io-threads-do-reads yes

  13. Agent场景:Agent平台高并发查询Redis时,多线程IO显著降低网络延迟,提升整体吞吐量

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 高并发场景下,Redis 6.0 多线程能带来多少性能提升?如何评估?

难度:⭐⭐⭐(benchmark测试、吞吐量对比、适用场景判断)

1️⃣ Common Answer 多线程肯定比单线程快,开启就行了

2️⃣ Impressive Answer

  1. 性能提升场景:
  2. 高并发小包(<1KB):多线程IO收益最大,吞吐量提升2-3倍
  3. 大包或复杂命令:命令执行是瓶颈,多线程IO收益有限
  4. 网络带宽受限:多线程IO无法突破带宽上限

  5. 评估方法:

  6. 基准测试:redis-benchmark -t get,set -n 1000000 -c 200 --threads 4
  7. 对比指标:QPS(每秒请求数)、P99延迟、CPU利用率
  8. 监控:INFO stats的instantaneousopspersec、INFO clients的connectedclients

  9. Agent场景评估:

  10. Agent会话查询(GET/HGETALL):高频小包,多线程IO收益显著
  11. 知识库向量检索(大数据量返回):网络IO是瓶颈,多线程IO有帮助
  12. Lua脚本执行(Token扣减):命令执行是瓶颈,多线程IO收益有限

  13. 配置建议:io-threads设置为CPU核数的一半(如8核CPU设置4个IO线程),避免线程切换开销

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


X.X Redis Pipeline 与事务

1、基础题:Redis Pipeline 和普通命令有什么区别?MULTI/EXEC 事务是什么?

难度:⭐⭐(批量发送、RTT优化、MULTI/EXEC原子性) 直接给答案:

  • Pipeline:将多个命令批量打包发送,一次网络往返执行多个命令,减少RTT(网络往返时间)。注意:Pipeline不保证原子性,命令之间可能被其他客户端命令插入

  • MULTI/EXEC事务:MULTI开启事务,命令入队,EXEC统一执行。保证命令顺序执行,执行期间不会被其他命令插入,但不支持回滚

2、进阶题:Redis 事务和 Lua 脚本的区别?为什么 Redis 事务不支持回滚?

难度:⭐⭐⭐⭐(MULTI/EXEC原子性局限、WATCH乐观锁、Lua原子性、错误处理)

1️⃣ Common Answer Lua脚本比事务更强大,支持条件判断,事务不支持回滚是Redis的设计缺陷

2️⃣ Impressive Answer

  1. Redis事务(MULTI/EXEC)特点:
  2. 命令入队阶段:语法错误会导致整个事务取消
  3. 执行阶段:运行时错误(如对String执行LPUSH)不会回滚,其他命令继续执行
  4. WATCH:乐观锁,监听key变化,如果EXEC前key被修改,事务取消(返回nil)

  5. 为什么不支持回滚:

  6. Redis认为运行时错误是编程错误(类型操作错误),不应该在生产环境发生
  7. 支持回滚需要记录undo log,增加复杂度和内存开销,违背Redis简单高效的设计哲学

  8. Lua脚本优势:

  9. 原子性更强:Lua脚本在执行期间不会被其他命令插入(类似单个命令)
  10. 支持条件逻辑:可以在脚本中做if/else判断,实现复杂的原子操作
  11. 减少网络往返:多个操作在服务端一次完成

  12. 选型建议:简单批量操作→Pipeline;需要原子性的复杂逻辑→Lua脚本;简单原子操作→MULTI/EXEC+WATCH

  13. Agent场景:Token扣减(先查余额再扣减)必须用Lua脚本保证原子性,避免并发超扣

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent Token 计费场景,如何用 Lua 脚本保证扣减原子性?

难度:⭐⭐⭐(Lua脚本原子性、余额不足处理、并发安全)

1️⃣ Common Answer 用Lua脚本把查余额和扣减放在一起执行,就是原子的了

2️⃣ Impressive Answer

  1. Lua脚本设计:
-- KEYS[1]: token:balance:{user_id}
-- ARGV[1]: 扣减数量
local balance = tonumber(redis.call('GET', KEYS[1]))
if balance == nil then
    return -1  -- key不存在
end
if balance < tonumber(ARGV[1]) then
    return -2  -- 余额不足
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return redis.call('GET', KEYS[1])  -- 返回扣减后余额
  1. 调用方式:EVALSHA script_sha 1 token:balance:user123 100

  2. 脚本预加载:使用SCRIPT LOAD预加载脚本,获取SHA,后续用EVALSHA调用,避免每次传输脚本

  3. 错误处理:返回-1(key不存在,需初始化)、-2(余额不足,拒绝请求)、正数(扣减后余额)

  4. 幂等性:结合请求ID(request_id)做幂等,同一请求重复调用不重复扣减

  5. 降级方案:Redis不可用时,降级到MySQL事务扣减(SELECT FOR UPDATE),保证数据一致性

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格


X.X Redis 热 Key 与大 Key 问题

1、基础题:什么是热 Key 和大 Key?会带来什么问题?

难度:⭐⭐(定义、问题影响、单点瓶颈) 直接给答案:

  • 热Key:某个key被极高频率访问(如QPS>10万),导致该key所在的Redis节点CPU/网络成为瓶颈,其他key的访问也受影响

  • 大Key:key对应的value体积很大(String>10KB,集合类型元素>10000),导致:读写耗时长、网络传输慢、内存分配/释放时阻塞主线程、集群迁移困难

2、进阶题:热 Key 的发现方式有哪些?大 Key 如何拆分和迁移?

难度:⭐⭐⭐⭐(monitor命令、hotkeys参数、大key扫描、渐进式迁移)

1️⃣ Common Answer 热Key可以用monitor命令发现,大Key可以拆分成多个小Key

2️⃣ Impressive Answer

  1. 热Key发现方式:
  2. redis-cli --hotkeys:基于LFU策略统计访问频率(需要maxmemory-policy设置为LFU类型)
  3. MONITOR命令:实时监控所有命令,统计key访问频率(生产慎用,性能影响大)
  4. 客户端统计:在应用层统计key访问频率,超过阈值上报告警
  5. 代理层统计:Twemproxy/Codis等代理层统计热Key

  6. 热Key解决方案:

  7. 本地缓存:在应用层(JVM堆内存)缓存热Key,减少Redis访问(Caffeine/Guava Cache)
  8. 读写分离:热Key读请求分散到多个从节点
  9. Key分片:将热Key复制为多个副本(model:config:gpt4:0 ~ model:config:gpt4:9),读取时随机选择

  10. 大Key发现:

  11. redis-cli --bigkeys:扫描所有key,统计各类型最大的key
  12. MEMORY USAGE key:查看单个key的内存占用
  13. SCAN + TYPE + STRLEN/LLEN/HLEN:批量扫描统计

  14. 大Key拆分:

  15. Hash大Key:按field范围拆分为多个Hash(user:profile:1:{field_group})
  16. List大Key:按时间或数量分页拆分
  17. 渐进式迁移:新数据写新Key,老数据逐步迁移,双读兼容

  18. Agent场景:模型配置(大JSON)是典型大Key,拆分为基础配置+参数配置+提示词配置三个Hash

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 平台某个热门模型配置成为热 Key,如何做多级缓存分散压力?

难度:⭐⭐⭐(多级缓存架构、本地缓存+Redis、缓存一致性)

1️⃣ Common Answer 在本地加一层缓存,减少Redis访问次数

2️⃣ Impressive Answer

  1. 多级缓存架构:
  2. L1:JVM本地缓存(Caffeine),TTL=30秒,容量=100个模型配置
  3. L2:Redis集群,TTL=5分钟,全量模型配置
  4. L3:MySQL,持久化存储

  5. 读取流程:L1命中→直接返回;L1未命中→查L2→回填L1;L2未命中→查L3→回填L2和L1

  6. 缓存一致性:

  7. 更新时:先更新MySQL,再删除Redis(Cache Aside),本地缓存等TTL自然过期
  8. 强一致场景:更新后主动推送失效通知(Redis Pub/Sub或MQ),各实例清除本地缓存

  9. 热Key分片:Redis层将模型配置复制为10个副本(model:config:gpt4:{0-9}),读取时按请求ID取模路由

  10. 降级保护:Redis不可用时,本地缓存兜底;本地缓存也过期时,直接读MySQL并延长本地缓存TTL

  11. 监控:统计各级缓存命中率,L1命中率<90%时告警,说明热Key访问模式异常

3️⃣ Key Differences

查看内嵌表格


4、容易一起考的题

查看内嵌表格