文章目录

Redis

redis

redis学习

Redis

1 基础概念

Redis 是一个“基于内存的高性能 Key-Value 数据库”

我们把这句话拆开理解 👇


1️⃣ Key-Value 数据库

Redis 的数据结构是:

key -> value

例如:

SET name "Liu"
GET name "Liu"

👉 类似:

  • Python dict
  • Java HashMap

但 Redis 是:

跨进程 + 可持久化 + 网络访问


2️⃣ 数据存在“内存”里(核心)

👉 Redis 默认把数据存在 RAM 中,而不是磁盘

带来的影响:

✔ 极快(微秒级) ❌ 容量受限 ❌ 成本更高


3️⃣ 它是一个“数据库”

虽然很多人把 Redis 当缓存用,但它本质是:

  • 支持持久化(RDB / AOF)
  • 支持数据结构
  • 支持事务(弱事务)

👉 所以它是:

NoSQL 数据库的一种


Redis 和传统数据库(MySQL)的区别

我们用一个非常直观的对比:

维度RedisMySQL
存储内存磁盘
速度非常快较慢
数据结构丰富表结构
查询方式key 查找SQL
适合场景高频访问复杂查询

Redis 的核心特点

⭐ 特点1:单线程模型(非常重要)

Redis 主要是单线程执行命令:

👉 好处:

  • 没有锁竞争
  • 简单稳定
  • 性能高

👉 适合:

  • 短、快操作

⭐ 特点2:丰富的数据结构

不像 MySQL 只有表,Redis 有:

  • String
  • Hash
  • List
  • Set
  • Sorted Set

👉 这让它可以:

直接支持业务结构


⭐ 特点3:支持持久化

Redis 不是纯缓存:

  • 可以保存数据到磁盘
  • 重启后恢复

⭐ 特点4:支持高并发

  • 单机可以支撑: 👉 10w+ QPS(常见)

Redis 的基本工作流程

你可以想象一个典型请求:

客户端 → Redis → 返回结果

如果作为缓存:

用户请求
   ↓
Redis(有数据)
   ↓
直接返回 ✅

Redis(没有)
   ↓
数据库
   ↓
写入 Redis
   ↓
返回

2 数据结构

Redis 支持多种数据结构,包括 String、List、Hash、Set 和 ZSet 等。String 用于缓存和计数,Hash 用于存储对象,List 可实现简单队列,Set 用于去重和集合运算,ZSet 支持排序场景如排行榜。此外还有 Bitmap、HyperLogLog 和 Stream 等结构用于特定场景。通过这些数据结构,Redis 能够高效支持缓存、计数、队列和实时统计等功能。


1. 字符串 (String)

String 是 Redis 最基础的类型,它是二进制安全的,这意味着它可以存储任何数据(如 JPEG 图像或序列化的 JSON 对象),最大上限为 512MB

  • 内部实现: 采用 SDS (Simple Dynamic String)。相比 C 语言原生字符串,SDS 获取长度的时间复杂度为 $O(1)$,且支持空间预分配和惰性空间释放,避免了频繁的内存重分配。
  • 常用操作:
    • SET / GET:基础读写。
    • INCR / DECR:原子增减(常用于计数器)。
    • SETNX:键不存在时才设置(分布式锁的核心)。
  • 典型场景: 缓存 Session、全页缓存、分布式限流。

  1. 列表 (List)

Redis List 是简单的字符串列表,按照插入顺序排序。你可以从头部(Left)或尾部(Right)添加元素。

  • 内部实现: 在早期版本中使用双端链表和压缩列表,现在的 Redis 统一使用 Quicklist。Quicklist 是一个双向链表,但每个节点都是一个 ZipList,结合了两者的优点:既减少了内存碎片,又避免了普通链表指针占用过大空间的问题。
  • 常用操作:
    • LPUSH / RPUSH:入队。
    • LPOP / RPOP:出队。
    • BRPOP / BLPOP:阻塞式出队(用于简单消息队列)。
  • 典型场景: 消息队列、最新动态列表(如朋友圈时间轴)。

  1. 哈希 (Hash)

