Redis 核心基础与持久化¶
⚡ 1. Redis 为什么快?单线程模型为什么能支撑高并发?¶
Redis 快,是因为它把所有数据都放在内存里,并且用单线程扫清了并发控制的一切障碍。
一个常见的误解是“单线程 = 慢”。在 Redis 的场景里恰恰相反:单线程让它避免了多线程的锁竞争、上下文切换、以及 CPU 缓存失效的开销。当一个命令执行时间在微秒级时,把这些时间花在调度线程上反而是最大的浪费。

单线程能支撑高并发的三个支柱:
-
纯内存操作:一次 SET 或 GET 只需要几微秒。瓶颈不在 CPU 计算,而在网络 IO。只要单线程能在微秒级处理完一个命令,它就能在 1 秒内处理几十万次请求。
-
IO 多路复用(epoll):Redis 使用 epoll(Linux 下)同时监听成千上万个客户端 socket。哪个 socket 有数据到达,就处理哪个;没有数据时,主线程就阻塞在
epoll_wait上,不浪费任何 CPU。这使得单线程可以轻松管理数万并发连接。 -
避免锁和上下文切换:多线程程序在访问共享数据结构时必须加锁,锁竞争和线程调度会吃掉大量 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 命令。

工作流程:当 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 必须按照某种策略淘汰一些旧数据,为新数据腾出空间。这就产生了八种策略,按“会不会淘汰”和“淘汰谁”两个维度分类。

