4.Redis 核心概念与实战深度

📌 专题 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。请求来时先看"点名册",不在册的直接拦截,绝不放行去查库。它说没有一定没有,它说有大概率有(存在极小误判率)。

🔍 详细代码示例:布隆过滤器的两种实现方式

方式 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 集群高可用。

📌 专题 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 秒)。如果没有这个,一旦拿到锁的服务器立刻断电,锁就永远解不开了,系统瞬间瘫痪。
    • 解锁核心逻辑 (必须用 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 (管道技术)
      • 本质 :完完全全的网络传输层优化,它并不是什么批量插入的新命令。
      • 原理 :把成百上千条独立的命令(哪怕是 SETGET 混着来),打包进一个网络包裹一次性发给 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 小时内雷同直接拦截),甚至叠加网关动态签名校验。从"幂等"跨入"限流防刷"领域,才算真正做到了系统安全。

📌 专题 9:高并发限流三大算法与 Lua 脚本浅析

❓ 核心问题

计数器限流有什么漏洞?滑动窗口、漏桶、令牌桶分别是怎么工作的?哪个能应对突发流量?Lua 脚本难学吗?

💡 要点解答

  • 一句话结论:摒弃有漏洞的普通计数器;滑动窗口解决临界点问题但过于死板;漏桶适合保护老旧脆弱系统;令牌桶兼顾突发承载与平滑过渡,是大厂限流绝对的王者。
  • 条理解析
    • 普通计数器 (固定窗口) 的致命漏洞
      • 限制 1 分钟 100 次。黑客在 00:59 发 100 次,01:01 重新计费时再发 100 次。结果在 2 秒内系统承受了 200 次暴击(临界点突发流量问题)。
    • 1. 滑动窗口 (Sliding Window)
      • 原理 :用 Redis 的 ZSET 实现。当前时间往前倒推 60 秒作为一个"滑动框"。每次请求来,先把框外的旧脚印删掉,查框里还剩多少,超标就拦截。
      • 痛点:能防住暴击,但惩罚太严厉。如果在第 1 秒瞬间涌入 100 个请求,接下来的 59 秒系统处于"绝对封杀"状态,不够平滑。
    • 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 单体应用架构图

📖 架构组件详解

  1. Nginx (反向代理服务器):处在最外层,直面公网。主要负责把几百万的访问量,均匀地分发给后面的多台服务器。
  2. 网关 (API Gateway):微服务的统一大门。在这里集中处理鉴权(查身份证)和限流(令牌桶)。如果没有网关,每个微服务都要自己写一遍鉴权逻辑,极其混乱。
  3. Tomcat/FastAPI (应用服务器):代码真正运行的地方。微服务架构下,这里会被拆分成"用户服务"、"订单服务"等几十个小集群。
  4. Redis (缓存):内存数据库,快如闪电。绝大多数的查询请求在这里就被拦截返回了,不用去麻烦 MySQL。
  5. MQ (消息队列):异步排队工具。遇到双十一秒杀,应用服务器直接告诉前端"下单成功",但实际上真正的扣款操作被扔进了 MQ 里,慢慢排队扣钱,防止系统被瞬间压垮。
  6. 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:1001order:1001,而它们算出来的哈希值在不同的物理机上,Redis 会直接报错抛异常。
  • 解法 :在命名时加上大括号 {},如 {1001}:user{1001}:order。Redis 只要看到大括号,就只对括号里的字符算哈希,从而强行把这两个 Key 塞进同一个大厨的抽屉里,完美避开跨槽错误!

📌 专题 14:Redis 生产环境运维必修课:安全、配置与备份恢复

❓ 核心问题

如果把 Redis 暴露在公网会怎样?内存写满了会宕机吗?RDB 和 AOF 备份到底哪个好?怎样防范实习生"删库跑路"?

💡 要点解答

1. 安全防范 (防黑客与防内鬼)

Redis 因为追求极速,早年的版本默认没有任何安全防护。

  • 禁止公网裸奔 :通过 bindprotected-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. 核心心法:建立系统排查思维

排查线上故障最忌讳"瞎猜"和"盲目重启"。必须遵循"望闻问切"的顺序:

  1. 先看日志:定位是最基础的连接报错,还是 OOM 内存溢出。
  2. 再查网络:检查防火墙、密码、端口是否正确。
  3. 查性能指标:是否出现了慢查询?QPS 是否异常飙升?
  4. 查内存与复制 :是否因为达到 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 个)并附带一个游标。然后立刻释放线程去处理别的用户请求。
    • 客户端拿着游标再去请求下一批数据。通过这种"少量多次、走走停停"的方式,既能遍历全库,又绝对不会卡死系统。
相关推荐
Java编程爱好者1 小时前
Reactor 网络模型演进:图解 7 种 Reactor 网络模型
后端
YuePeng1 小时前
一个注解起家,25 个模块收尾:Erupt 的生态版图长这样
后端·github
睡觉时不困4421 小时前
7.从底层讲清楚Git仓库
后端
神奇小汤圆2 小时前
解放双手!SpringBoot 6种自动填充公共字的手段,这些代码真没必要手写!
后端
鱼人2 小时前
多态底层原理:向上转型、动态绑定,一文彻底搞懂
后端
神奇小汤圆2 小时前
吃透Redis缓存核心:淘汰策略、LRU/LFU区别与四大缓存问题全解
后端
无责任此方_修行中3 小时前
搓了一个国产大模型与 AI Agent 比价工具
前端·后端·ai编程
yunyi3 小时前
改个 SSH 端口,我把自己锁在了服务器外面
运维·后端