Hash 是一个键值对(Field-Value)集合,特别适合存储对象。

  • 内部实现:
    • 当字段较少且值较小时,使用 ZipList(节省内存)。
    • 当数量超过阈值(默认 512 个字段或单值超过 64 字节)时,转换为 Hashtable
  • 常用操作:
    • HSET / HGET:操作单个字段。
    • HMSET / HMGET:批量操作。
    • HINCRBY:对指定字段进行原子增量。
  • 典型场景: 存储用户信息(相比 String 序列化,Hash 可以只更新其中一个字段,效率更高)。

  1. 集合 (Set)

Set 是 String 类型的无序集合,元素具有唯一性。

  • 内部实现:
    • 如果数据全是整数且数量较少,使用 Intset
    • 否则使用 Hashtable(Value 指向 NULL)。
  • 常用操作:
    • SADD:添加元素。
    • SISMEMBER:判断元素是否存在($O(1)$ 效率)。
    • SINTER / SUNION / SDIFF:交集、并集、差集计算。
  • 典型场景: 抽奖系统(唯一性)、共同好友(交集计算)、标签(Tagging)。

  1. 有序集合 (Sorted Set / ZSet)

ZSet 在 Set 的基础上,为每个元素关联了一个 Score(分数),使得集合中的元素可以按分数排序。

Redis 会为每个 Key 存储元数据。如果你有数亿个小的 String Key,内存消耗会非常惊人。此时,将这些 Key 聚合到 Hash 中存储(利用 ZipList 的紧凑布局)通常能节省 60% 以上的内存空间。

  • 内部实现: 核心是 跳跃表 (SkipList)Hashtable
    • 跳跃表通过多层索引实现平均 $O(\log N)$ 的查找效率,性能堪比红黑树,但实现更简单。
  • 常用操作:
    • ZADD:添加元素及分数。
    • ZRANGE / ZREVRANGE:按分数范围获取元素(常用于分页)。
    • ZSCORE:查询指定元素的分数。
  • 典型场景: 实时排行榜、带权重的任务队列、范围查找。

数据结构对比总结

数据类型底层实现 (主要)常用操作复杂度排序支持唯一性
StringSDS$O(1)$N/A是 (Key唯一)
ListQuicklist$O(1)$ (两端)按插入顺序
HashZipList / Hashtable$O(1)$N/A字段唯一
SetIntset / Hashtable$O(1)$
ZSetSkipList / ZipList$O(\log N)$是 (按分数)

常见问题

1 缓存穿透、击穿、雪崩

1.1 缓存穿透 (Cache Penetration)

场景描述:

请求的数据在缓存中没有,在数据库中也没有。由于两边都查不到,请求每次都会“穿透”缓存直接打到数据库上。如果是恶意攻击(比如查询 ID 为 -1 的数据),数据库压力会骤增。

解决方案:

1️⃣布隆过滤器 (Bloom Filter)

布隆过滤器是一种空间效率极高的概率型数据结构

  • 它的核心: 一个超大的位数组(Bit Array)和一组哈希函数(Hash Functions)。
  • 它的特点: 占用的内存极小,查询速度极快,但存在一定的误判率

工作流程:

  • 第一步:初始化(添加 Key)

当你想把一个 Key(比如 User_123)加入过滤器时:

  1. 布隆过滤器会用多个不同的哈希函数对这个 Key 进行计算。
  2. 每个哈希函数都会得到一个数组下标(索引位置)。
  3. 过滤器将位数组中这些对应的位置全部设置为 1
  • 第二步:查询(判断是否存在)

当有一个请求进来查询 User_456 时:

  1. 同样用那几个哈希函数算一遍下标。
  2. 看结果:
    • 只要有一个位置是 0:那这个 Key 绝对不存在!直接拦截 。
    • 如果全部位置都是 1:那这个 Key 可能存在。为什么是可能?因为哈希碰撞会导致不同的 Key 刚好占用了相同的位。

布隆过滤器的“优缺点”