在 Agent 场景下如何选择?
Agent 系统通常用 Redis 存储三类数据,它们的淘汰策略应该区别对待:
-
对话上下文 / 任务状态:这是 Agent 的“短期记忆”,丢失意味着任务中断。这类数据绝不希望被淘汰。应该使用
noeviction,并在应用层做好过期清理(比如设置过期时间EXPIRE session_id 3600)。当内存不足时,宁可拒绝新任务,也不要把正在进行的任务状态删掉。 -
LLM 调用结果缓存 / Embedding 缓存:这是典型的“热缓存”,用 Redis 就是为了加速。推荐
allkeys-lru或allkeys-lfu。LRU 适合访问模式均匀的场景,LFU 适合有明显冷热之分的场景(比如高频 FAQ 的缓存,不希望被一次性的大流量查询冲掉)。 -
工具调用结果缓存(如网页搜索、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 想象成一座大坝,数据请求就是洪水,而它们分别对应着大坝的 “渗漏”、“管涌”和“溃堤”。

缓存穿透
现象:客户端疯狂查询一个数据库和缓存中都不存在的 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 通过槽位机制实现数据分片和故障转移,核心机制包括:
- 数据分片机制
- 16384 槽位:Cluster 将整个键空间划分为 16384 个 slot,每个主节点负责一部分槽位
- 槽位计算:
CRC16(key) % 16384确定键对应的 slot,保证相同 key 路由到同一节点 - 槽位迁移:支持在线迁移槽位,使用
ASKING/MOVED重定向机制处理迁移期间的数据访问 -
Hash Tag:使用
{}包裹部分 key(如user:{1001}:profile)强制相关数据落到同一节点,支持多 key 操作 -
故障检测与转移
- Gossip 协议:节点间定期交换 PING/PONG 消息,维护集群拓扑状态
- 故障检测:节点超时未响应标记为 PFAIL(可能故障),半数以上主节点确认后升级为 FAIL(确认故障)
- 从节点选举:故障主节点的从节点发起选举,通过 epoch 机制保证选举唯一性,复制偏移量最大的从节点优先
-
故障转移:选举出的新主节点广播新配置,其他节点更新槽位映射
-
脑裂防护
- min-slaves-to-write:主节点至少需要 N 个从节点连接才接受写操作
- min-slaves-max-lag:从节点复制延迟超过指定秒数时拒绝写操作
-
防止网络分区时旧主节点继续写入导致数据不一致
-
Agent 场景应用
- 多租户 Agent 平台:按租户 ID 使用 Hash Tag 将同一租户的会话数据路由到同一节点,避免跨节点事务
- 会话数据优化:热点 Agent 的会话数据可通过槽位迁移均衡负载
- 高可用保障:关键 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 三个关键要素:
- 全量同步(首次连接或复制偏移量过大)
- 触发条件:从节点首次连接主节点,或复制偏移量差距超过 repl_backlog 大小
- 同步流程:
- 从节点发送
PSYNC ? -1请求全量同步 - 主节点执行
BGSAVE生成 RDB 文件(fork 子进程) - 主节点将 RDB 文件发送给从节点
- RDB 传输期间,主节点将新写命令缓存到 replication buffer
- RDB 传输完成后,主节点将缓存命令发送给从节点
- 从节点加载 RDB 并执行缓存命令,完成数据同步
- 从节点发送
-
缺点:fork 子进程导致主节点内存翻倍,RDB 传输占用网络带宽
-
增量同步(短时间断开重连)
- 触发条件:从节点断开后短时间内重连,且复制偏移量在 repl_backlog 范围内
- 核心机制:
- runid:主节点唯一标识,从节点记录上次同步的主节点 runid
- offset:复制偏移量,记录从节点同步到主节点的哪个位置
- repl_backlog:主节点的复制积压缓冲区(默认 1MB),环形队列存储最近写命令
-
同步流程:
- 从节点发送
PSYNC <runid> <offset>请求增量同步 - 主节点检查 runid 是否匹配(不匹配则触发全量同步)
- 主节点检查 offset 是否在 repl_backlog 范围内
- 如果在范围内,从 repl_backlog 获取 offset 之后的所有命令发送给从节点
- 从节点执行命令完成增量同步
- 从节点发送
-
命令传播(持续同步)
- 全量/增量同步完成后,主节点持续将写命令发送给从节点
- 从节点执行命令并更新 offset
-
使用心跳机制(PING/PONG)检测连接状态
-
Agent 场景应用
- 会话数据同步:主节点处理 Agent 会话写操作,从节点处理读操作,实现读写分离
- repl_backlog 优化:根据 Agent 写入频率调整 repl_backlog 大小,避免频繁全量同步
- 故障恢复:从节点重启后优先尝试增量同步,减少恢复时间
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 是两个重要演进:
- 基础实现与陷阱
- 原子加锁:
SET lock_key unique_value NX EX 30,unique_value通常是 UUID + 线程 ID,用于识别锁持有者 - 非原子释放风险:先
GET再DEL存在竞态条件,可能误删其他客户端的锁 - Lua 脚本释放:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end保证"检查+删除"原子性 -
超时困境:过期时间设置过短导致业务未完成锁过期,设置过长导致故障后无法快速恢复
-
Redisson 看门狗机制
- 租约模式:默认 30 秒锁租约,看门狗线程每 10 秒检测并续期
- 自动续期:业务执行期间自动续期,业务完成或客户端宕机后锁自动释放
- 续期条件:只有锁持有者才能续期,基于
unique_value校验 -
适用场景:执行时间不确定但可控的任务,如 Agent 工具调用、知识库索引构建
-
RedLock 算法与争议
- 算法原理:在 N 个独立 Redis 实例上同时加锁,获取到 N/2+1 个锁才算成功,释放时向所有节点解锁
- 时钟漂移问题:不同 Redis 节点时钟不同步可能导致锁过期时间计算偏差
- Martin Kleppmann 批评:
- RedLock 在网络分区时可能违反安全性,多个客户端同时持有锁
- 提出使用 fencing token(递增令牌)保证操作顺序,依赖存储系统检查令牌
- Antirez 回应:RedLock 的目标是"尽力而为"的可用性,适用于性能要求高于强一致性的场景
-
实际选择:强一致性场景选 Zookeeper/Etcd,高性能场景选 Redis 单节点锁,极端高可用场景选 RedLock
-
Agent 场景应用
- 工具调用幂等控制:Agent 调用外部 API 时使用分布式锁保证同一请求不重复执行
- 并发任务互斥:多 Agent 实例处理同一任务时加锁,避免重复消费消息队列
- 知识库索引构建锁:向量数据库索引构建期间加锁,防止并发写入导致索引损坏
- 锁粒度设计:租户级锁 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 脚本,锁粒度设计需要平衡并发控制和性能:
- 可重入锁实现原理
- 数据结构:使用 Redis Hash 存储锁信息
\HSET lock:resource`` lock:resource:锁名称<threadId>:线程唯一标识(UUID + 线程 ID)<count>:重入次数计数器
- 加锁流程(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 并续期
- 否则加锁失败
-
解锁流程(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 时删除锁
-
锁粒度设计原则
- 粗粒度锁:锁的范围大,如用户级锁、订单级锁
- 优点:实现简单,锁竞争少
- 缺点:并发度低,容易阻塞
- 适用场景:低并发、业务逻辑简单的场景
- 细粒度锁:锁的范围小,如订单项锁、库存 SKU 锁
- 优点:并发度高,性能好
- 缺点:实现复杂,容易死锁,锁竞争多
- 适用场景:高并发、需要精细控制的场景
-
锁粒度选择矩阵: | 维度 | 粗粒度锁 | 细粒度锁 || --- | --- | --- || 并发度 | 低 | 高 || 实现复杂度 | 简单 | 复杂 || 死锁风险 | 低 | 高 || 适用场景 | 低并发、简单业务 | 高并发、复杂业务 |
-
Agent 场景锁粒度设计
- 用户会话锁(粗粒度):
lock:session:<userId>,同一用户的会话操作串行化- 原因:用户操作频率低,粗粒度锁足够
- 知识库文档锁(中粒度):
lock:doc:<docId>,同一文档的索引/更新操作互斥- 原因:文档操作中等频率,需要避免并发冲突
- 工具调用锁(细粒度):
lock:tool:<toolName>:<userId>,同一用户调用同一工具时互斥- 原因:工具调用高频,细粒度锁提升并发度
- 库存扣减锁(超细粒度):
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
- ZSet双结构设计原因:
- skiplist:支持按score范围查询(ZRANGEBYSCORE),O(logN)复杂度
- hashtable:支持按member查score(ZSCORE),O(1)复杂度
-
两者共享member对象(指针引用),内存开销可控
-
小数据量用listpack(ziplist):元素少时,listpack连续内存布局,CPU缓存友好,内存占用远小于skiplist
-
跳表复杂度保证:
- 层级随机生成:每个节点以50%概率晋升到上一层,期望层数O(logN)
- 查询路径:从最高层开始,逐层向右向下,期望比较次数O(logN)
-
最大层数:Redis默认32层,支持2^32个元素
-
跳表 vs 红黑树:跳表实现简单、支持范围查询(红黑树需要中序遍历)、并发友好(锁粒度更细)
-
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
- 数据结构设计:
- Key:model:call:rank:daily:{date}(按天分榜)
- Member:model_id
-
Score:调用次数
-
核心操作:
- 调用时:ZINCRBY model:call:rank:daily:20240101 1 gpt-4(原子自增)
- 查Top N:ZREVRANGEBYSCORE model:call:rank:daily:20240101 +inf -inf WITHSCORES LIMIT 0 10
-
查某模型排名:ZREVRANK model:call:rank:daily:20240101 gpt-4
-
性能优化:
- 写入:批量调用用Pipeline,减少网络RTT
- 过期:设置TTL自动清理历史榜单(EXPIRE key 86400)
-
分页:ZREVRANGE key start stop WITHSCORES,游标分页
-
多维度排行:按天/周/月分别维护ZSet,定时任务汇总周榜月榜
-
防刷:结合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
- 问题分析:
- 定期删除每次最多扫描25%的过期key,如果过期key比例超过25%,会持续扫描直到比例降低
- 大量key同时过期时,定期删除占用大量CPU时间,导致其他命令延迟升高
-
大key过期时,同步删除耗时长(如删除一个有10万元素的Hash),阻塞主线程
-
解决方案:
- 随机过期时间:TTL = base_ttl + random(0, jitter),分散过期时间,避免集中过期
- lazy-free异步删除:配置lazyfree-lazy-expire yes,过期key异步删除,不阻塞主线程
- 大key拆分:将大key拆分为多个小key,单个key删除耗时可控
-
主动扫描限速:hz参数控制定期删除频率(默认10次/秒),避免过度占用CPU
-
监控:通过INFO stats的expired_keys指标监控过期删除速率,异常时告警
-
Agent场景:Agent会话TTL统一设置为1小时,但加±5分钟随机抖动,避免整点大量会话同时过期
3️⃣ Key Differences
3、场景题:Agent 会话 TTL 管理中,如何设计过期策略避免内存突增?¶
难度:⭐⭐⭐(TTL设计、内存预算、过期通知)
1️⃣ Common Answer 给会话设置TTL,过期自动删除就行了
2️⃣ Impressive Answer
- TTL分层设计:
- 活跃会话:TTL=2小时,每次交互时EXPIRE续期(滑动过期)
- 非活跃会话:TTL=24小时,超过24小时无交互自动清理
-
历史会话摘要:TTL=7天,保留最近会话的摘要信息
-
内存预算控制:
- 预估单会话内存:消息列表(平均10条×500字节)+ 元数据 ≈ 10KB
-
设置maxmemory和maxmemory-policy(allkeys-lru),超出内存上限时淘汰最久未使用的会话
-
过期通知:开启notify-keyspace-events Ex,监听会话过期事件,触发会话数据归档到MySQL
-
随机抖动:TTL = 7200 + random(-300, 300),避免大量会话同时过期
-
内存监控:定期执行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
- 多线程IO协作流程:
- 主线程accept新连接,分配给IO线程
- IO线程并发读取socket数据,解析请求
- 主线程统一执行所有命令(单线程,保证原子性)
-
IO线程并发将响应数据写回socket
-
为什么命令执行仍是单线程:
- 避免锁竞争:多线程访问共享数据结构需要加锁,锁竞争开销可能超过多线程收益
- 原子性保证:单线程天然保证命令原子执行,无需额外同步机制
- 简化实现:Redis的数据结构(dict、skiplist等)均非线程安全,改造成本极高
-
瓶颈不在CPU:Redis的性能瓶颈主要在网络IO,而非命令执行的CPU计算
-
性能提升:多线程IO在高并发小包场景下,吞吐量可提升2-3倍
-
配置:io-threads 4(IO线程数,建议=CPU核数/2),io-threads-do-reads yes
-
Agent场景:Agent平台高并发查询Redis时,多线程IO显著降低网络延迟,提升整体吞吐量
3️⃣ Key Differences
3、场景题:Agent 高并发场景下,Redis 6.0 多线程能带来多少性能提升?如何评估?¶
难度:⭐⭐⭐(benchmark测试、吞吐量对比、适用场景判断)
1️⃣ Common Answer 多线程肯定比单线程快,开启就行了
2️⃣ Impressive Answer
- 性能提升场景:
- 高并发小包(<1KB):多线程IO收益最大,吞吐量提升2-3倍
- 大包或复杂命令:命令执行是瓶颈,多线程IO收益有限
-
网络带宽受限:多线程IO无法突破带宽上限
-
评估方法:
- 基准测试:redis-benchmark -t get,set -n 1000000 -c 200 --threads 4
- 对比指标:QPS(每秒请求数)、P99延迟、CPU利用率
-
监控:INFO stats的instantaneousopspersec、INFO clients的connectedclients
-
Agent场景评估:
- Agent会话查询(GET/HGETALL):高频小包,多线程IO收益显著
- 知识库向量检索(大数据量返回):网络IO是瓶颈,多线程IO有帮助
-
Lua脚本执行(Token扣减):命令执行是瓶颈,多线程IO收益有限
-
配置建议: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
- Redis事务(MULTI/EXEC)特点:
- 命令入队阶段:语法错误会导致整个事务取消
- 执行阶段:运行时错误(如对String执行LPUSH)不会回滚,其他命令继续执行
-
WATCH:乐观锁,监听key变化,如果EXEC前key被修改,事务取消(返回nil)
-
为什么不支持回滚:
- Redis认为运行时错误是编程错误(类型操作错误),不应该在生产环境发生
-
支持回滚需要记录undo log,增加复杂度和内存开销,违背Redis简单高效的设计哲学
-
Lua脚本优势:
- 原子性更强:Lua脚本在执行期间不会被其他命令插入(类似单个命令)
- 支持条件逻辑:可以在脚本中做if/else判断,实现复杂的原子操作
-
减少网络往返:多个操作在服务端一次完成
-
选型建议:简单批量操作→Pipeline;需要原子性的复杂逻辑→Lua脚本;简单原子操作→MULTI/EXEC+WATCH
-
Agent场景:Token扣减(先查余额再扣减)必须用Lua脚本保证原子性,避免并发超扣
3️⃣ Key Differences
3、场景题:Agent Token 计费场景,如何用 Lua 脚本保证扣减原子性?¶
难度:⭐⭐⭐(Lua脚本原子性、余额不足处理、并发安全)
1️⃣ Common Answer 用Lua脚本把查余额和扣减放在一起执行,就是原子的了
2️⃣ Impressive Answer
- 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]) -- 返回扣减后余额
-
调用方式:EVALSHA script_sha 1 token:balance:user123 100
-
脚本预加载:使用SCRIPT LOAD预加载脚本,获取SHA,后续用EVALSHA调用,避免每次传输脚本
-
错误处理:返回-1(key不存在,需初始化)、-2(余额不足,拒绝请求)、正数(扣减后余额)
-
幂等性:结合请求ID(request_id)做幂等,同一请求重复调用不重复扣减
-
降级方案: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
- 热Key发现方式:
- redis-cli --hotkeys:基于LFU策略统计访问频率(需要maxmemory-policy设置为LFU类型)
- MONITOR命令:实时监控所有命令,统计key访问频率(生产慎用,性能影响大)
- 客户端统计:在应用层统计key访问频率,超过阈值上报告警
-
代理层统计:Twemproxy/Codis等代理层统计热Key
-
热Key解决方案:
- 本地缓存:在应用层(JVM堆内存)缓存热Key,减少Redis访问(Caffeine/Guava Cache)
- 读写分离:热Key读请求分散到多个从节点
-
Key分片:将热Key复制为多个副本(model:config:gpt4:0 ~ model:config:gpt4:9),读取时随机选择
-
大Key发现:
- redis-cli --bigkeys:扫描所有key,统计各类型最大的key
- MEMORY USAGE key:查看单个key的内存占用
-
SCAN + TYPE + STRLEN/LLEN/HLEN:批量扫描统计
-
大Key拆分:
- Hash大Key:按field范围拆分为多个Hash(user:profile:1:{field_group})
- List大Key:按时间或数量分页拆分
-
渐进式迁移:新数据写新Key,老数据逐步迁移,双读兼容
-
Agent场景:模型配置(大JSON)是典型大Key,拆分为基础配置+参数配置+提示词配置三个Hash
3️⃣ Key Differences
3、场景题:Agent 平台某个热门模型配置成为热 Key,如何做多级缓存分散压力?¶
难度:⭐⭐⭐(多级缓存架构、本地缓存+Redis、缓存一致性)
1️⃣ Common Answer 在本地加一层缓存,减少Redis访问次数
2️⃣ Impressive Answer
- 多级缓存架构:
- L1:JVM本地缓存(Caffeine),TTL=30秒,容量=100个模型配置
- L2:Redis集群,TTL=5分钟,全量模型配置
-
L3:MySQL,持久化存储
-
读取流程:L1命中→直接返回;L1未命中→查L2→回填L1;L2未命中→查L3→回填L2和L1
-
缓存一致性:
- 更新时:先更新MySQL,再删除Redis(Cache Aside),本地缓存等TTL自然过期
-
强一致场景:更新后主动推送失效通知(Redis Pub/Sub或MQ),各实例清除本地缓存
-
热Key分片:Redis层将模型配置复制为10个副本(model:config:gpt4:{0-9}),读取时按请求ID取模路由
-
降级保护:Redis不可用时,本地缓存兜底;本地缓存也过期时,直接读MySQL并延长本地缓存TTL
-
监控:统计各级缓存命中率,L1命中率<90%时告警,说明热Key访问模式异常
3️⃣ Key Differences