📌 专题 1:Redis Key 的命名规范与分层设计
❓ 核心问题
Redis 的 Key 该怎么命名?"分层设计"具体是指什么?
💡 要点解答
- 一句话结论 :标准的命名规范是
业务线:模块:实体:ID,通过冒号:来实现可视化客户端中的"文件夹"树状分层效果。 - 条理解析 :
- 规范格式 :例如
toutiao:user:info:1001,避免使用拼音缩写和特殊字符。 - 分层设计的好处:不仅能完美防止不同业务间的 Key 冲突(如电商订单和外卖订单),绝大多数可视化工具还会自动识别冒号并折叠显示为层级目录,极大地提升了管理效率。
- 规范格式 :例如
📌 专题 2:Redis 缓存过期清理机制与内存淘汰
❓ 核心问题
"惰性删除"和"定期删除"结合使用,依然会有过期的 Key 清理不掉吗?底层 LRU/LFU 是什么意思?代码里怎么体现的?
💡 要点解答
- 一句话结论:惰性删除与定期删除结合,依然会有漏网之鱼;当内存真正被打满时,会触发底层的"内存淘汰机制"(如 LRU、LFU)作为最终兜底方案来强制腾出空间。
- 条理解析 :
- 惰性删除(Lazy Deletion):你来查这个数据的时候,我才顺便看看它过期没。过期了就删,没过期就返回给你。优点是不浪费 CPU 去主动巡逻,缺点是如果一个过期数据永远没人查,它就永远霸占内存。
- 定期删除(Periodic Deletion):Redis 后台有个定时任务(默认一秒 10 次),随机抽查一部分设置了过期时间的 Key,过期的就删掉。这是为了弥补惰性删除造成的内存浪费。
- 内存淘汰机制(兜底杀招) :即使有上面两种机制,极端情况下 Redis 内存还是满了,这时候必须强行赶人(淘汰):
- LRU (Least Recently Used - 最近最少使用) :看时间。谁最久没有被访问过,谁就被淘汰。就像清理衣柜,一年没穿过的衣服先扔。
- LFU (Least Frequently Used - 最不经常使用) :看频率。谁被访问的次数最少,谁就被淘汰。就像你有一件衣服虽然昨天刚穿过一次,但另一件衣服本周穿了五次,LFU 会把只穿过一次的衣服扔掉。
- 通用性 :这种 LRU/LFU 思想是计算机科学通用的缓存淘汰算法。比如在 iOS 开发中常用的图片缓存库
SDWebImage,清理内存或磁盘缓存时,底层默认采用的也是同样的逻辑。
📌 专题 3:缓存穿透的本质与解决方案
❓ 核心问题
"缓存穿透"是不是指 Redis 未命中?如何通过缓存空值或布隆过滤器解决?Python 代码具体该怎么写?
💡 要点解答
- 一句话结论 :普通的未命中是正常的,只有当请求的数据在 Redis 和 MySQL 中都绝对不存在时,导致每次查询都直击并暴击 MySQL,才叫"缓存穿透"。
- 条理解析 :
- 方案一:缓存空值 :如果 MySQL 查不到,也强行把
null存入 Redis 并设置一个非常短的 TTL。实现简单,但遭遇海量不同假 ID 攻击时会浪费内存。 - 方案二:布隆过滤器 (Bloom Filter) :
- 原理:用极小内存的 BitMap(0和1)记录所有真实存在的 ID。请求来时先看"点名册",不在册的直接拦截,绝不放行去查库。它说没有一定没有,它说有大概率有(存在极小误判率)。
- 方案一:缓存空值 :如果 MySQL 查不到,也强行把
🔍 详细代码示例:布隆过滤器的两种实现方式
方式 1:纯 Python 本地内存实现 (使用 pybloom_live 库) 完全脱离 Redis,在 Python 服务端自己的内存中维护。
python
# 需要先安装依赖: pip install pybloom_live
from pybloom_live import BloomFilter
# 1. 初始化布隆过滤器(预估10万数据,允许千分之一的误判率)
bf = BloomFilter(capacity=100000, error_rate=0.001)
# 2. 系统启动时,将 MySQL 里的真实 ID 加载进来
bf.add("1001")
bf.add("1002")
# 3. 拦截逻辑:当用户请求查询某个 ID 时
request_id = "9999"
if request_id in bf:
print("布隆过滤器判断:可能存在。放行,去查 Redis 和 MySQL")
else:
print("布隆过滤器判断:绝对不存在!直接拦截请求,保护 MySQL!")
方式 2:使用 Redis 官方插件 (RedisBloom) 在分布式集群中更常用,底层计算由 Redis 完成。
python
import redis
# 连接到开启了 RedisBloom 插件的 Redis 服务
r = redis.Redis(host='localhost', port=6379)
# 1. 往布隆过滤器(假设 key 为 'my_bloom')添加一个已存在的真实 ID:1001
# 对应 Redis 原生命令: BF.ADD my_bloom 1001
r.execute_command('BF.ADD', 'my_bloom', '1001')
# 2. 验证请求带来的 ID 是否存在
# 对应 Redis 原生命令: BF.EXISTS my_bloom 9999
result = r.execute_command('BF.EXISTS', 'my_bloom', '9999')
if result == 0:
print("拦截请求,去 MySQL 查也是白查!")
else:
print("放行去查数据库")
📌 专题 4:缓存击穿 vs 缓存雪崩
❓ 核心问题
什么是缓存击穿和雪崩?互斥锁和逻辑过期哪个性能更好?
💡 要点解答
- 一句话结论:击穿是"单点漏洞"(一个热门 Key 突然失效),雪崩是"全面崩溃"(大批 Key 同时失效)。单论性能,逻辑过期碾压互斥锁。
- 条理解析 :
- 缓存击穿 (单个热点 Key 瞬间失效) :比如双十一秒杀的 iPhone,缓存刚到期,一万个人同时冲向 MySQL 查这一台手机。
- 互斥锁方案:保证强一致性。1万人并发,只放1个人去查 MySQL 重建缓存,其余 9999 人排队干等。牺牲性能保准确(适用于金额、库存)。
- 逻辑过期方案:追求极致性能。不设物理过期时间,发现逻辑超时后,立刻返回旧数据给前端(前端0阻塞),同时后台开独立异步线程去更新数据(适用于热搜榜、点赞数)。
- 缓存雪崩 (海量 Key 同时失效或节点宕机) :
- 解法 :在设置批量缓存时,坚决避免使用固定 TTL,必须加上随机数(如 8小时 + 0~300秒随机)打散过期时间。同时保障 Redis 集群高可用。
- 缓存击穿 (单个热点 Key 瞬间失效) :比如双十一秒杀的 iPhone,缓存刚到期,一万个人同时冲向 MySQL 查这一台手机。
📌 专题 5:缓存一致性、并发错乱与延迟双删
❓ 核心问题
为什么 Cache Aside 模式要求"先更新数据库,再删除缓存"?为什么"更新缓存"会导致错乱,而 MySQL 更新却不会乱?什么是"延迟双删"?
💡 要点解答
- 一句话结论:"先更新数据库,再删除缓存"是避开并发更新竞态条件的最优解;对于数据库读写分离架构,则必须叠加"延迟双删"来弥补主从同步时间差带来的漏洞。
- 条理解析 :
- 为何删除缓存而非更新:如果有请求 A 和 B 并发更新数据,离开 MySQL 后,到达 Redis 的顺序受网络抖动影响极大。B 虽然在 MySQL 里后执行(拥有最新数据),但可能在 Redis 里先执行完,导致 A 的旧数据最终覆盖了 B 的新数据。选择"删除缓存"则完美避开了这种"抢红绿灯"的乱序争抢。
- MySQL 为何不乱 :MySQL 自身带有行锁(Row Lock)。A 在改的时候会把这行数据锁死,B 必须老老实实排队等 A 改完才能改。强行排成了单行道,保证了底层数据的绝对正确。
- 延迟双删解决主从延迟 :在读写分离架构下,写库完成到同步给读库(从库)通常有几十到几百毫秒延迟。
- 步骤 :更新主库 -> 删缓存 -> 休眠 500ms -> 第二次删缓存。
- 意义:在这休眠的 500ms 内(主从同步空窗期),若有请求去从库读到了旧数据并塞进缓存,这"第二把刀"能在醒来后将脏数据彻底清除。
🔍 架构师的终极取舍:强一致性 vs 最终一致性
| 架构选择 | 一致性级别 | 性能表现 | 适用场景 |
|---|---|---|---|
| 延迟双删 (读写分离) | 最终一致性(500ms内从库查到的必定是旧数据,但最终自愈) | 极高(读库无锁抗压) | 电商、社交等绝大多数互联网高并发业务 |
| 强制读写主库 + 互斥锁 | 强一致性(任何时刻绝对不读旧数据) | 较差(无法读写分离,排队阻塞) | 银行转账、金融核心交易等不容许微小误差的场景 |
📌 专题 6:Redis 分布式锁与看门狗机制 (防止并发冲突)
❓ 核心问题
如何用 Redis 实现分布式锁?Key 和 Value 应该存什么?业务没执行完锁就过期了怎么办?如果服务器突然宕机,会导致死锁吗?
💡 要点解答
- 一句话结论 :利用
SET NX EX实现抢锁,利用 Lua 脚本保证解锁原子性;利用"看门狗机制"解决长耗时业务的锁续期问题,同时天然防死锁。 - 条理解析 :
- 加锁核心逻辑 (
SET key value NX EX 30) :- Key 必须相同 :代表大家都来争抢的同一个门把手(例如
lock:product:iphone15)。 - Value 必须唯一:存入每个请求专属的 UUID。这是为了在解锁时"验明正身",防止张三误删了李四刚抢到的锁。
- NX (Not eXists):只有门把手没被人占着时,你才能占领。保证同一时刻只有一个人能成功加锁。
- EX (Expire):必须设置过期时间(如 30 秒)。如果没有这个,一旦拿到锁的服务器立刻断电,锁就永远解不开了,系统瞬间瘫痪。
- Key 必须相同 :代表大家都来争抢的同一个门把手(例如
- 解锁核心逻辑 (必须用 Lua 脚本) :
- 必须先判断
Value是不是自己的 UUID,确认是自己的才能执行删除DEL。因为"判断"和"删除"是两步操作,为了防止这两步中间被人插队打断,必须用 Lua 脚本把它们打包成一个绝对原子的整体操作。
- 必须先判断
- 看门狗机制 (Watch Dog,如 Redisson 框架) :
- 痛点:我设了 30 秒过期,但我查数据库太慢花了 40 秒。第 30 秒时锁自动开了,别人冲进来,数据又乱了。
- 续命原理:只要你抢到了锁,后台就会启动一条"看门狗"线程。如果锁是 30 秒过期,狗会默认每 10 秒醒来一次。只要发现主业务还在运行,狗就会把过期时间重新拨回 30 秒,实现"无限续命"。
- 完美防死锁 :如果是服务器意外断电,主业务死了,这只活在内存里的看门狗也会同时瞬间暴毙。没有狗去续命了,剩下的十几秒耗尽后,Redis 就会自动把锁解开,让其他服务器接管,绝不发生死锁!
- 加锁核心逻辑 (
📌 专题 7:Redis 进阶外挂:持久化、事务、Pipeline 与 Lua
❓ 核心问题
Redis 怎么保证断电不丢数据?Redis 事务能回滚吗?Pipeline 和 Lua 脚本分别是为了解决什么问题?
💡 要点解答
- 一句话结论:用"混合持久化"保命,摒弃鸡肋的自带"事务",用 Pipeline 大幅优化网络性能,用 Lua 脚本实现终极原子操作。
- 条理解析 :
- 持久化机制 (防断电丢数据) :
- RDB (快照):像拍照片,把某一刻的内存直接存盘。恢复极快,但如果两次快照间隔期宕机,会丢失期间的新数据。
- AOF (日志):像记日记,记录每一次写的操作。数据最安全,但文件极大,重启一条条回放时恢复极慢。
- 混合持久化 (现代王道):前半段快照 + 后半段日志,完美兼顾恢复速度与数据绝对安全。
- 事务 (MULTI/EXEC) :
- Redis 事务只是保证把一批命令打包,执行时不被别人插队,但它绝对不支持回滚 (Rollback)。如果中间一条命令报错,剩下的依然会继续执行。由于这种"半吊子"特性,大厂实战中极少使用。
- Pipeline (管道技术) :
- 本质 :完完全全的网络传输层优化,它并不是什么批量插入的新命令。
- 原理 :把成百上千条独立的命令(哪怕是
SET、GET混着来),打包进一个网络包裹一次性发给 Redis,Redis 执行完再把结果打包一次性退回。 - 意义:消灭了成千上万次的网络往返时间(RTT)。在"缓存预热"、"批量查询推荐列表"等场景下,性能可以瞬间飙升几十上百倍。
- Lua 脚本 (绝对原子性) :
- 区别于 Pipeline 只是"纯网络打包",Lua 是把一段带逻辑判断的代码(比如:先查库存够不够,够的话再扣减)发给 Redis。
- Redis 在执行 Lua 脚本时,会锁死自己的单线程,实现绝对无懈可击的原子操作。它是实现复杂分布式锁、高并发限流的终极必杀技。
- 持久化机制 (防断电丢数据) :
📌 专题 8:幂等防重与业务限流 (系统防御大招)
❓ 核心问题
什么是幂等?怎么区分是用户网卡了导致的"手抖连击",还是真实的"二次购买"意图?幂等能防住懂技术的人用脚本故意卡时间差刷接口吗?
💡 要点解答
- 一句话结论:幂等是防手抖的"护盾",限流是防恶意刷单的"防弹衣"。两者结合才能构筑高并发下的铜墙铁壁。
- 条理解析 :
- 什么是幂等 (Idempotency) :
- 不管请求执行多少次,最终产生的业务结果和只执行一次一模一样。比如扣款接口,点一次扣 100,疯狂连点 10 次,最终还是只扣 100,绝对不会扣 1000。
- 防手抖 (短效幂等) :
- 做法:在 Redis 存入单号,TTL 设置为 5-10 秒。
- 逻辑:正常情况下,几秒钟内请求就会处理完并且前端页面会发生跳转。这 5-10 秒的防线纯粹是为了拦截在页面没跳转的卡顿期间,用户着急产生的连击请求。
- 跨系统防重 (中长效幂等,如微信/支付宝付款) :
- 我们调第三方支付时必传的"商户订单号 (
out_trade_no)",就是跨系统幂等的完美体现。依靠这个唯一标识,哪怕超时重试一万次,支付宝也绝对不会扣用户两笔钱。
- 我们调第三方支付时必传的"商户订单号 (
- 神级推演:如何区分"手抖"与"真实二次购买"?
- 手抖连击 :用户是在同一个页面连续点击,这些请求携带的是同一个后端提前下发的 Token。
- 二次购买 :用户必须退出再重新进入下单页面,此时后端会下发一个全新的 Token。系统通过识别 Token 是否变化,完美区分用户的真实意图。
- 防刷限流 (对付恶意脚本的降维打击) :
- 漏洞:如果黑客写脚本,故意每隔 15 秒调一次接口,就能完美绕过 5-10 秒的短效防抖幂等。
- 防御方案 :必须升级到业务限流 (如 Redis 记录单用户 1 分钟最多发 1 帖)、内容去重 (校验发帖内容的 MD5 指纹,24 小时内雷同直接拦截),甚至叠加网关动态签名校验。从"幂等"跨入"限流防刷"领域,才算真正做到了系统安全。
- 什么是幂等 (Idempotency) :
📌 专题 9:高并发限流三大算法与 Lua 脚本浅析
❓ 核心问题
计数器限流有什么漏洞?滑动窗口、漏桶、令牌桶分别是怎么工作的?哪个能应对突发流量?Lua 脚本难学吗?
💡 要点解答
- 一句话结论:摒弃有漏洞的普通计数器;滑动窗口解决临界点问题但过于死板;漏桶适合保护老旧脆弱系统;令牌桶兼顾突发承载与平滑过渡,是大厂限流绝对的王者。
- 条理解析 :
- 普通计数器 (固定窗口) 的致命漏洞 :
- 限制 1 分钟 100 次。黑客在 00:59 发 100 次,01:01 重新计费时再发 100 次。结果在 2 秒内系统承受了 200 次暴击(临界点突发流量问题)。
- 1. 滑动窗口 (Sliding Window) :
- 原理 :用 Redis 的
ZSET实现。当前时间往前倒推 60 秒作为一个"滑动框"。每次请求来,先把框外的旧脚印删掉,查框里还剩多少,超标就拦截。 - 痛点:能防住暴击,但惩罚太严厉。如果在第 1 秒瞬间涌入 100 个请求,接下来的 59 秒系统处于"绝对封杀"状态,不够平滑。
- 原理 :用 Redis 的
- 2. 漏桶算法 (Leaky Bucket) :
- 原理 :桶底下破个洞。水(请求)随便往里倒,满了就直接丢弃。底部永远以绝对匀速往下漏给系统处理。
- 特点:极其死板。哪怕服务器现在闲得发慌,它也强行逼着请求匀速排队。适用于保护一碰就碎的古董级系统。
- 3. 令牌桶算法 (Token Bucket) ------ 终极王者 :
- 原理:系统以恒定速度往桶里放令牌(Token)。用户拿不到令牌就被拦截。
- 无敌优势 :
- 应对突发:如果系统闲了一阵子,桶里攒满了 100 个令牌。此时瞬间来 100 个请求,瞬间全部放行,用户秒开体验极佳。
- 平滑过渡:放完这 100 个后,不用像滑动窗口那样傻等一分钟。因为系统还在按"一秒 2 个"持续发新令牌,后续流量可以平滑降速处理。
- 参数怎么定?(大厂压测思维) :
- 令牌桶的容量 和生成速度 绝对不是拍脑袋定的。是通过 JMeter 等工具狂轰滥炸"压测"出来的系统物理极限。上线后,必须接入配置中心(如 Nacos),随时根据监控大屏的 CPU 报警情况进行动态调参。
- 普通计数器 (固定窗口) 的致命漏洞 :
🐍 扩展:写限流为什么非得用 Lua 脚本?难学吗?
- 为什么用 :限流涉及到"先查时间差 -> 算水量/令牌数 -> 判断 -> 扣减"一整套连招。这套连招如果被并发打断就全乱了。扔给 Redis 去单线程跑 Lua 脚本,能保证这套连招的绝对原子性。
- 难学吗 :极其简单 。Lua 就是个"给大哥打辅助"的超轻量插件语言。语法神似 Python 去了冒号加了
end。只要会定义变量(local)、写if...else,以及用redis.call()调原生命令,30 分钟就能完全上手。唯一需要注意的是:它的数组是从1开始的。
💻 附:三大限流算法的 Python + Lua 完整代码实战
python
import redis
import time
# 连接 Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# ==========================================
# 1. 滑动窗口 (Sliding Window) 算法实现
# ==========================================
SLIDING_WINDOW_LUA = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window_size_ms = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local request_id = ARGV[4]
local window_start = now - window_size_ms
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local current_count = redis.call('ZCARD', key)
if current_count < limit then
redis.call('ZADD', key, now, request_id)
redis.call('PEXPIRE', key, window_size_ms)
return 1
else
return 0
end
"""
sliding_window_eval = r.register_script(SLIDING_WINDOW_LUA)
def check_sliding_window(user_id):
key = f"rate_limit:sliding_window:{user_id}"
limit = 5 # 窗口内最多允许 5 次请求
window_size_ms = 1000 # 窗口大小:1000毫秒 (1秒)
now_ms = int(time.time() * 1000)
import uuid
request_id = str(uuid.uuid4())
result = sliding_window_eval(keys=[key], args=[limit, window_size_ms, now_ms, request_id])
return result == 1
# ==========================================
# 2. 漏桶 (Leaky Bucket) 算法实现
# ==========================================
LEAKY_BUCKET_LUA = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local leak_rate = tonumber(ARGV[2]) -- 滴/毫秒
local now = tonumber(ARGV[3])
local water = tonumber(redis.call('HGET', key, 'water') or '0')
local last_time = tonumber(redis.call('HGET', key, 'last_time') or tostring(now))
local leaked_water = math.floor((now - last_time) * leak_rate)
water = math.max(0, water - leaked_water)
if water + 1 <= capacity then
water = water + 1
redis.call('HSET', key, 'water', water, 'last_time', now)
redis.call('PEXPIRE', key, 60000)
return 1
else
return 0
end
"""
leaky_bucket_eval = r.register_script(LEAKY_BUCKET_LUA)
def check_leaky_bucket(user_id):
key = f"rate_limit:leaky_bucket:{user_id}"
capacity = 10 # 桶最多装 10 个请求
leak_rate = 5 / 1000.0 # 漏水速度:每秒 5 个 (每毫秒 0.005)
now_ms = int(time.time() * 1000)
result = leaky_bucket_eval(keys=[key], args=[capacity, leak_rate, now_ms])
return result == 1
# ==========================================
# 3. 令牌桶 (Token Bucket) 算法实现
# ==========================================
TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local token_rate = tonumber(ARGV[2]) -- 生成速度 (个/毫秒)
local now = tonumber(ARGV[3])
local requested_tokens = tonumber(ARGV[4])
local tokens = tonumber(redis.call('HGET', key, 'tokens') or tostring(capacity))
local last_time = tonumber(redis.call('HGET', key, 'last_time') or tostring(now))
local generated_tokens = math.floor((now - last_time) * token_rate)
tokens = math.min(capacity, tokens + generated_tokens)
if tokens >= requested_tokens then
tokens = tokens - requested_tokens
redis.call('HSET', key, 'tokens', tokens, 'last_time', now)
redis.call('PEXPIRE', key, 60000)
return 1
else
return 0
end
"""
token_bucket_eval = r.register_script(TOKEN_BUCKET_LUA)
def check_token_bucket(user_id):
key = f"rate_limit:token_bucket:{user_id}"
capacity = 100 # 桶最多装 100 个令牌 (能抗 100 的突发)
token_rate = 20 / 1000.0 # 恢复速度:每秒 20 个 (每毫秒 0.02)
now_ms = int(time.time() * 1000)
requested = 1 # 本次消耗 1 个令牌
result = token_bucket_eval(keys=[key], args=[capacity, token_rate, now_ms, requested])
return result == 1
📌 专题 10:经典互联网后端架构全景图 (超级饭店模型)
❓ 核心问题
经常听说的 Nginx、网关 (Gateway)、Tomcat、Redis、MQ 到底在整个后端系统里处于什么位置?各自起什么作用?
💡 要点解答
我们可以把整个后端架构完美类比为一家**"超级大饭店"**。一切中间件的设计,本质上都是为了在高并发下保护最脆弱、但也最重要的"地下冷库(MySQL)"。
🗺️ 架构拓扑图
1. Java 微服务架构全景图
2. Java 单体应用架构图

3. Python FastAPI 单体应用架构图 
📖 架构组件详解
- Nginx (反向代理服务器):处在最外层,直面公网。主要负责把几百万的访问量,均匀地分发给后面的多台服务器。
- 网关 (API Gateway):微服务的统一大门。在这里集中处理鉴权(查身份证)和限流(令牌桶)。如果没有网关,每个微服务都要自己写一遍鉴权逻辑,极其混乱。
- Tomcat/FastAPI (应用服务器):代码真正运行的地方。微服务架构下,这里会被拆分成"用户服务"、"订单服务"等几十个小集群。
- Redis (缓存):内存数据库,快如闪电。绝大多数的查询请求在这里就被拦截返回了,不用去麻烦 MySQL。
- MQ (消息队列):异步排队工具。遇到双十一秒杀,应用服务器直接告诉前端"下单成功",但实际上真正的扣款操作被扔进了 MQ 里,慢慢排队扣钱,防止系统被瞬间压垮。
- MySQL (关系型数据库):所有架构设计的最终保护对象。非常脆弱(并发几千就容易卡死),但极其重要(数据绝不能丢)。
💡 进阶延伸:单体架构 vs 微服务,以及"翻译官"的本质
- 单体架构没有网关:如果是个纯粹的单体应用(所有代码写在一起),通常是不需要网关的。Nginx 会直接把请求转发给 Tomcat/Uvicorn,鉴权和限流直接在框架内做。网关纯粹是为了解决微服务架构下"成百上千个小服务如何统一入口和路由"的问题。
- Tomcat 和 Uvicorn 到底在干嘛? :浏览器通过网络发来的是干瘪的纯文本(
GET /login HTTP/1.1)。Tomcat (Java) 和 Uvicorn (Python) 的核心作用就是个**"翻译派单员"**,它们监听底层端口,把文本请求解析并包装好,然后准确无误地交给您写的@app.get("/login")业务代码去执行。 - 配置 vs 写代码 :在整个宏观架构里,Nginx、Tomcat、Uvicorn 都是大神们写好并编译好的"黑盒工具",属于运维范畴,我们通常只需修改
nginx.conf或在终端传参数启动即可。而真正需要我们每天烧脑敲代码的地方,是跑在 Uvicorn 后面的 FastAPI 业务逻辑(比如连接Redis、操作MySQL、写限流算法)。
📌 专题 11:CDN、DNS与动静分离架构
❓ 核心问题
为什么单体架构还要套一层 Nginx?前端静态资源放在 CDN/OSS 上到底是怎么工作的?CDN 的"就近分配"是靠路由器实现的吗?对于动态的 API 接口请求,CDN 又是怎么处理的?
💡 要点解答
1. 为什么单体应用(Tomcat/Uvicorn)前面必须要挡一个 Nginx?
- 不是必选项,但是极佳的最佳实践 。本地开发时完全可以直接请求
localhost:8000(直连 Uvicorn),但在生产环境强烈建议加上 Nginx。 - 核心原因:安全与权限 。使用 HTTP/HTTPS 默认的
80/443端口需要 Linux 操作系统的root权限。绝不能让后端的 Java/Python 业务代码以 root 权限运行(一旦被黑客攻破,服务器直接沦陷)。Nginx 以 root 权限监听 80 端口,然后安全地将请求转发给只有普通权限的 Tomcat/Uvicorn(运行在 8080 等高端口)。 - 附加优势:Nginx 配置 HTTPS(SSL证书)极其简单,且处理静态文件(图片、CSS)的速度远超 Tomcat/Uvicorn。
2. CDN 的"就近分配"魔法(智能 DNS)
- 误区 :很多人以为是沿途的路由器篡改了 IP 地址。完全错误! 路由器只负责无脑送快递,不敢改地址。
- 真相 :CDN 能够"就近分配"的核心黑科技是智能 DNS 解析 (GSLB) 。
- 当北京联通 用户访问
www.abc.com时,用户的 DNS 请求会打到 CDN 厂商的智能 DNS 服务器。 - 智能 DNS 发现请求源 IP 属于"北京联通",就会去自己的节点库里挑一台北京联通的 CDN 服务器(如
10.10.10.10)返回给用户。 - 同一时刻,深圳电信 用户请求同一个域名,智能 DNS 会返回一台深圳电信的 CDN 服务器(如
20.20.20.20)。 - 千人千面:同一个域名,不同地区的用户在建立连接前,解析到的 IP 地址完全不同。
- 当北京联通 用户访问
3. 静态资源的"回源"机制(Nginx vs 云存储 OSS)
- 传统方式(以 Nginx 为源站):如果北京 CDN 节点缓存里没有某张图片,它会顺着网线回到您自己部署的 Nginx 服务器上去下载(这叫"回源")。这依然会消耗您自己服务器的宽带。
- 现代大厂方式(以云网盘 OSS 为源站) :图片等静态文件直接上传到阿里云 OSS(对象存储)。在配置 CDN 时,源站直接填 OSS 桶的地址。
- 无敌优势 :当 CDN 缓存未命中时,CDN 直接走阿里云内部的高速网络去 OSS 拿文件。您的核心业务服务器(Nginx/Tomcat)连个影儿都见不到,实现静态流量 100% 卸载,0 消耗。
4. 动态接口请求(API)怎么走?
对于需要查询数据库的动态请求(如 POST /api/login),CDN 是绝对不能缓存的。
- 标准玩法(双域名动静分离) :
- 图片请求
static.abc.com,走 CDN + OSS。 - 接口请求
api.abc.com,域名根本不绑 CDN,DNS 直接解析到您的真实 Nginx 服务器。
- 图片请求
- 单域名硬抗玩法(全走 CDN) :
- 接口和图片都走
www.abc.com。 - 我们会在 CDN 配置特殊规则:"遇到带有
/api/路径的请求,禁止缓存!" - 此时,CDN 节点瞬间化身为一根**"透明网线"**(透明代理)。收到 API 请求看都不看,直接原封不动地透传给您的老家 Nginx 服务器。
- 接口和图片都走
- 结论 :无论哪种玩法,动态接口的宿命一定是直达您的 Nginx 和业务服务器(Tomcat/Uvicorn),CDN 对动态接口只做透明转发,或干脆不参与。
📌 专题 12:Redis 高可用基石:一主二从三哨兵架构
❓ 核心问题
什么是"一主二从三哨兵"?为什么要 3 个哨兵?哨兵到底是个什么东西?最少需要几台物理服务器?
💡 要点解答
1. 架构角色拆解
- 1 主 (Master):唯一的"大厨",只负责写数据。避免多主写入导致的并发冲突。
- 2 从 (Slave) :主节点的"帮厨"兼"备胎"。大厨干什么他们就同步什么。除了做数据冗余备份,还可以开启读写分离,让从节点去扛海量的读请求。如果有 1 个从节点挂了,还有另 1 个做备份;如果主挂了提拔 1 个,也还能剩 1 个继续做新主的备份。
- 3 哨兵 (Sentinel) :铁面无私的"监工"。负责 24 小时不断
PING节点。如果主节点宕机,哨兵们会开会投票,自动把从节点提拔为新主节点(自动故障转移),保证系统不瘫痪。
2. 为什么偏偏是"3 个"哨兵?(分布式选举的半数原则)
哨兵要想提拔新主节点,必须经过**"超过半数"**的哨兵投票同意。
- 1 个哨兵:如果它自己挂了,整个监控系统瞬间瘫痪(单点故障)。
- 2 个哨兵:半数以上依然是 2。如果网络抖动断了 1 个,剩下的 1 个哨兵手里只有 1 票,永远达不到法定票数,无法发起救援。
- 3 个哨兵(黄金数字):半数以上是 2。即使物理断电烧毁了 1 个哨兵,剩下的 2 个哨兵依然能凑够"半数以上"的选票,成功完成主从切换。
3. 哨兵到底是什么?
- 本质 :哨兵并不是一个新软件,它就是普通的
redis-server进程启动在了一个**"特殊模式(保安模式)"**下。 - 特点 :进入该模式后,进程会"自我阉割",拒绝存储任何数据,只负责收发网络心跳包。因此它占用内存极小,CPU 消耗极低。
4. 终极一问:这套架构最少需要几台机器?
- 答案:3 台物理机器。
- 部署方式 :
- 机器 A:主节点 + 哨兵 1
- 机器 B:从节点 1 + 哨兵 2
- 机器 C:从节点 2 + 哨兵 3
- 经济学原理(用空间换高可用):您花了买 6 个进程/3 台机器的钱,实际上整个集群只能存得下 1 台机器的容量。多出来的容量全都是为了容灾备份。大厂心甘情愿花这笔"保险费",因为一旦宕机引发业务瘫痪,损失的钱够买一万台服务器了。
📌 专题 13:Redis Cluster 终极分片集群 (突破单机极限)
❓ 核心问题
既然有了哨兵,为什么还要用 Redis Cluster?Cluster 和 MySQL 分库分表有什么关系?用了云厂商的集群还需要懂哈希槽吗?
💡 要点解答
1. Cluster 是为了解决什么?
- 哨兵架构(一主二从)解决了"高可用"问题(死不掉),但没有解决"单机容量瓶颈"。所有的数据依然全挤在那 1 台主节点上。
- Redis Cluster 相当于 MySQL 的分库分表。如果 1 台大厨炒不过来,我就找 3 台大厨,大家平分这海量的数据,一起并行写入。
2. 核心黑科技:16384 个哈希槽 (Hash Slots)
- Redis 凭空创造了 16384 个"虚拟抽屉"。
- 3 台大厨各自认领三分之一的抽屉(比如 A 管 0~5000)。
- 当一条新数据
user:1001进来时,Redis 会用 CRC16 算法对user:1001算出一个 0~16383 之间的数字。算出来是几,这条数据就绝对会被放入对应的抽屉里,交给特定的大厨处理。 - 平滑扩容:双十一流量暴增,只需买台新机器(大厨 D),把 A、B、C 柜子里的抽屉挪一部分给 D 即可,前端完全无感。
3. 去中心化(自带哨兵)与 3主3从
- 在 Cluster 集群中,不再需要独立部署哨兵。
- 标配是"3主3从"共 6 台机器。大厨 A、B、C 之间互相查岗。如果大厨 A 挂了,大家会直接把帮厨 A 提拔上来,接管那 5000 个抽屉。
- 如果大厨 A 和帮厨 A 同时物理摧毁(某一块分片的数据彻底死绝),整个集群会崩溃(
CLUSTERDOWN)。
4. 开发者的必修课:哈希标签 (Key Tags)
- 运维交给了云厂商:在生产环境中,极少有人手工敲命令搭建 Cluster。大家都直接买阿里云的"云数据库 Redis 集群版",点几下鼠标,加机器、挪槽位全部全自动完成。
- 但代码的坑只能自己填 :如果您在写 Lua 脚本时,试图同时操作
user:1001和order:1001,而它们算出来的哈希值在不同的物理机上,Redis 会直接报错抛异常。 - 解法 :在命名时加上大括号
{},如{1001}:user和{1001}:order。Redis 只要看到大括号,就只对括号里的字符算哈希,从而强行把这两个 Key 塞进同一个大厨的抽屉里,完美避开跨槽错误!
📌 专题 14:Redis 生产环境运维必修课:安全、配置与备份恢复
❓ 核心问题
如果把 Redis 暴露在公网会怎样?内存写满了会宕机吗?RDB 和 AOF 备份到底哪个好?怎样防范实习生"删库跑路"?
💡 要点解答
1. 安全防范 (防黑客与防内鬼)
Redis 因为追求极速,早年的版本默认没有任何安全防护。
- 禁止公网裸奔 :通过
bind和protected-mode配置,强制 Redis 只能被内网后端的机器(如 Tomcat/FastAPI)访问。如果暴露在公网且没有密码,100% 会被黑客植入挖矿木马。 - 精细化权限控制 (ACL) :Redis 6.0 后支持像 MySQL 一样建账号。可以给数据分析师建一个"只能执行
GET,绝不能执行SET"的只读账号,防止误操作。 - 重命名高危命令 :对于
FLUSHALL(清空所有数据)这种核弹级命令,可以在配置文件中将其重命名为secret_flush_999或者直接禁用,彻底杜绝新手"删库跑路"。
2. 核心内存配置 (防系统撑爆)
内存管理是 Redis 运维的生命线。
maxmemory(内存红线):必须设置!如果不设,Redis 会无节制地吞噬物理机器的所有内存,直到触发 Linux 的 OOM 机制被操作系统强行杀掉。maxmemory-policy(淘汰策略) :当内存触及红线时怎么办?通过配置allkeys-lru(淘汰最久没访问的旧数据)等策略,让 Redis 自动吐出旧数据,保证新数据能写进去,实现平稳降级。
3. 备份与恢复 (最后的救命稻草)
如果机房着火或者数据被误删,只能靠备份文件救命。
- RDB (快照备份) :
- 原理:像照相机一样,每隔一段时间把整个内存的数据拍张照存入硬盘。
- 优缺点:文件极小,恢复极快;但如果下午 3 点宕机,凌晨到下午 3 点之间的新数据就全丢了。
- AOF (增量日志备份) :
- 原理:像记账本一样,每执行一条写命令,就往日志文件里追加一行记录。
- 优缺点:数据极度安全(最多丢 1 秒);但日志文件巨大,恢复时要一行行重新执行命令,耗时极长。
- 混合持久化 (现代王道) :
- Redis 4.0 推出的终极方案,结合了 RDB 和 AOF 的优点。前半段是紧凑的 RDB 快照(保证 99% 的恢复速度),后半段是最近几分钟的 AOF 日志(补齐最后 1% 的数据),完美兼顾极速与安全。
⚠️ 架构师实战忠告
业界铁律:"未经恢复演练的备份,等于没有备份。" 很多公司每天都在勤勤恳恳地备份 RDB 文件,结果真出事的那天,发现备份脚本早挂了或者文件已经损坏。因此,定期把备份文件放到测试环境"走一遍真实的恢复流程",是衡量一个团队运维水平的试金石。
📌 专题 15:Redis 线上故障排查 (老中医诊脉指南)
❓ 核心问题
如果线上 Redis 突然变慢或者连不上,应该怎么一步步排查?为什么生产环境严禁使用
KEYS *命令?该用什么代替?
💡 要点解答
1. 核心心法:建立系统排查思维
排查线上故障最忌讳"瞎猜"和"盲目重启"。必须遵循"望闻问切"的顺序:
- 先看日志:定位是最基础的连接报错,还是 OOM 内存溢出。
- 再查网络:检查防火墙、密码、端口是否正确。
- 查性能指标:是否出现了慢查询?QPS 是否异常飙升?
- 查内存与复制 :是否因为达到
maxmemory正在疯狂淘汰数据?主从同步是否断开导致频繁全量重连?
2. 最常用的排查神仙命令
INFO(全能体检报告) :排查的总入口。info memory:查看当前内存使用量。info replication:查看主从同步状态和延迟。
SLOWLOG(抓内鬼神器) :能够揪出所有执行时间过长的慢查询命令,精确定位是哪段代码拖垮了系统。完整实战用法如下:- 设置红线(秒/微秒) :
CONFIG SET slowlog-log-slower-than 10000(单位是微秒。10000 微秒 = 10 毫秒。意味着执行超过 10 毫秒的命令都会被当成"内鬼"记录下来)。 - 设置记录数量 :
CONFIG SET slowlog-max-len 128(最多保留最近 128 条记录,防止日志本身撑爆内存)。 - 获取记录 :
SLOWLOG GET 5(获取最近的 5 条慢查询记录。注意:get 后面的数字代表的是"条数",而不是秒数)。 - 清空记录 :
SLOWLOG RESET(清空历史日志,方便在新一轮压测时抓取新内鬼)。
- 设置红线(秒/微秒) :
monitor(上帝视角的实时监控) :实时打印服务器正在接收的每一条命令。- ⚠️ 高危警告:在生产环境极度消耗性能,千万不要轻易开启,通常只在测试环境调试时使用。
3. 经典故障案例:为什么严禁使用 KEYS *?
- 致命弱点 :Redis 执行命令的核心模型是单线程的。
- 故障还原 :如果库里有 1000 万个 Key,执行一次
KEYS *可能会耗时 3~5 秒。在这 5 秒内,唯一的处理线程被死死卡住,其他成千上万个用户的GET/SET请求全都会被阻塞超时,最终导致整个系统雪崩。 - 官方解药:
SCAN命令 。SCAN类似于数据库的**"分页查询"**。- 每次只返回一小批数据(比如 10 个)并附带一个游标。然后立刻释放线程去处理别的用户请求。
- 客户端拿着游标再去请求下一批数据。通过这种"少量多次、走走停停"的方式,既能遍历全库,又绝对不会卡死系统。