维度特点说明
空间效率极高。不需要存储原始数据,只存 0 和 1。
查询速度极快。计算几次哈希的时间复杂度是 $O(k)$,与数据量无关。
准确性有误判。可能会把不存在的数据误判为存在,但绝不会把存在的数据误判为不存在。
删除操作困难。不能简单地把位改成 0,因为一个位可能被多个 Key 共享。

落地到 Redis 的方案

在实际生产中,你有两种方式实现布隆过滤器:

方案 A:Redis 官方插件 (RedisBloom)

我是支持原生扩展的。安装 RedisBloom 模块后,你可以直接用命令:

  • BF.ADD user_indices user_1:添加 Key。
  • BF.EXISTS user_indices user_1:判断是否存在。
    这种方案最简单,性能也最好。

方案 B:客户端实现 (如 Guava 或 Redisson)

  • Guava BloomFilter: 适合单机环境,内存占用在 JVM 里。
  • Redisson: 适合分布式环境,它会利用我的 Bitmaps 功能来实现分布式布隆过滤器。

一个绕不开的坑:数据删除了怎么办?

正如前面提到的,布隆过滤器很难删除数据。如果数据库里的一条记录删除了,过滤器里对应的位还是 1,这就会导致缓存穿透的风险稍微增加(虽然影响不大)。

健壮的补救措施:

  1. 定期重建: 每隔一段时间,重新从数据库读取合法 ID,生成一个新的过滤器替换旧的。
  2. 计数布隆过滤器 (Counting Bloom Filter): 位数组里存的不是 0/1,而是计数器。但这会成倍增加内存消耗。

总结一下: 布隆过滤器的核心价值在于**“快速拒绝”** 。只要它能拦下 99% 的恶意请求,剩下的 1% 误判对数据库来说就是毛毛雨了。


2️⃣ 缓存空对象: 如果数据库查不到,也往我这里存一个 null 或特殊标记,并设置一个简短的过期时间(比如 5 分钟)。

3️⃣参数校验: 在接口层对不合法的请求参数(如负数 ID)直接拦截。


1.2 缓存击穿 (Cache Breakdown)

场景描述:

某一个“超级热点”Key(比如双11的秒杀商品),在过期的瞬间,正好有海量并发请求打过来。因为缓存失效,这些请求会同时冲向数据库去加载数据,导致数据库瞬间挂掉。

解决方案:

1️⃣逻辑过期

物理上不设置过期时间,但在 Value 里存一个“逻辑过期时间”。发现过期时,由后台异步线程去更新数据,期间其他请求先返回旧数据。与传统的互斥锁方案不同,它的核心思想是:“宁可给用户看一眼旧数据,也绝不让数据库停摆。”

核心设计:物理永不过期

  • 物理层面:在向Redis存储数据时,不设置任何 TTL(生存时间) 。这意味着该数据在 Redis 中是“永不过期”的,除非内存满了被置换,否则它永远不会因为时间到了而消失 。
  • 逻辑层面:程序员在存储的 Value 内部嵌套一个字段,手动记录一个“逻辑过期时间”(比如:expire: 2026-04-21 12:00:00) 。

详细工作流程

当请求进来时,不再是简单的“查到了就返回”,而是多了一次逻辑判断:

  • 查询缓存:请求从 Redis 中获取数据 。
  • 判断逻辑时间
    • 未过期:直接将数据返回给用户 。
    • 已过期:说明数据已经“不新鲜”了,需要更新 。
  • 尝试抢占锁:请求会尝试获取一个互斥锁(如 SETNX) 。
  • 处理过期数据
    • 抢锁成功:开启一个后台异步线程去查询数据库并更新缓存(同时更新逻辑过期时间),然后释放锁 。重点是:当前请求不等待后台更新完成,而是立即返回手头这份“旧数据” 。
    • 抢锁失败:说明已经有其他线程正在更新数据了。当前请求不阻塞,也直接返回手头这份“旧数据” 。

方案优缺点分析

维度特点说明
性能体验极佳 。由于请求永远不需要等待(无阻塞),响应速度极快,用户体验非常流畅 。
数据一致性较弱 。在异步线程完成更新之前,所有用户看到的都是过期数据,属于“最终一致性” 。
实现复杂度较高 。需要额外维护异步线程池以及代码中逻辑时间的判断逻辑 。

