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)的区别
我们用一个非常直观的对比:
| 维度 | Redis | MySQL |
|---|---|---|
| 存储 | 内存 | 磁盘 |
| 速度 | 非常快 | 较慢 |
| 数据结构 | 丰富 | 表结构 |
| 查询方式 | 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、全页缓存、分布式限流。
- 列表 (List)
Redis List 是简单的字符串列表,按照插入顺序排序。你可以从头部(Left)或尾部(Right)添加元素。
- 内部实现: 在早期版本中使用双端链表和压缩列表,现在的 Redis 统一使用 Quicklist。Quicklist 是一个双向链表,但每个节点都是一个 ZipList,结合了两者的优点:既减少了内存碎片,又避免了普通链表指针占用过大空间的问题。
- 常用操作:
LPUSH/RPUSH:入队。LPOP/RPOP:出队。BRPOP/BLPOP:阻塞式出队(用于简单消息队列)。
- 典型场景: 消息队列、最新动态列表(如朋友圈时间轴)。
- 哈希 (Hash)
Hash 是一个键值对(Field-Value)集合,特别适合存储对象。
- 内部实现:
- 当字段较少且值较小时,使用 ZipList(节省内存)。
- 当数量超过阈值(默认 512 个字段或单值超过 64 字节)时,转换为 Hashtable。
- 常用操作:
HSET/HGET:操作单个字段。HMSET/HMGET:批量操作。HINCRBY:对指定字段进行原子增量。
- 典型场景: 存储用户信息(相比 String 序列化,Hash 可以只更新其中一个字段,效率更高)。
- 集合 (Set)
Set 是 String 类型的无序集合,元素具有唯一性。
- 内部实现:
- 如果数据全是整数且数量较少,使用 Intset。
- 否则使用 Hashtable(Value 指向 NULL)。
- 常用操作:
SADD:添加元素。SISMEMBER:判断元素是否存在($O(1)$ 效率)。SINTER/SUNION/SDIFF:交集、并集、差集计算。
- 典型场景: 抽奖系统(唯一性)、共同好友(交集计算)、标签(Tagging)。
- 有序集合 (Sorted Set / ZSet)
ZSet 在 Set 的基础上,为每个元素关联了一个 Score(分数),使得集合中的元素可以按分数排序。
Redis 会为每个 Key 存储元数据。如果你有数亿个小的 String Key,内存消耗会非常惊人。此时,将这些 Key 聚合到 Hash 中存储(利用 ZipList 的紧凑布局)通常能节省 60% 以上的内存空间。
- 内部实现: 核心是 跳跃表 (SkipList) 和 Hashtable。
- 跳跃表通过多层索引实现平均 $O(\log N)$ 的查找效率,性能堪比红黑树,但实现更简单。
- 常用操作:
ZADD:添加元素及分数。ZRANGE/ZREVRANGE:按分数范围获取元素(常用于分页)。ZSCORE:查询指定元素的分数。
- 典型场景: 实时排行榜、带权重的任务队列、范围查找。
数据结构对比总结
| 数据类型 | 底层实现 (主要) | 常用操作复杂度 | 排序支持 | 唯一性 |
|---|---|---|---|---|
| String | SDS | $O(1)$ | N/A | 是 (Key唯一) |
| List | Quicklist | $O(1)$ (两端) | 按插入顺序 | 否 |
| Hash | ZipList / Hashtable | $O(1)$ | N/A | 字段唯一 |
| Set | Intset / Hashtable | $O(1)$ | 否 | 是 |
| ZSet | SkipList / ZipList | $O(\log N)$ | 是 (按分数) | 是 |
常见问题
1 缓存穿透、击穿、雪崩
1.1 缓存穿透 (Cache Penetration)
场景描述:
请求的数据在缓存中没有,在数据库中也没有。由于两边都查不到,请求每次都会“穿透”缓存直接打到数据库上。如果是恶意攻击(比如查询 ID 为 -1 的数据),数据库压力会骤增。
解决方案:
1️⃣布隆过滤器 (Bloom Filter)
布隆过滤器是一种空间效率极高的概率型数据结构 。
- 它的核心: 一个超大的位数组(Bit Array)和一组哈希函数(Hash Functions)。
- 它的特点: 占用的内存极小,查询速度极快,但存在一定的误判率。
工作流程:
- 第一步:初始化(添加 Key)
当你想把一个 Key(比如 User_123)加入过滤器时:
- 布隆过滤器会用多个不同的哈希函数对这个 Key 进行计算。
- 每个哈希函数都会得到一个数组下标(索引位置)。
- 过滤器将位数组中这些对应的位置全部设置为 1。
- 第二步:查询(判断是否存在)
当有一个请求进来查询 User_456 时:
- 同样用那几个哈希函数算一遍下标。
- 看结果:
- 只要有一个位置是 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,这就会导致缓存穿透的风险稍微增加(虽然影响不大)。
健壮的补救措施:
- 定期重建: 每隔一段时间,重新从数据库读取合法 ID,生成一个新的过滤器替换旧的。
- 计数布隆过滤器 (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):
简单来说,只允许一个请求去数据库查数据并写回缓存,其他请求等待或重试。它的核心逻辑就是:当热点数据失效时,只允许一个请求去“重建”缓存,其他请求必须等待。
核心工作流程
我们可以把这个过程想象成“修路”:路(缓存)断了,只能派一个维修工(请求)去修,修好之前,其他车辆(并发请求)只能在路口等着。
- 尝试获取数据: 请求首先查询 Redis 。
- 触发击穿: 发现数据已过期(Cache Miss) 。
- 抢占互斥锁: 请求尝试使用
SETNX或SET key val NX EX命令在我这里设置一个特殊的 Key(锁) 。- 抢锁成功: 进入“重建模式”,去查询数据库并将结果回写到 Redis,随后删除锁 Key 释放资源 。
- 抢锁失败: 进入“等待模式”,线程休眠一小段时间(如 50ms),然后重新尝试从缓存获取数据 。
“双检锁”(Double-Check)
这是互斥锁方案中最容易被忽略、也最重要的优化细节 。
如果不做双检:
假设有 1000 个请求同时发现缓存失效。请求 A 抢锁成功去查 DB 了;请求 B 抢锁失败开始休眠。等请求 A 查完数据库并更新完缓存,释放了锁,请求 B 醒来立刻又去抢锁,抢锁成功后如果不检查缓存,它会再次冲向数据库。
正确的双检逻辑:
- 第一次检查:缓存不存在,准备抢锁。
- 抢到锁后。
- 第二次检查: 再次查询 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 (哨兵)
哨兵模式是在“主从复制”的基础上增加了一组独立的哨兵节点。它的核心逻辑是自动化运维。
三大职责:- 监控 (Monitoring): 哨兵会不断地检查你的主节点(Master)和从节点(Slave)是否运作正常。
- 自动故障迁移 (Automatic Failover): 这是最关键的。如果主节点挂了,哨兵们会通过“投票”选出一个从节点,把它拉拔成新的主节点。
- 通知 (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 请求链路- 算槽:客户端算出 Key 的槽位 。
- 直连:客户端直连对应的主节点 。
- 纠错:若节点变动,通过
MOVED机制重定向 。 - 执行:主节点处理,并异步同步从节点 。
这种设计让 Redis Cluster 能够轻松处理 TB 级数据和极致的并发,因为压力被均匀地分摊到了不同的主节点对上 。 - 第一步:计算哈希槽 (Slot Calculation)
- 核心机制:
哨兵 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:当前桶里的水量(待处理请求数)。
- 变量定义:
- 计算逻辑:
- 每当请求进来,先计算从上次请求到现在流走了多少水:$water = \max(0, water - (now - last_time) \times leak_rate)$。
- 判断当前桶是否已满:如果 $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:当前可用的令牌数。
- 计算逻辑:
- 计算自上次请求以来生成的令牌:$tokens = \min(capacity, tokens + (now - last_time) \times refill_rate)$ 。
- 判断是否有可用令牌:如果 $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 服务,实现限流有以下几种健壮的选择:- Sentinel-Python:
- 支持热点参数限流(例如限制某个特定 ID 的访问频率) 。
- 可以配合 Dashboard 实时调整限流阈值,无需重启服务 。
- Redis + Lua 脚本:
- 这是分布式限流的经典做法。利用 Redis 的单线程原子性,配合 Lua 脚本实现令牌桶逻辑,可以保证多个 Python 节点共用一套限流规则。
- FastAPI / Flask 插件:
- 如
slowapi(基于 Python 的 limits 库),可以非常简单地通过装饰器实现接口级的限流。
- 如
总结:在处理缓存雪崩时,限流是第一道关卡 。它像“入场安检”一样,从源头确保数据库不会因为流量过载而崩溃 。
你现在的业务场景中,流量是比较平稳的,还是经常会有突然爆发的情况(如整点秒杀)?这决定了你更适合选择哪种算法。
总结对比
| 场景 | 问题核心 | 核心对策 |
|---|---|---|
| 穿透 | 数据不存在 | 布隆过滤器 / 缓存空值 |
| 击穿 | 热点 Key 过期 | 互斥锁 / 逻辑过期 |
| 雪崩 | 大量 Key 同时过期 | 随机 TTL / 高可用集群 |