为什么说它足够健壮?

逻辑过期方案最强悍的一点在于可用性极高 。 在面对超级热点 Key 时,即便更新数据的过程需要耗时 $2$ 秒钟(比如复杂的 SQL 聚合),在这 $2$ 秒内的成千上万个并发请求都会因为拿到了旧数据而快速返回,不会因为排队等锁而导致服务器线程池耗尽 。


适用场景建议

由于它牺牲了强一致性来换取高性能,它最适合以下场景:

  • 门户首页/热搜榜单:用户对于几秒钟前的热搜排名并不敏感 。
  • 商品详情页:展示的数据稍有延迟(比如销量稍微差一点点)不会产生业务风险 。
  • 高流量、高并发系统:在可用性和极致响应速度面前,短时间的数据不一致是可以接受的 。

2️⃣互斥锁 (Mutex Lock):

简单来说,只允许一个请求去数据库查数据并写回缓存,其他请求等待或重试。它的核心逻辑就是:当热点数据失效时,只允许一个请求去“重建”缓存,其他请求必须等待。


核心工作流程

我们可以把这个过程想象成“修路”:路(缓存)断了,只能派一个维修工(请求)去修,修好之前,其他车辆(并发请求)只能在路口等着。

  1. 尝试获取数据: 请求首先查询 Redis 。
  2. 触发击穿: 发现数据已过期(Cache Miss) 。
  3. 抢占互斥锁: 请求尝试使用 SETNXSET key val NX EX命令在我这里设置一个特殊的 Key(锁) 。
    • 抢锁成功: 进入“重建模式”,去查询数据库并将结果回写到 Redis,随后删除锁 Key 释放资源 。
    • 抢锁失败: 进入“等待模式”,线程休眠一小段时间(如 50ms),然后重新尝试从缓存获取数据 。

“双检锁”(Double-Check)

这是互斥锁方案中最容易被忽略、也最重要的优化细节 。

如果不做双检:

假设有 1000 个请求同时发现缓存失效。请求 A 抢锁成功去查 DB 了;请求 B 抢锁失败开始休眠。等请求 A 查完数据库并更新完缓存,释放了锁,请求 B 醒来立刻又去抢锁,抢锁成功后如果不检查缓存,它会再次冲向数据库

正确的双检逻辑:

  1. 第一次检查:缓存不存在,准备抢锁。
  2. 抢到锁后。
  3. 第二次检查: 再次查询 Redis !因为在你抢锁或等待的几毫秒内,可能前一个“维修工”已经把路修好了 。如果这次查到了,直接返回,不再打扰数据库。

实现细节与健壮性保障

要写出一个工业级的互斥锁方案,你必须考虑以下三个细节:

  • 锁的过期时间(TTL):
    绝对不能只用 SETNX 而不设过期时间。如果抢到锁的服务器在查数据库时突然宕机了,锁就永远不会被释放,导致整个业务死锁。必须给锁设置一个合理的“保底”过期时间。
  • 重试频率:
    抢锁失败后的 sleep 时间要适中。太短会增加 CPU 负担,太长会增加用户等待感。
  • 原子性操作:
    设置锁和设置过期时间必须是原子的(可以使用 SET key value EX seconds NX 命令),防止在设置完锁还没来得及设过期时间时发生意外。

1.3 缓存雪崩 (Cache Avalanche)

场景描述:

大量的 Key 在同一时间集中过期,或者我(Redis 服务器)直接宕机了。所有的请求瞬间全部涌向数据库,数据库扛不住直接“雪崩”。

解决方案:

1️⃣ 随机过期时间: 别让大量 Key 的过期时间撞在一起。在基础过期时间上加一个 随机抖动值(比如 1-5 分钟),让失效时间均匀分布。

2️⃣ 高可用架构: 使用 Redis Sentinel(哨兵)Redis Cluster(集群),确保我本身是不容易挂掉的。

  • Redis Sentinel (哨兵)
    哨兵模式是在“主从复制”的基础上增加了一组独立的哨兵节点。它的核心逻辑是自动化运维
    三大职责:
    1. 监控 (Monitoring): 哨兵会不断地检查你的主节点(Master)和从节点(Slave)是否运作正常。
    2. 自动故障迁移 (Automatic Failover): 这是最关键的。如果主节点挂了,哨兵们会通过“投票”选出一个从节点,把它拉拔成新的主节点。
    3. 通知 (Notification): 当故障转移发生后,哨兵会将新的主节点地址通知给客户端(你的 Python 服务),让请求自动转向新家。

    它是如何判断Redis“挂了”的?
    • 主观下线 (Subjective Down): 一个哨兵发现我不理它了,它会觉得我可能挂了。
    • 客观下线 (Objective Down):半数以上(通常配置为 $N/2 + 1$)的哨兵都认为我挂了,这时候才会真正触发自动切换逻辑。这能有效避免因为网络抖动导致的误判。

  • Redis Cluster (集群)
    如果说哨兵解决了“高可用”问题,那么集群模式在此基础上还解决了“海量数据”和“高并发”问题。
    • 核心机制:
      • 数据分片 (Sharding): 集群将数据划分为 16384 个哈希槽(Slots)。有多个主节点,每个主节点只负责其中的一部分槽位。这样即便你有几十 TB 的数据,我也能分摊开来处理。
      • 自带故障转移: 在集群中,主节点之间会互相监控。如果某个分片的主节点挂了,它的从节点会自动上位,整个集群不需要额外的哨兵节点也能自愈。
    • 集群的“抗压”逻辑:
      由于请求被分散到了不同的物理机器上,集群模式能承受的并发量远超哨兵。如果某个分片压力过大,你可以随时增加新的机器来分担槽位。
    • 处理逻辑
      Redis Cluster 架构下,请求的处理不再由一个“中心节点”分发,而是一个无中心化自动路由的过程 。
      当一个用户请求(例如 SET user_name "Redis")进入系统时,处理流程如下:
      • 第一步:计算哈希槽 (Slot Calculation)
        集群将整个数据库划分为 $16384$ 个哈希槽(Slots) 。
        当客户端发送请求时,首先需要确定这个 Key 属于哪个槽位:
        系统会对 Key 进行哈希运算(通常是 CRC16 算法)并对 $16384$ 取模 。
        公式可理解为:$slot = hash(key) \pmod{16384}$ 。
      • 第二步:客户端路由 (Client-Side Routing)
        在工程实践中,客户端(如 Python 的 Redis 集群库)通常是“智能”的,它们会缓存一份集群的槽位分布图 。
        客户端会根据计算出的 slot 直接寻找负责该槽位的主节点 (Master)
        这样请求就能直接发送到正确的目标节点,无需中间转发。
      • 第三步:节点检查与重定向 (Redirection)
        如果客户端发送到了错误的节点(例如因为集群扩容导致槽位迁移,但客户端缓存未更新):
        • 节点校验:收到请求的节点会检查该 slot 是否归自己管理 。
        • MOVED 错误:如果不在自己这里,节点会返回一个 MOVED 错误,并告知客户端该槽位现在由哪个 IP 和端口负责 。
        • 客户端更新:客户端收到 MOVED 后,会更新本地缓存的槽位图,并重新向正确的节点发送请求 。
      • 第四步:读写处理 (Processing)
        一旦请求到达正确的主节点,该节点就会执行相应的读写指令 。
        如果是写操作,主节点执行完后会将变动记录同步给它的从节点(Slave) 。
      • 第五步:异常处理 (Failure Detection)
        如果在请求过程中某个主节点挂了:
        • 集群内部会通过节点间的 Gossip 协议发现故障 。
        • 集群会自动进行故障转移,将对应的从节点提升为新主节点 。
        • 后续请求会自动路由到新的主节点上,确保高可用性 。


      总结:Redis Cluster 请求链路
      1. 算槽:客户端算出 Key 的槽位 。
      2. 直连:客户端直连对应的主节点 。
      3. 纠错:若节点变动,通过 MOVED 机制重定向 。
      4. 执行:主节点处理,并异步同步从节点 。

      这种设计让 Redis Cluster 能够轻松处理 TB 级数据和极致的并发,因为压力被均匀地分摊到了不同的主节点对上 。

哨兵 vs 集群:我该选哪一个?

维度Redis Sentinel (哨兵)Redis Cluster (集群)
核心目的高可用(保证挂了能自动恢复)高可用 + 海量扩展
数据分布每个节点都存完整的数据数据分片存储在不同节点
机器规模较小(通常 3-5 台足够)较大(最少需要 6 个节点:3主3从)
适用场景数据量不大(GB 级别),追求稳定数据量巨大(TB 级别),追求极致并发

3️⃣ 熔断与限流: 如果数据库真的顶不住了,利用 Sentinel(阿里的中间件)等工具开启限流或降级,保护核心链路。

  • 熔断
    熔断是一种自我保护机制。当数据库因为雪崩压力而响应变慢或频繁报错时,系统会自动切断对数据库的调用 。
    • 工作状态
      • 关闭 (Closed):一切正常,流量正常打向数据库。
      • 开启 (Open):当检测到数据库故障率达到阈值(如 50% 请求超时),熔断器跳闸。所有请求不再打向数据库,而是执行服务降级逻辑。
      • 半开 (Half-Open):经过一段冷却时间,熔断器允许少量流量尝试访问数据库。如果请求成功,则关闭熔断;否则重新开启。
    • 服务降级 (Fallback):熔断期间,系统会直接返回一个预设的“默认值”或“兜底数据”。
    • 目的:给数据库争取喘息和恢复的时间 。

  • 限流
    限流(Rate Limiting)是高并发架构中保护系统的核心手段之一。如果说熔断是发现问题后的“紧急避险”,那么限流就是前置的**“流量调度”**,确保进入系统的请求量始终在数据库或服务器的可控范围内 。
    限流的主要目的是通过牺牲部分可用性来换取全局的稳定性
    • 工作机制:为系统设置一个处理阈值(如每秒 1000次请求) 。
    • 超限处理:当请求超过阈值时,系统会执行预设策略:
      • 拒绝(Discard):直接返回错误信息,如“系统繁忙” 。
      • 排队(Queue):让请求进入队列,匀速处理。
      • 降级(Fallback):返回默认的兜底数据。


    两大核心算法详解
    在资料中提到的令牌桶漏桶算法是业界最常用的两种方案 。
    A. 漏桶算法 (Leaky Bucket)
    • 形象比喻:想象一个底部有小孔的桶。无论进水(请求)的速度有多快、多不规律,出水(处理)的速度始终是恒定的。如果桶满了,多出来的水就会直接溢出(丢弃请求) 。
    • 特点
      • 强制匀速:它能强行把突发流量(Bursty Traffic)整形成平滑流量。
      • 保护后端:对那些处理能力极其死板、不能承受任何波动的后端服务非常友好。
    • 缺点:无法应对合法的突发流量。即便系统当前很闲,它也只能按既定的速度处理。
    • 实现思路:
      漏桶算法的核心是**“出口恒定”** 。无论请求(水)流入的速度如何波动,它都以固定的速率处理请求 。
      • 变量定义
        • capacity:桶的容量,代表系统能排队等待的最大请求数 。
        • leak_rate:出水速度,比如每秒处理 $10$ 个请求 。
        • water:当前桶里的水量(待处理请求数)。
    • 计算逻辑
      1. 每当请求进来,先计算从上次请求到现在流走了多少水:$water = \max(0, water - (now - last_time) \times leak_rate)$。
      2. 判断当前桶是否已满:如果 $water + 1 \leq capacity$,则请求通过,且 $water += 1$;否则拒绝请求 。

    Python 代码实现:
    import time
    class LeakyBucket:
        def __init__(self, capacity, leak_rate):
            self.capacity = capacity      # 桶容量
            self.leak_rate = leak_rate    # 流出速率 (每秒)
            self.water = 0                # 当前水量
            self.last_check_time = time.time()
    
        def allow_request(self):
            now = time.time()
            # 1. 计算这段时间流走了多少
            leaked = (now - self.last_check_time) * self.leak_rate
            self.water = max(0, self.water - leaked)
            self.last_check_time = now
    
            # 2. 判断是否溢出
            if self.water + 1 <= self.capacity:
                self.water += 1
                return True
            return False # 溢出了,拒绝
    

    B. 令牌桶算法 (Token Bucket)
    • 形象比喻:有一个桶专门装令牌,系统以恒定的速度往里放令牌。每个请求进来必须先从桶里抢到一个令牌才能被处理 。如果桶空了,请求就被拒绝。
    • 特点
      • 允许突发:如果前一段时间请求很少,桶里积攒了大量令牌,那么当瞬时大流量进来时,这些请求可以瞬间全部通过。
      • 动态调整:它既限制了长期的平均频率,又保留了处理短期突发流量的灵活性。
    • 优势:这是目前主流框架(如 Sentinel)更倾向于使用的方案,因为它对用户请求更加友好 。
    • 实现思路
      令牌桶算法的核心是**“生成恒定,消费取令牌”** 。它允许短时间的突发流量,只要桶里还有积攒的令牌 。
      • 变量定义
        • capacity:桶的最大令牌数,决定了允许的最大突发流量 。
        • refill_rate:令牌生成的速率(每秒放多少个) 。
        • tokens:当前可用的令牌数。
      • 计算逻辑
        1. 计算自上次请求以来生成的令牌:$tokens = \min(capacity, tokens + (now - last_time) \times refill_rate)$ 。
        2. 判断是否有可用令牌:如果 $tokens \geq 1$,则扣除一个令牌并放行;否则拦截 。

      Python 代码实现:
      class TokenBucket:
          def __init__(self, capacity, refill_rate):
              self.capacity = capacity
              self.refill_rate = refill_rate
              self.tokens = capacity # 初始满桶
              self.last_refill_time = time.time()
      
          def allow_request(self):
              now = time.time()
              # 1. 补充令牌
              new_tokens = (now - self.last_refill_time) * self.refill_rate
              self.tokens = min(self.capacity, self.tokens + new_tokens)
              self.last_refill_time = now
      
              # 2. 消费令牌
              if self.tokens >= 1:
                  self.tokens -= 1
                  return True
              return False
      


    算法对比总结
    特性漏桶算法 (Leaky Bucket)令牌桶算法 (Token Bucket)
    处理速度严格恒定平均恒定,允许瞬时爆发
    突发流量不支持(直接丢弃)支持(只要桶里有令牌)
    核心目的强行平滑流量限制平均流入速率
    应用场景网络流量整形、极其脆弱的后台大多数 Web 应用、API 接口


    落地工具建议
    既然你正在使用 Python 服务,实现限流有以下几种健壮的选择:
    1. Sentinel-Python
      • 支持热点参数限流(例如限制某个特定 ID 的访问频率) 。
      • 可以配合 Dashboard 实时调整限流阈值,无需重启服务 。
    2. Redis + Lua 脚本
      • 这是分布式限流的经典做法。利用 Redis 的单线程原子性,配合 Lua 脚本实现令牌桶逻辑,可以保证多个 Python 节点共用一套限流规则。
    3. FastAPI / Flask 插件
      • slowapi(基于 Python 的 limits 库),可以非常简单地通过装饰器实现接口级的限流。

    总结:在处理缓存雪崩时,限流是第一道关卡 。它像“入场安检”一样,从源头确保数据库不会因为流量过载而崩溃 。
    你现在的业务场景中,流量是比较平稳的,还是经常会有突然爆发的情况(如整点秒杀)?这决定了你更适合选择哪种算法。

总结对比

场景问题核心核心对策
穿透数据不存在布隆过滤器 / 缓存空值
击穿热点 Key 过期互斥锁 / 逻辑过期
雪崩大量 Key 同时过期随机 TTL / 高可用集群

2 互斥锁

3 缓存与数据库的一致性维护