[{"data":1,"prerenderedAt":2964},["ShallowReactive",2],{"blog-post-/docs/redis":3},{"id":4,"title":5,"body":6,"description":2952,"extension":2953,"meta":2954,"navigation":2342,"ogImage":2956,"path":2960,"seo":2961,"stem":2962,"__hash__":2963},"content/docs/Redis.md","Redis",{"type":7,"value":8,"toc":2939},"minimark",[9,13,18,28,31,34,37,40,51,54,90,93,103,106,113,115,118,121,124,127,129,132,135,146,149,156,158,163,166,243,245,250,255,258,261,272,275,280,282,287,290,307,310,317,319,324,327,335,337,342,347,349,354,357,363,366,372,374,378,381,383,388,399,447,449,457,460,513,515,523,526,581,583,591,598,653,655,663,670,679,730,732,737,855,858,863,868,873,876,881,887,894,919,924,931,938,952,959,966,997,999,1004,1073,1075,1080,1083,1088,1095,1113,1118,1136,1138,1143,1150,1155,1169,1175,1177,1188,1195,1197,1201,1205,1208,1212,1217,1223,1228,1246,1248,1253,1256,1319,1321,1326,1381,1383,1388,1395,1397,1402,1413,1433,1435,1441,1447,1449,1454,1457,1498,1500,1505,1508,1513,1519,1524,1538,1540,1545,1548,1585,1587,1591,1595,1598,1602,1613,1627,1916,1918,1923,2000,2002,2009,2067,2069,2856,2858,2863,2925,2927,2931,2935],[10,11,5],"h1",{"id":12},"redis",[14,15,17],"h2",{"id":16},"_1-基础概念","1 基础概念",[19,20,21],"blockquote",{},[22,23,24],"p",{},[25,26,27],"strong",{},"Redis 是一个“基于内存的高性能 Key-Value 数据库”",[22,29,30],{},"我们把这句话拆开理解 👇",[32,33],"hr",{},[22,35,36],{},"1️⃣ Key-Value 数据库",[22,38,39],{},"Redis 的数据结构是：",[41,42,48],"pre",{"className":43,"code":45,"language":46,"meta":47},[44],"language-text","key -> value\n","text","",[49,50,45],"code",{"__ignoreMap":47},[22,52,53],{},"例如：",[41,55,59],{"className":56,"code":57,"language":58,"meta":47,"style":47},"language-bash shiki shiki-themes github-light github-dark","SET name \"Liu\"\nGET name  → \"Liu\"\n","bash",[49,60,61,77],{"__ignoreMap":47},[62,63,66,70,74],"span",{"class":64,"line":65},"line",1,[62,67,69],{"class":68},"sScJk","SET",[62,71,73],{"class":72},"sZZnC"," name",[62,75,76],{"class":72}," \"Liu\"\n",[62,78,80,83,85,88],{"class":64,"line":79},2,[62,81,82],{"class":68},"GET",[62,84,73],{"class":72},[62,86,87],{"class":72},"  →",[62,89,76],{"class":72},[22,91,92],{},"👉 类似：",[94,95,96,100],"ul",{},[97,98,99],"li",{},"Python dict",[97,101,102],{},"Java HashMap",[22,104,105],{},"但 Redis 是：",[19,107,108],{},[22,109,110],{},[25,111,112],{},"跨进程 + 可持久化 + 网络访问",[32,114],{},[22,116,117],{},"2️⃣ 数据存在“内存”里（核心）",[22,119,120],{},"👉 Redis 默认把数据存在 RAM 中，而不是磁盘",[22,122,123],{},"带来的影响：",[22,125,126],{},"✔ 极快（微秒级）\n❌ 容量受限\n❌ 成本更高",[32,128],{},[22,130,131],{},"3️⃣ 它是一个“数据库”",[22,133,134],{},"虽然很多人把 Redis 当缓存用，但它本质是：",[94,136,137,140,143],{},[97,138,139],{},"支持持久化（RDB / AOF）",[97,141,142],{},"支持数据结构",[97,144,145],{},"支持事务（弱事务）",[22,147,148],{},"👉 所以它是：",[19,150,151],{},[22,152,153],{},[25,154,155],{},"NoSQL 数据库的一种",[32,157],{},[22,159,160],{},[25,161,162],{},"Redis 和传统数据库（MySQL）的区别",[22,164,165],{},"我们用一个非常直观的对比：",[167,168,169,184],"table",{},[170,171,172],"thead",{},[173,174,175,179,181],"tr",{},[176,177,178],"th",{},"维度",[176,180,5],{},[176,182,183],{},"MySQL",[185,186,187,199,210,221,232],"tbody",{},[173,188,189,193,196],{},[190,191,192],"td",{},"存储",[190,194,195],{},"内存",[190,197,198],{},"磁盘",[173,200,201,204,207],{},[190,202,203],{},"速度",[190,205,206],{},"非常快",[190,208,209],{},"较慢",[173,211,212,215,218],{},[190,213,214],{},"数据结构",[190,216,217],{},"丰富",[190,219,220],{},"表结构",[173,222,223,226,229],{},[190,224,225],{},"查询方式",[190,227,228],{},"key 查找",[190,230,231],{},"SQL",[173,233,234,237,240],{},[190,235,236],{},"适合场景",[190,238,239],{},"高频访问",[190,241,242],{},"复杂查询",[32,244],{},[22,246,247],{},[25,248,249],{},"Redis 的核心特点",[22,251,252],{},[25,253,254],{},"⭐ 特点1：单线程模型（非常重要）",[22,256,257],{},"Redis 主要是单线程执行命令：",[22,259,260],{},"👉 好处：",[94,262,263,266,269],{},[97,264,265],{},"没有锁竞争",[97,267,268],{},"简单稳定",[97,270,271],{},"性能高",[22,273,274],{},"👉 适合：",[94,276,277],{},[97,278,279],{},"短、快操作",[32,281],{},[22,283,284],{},[25,285,286],{},"⭐ 特点2：丰富的数据结构",[22,288,289],{},"不像 MySQL 只有表，Redis 有：",[94,291,292,295,298,301,304],{},[97,293,294],{},"String",[97,296,297],{},"Hash",[97,299,300],{},"List",[97,302,303],{},"Set",[97,305,306],{},"Sorted Set",[22,308,309],{},"👉 这让它可以：",[19,311,312],{},[22,313,314],{},[25,315,316],{},"直接支持业务结构",[32,318],{},[22,320,321],{},[25,322,323],{},"⭐ 特点3：支持持久化",[22,325,326],{},"Redis 不是纯缓存：",[94,328,329,332],{},[97,330,331],{},"可以保存数据到磁盘",[97,333,334],{},"重启后恢复",[32,336],{},[22,338,339],{},[25,340,341],{},"⭐ 特点4：支持高并发",[94,343,344],{},[97,345,346],{},"单机可以支撑：\n👉 10w+ QPS（常见）",[32,348],{},[22,350,351],{},[25,352,353],{},"Redis 的基本工作流程",[22,355,356],{},"你可以想象一个典型请求：",[41,358,361],{"className":359,"code":360,"language":46,"meta":47},[44],"客户端 → Redis → 返回结果\n",[49,362,360],{"__ignoreMap":47},[22,364,365],{},"如果作为缓存：",[41,367,370],{"className":368,"code":369,"language":46,"meta":47},[44],"用户请求\n   ↓\nRedis（有数据）\n   ↓\n直接返回 ✅\n\nRedis（没有）\n   ↓\n数据库\n   ↓\n写入 Redis\n   ↓\n返回\n",[49,371,369],{"__ignoreMap":47},[32,373],{},[14,375,377],{"id":376},"_2-数据结构","2 数据结构",[22,379,380],{},"Redis 支持多种数据结构，包括 String、List、Hash、Set 和 ZSet 等。String 用于缓存和计数，Hash 用于存储对象，List 可实现简单队列，Set 用于去重和集合运算，ZSet 支持排序场景如排行榜。此外还有 Bitmap、HyperLogLog 和 Stream 等结构用于特定场景。通过这些数据结构，Redis 能够高效支持缓存、计数、队列和实时统计等功能。",[32,382],{},[22,384,385],{},[25,386,387],{},"1. 字符串 (String)",[22,389,390,391,394,395,398],{},"String 是 Redis 最基础的类型，它是",[25,392,393],{},"二进制安全","的，这意味着它可以存储任何数据（如 JPEG 图像或序列化的 JSON 对象），最大上限为 ",[25,396,397],{},"512MB","。",[94,400,401,411,441],{},[97,402,403,406,407,410],{},[25,404,405],{},"内部实现："," 采用 ",[25,408,409],{},"SDS (Simple Dynamic String)","。相比 C 语言原生字符串，SDS 获取长度的时间复杂度为 $O(1)$，且支持空间预分配和惰性空间释放，避免了频繁的内存重分配。",[97,412,413,416],{},[25,414,415],{},"常用操作：",[94,417,418,426,435],{},[97,419,420,422,423,425],{},[49,421,69],{}," / ",[49,424,82],{},"：基础读写。",[97,427,428,422,431,434],{},[49,429,430],{},"INCR",[49,432,433],{},"DECR","：原子增减（常用于计数器）。",[97,436,437,440],{},[49,438,439],{},"SETNX","：键不存在时才设置（分布式锁的核心）。",[97,442,443,446],{},[25,444,445],{},"典型场景："," 缓存 Session、全页缓存、分布式限流。",[32,448],{},[450,451,452],"ol",{"start":79},[97,453,454],{},[25,455,456],{},"列表 (List)",[22,458,459],{},"Redis List 是简单的字符串列表，按照插入顺序排序。你可以从头部（Left）或尾部（Right）添加元素。",[94,461,462,475,508],{},[97,463,464,466,467,470,471,474],{},[25,465,405],{}," 在早期版本中使用双端链表和压缩列表，现在的 Redis 统一使用 ",[25,468,469],{},"Quicklist","。Quicklist 是一个双向链表，但每个节点都是一个 ",[25,472,473],{},"ZipList","，结合了两者的优点：既减少了内存碎片，又避免了普通链表指针占用过大空间的问题。",[97,476,477,479],{},[25,478,415],{},[94,480,481,490,499],{},[97,482,483,422,486,489],{},[49,484,485],{},"LPUSH",[49,487,488],{},"RPUSH","：入队。",[97,491,492,422,495,498],{},[49,493,494],{},"LPOP",[49,496,497],{},"RPOP","：出队。",[97,500,501,422,504,507],{},[49,502,503],{},"BRPOP",[49,505,506],{},"BLPOP","：阻塞式出队（用于简单消息队列）。",[97,509,510,512],{},[25,511,445],{}," 消息队列、最新动态列表（如朋友圈时间轴）。",[32,514],{},[450,516,518],{"start":517},3,[97,519,520],{},[25,521,522],{},"哈希 (Hash)",[22,524,525],{},"Hash 是一个键值对（Field-Value）集合，特别适合存储对象。",[94,527,528,546,576],{},[97,529,530,532],{},[25,531,405],{},[94,533,534,540],{},[97,535,536,537,539],{},"当字段较少且值较小时，使用 ",[25,538,473],{},"（节省内存）。",[97,541,542,543,398],{},"当数量超过阈值（默认 512 个字段或单值超过 64 字节）时，转换为 ",[25,544,545],{},"Hashtable",[97,547,548,550],{},[25,549,415],{},[94,551,552,561,570],{},[97,553,554,422,557,560],{},[49,555,556],{},"HSET",[49,558,559],{},"HGET","：操作单个字段。",[97,562,563,422,566,569],{},[49,564,565],{},"HMSET",[49,567,568],{},"HMGET","：批量操作。",[97,571,572,575],{},[49,573,574],{},"HINCRBY","：对指定字段进行原子增量。",[97,577,578,580],{},[25,579,445],{}," 存储用户信息（相比 String 序列化，Hash 可以只更新其中一个字段，效率更高）。",[32,582],{},[450,584,586],{"start":585},4,[97,587,588],{},[25,589,590],{},"集合 (Set)",[22,592,593,594,597],{},"Set 是 String 类型的",[25,595,596],{},"无序集合","，元素具有唯一性。",[94,599,600,618,648],{},[97,601,602,604],{},[25,603,405],{},[94,605,606,612],{},[97,607,608,609,398],{},"如果数据全是整数且数量较少，使用 ",[25,610,611],{},"Intset",[97,613,614,615,617],{},"否则使用 ",[25,616,545],{},"（Value 指向 NULL）。",[97,619,620,622],{},[25,621,415],{},[94,623,624,630,636],{},[97,625,626,629],{},[49,627,628],{},"SADD","：添加元素。",[97,631,632,635],{},[49,633,634],{},"SISMEMBER","：判断元素是否存在（$O(1)$ 效率）。",[97,637,638,422,641,422,644,647],{},[49,639,640],{},"SINTER",[49,642,643],{},"SUNION",[49,645,646],{},"SDIFF","：交集、并集、差集计算。",[97,649,650,652],{},[25,651,445],{}," 抽奖系统（唯一性）、共同好友（交集计算）、标签（Tagging）。",[32,654],{},[450,656,658],{"start":657},5,[97,659,660],{},[25,661,662],{},"有序集合 (Sorted Set / ZSet)",[22,664,665,666,669],{},"ZSet 在 Set 的基础上，为每个元素关联了一个 ",[25,667,668],{},"Score（分数）","，使得集合中的元素可以按分数排序。",[22,671,672,675,676,678],{},[25,673,674],{},"Redis 会为每个 Key 存储元数据","。如果你有数亿个小的 String Key，内存消耗会非常惊人。此时，将这些 Key 聚合到 ",[25,677,297],{}," 中存储（利用 ZipList 的紧凑布局）通常能节省 60% 以上的内存空间。",[94,680,681,698,725],{},[97,682,683,685,686,689,690,692,693],{},[25,684,405],{}," 核心是 ",[25,687,688],{},"跳跃表 (SkipList)"," 和 ",[25,691,545],{},"。\n",[94,694,695],{},[97,696,697],{},"跳跃表通过多层索引实现平均 $O(\\log N)$ 的查找效率，性能堪比红黑树，但实现更简单。",[97,699,700,702],{},[25,701,415],{},[94,703,704,710,719],{},[97,705,706,709],{},[49,707,708],{},"ZADD","：添加元素及分数。",[97,711,712,422,715,718],{},[49,713,714],{},"ZRANGE",[49,716,717],{},"ZREVRANGE","：按分数范围获取元素（常用于分页）。",[97,720,721,724],{},[49,722,723],{},"ZSCORE","：查询指定元素的分数。",[97,726,727,729],{},[25,728,445],{}," 实时排行榜、带权重的任务队列、范围查找。",[32,731],{},[22,733,734],{},[25,735,736],{},"数据结构对比总结",[167,738,739,768],{},[170,740,741],{},[173,742,743,748,753,758,763],{},[176,744,745],{},[25,746,747],{},"数据类型",[176,749,750],{},[25,751,752],{},"底层实现 (主要)",[176,754,755],{},[25,756,757],{},"常用操作复杂度",[176,759,760],{},[25,761,762],{},"排序支持",[176,764,765],{},[25,766,767],{},"唯一性",[185,769,770,788,805,821,837],{},[173,771,772,776,779,782,785],{},[190,773,774],{},[25,775,294],{},[190,777,778],{},"SDS",[190,780,781],{},"$O(1)$",[190,783,784],{},"N/A",[190,786,787],{},"是 (Key唯一)",[173,789,790,794,796,799,802],{},[190,791,792],{},[25,793,300],{},[190,795,469],{},[190,797,798],{},"$O(1)$ (两端)",[190,800,801],{},"按插入顺序",[190,803,804],{},"否",[173,806,807,811,814,816,818],{},[190,808,809],{},[25,810,297],{},[190,812,813],{},"ZipList / Hashtable",[190,815,781],{},[190,817,784],{},[190,819,820],{},"字段唯一",[173,822,823,827,830,832,834],{},[190,824,825],{},[25,826,303],{},[190,828,829],{},"Intset / Hashtable",[190,831,781],{},[190,833,804],{},[190,835,836],{},"是",[173,838,839,844,847,850,853],{},[190,840,841],{},[25,842,843],{},"ZSet",[190,845,846],{},"SkipList / ZipList",[190,848,849],{},"$O(\\log N)$",[190,851,852],{},"是 (按分数)",[190,854,836],{},[14,856,857],{"id":857},"常见问题",[859,860,862],"h3",{"id":861},"_1-缓存穿透击穿雪崩","1 缓存穿透、击穿、雪崩",[864,865,867],"h4",{"id":866},"_11-缓存穿透-cache-penetration","1.1 缓存穿透 (Cache Penetration)",[22,869,870],{},[25,871,872],{},"场景描述：",[22,874,875],{},"请求的数据在缓存中没有，在数据库中也没有。由于两边都查不到，请求每次都会“穿透”缓存直接打到数据库上。如果是恶意攻击（比如查询 ID 为 -1 的数据），数据库压力会骤增。",[22,877,878],{},[25,879,880],{},"解决方案：",[22,882,883,884],{},"1️⃣",[25,885,886],{},"布隆过滤器 (Bloom Filter)",[22,888,889,890,893],{},"布隆过滤器是一种空间效率极高的",[25,891,892],{},"概率型数据结构"," 。",[94,895,896,910],{},[97,897,898,901,902,905,906,909],{},[25,899,900],{},"它的核心："," 一个超大的",[25,903,904],{},"位数组","（Bit Array）和一组",[25,907,908],{},"哈希函数","（Hash Functions）。",[97,911,912,915,916,398],{},[25,913,914],{},"它的特点："," 占用的内存极小，查询速度极快，但存在一定的",[25,917,918],{},"误判率",[22,920,921],{},[25,922,923],{},"工作流程：",[94,925,926],{},[97,927,928],{},[25,929,930],{},"第一步：初始化（添加 Key）",[22,932,933,934,937],{},"当你想把一个 Key（比如 ",[49,935,936],{},"User_123","）加入过滤器时：",[450,939,940,943,946],{},[97,941,942],{},"布隆过滤器会用多个不同的哈希函数对这个 Key 进行计算。",[97,944,945],{},"每个哈希函数都会得到一个数组下标（索引位置）。",[97,947,948,949,398],{},"过滤器将位数组中这些对应的位置全部设置为 ",[25,950,951],{},"1",[94,953,954],{},[97,955,956],{},[25,957,958],{},"第二步：查询（判断是否存在）",[22,960,961,962,965],{},"当有一个请求进来查询 ",[49,963,964],{},"User_456"," 时：",[450,967,968,971],{},[97,969,970],{},"同样用那几个哈希函数算一遍下标。",[97,972,973,976],{},[25,974,975],{},"看结果：",[94,977,978,988],{},[97,979,980,983,984,987],{},[25,981,982],{},"只要有一个位置是 0","：那这个 Key ",[25,985,986],{},"绝对不存在","！直接拦截 。",[97,989,990,983,993,996],{},[25,991,992],{},"如果全部位置都是 1",[25,994,995],{},"可能存在","。为什么是可能？因为哈希碰撞会导致不同的 Key 刚好占用了相同的位。",[32,998],{},[22,1000,1001],{},[25,1002,1003],{},"布隆过滤器的“优缺点”",[167,1005,1006,1019],{},[170,1007,1008],{},[173,1009,1010,1014],{},[176,1011,1012],{},[25,1013,178],{},[176,1015,1016],{},[25,1017,1018],{},"特点说明",[185,1020,1021,1034,1047,1060],{},[173,1022,1023,1028],{},[190,1024,1025],{},[25,1026,1027],{},"空间效率",[190,1029,1030,1033],{},[25,1031,1032],{},"极高","。不需要存储原始数据，只存 0 和 1。",[173,1035,1036,1041],{},[190,1037,1038],{},[25,1039,1040],{},"查询速度",[190,1042,1043,1046],{},[25,1044,1045],{},"极快","。计算几次哈希的时间复杂度是 $O(k)$，与数据量无关。",[173,1048,1049,1054],{},[190,1050,1051],{},[25,1052,1053],{},"准确性",[190,1055,1056,1059],{},[25,1057,1058],{},"有误判","。可能会把不存在的数据误判为存在，但绝不会把存在的数据误判为不存在。",[173,1061,1062,1067],{},[190,1063,1064],{},[25,1065,1066],{},"删除操作",[190,1068,1069,1072],{},[25,1070,1071],{},"困难","。不能简单地把位改成 0，因为一个位可能被多个 Key 共享。",[32,1074],{},[22,1076,1077],{},[25,1078,1079],{},"落地到 Redis 的方案",[22,1081,1082],{},"在实际生产中，你有两种方式实现布隆过滤器：",[22,1084,1085],{},[25,1086,1087],{},"方案 A：Redis 官方插件 (RedisBloom)",[22,1089,1090,1091,1094],{},"我是支持原生扩展的。安装 ",[49,1092,1093],{},"RedisBloom"," 模块后，你可以直接用命令：",[94,1096,1097,1103],{},[97,1098,1099,1102],{},[49,1100,1101],{},"BF.ADD user_indices user_1","：添加 Key。",[97,1104,1105,1108,1109,1112],{},[49,1106,1107],{},"BF.EXISTS user_indices user_1","：判断是否存在。",[1110,1111],"br",{},"这种方案最简单，性能也最好。",[22,1114,1115],{},[25,1116,1117],{},"方案 B：客户端实现 (如 Guava 或 Redisson)",[94,1119,1120,1126],{},[97,1121,1122,1125],{},[25,1123,1124],{},"Guava BloomFilter："," 适合单机环境，内存占用在 JVM 里。",[97,1127,1128,1131,1132,1135],{},[25,1129,1130],{},"Redisson："," 适合分布式环境，它会利用我的 ",[49,1133,1134],{},"Bitmaps"," 功能来实现分布式布隆过滤器。",[32,1137],{},[22,1139,1140],{},[25,1141,1142],{},"一个绕不开的坑：数据删除了怎么办？",[22,1144,1145,1146,1149],{},"正如前面提到的，布隆过滤器",[25,1147,1148],{},"很难删除数据","。如果数据库里的一条记录删除了，过滤器里对应的位还是 1，这就会导致缓存穿透的风险稍微增加（虽然影响不大）。",[22,1151,1152],{},[25,1153,1154],{},"健壮的补救措施：",[450,1156,1157,1163],{},[97,1158,1159,1162],{},[25,1160,1161],{},"定期重建："," 每隔一段时间，重新从数据库读取合法 ID，生成一个新的过滤器替换旧的。",[97,1164,1165,1168],{},[25,1166,1167],{},"计数布隆过滤器 (Counting Bloom Filter)："," 位数组里存的不是 0/1，而是计数器。但这会成倍增加内存消耗。",[22,1170,1171,1174],{},[25,1172,1173],{},"总结一下："," 布隆过滤器的核心价值在于**“快速拒绝”** 。只要它能拦下 99% 的恶意请求，剩下的 1% 误判对数据库来说就是毛毛雨了。",[32,1176],{},[22,1178,1179,1180,1183,1184,1187],{},"2️⃣ ",[25,1181,1182],{},"缓存空对象："," 如果数据库查不到，也往我这里存一个 ",[49,1185,1186],{},"null"," 或特殊标记，并设置一个简短的过期时间（比如 5 分钟）。",[22,1189,1190,1191,1194],{},"3️⃣",[25,1192,1193],{},"参数校验："," 在接口层对不合法的请求参数（如负数 ID）直接拦截。",[32,1196],{},[864,1198,1200],{"id":1199},"_12-缓存击穿-cache-breakdown","1.2 缓存击穿 (Cache Breakdown)",[22,1202,1203],{},[25,1204,872],{},[22,1206,1207],{},"某一个“超级热点”Key（比如双11的秒杀商品），在过期的瞬间，正好有海量并发请求打过来。因为缓存失效，这些请求会同时冲向数据库去加载数据，导致数据库瞬间挂掉。",[22,1209,1210],{},[25,1211,880],{},[22,1213,883,1214],{},[25,1215,1216],{},"逻辑过期",[22,1218,1219,1220],{},"物理上不设置过期时间，但在 Value 里存一个“逻辑过期时间”。发现过期时，由后台异步线程去更新数据，期间其他请求先返回旧数据。与传统的互斥锁方案不同，它的核心思想是：",[25,1221,1222],{},"“宁可给用户看一眼旧数据，也绝不让数据库停摆。”",[22,1224,1225],{},[25,1226,1227],{},"核心设计：物理永不过期",[94,1229,1230,1236],{},[97,1231,1232,1235],{},[25,1233,1234],{},"物理层面","：在向Redis存储数据时，不设置任何 TTL（生存时间） 。这意味着该数据在 Redis 中是“永不过期”的，除非内存满了被置换，否则它永远不会因为时间到了而消失 。",[97,1237,1238,1241,1242,1245],{},[25,1239,1240],{},"逻辑层面","：程序员在存储的 Value 内部嵌套一个字段，手动记录一个“逻辑过期时间”（比如：",[49,1243,1244],{},"expire: 2026-04-21 12:00:00","） 。",[32,1247],{},[22,1249,1250],{},[25,1251,1252],{},"详细工作流程",[22,1254,1255],{},"当请求进来时，不再是简单的“查到了就返回”，而是多了一次逻辑判断：",[94,1257,1258,1264,1284,1292],{},[97,1259,1260,1263],{},[25,1261,1262],{},"查询缓存","：请求从 Redis 中获取数据 。",[97,1265,1266,1269,1270],{},[25,1267,1268],{},"判断逻辑时间","：",[94,1271,1272,1278],{},[97,1273,1274,1277],{},[25,1275,1276],{},"未过期","：直接将数据返回给用户 。",[97,1279,1280,1283],{},[25,1281,1282],{},"已过期","：说明数据已经“不新鲜”了，需要更新 。",[97,1285,1286,1289,1290,1245],{},[25,1287,1288],{},"尝试抢占锁","：请求会尝试获取一个互斥锁（如 ",[49,1291,439],{},[97,1293,1294,1269,1297],{},[25,1295,1296],{},"处理过期数据",[94,1298,1299,1313],{},[97,1300,1301,1304,1305,1308,1309,1312],{},[25,1302,1303],{},"抢锁成功","：开启一个",[25,1306,1307],{},"后台异步线程","去查询数据库并更新缓存（同时更新逻辑过期时间），然后释放锁 。",[25,1310,1311],{},"重点是","：当前请求不等待后台更新完成，而是立即返回手头这份“旧数据” 。",[97,1314,1315,1318],{},[25,1316,1317],{},"抢锁失败","：说明已经有其他线程正在更新数据了。当前请求不阻塞，也直接返回手头这份“旧数据” 。",[32,1320],{},[22,1322,1323],{},[25,1324,1325],{},"方案优缺点分析",[167,1327,1328,1340],{},[170,1329,1330],{},[173,1331,1332,1336],{},[176,1333,1334],{},[25,1335,178],{},[176,1337,1338],{},[25,1339,1018],{},[185,1341,1342,1355,1368],{},[173,1343,1344,1349],{},[190,1345,1346],{},[25,1347,1348],{},"性能体验",[190,1350,1351,1354],{},[25,1352,1353],{},"极佳"," 。由于请求永远不需要等待（无阻塞），响应速度极快，用户体验非常流畅 。",[173,1356,1357,1362],{},[190,1358,1359],{},[25,1360,1361],{},"数据一致性",[190,1363,1364,1367],{},[25,1365,1366],{},"较弱"," 。在异步线程完成更新之前，所有用户看到的都是过期数据，属于“最终一致性” 。",[173,1369,1370,1375],{},[190,1371,1372],{},[25,1373,1374],{},"实现复杂度",[190,1376,1377,1380],{},[25,1378,1379],{},"较高"," 。需要额外维护异步线程池以及代码中逻辑时间的判断逻辑 。",[32,1382],{},[22,1384,1385],{},[25,1386,1387],{},"为什么说它足够健壮？",[22,1389,1390,1391,1394],{},"逻辑过期方案最强悍的一点在于",[25,1392,1393],{},"可用性极高"," 。 在面对超级热点 Key 时，即便更新数据的过程需要耗时 $2$ 秒钟（比如复杂的 SQL 聚合），在这 $2$ 秒内的成千上万个并发请求都会因为拿到了旧数据而快速返回，不会因为排队等锁而导致服务器线程池耗尽 。",[32,1396],{},[22,1398,1399],{},[25,1400,1401],{},"适用场景建议",[22,1403,1404,1405,1408,1409,1412],{},"由于它牺牲了",[25,1406,1407],{},"强一致性","来换取",[25,1410,1411],{},"高性能","，它最适合以下场景：",[94,1414,1415,1421,1427],{},[97,1416,1417,1420],{},[25,1418,1419],{},"门户首页/热搜榜单","：用户对于几秒钟前的热搜排名并不敏感 。",[97,1422,1423,1426],{},[25,1424,1425],{},"商品详情页","：展示的数据稍有延迟（比如销量稍微差一点点）不会产生业务风险 。",[97,1428,1429,1432],{},[25,1430,1431],{},"高流量、高并发系统","：在可用性和极致响应速度面前，短时间的数据不一致是可以接受的 。",[32,1434],{},[22,1436,1437,1438],{},"2️⃣",[25,1439,1440],{},"互斥锁 (Mutex Lock)：",[22,1442,1443,1444],{},"简单来说，只允许一个请求去数据库查数据并写回缓存，其他请求等待或重试。它的核心逻辑就是：",[25,1445,1446],{},"当热点数据失效时，只允许一个请求去“重建”缓存，其他请求必须等待。",[32,1448],{},[22,1450,1451],{},[25,1452,1453],{},"核心工作流程",[22,1455,1456],{},"我们可以把这个过程想象成“修路”：路（缓存）断了，只能派一个维修工（请求）去修，修好之前，其他车辆（并发请求）只能在路口等着。",[450,1458,1459,1465,1471],{},[97,1460,1461,1464],{},[25,1462,1463],{},"尝试获取数据："," 请求首先查询 Redis 。",[97,1466,1467,1470],{},[25,1468,1469],{},"触发击穿："," 发现数据已过期（Cache Miss） 。",[97,1472,1473,1476,1477,1479,1480,1483,1484],{},[25,1474,1475],{},"抢占互斥锁："," 请求尝试使用 ",[49,1478,439],{},"或",[49,1481,1482],{},"SET key val NX EX","命令在我这里设置一个特殊的 Key（锁） 。\n",[94,1485,1486,1492],{},[97,1487,1488,1491],{},[25,1489,1490],{},"抢锁成功："," 进入“重建模式”，去查询数据库并将结果回写到 Redis，随后删除锁 Key 释放资源 。",[97,1493,1494,1497],{},[25,1495,1496],{},"抢锁失败："," 进入“等待模式”，线程休眠一小段时间（如 50ms），然后重新尝试从缓存获取数据 。",[32,1499],{},[22,1501,1502],{},[25,1503,1504],{},"“双检锁”（Double-Check）",[22,1506,1507],{},"这是互斥锁方案中最容易被忽略、也最重要的优化细节 。",[22,1509,1510],{},[25,1511,1512],{},"如果不做双检：",[22,1514,1515,1516,398],{},"假设有 1000 个请求同时发现缓存失效。请求 A 抢锁成功去查 DB 了；请求 B 抢锁失败开始休眠。等请求 A 查完数据库并更新完缓存，释放了锁，请求 B 醒来立刻又去抢锁，抢锁成功后如果不检查缓存，",[25,1517,1518],{},"它会再次冲向数据库",[22,1520,1521],{},[25,1522,1523],{},"正确的双检逻辑：",[450,1525,1526,1529,1532],{},[97,1527,1528],{},"第一次检查：缓存不存在，准备抢锁。",[97,1530,1531],{},"抢到锁后。",[97,1533,1534,1537],{},[25,1535,1536],{},"第二次检查："," 再次查询 Redis ！因为在你抢锁或等待的几毫秒内，可能前一个“维修工”已经把路修好了 。如果这次查到了，直接返回，不再打扰数据库。",[32,1539],{},[22,1541,1542],{},[25,1543,1544],{},"实现细节与健壮性保障",[22,1546,1547],{},"要写出一个工业级的互斥锁方案，你必须考虑以下三个细节：",[94,1549,1550,1561,1573],{},[97,1551,1552,1555,1557,1558,1560],{},[25,1553,1554],{},"锁的过期时间（TTL）：",[1110,1556],{},"绝对不能只用 ",[49,1559,439],{}," 而不设过期时间。如果抢到锁的服务器在查数据库时突然宕机了，锁就永远不会被释放，导致整个业务死锁。必须给锁设置一个合理的“保底”过期时间。",[97,1562,1563,1566,1568,1569,1572],{},[25,1564,1565],{},"重试频率：",[1110,1567],{},"抢锁失败后的 ",[49,1570,1571],{},"sleep"," 时间要适中。太短会增加 CPU 负担，太长会增加用户等待感。",[97,1574,1575,1578,1580,1581,1584],{},[25,1576,1577],{},"原子性操作：",[1110,1579],{},"设置锁和设置过期时间必须是原子的（可以使用 ",[49,1582,1583],{},"SET key value EX seconds NX"," 命令），防止在设置完锁还没来得及设过期时间时发生意外。",[32,1586],{},[864,1588,1590],{"id":1589},"_13-缓存雪崩-cache-avalanche","1.3 缓存雪崩 (Cache Avalanche)",[22,1592,1593],{},[25,1594,872],{},[22,1596,1597],{},"大量的 Key 在同一时间集中过期，或者我（Redis 服务器）直接宕机了。所有的请求瞬间全部涌向数据库，数据库扛不住直接“雪崩”。",[22,1599,1600],{},[25,1601,880],{},[22,1603,1604,1605,1608,1609,1612],{},"1️⃣ ",[25,1606,1607],{},"随机过期时间："," 别让大量 Key 的过期时间撞在一起。在基础过期时间上加一个 ",[25,1610,1611],{},"随机抖动值","（比如 1-5 分钟），让失效时间均匀分布。",[22,1614,1179,1615,1618,1619,1622,1623,1626],{},[25,1616,1617],{},"高可用架构："," 使用 ",[25,1620,1621],{},"Redis Sentinel（哨兵）"," 或 ",[25,1624,1625],{},"Redis Cluster（集群）","，确保我本身是不容易挂掉的。",[94,1628,1629,1693],{},[97,1630,1631,1634,1636,1639,1640,398,1643,1645,1648,1668,1670,1673,1691],{},[25,1632,1633],{},"Redis Sentinel (哨兵)",[1110,1635],{},[25,1637,1638],{},"哨兵模式","是在“主从复制”的基础上增加了一组独立的哨兵节点。它的核心逻辑是",[25,1641,1642],{},"自动化运维",[1110,1644],{},[25,1646,1647],{},"三大职责：",[450,1649,1650,1656,1662],{},[97,1651,1652,1655],{},[25,1653,1654],{},"监控 (Monitoring)："," 哨兵会不断地检查你的主节点（Master）和从节点（Slave）是否运作正常。",[97,1657,1658,1661],{},[25,1659,1660],{},"自动故障迁移 (Automatic Failover)："," 这是最关键的。如果主节点挂了，哨兵们会通过“投票”选出一个从节点，把它拉拔成新的主节点。",[97,1663,1664,1667],{},[25,1665,1666],{},"通知 (Notification)："," 当故障转移发生后，哨兵会将新的主节点地址通知给客户端（你的 Python 服务），让请求自动转向新家。",[1110,1669],{},[25,1671,1672],{},"它是如何判断Redis“挂了”的？",[94,1674,1675,1681],{},[97,1676,1677,1680],{},[25,1678,1679],{},"主观下线 (Subjective Down)："," 一个哨兵发现我不理它了，它会觉得我可能挂了。",[97,1682,1683,1686,1687,1690],{},[25,1684,1685],{},"客观下线 (Objective Down)："," 当",[25,1688,1689],{},"半数以上","（通常配置为 $N/2 + 1$）的哨兵都认为我挂了，这时候才会真正触发自动切换逻辑。这能有效避免因为网络抖动导致的误判。",[32,1692],{},[97,1694,1695,1698,1700,1701,1704,1705],{},[25,1696,1697],{},"Redis Cluster (集群)",[1110,1699],{},"如果说哨兵解决了“高可用”问题，那么",[25,1702,1703],{},"集群模式","在此基础上还解决了“海量数据”和“高并发”问题。",[94,1706,1707,1734,1742],{},[97,1708,1709,1712],{},[25,1710,1711],{},"核心机制：",[94,1713,1714,1724],{},[97,1715,1716,1719,1720,1723],{},[25,1717,1718],{},"数据分片 (Sharding)："," 集群将数据划分为 ",[25,1721,1722],{},"16384 个哈希槽（Slots）","。有多个主节点，每个主节点只负责其中的一部分槽位。这样即便你有几十 TB 的数据，我也能分摊开来处理。",[97,1725,1726,1729,1730,1733],{},[25,1727,1728],{},"自带故障转移："," 在集群中，主节点之间会互相监控。如果某个分片的主节点挂了，它的从节点会",[25,1731,1732],{},"自动上位","，整个集群不需要额外的哨兵节点也能自愈。",[97,1735,1736,1739,1741],{},[25,1737,1738],{},"集群的“抗压”逻辑：",[1110,1740],{},"由于请求被分散到了不同的物理机器上，集群模式能承受的并发量远超哨兵。如果某个分片压力过大，你可以随时增加新的机器来分担槽位。",[97,1743,1744,1747,1749,1750,1753,1754,1757,1758,1761,1762,1764,1765,1768,1769,1877,1879,1881,1884,1913,1915],{},[25,1745,1746],{},"处理逻辑",[1110,1748],{},"在 ",[25,1751,1752],{},"Redis Cluster"," 架构下，请求的处理不再由一个“中心节点”分发，而是一个",[25,1755,1756],{},"无中心化","、",[25,1759,1760],{},"自动路由","的过程 。",[1110,1763],{},"当一个用户请求（例如 ",[49,1766,1767],{},"SET user_name \"Redis\"","）进入系统时，处理流程如下：",[94,1770,1771,1788,1809,1847,1858],{},[97,1772,1773,1776,1778,1779,1781,1782,1784,1785,1787],{},[25,1774,1775],{},"第一步：计算哈希槽 (Slot Calculation)",[1110,1777],{},"集群将整个数据库划分为 $16384$ 个哈希槽（Slots） 。",[1110,1780],{},"当客户端发送请求时，首先需要确定这个 Key 属于哪个槽位：",[1110,1783],{},"系统会对 Key 进行哈希运算（通常是 CRC16 算法）并对 $16384$ 取模 。",[1110,1786],{},"公式可理解为：$slot = hash(key) \\pmod{16384}$ 。",[97,1789,1790,1793,1795,1796,1798,1799,1802,1803,893,1806,1808],{},[25,1791,1792],{},"第二步：客户端路由 (Client-Side Routing)",[1110,1794],{},"在工程实践中，客户端（如 Python 的 Redis 集群库）通常是“智能”的，它们会缓存一份集群的槽位分布图 。",[1110,1797],{},"客户端会根据计算出的 ",[49,1800,1801],{},"slot"," 直接寻找负责该槽位的",[25,1804,1805],{},"主节点 (Master)",[1110,1807],{},"这样请求就能直接发送到正确的目标节点，无需中间转发。",[97,1810,1811,1814,1816,1817],{},[25,1812,1813],{},"第三步：节点检查与重定向 (Redirection)",[1110,1815],{},"如果客户端发送到了错误的节点（例如因为集群扩容导致槽位迁移，但客户端缓存未更新）：",[94,1818,1819,1828,1838],{},[97,1820,1821,1824,1825,1827],{},[25,1822,1823],{},"节点校验","：收到请求的节点会检查该 ",[49,1826,1801],{}," 是否归自己管理 。",[97,1829,1830,1833,1834,1837],{},[25,1831,1832],{},"MOVED 错误","：如果不在自己这里，节点会返回一个 ",[49,1835,1836],{},"MOVED"," 错误，并告知客户端该槽位现在由哪个 IP 和端口负责 。",[97,1839,1840,1843,1844,1846],{},[25,1841,1842],{},"客户端更新","：客户端收到 ",[49,1845,1836],{}," 后，会更新本地缓存的槽位图，并重新向正确的节点发送请求 。",[97,1848,1849,1852,1854,1855,1857],{},[25,1850,1851],{},"第四步：读写处理 (Processing)",[1110,1853],{},"一旦请求到达正确的主节点，该节点就会执行相应的读写指令 。",[1110,1856],{},"如果是写操作，主节点执行完后会将变动记录同步给它的从节点（Slave） 。",[97,1859,1860,1863,1865,1866],{},[25,1861,1862],{},"第五步：异常处理 (Failure Detection)",[1110,1864],{},"如果在请求过程中某个主节点挂了：",[94,1867,1868,1871,1874],{},[97,1869,1870],{},"集群内部会通过节点间的 Gossip 协议发现故障 。",[97,1872,1873],{},"集群会自动进行故障转移，将对应的从节点提升为新主节点 。",[97,1875,1876],{},"后续请求会自动路由到新的主节点上，确保高可用性 。",[32,1878],{},[1110,1880],{},[25,1882,1883],{},"总结：Redis Cluster 请求链路",[450,1885,1886,1892,1898,1907],{},[97,1887,1888,1891],{},[25,1889,1890],{},"算槽","：客户端算出 Key 的槽位 。",[97,1893,1894,1897],{},[25,1895,1896],{},"直连","：客户端直连对应的主节点 。",[97,1899,1900,1903,1904,1906],{},[25,1901,1902],{},"纠错","：若节点变动，通过 ",[49,1905,1836],{}," 机制重定向 。",[97,1908,1909,1912],{},[25,1910,1911],{},"执行","：主节点处理，并异步同步从节点 。",[1110,1914],{},"这种设计让 Redis Cluster 能够轻松处理 TB 级数据和极致的并发，因为压力被均匀地分摊到了不同的主节点对上 。",[32,1917],{},[22,1919,1920],{},[25,1921,1922],{},"哨兵 vs 集群：我该选哪一个？",[167,1924,1925,1941],{},[170,1926,1927],{},[173,1928,1929,1933,1937],{},[176,1930,1931],{},[25,1932,178],{},[176,1934,1935],{},[25,1936,1633],{},[176,1938,1939],{},[25,1940,1697],{},[185,1942,1943,1961,1974,1987],{},[173,1944,1945,1950,1956],{},[190,1946,1947],{},[25,1948,1949],{},"核心目的",[190,1951,1952,1955],{},[25,1953,1954],{},"高可用","（保证挂了能自动恢复）",[190,1957,1958],{},[25,1959,1960],{},"高可用 + 海量扩展",[173,1962,1963,1968,1971],{},[190,1964,1965],{},[25,1966,1967],{},"数据分布",[190,1969,1970],{},"每个节点都存完整的数据",[190,1972,1973],{},"数据分片存储在不同节点",[173,1975,1976,1981,1984],{},[190,1977,1978],{},[25,1979,1980],{},"机器规模",[190,1982,1983],{},"较小（通常 3-5 台足够）",[190,1985,1986],{},"较大（最少需要 6 个节点：3主3从）",[173,1988,1989,1994,1997],{},[190,1990,1991],{},[25,1992,1993],{},"适用场景",[190,1995,1996],{},"数据量不大（GB 级别），追求稳定",[190,1998,1999],{},"数据量巨大（TB 级别），追求极致并发",[32,2001],{},[22,2003,2004,2005,2008],{},"3️⃣ ",[25,2006,2007],{},"熔断与限流："," 如果数据库真的顶不住了，利用 Sentinel（阿里的中间件）等工具开启限流或降级，保护核心链路。",[94,2010,2011],{},[97,2012,2013,2016,2018,2019,2022,2023],{},[25,2014,2015],{},"熔断",[1110,2017],{},"熔断是一种",[25,2020,2021],{},"自我保护机制","。当数据库因为雪崩压力而响应变慢或频繁报错时，系统会自动切断对数据库的调用 。",[94,2024,2025,2055,2061],{},[97,2026,2027,2030,2031],{},[25,2028,2029],{},"工作状态","：\n",[94,2032,2033,2039,2049],{},[97,2034,2035,2038],{},[25,2036,2037],{},"关闭 (Closed)","：一切正常，流量正常打向数据库。",[97,2040,2041,2044,2045,2048],{},[25,2042,2043],{},"开启 (Open)","：当检测到数据库故障率达到阈值（如 50% 请求超时），熔断器跳闸。所有请求不再打向数据库，而是执行",[25,2046,2047],{},"服务降级","逻辑。",[97,2050,2051,2054],{},[25,2052,2053],{},"半开 (Half-Open)","：经过一段冷却时间，熔断器允许少量流量尝试访问数据库。如果请求成功，则关闭熔断；否则重新开启。",[97,2056,2057,2060],{},[25,2058,2059],{},"服务降级 (Fallback)","：熔断期间，系统会直接返回一个预设的“默认值”或“兜底数据”。",[97,2062,2063,2066],{},[25,2064,2065],{},"目的","：给数据库争取喘息和恢复的时间 。",[32,2068],{},[94,2070,2071],{},[97,2072,2073,2076,2078,2079,2081,2082,1408,2085,893,2088,2122,2124,2126,2129,2131,2132,2135,2136,2139,2140,2142,2145,2230,2232,2235,2512,2514,2517,2705,2707,2709,2712,2791,2793,2795,2798,2800,2801,2843,2845,2848,2849,2852,2853,2855],{},[25,2074,2075],{},"限流",[1110,2077],{},"限流（Rate Limiting）是高并发架构中保护系统的核心手段之一。如果说熔断是发现问题后的“紧急避险”，那么限流就是前置的**“流量调度”**，确保进入系统的请求量始终在数据库或服务器的可控范围内 。",[1110,2080],{},"限流的主要目的是通过",[25,2083,2084],{},"牺牲部分可用性",[25,2086,2087],{},"全局的稳定性",[94,2089,2090,2096],{},[97,2091,2092,2095],{},[25,2093,2094],{},"工作机制","：为系统设置一个处理阈值（如每秒 1000次请求） 。",[97,2097,2098,2101,2102],{},[25,2099,2100],{},"超限处理","：当请求超过阈值时，系统会执行预设策略：\n",[94,2103,2104,2110,2116],{},[97,2105,2106,2109],{},[25,2107,2108],{},"拒绝（Discard）","：直接返回错误信息，如“系统繁忙” 。",[97,2111,2112,2115],{},[25,2113,2114],{},"排队（Queue）","：让请求进入队列，匀速处理。",[97,2117,2118,2121],{},[25,2119,2120],{},"降级（Fallback）","：返回默认的兜底数据。",[32,2123],{},[1110,2125],{},[25,2127,2128],{},"两大核心算法详解",[1110,2130],{},"在资料中提到的",[25,2133,2134],{},"令牌桶","和",[25,2137,2138],{},"漏桶","算法是业界最常用的两种方案 。",[1110,2141],{},[25,2143,2144],{},"A. 漏桶算法 (Leaky Bucket)",[94,2146,2147,2157,2176,2182,2217],{},[97,2148,2149,2152,2153,2156],{},[25,2150,2151],{},"形象比喻","：想象一个底部有小孔的桶。无论进水（请求）的速度有多快、多不规律，出水（处理）的速度始终是",[25,2154,2155],{},"恒定","的。如果桶满了，多出来的水就会直接溢出（丢弃请求） 。",[97,2158,2159,1269,2162],{},[25,2160,2161],{},"特点",[94,2163,2164,2170],{},[97,2165,2166,2169],{},[25,2167,2168],{},"强制匀速","：它能强行把突发流量（Bursty Traffic）整形成平滑流量。",[97,2171,2172,2175],{},[25,2173,2174],{},"保护后端","：对那些处理能力极其死板、不能承受任何波动的后端服务非常友好。",[97,2177,2178,2181],{},[25,2179,2180],{},"缺点","：无法应对合法的突发流量。即便系统当前很闲，它也只能按既定的速度处理。",[97,2183,2184,2187,2189,2190],{},[25,2185,2186],{},"实现思路：",[1110,2188],{},"漏桶算法的核心是**“出口恒定”** 。无论请求（水）流入的速度如何波动，它都以固定的速率处理请求 。",[94,2191,2192],{},[97,2193,2194,2030,2197],{},[25,2195,2196],{},"变量定义",[94,2198,2199,2205,2211],{},[97,2200,2201,2204],{},[49,2202,2203],{},"capacity","：桶的容量，代表系统能排队等待的最大请求数 。",[97,2206,2207,2210],{},[49,2208,2209],{},"leak_rate","：出水速度，比如每秒处理 $10$ 个请求 。",[97,2212,2213,2216],{},[49,2214,2215],{},"water","：当前桶里的水量（待处理请求数）。",[97,2218,2219,1269,2222],{},[25,2220,2221],{},"计算逻辑",[450,2223,2224,2227],{},[97,2225,2226],{},"每当请求进来，先计算从上次请求到现在流走了多少水：$water = \\max(0, water - (now - last_time) \\times leak_rate)$。",[97,2228,2229],{},"判断当前桶是否已满：如果 $water + 1 \\leq capacity$，则请求通过，且 $water += 1$；否则拒绝请求 。",[1110,2231],{},[25,2233,2234],{},"Python 代码实现：",[41,2236,2240],{"className":2237,"code":2238,"language":2239,"meta":47,"style":47},"language-python shiki shiki-themes github-light github-dark","import time\nclass LeakyBucket:\n    def __init__(self, capacity, leak_rate):\n        self.capacity = capacity      # 桶容量\n        self.leak_rate = leak_rate    # 流出速率 (每秒)\n        self.water = 0                # 当前水量\n        self.last_check_time = time.time()\n\n    def allow_request(self):\n        now = time.time()\n        # 1. 计算这段时间流走了多少\n        leaked = (now - self.last_check_time) * self.leak_rate\n        self.water = max(0, self.water - leaked)\n        self.last_check_time = now\n\n        # 2. 判断是否溢出\n        if self.water + 1 \u003C= self.capacity:\n            self.water += 1\n            return True\n        return False # 溢出了，拒绝\n","python",[49,2241,2242,2252,2263,2275,2293,2308,2324,2337,2344,2355,2365,2371,2399,2430,2442,2447,2453,2477,2491,2500],{"__ignoreMap":47},[62,2243,2244,2248],{"class":64,"line":65},[62,2245,2247],{"class":2246},"szBVR","import",[62,2249,2251],{"class":2250},"sVt8B"," time\n",[62,2253,2254,2257,2260],{"class":64,"line":79},[62,2255,2256],{"class":2246},"class",[62,2258,2259],{"class":68}," LeakyBucket",[62,2261,2262],{"class":2250},":\n",[62,2264,2265,2268,2272],{"class":64,"line":517},[62,2266,2267],{"class":2246},"    def",[62,2269,2271],{"class":2270},"sj4cs"," __init__",[62,2273,2274],{"class":2250},"(self, capacity, leak_rate):\n",[62,2276,2277,2280,2283,2286,2289],{"class":64,"line":585},[62,2278,2279],{"class":2270},"        self",[62,2281,2282],{"class":2250},".capacity ",[62,2284,2285],{"class":2246},"=",[62,2287,2288],{"class":2250}," capacity      ",[62,2290,2292],{"class":2291},"sJ8bj","# 桶容量\n",[62,2294,2295,2297,2300,2302,2305],{"class":64,"line":657},[62,2296,2279],{"class":2270},[62,2298,2299],{"class":2250},".leak_rate ",[62,2301,2285],{"class":2246},[62,2303,2304],{"class":2250}," leak_rate    ",[62,2306,2307],{"class":2291},"# 流出速率 (每秒)\n",[62,2309,2311,2313,2316,2318,2321],{"class":64,"line":2310},6,[62,2312,2279],{"class":2270},[62,2314,2315],{"class":2250},".water ",[62,2317,2285],{"class":2246},[62,2319,2320],{"class":2270}," 0",[62,2322,2323],{"class":2291},"                # 当前水量\n",[62,2325,2327,2329,2332,2334],{"class":64,"line":2326},7,[62,2328,2279],{"class":2270},[62,2330,2331],{"class":2250},".last_check_time ",[62,2333,2285],{"class":2246},[62,2335,2336],{"class":2250}," time.time()\n",[62,2338,2340],{"class":64,"line":2339},8,[62,2341,2343],{"emptyLinePlaceholder":2342},true,"\n",[62,2345,2347,2349,2352],{"class":64,"line":2346},9,[62,2348,2267],{"class":2246},[62,2350,2351],{"class":68}," allow_request",[62,2353,2354],{"class":2250},"(self):\n",[62,2356,2358,2361,2363],{"class":64,"line":2357},10,[62,2359,2360],{"class":2250},"        now ",[62,2362,2285],{"class":2246},[62,2364,2336],{"class":2250},[62,2366,2368],{"class":64,"line":2367},11,[62,2369,2370],{"class":2291},"        # 1. 计算这段时间流走了多少\n",[62,2372,2374,2377,2379,2382,2385,2388,2391,2394,2396],{"class":64,"line":2373},12,[62,2375,2376],{"class":2250},"        leaked ",[62,2378,2285],{"class":2246},[62,2380,2381],{"class":2250}," (now ",[62,2383,2384],{"class":2246},"-",[62,2386,2387],{"class":2270}," self",[62,2389,2390],{"class":2250},".last_check_time) ",[62,2392,2393],{"class":2246},"*",[62,2395,2387],{"class":2270},[62,2397,2398],{"class":2250},".leak_rate\n",[62,2400,2402,2404,2406,2408,2411,2414,2417,2420,2423,2425,2427],{"class":64,"line":2401},13,[62,2403,2279],{"class":2270},[62,2405,2315],{"class":2250},[62,2407,2285],{"class":2246},[62,2409,2410],{"class":2270}," max",[62,2412,2413],{"class":2250},"(",[62,2415,2416],{"class":2270},"0",[62,2418,2419],{"class":2250},", ",[62,2421,2422],{"class":2270},"self",[62,2424,2315],{"class":2250},[62,2426,2384],{"class":2246},[62,2428,2429],{"class":2250}," leaked)\n",[62,2431,2433,2435,2437,2439],{"class":64,"line":2432},14,[62,2434,2279],{"class":2270},[62,2436,2331],{"class":2250},[62,2438,2285],{"class":2246},[62,2440,2441],{"class":2250}," now\n",[62,2443,2445],{"class":64,"line":2444},15,[62,2446,2343],{"emptyLinePlaceholder":2342},[62,2448,2450],{"class":64,"line":2449},16,[62,2451,2452],{"class":2291},"        # 2. 判断是否溢出\n",[62,2454,2456,2459,2461,2463,2466,2469,2472,2474],{"class":64,"line":2455},17,[62,2457,2458],{"class":2246},"        if",[62,2460,2387],{"class":2270},[62,2462,2315],{"class":2250},[62,2464,2465],{"class":2246},"+",[62,2467,2468],{"class":2270}," 1",[62,2470,2471],{"class":2246}," \u003C=",[62,2473,2387],{"class":2270},[62,2475,2476],{"class":2250},".capacity:\n",[62,2478,2480,2483,2485,2488],{"class":64,"line":2479},18,[62,2481,2482],{"class":2270},"            self",[62,2484,2315],{"class":2250},[62,2486,2487],{"class":2246},"+=",[62,2489,2490],{"class":2270}," 1\n",[62,2492,2494,2497],{"class":64,"line":2493},19,[62,2495,2496],{"class":2246},"            return",[62,2498,2499],{"class":2270}," True\n",[62,2501,2503,2506,2509],{"class":64,"line":2502},20,[62,2504,2505],{"class":2246},"        return",[62,2507,2508],{"class":2270}," False",[62,2510,2511],{"class":2291}," # 溢出了，拒绝\n",[1110,2513],{},[25,2515,2516],{},"B. 令牌桶算法 (Token Bucket)",[94,2518,2519,2528,2546,2556],{},[97,2520,2521,2523,2524,2527],{},[25,2522,2151],{},"：有一个桶专门装令牌，系统以恒定的速度往里放令牌。每个请求进来必须先从桶里",[25,2525,2526],{},"抢到一个令牌","才能被处理 。如果桶空了，请求就被拒绝。",[97,2529,2530,1269,2532],{},[25,2531,2161],{},[94,2533,2534,2540],{},[97,2535,2536,2539],{},[25,2537,2538],{},"允许突发","：如果前一段时间请求很少，桶里积攒了大量令牌，那么当瞬时大流量进来时，这些请求可以瞬间全部通过。",[97,2541,2542,2545],{},[25,2543,2544],{},"动态调整","：它既限制了长期的平均频率，又保留了处理短期突发流量的灵活性。",[97,2547,2548,2551,2552,2555],{},[25,2549,2550],{},"优势","：这是目前主流框架（如 ",[25,2553,2554],{},"Sentinel","）更倾向于使用的方案，因为它对用户请求更加友好 。",[97,2557,2558,2561,2563,2564,2601,2603,2605],{},[25,2559,2560],{},"实现思路",[1110,2562],{},"令牌桶算法的核心是**“生成恒定，消费取令牌”** 。它允许短时间的突发流量，只要桶里还有积攒的令牌 。",[94,2565,2566,2589],{},[97,2567,2568,2030,2570],{},[25,2569,2196],{},[94,2571,2572,2577,2583],{},[97,2573,2574,2576],{},[49,2575,2203],{},"：桶的最大令牌数，决定了允许的最大突发流量 。",[97,2578,2579,2582],{},[49,2580,2581],{},"refill_rate","：令牌生成的速率（每秒放多少个） 。",[97,2584,2585,2588],{},[49,2586,2587],{},"tokens","：当前可用的令牌数。",[97,2590,2591,2030,2593],{},[25,2592,2221],{},[450,2594,2595,2598],{},[97,2596,2597],{},"计算自上次请求以来生成的令牌：$tokens = \\min(capacity, tokens + (now - last_time) \\times refill_rate)$ 。",[97,2599,2600],{},"判断是否有可用令牌：如果 $tokens \\geq 1$，则扣除一个令牌并放行；否则拦截 。",[1110,2602],{},[25,2604,2234],{},[41,2606,2610],{"className":2607,"code":2608,"language":2609,"meta":47,"style":47},"language-Python shiki shiki-themes github-light github-dark","class TokenBucket:\n    def __init__(self, capacity, refill_rate):\n        self.capacity = capacity\n        self.refill_rate = refill_rate\n        self.tokens = capacity # 初始满桶\n        self.last_refill_time = time.time()\n\n    def allow_request(self):\n        now = time.time()\n        # 1. 补充令牌\n        new_tokens = (now - self.last_refill_time) * self.refill_rate\n        self.tokens = min(self.capacity, self.tokens + new_tokens)\n        self.last_refill_time = now\n\n        # 2. 消费令牌\n        if self.tokens >= 1:\n            self.tokens -= 1\n            return True\n        return False\n","Python",[49,2611,2612,2617,2622,2627,2632,2637,2642,2646,2651,2656,2661,2666,2671,2676,2680,2685,2690,2695,2700],{"__ignoreMap":47},[62,2613,2614],{"class":64,"line":65},[62,2615,2616],{},"class TokenBucket:\n",[62,2618,2619],{"class":64,"line":79},[62,2620,2621],{},"    def __init__(self, capacity, refill_rate):\n",[62,2623,2624],{"class":64,"line":517},[62,2625,2626],{},"        self.capacity = capacity\n",[62,2628,2629],{"class":64,"line":585},[62,2630,2631],{},"        self.refill_rate = refill_rate\n",[62,2633,2634],{"class":64,"line":657},[62,2635,2636],{},"        self.tokens = capacity # 初始满桶\n",[62,2638,2639],{"class":64,"line":2310},[62,2640,2641],{},"        self.last_refill_time = time.time()\n",[62,2643,2644],{"class":64,"line":2326},[62,2645,2343],{"emptyLinePlaceholder":2342},[62,2647,2648],{"class":64,"line":2339},[62,2649,2650],{},"    def allow_request(self):\n",[62,2652,2653],{"class":64,"line":2346},[62,2654,2655],{},"        now = time.time()\n",[62,2657,2658],{"class":64,"line":2357},[62,2659,2660],{},"        # 1. 补充令牌\n",[62,2662,2663],{"class":64,"line":2367},[62,2664,2665],{},"        new_tokens = (now - self.last_refill_time) * self.refill_rate\n",[62,2667,2668],{"class":64,"line":2373},[62,2669,2670],{},"        self.tokens = min(self.capacity, self.tokens + new_tokens)\n",[62,2672,2673],{"class":64,"line":2401},[62,2674,2675],{},"        self.last_refill_time = now\n",[62,2677,2678],{"class":64,"line":2432},[62,2679,2343],{"emptyLinePlaceholder":2342},[62,2681,2682],{"class":64,"line":2444},[62,2683,2684],{},"        # 2. 消费令牌\n",[62,2686,2687],{"class":64,"line":2449},[62,2688,2689],{},"        if self.tokens >= 1:\n",[62,2691,2692],{"class":64,"line":2455},[62,2693,2694],{},"            self.tokens -= 1\n",[62,2696,2697],{"class":64,"line":2479},[62,2698,2699],{},"            return True\n",[62,2701,2702],{"class":64,"line":2493},[62,2703,2704],{},"        return False\n",[32,2706],{},[1110,2708],{},[25,2710,2711],{},"算法对比总结",[167,2713,2714,2733],{},[170,2715,2716],{},[173,2717,2718,2723,2728],{},[176,2719,2720],{},[25,2721,2722],{},"特性",[176,2724,2725],{},[25,2726,2727],{},"漏桶算法 (Leaky Bucket)",[176,2729,2730],{},[25,2731,2732],{},"令牌桶算法 (Token Bucket)",[185,2734,2735,2748,2764,2776],{},[173,2736,2737,2742,2745],{},[190,2738,2739],{},[25,2740,2741],{},"处理速度",[190,2743,2744],{},"严格恒定",[190,2746,2747],{},"平均恒定，允许瞬时爆发",[173,2749,2750,2755,2758],{},[190,2751,2752],{},[25,2753,2754],{},"突发流量",[190,2756,2757],{},"不支持（直接丢弃）",[190,2759,2760,2763],{},[25,2761,2762],{},"支持","（只要桶里有令牌）",[173,2765,2766,2770,2773],{},[190,2767,2768],{},[25,2769,1949],{},[190,2771,2772],{},"强行平滑流量",[190,2774,2775],{},"限制平均流入速率",[173,2777,2778,2783,2786],{},[190,2779,2780],{},[25,2781,2782],{},"应用场景",[190,2784,2785],{},"网络流量整形、极其脆弱的后台",[190,2787,2788],{},[25,2789,2790],{},"大多数 Web 应用、API 接口",[32,2792],{},[1110,2794],{},[25,2796,2797],{},"落地工具建议",[1110,2799],{},"既然你正在使用 Python 服务，实现限流有以下几种健壮的选择：",[450,2802,2803,2819,2829],{},[97,2804,2805,2030,2808],{},[25,2806,2807],{},"Sentinel-Python",[94,2809,2810,2816],{},[97,2811,2762,2812,2815],{},[25,2813,2814],{},"热点参数限流","（例如限制某个特定 ID 的访问频率） 。",[97,2817,2818],{},"可以配合 Dashboard 实时调整限流阈值，无需重启服务 。",[97,2820,2821,2030,2824],{},[25,2822,2823],{},"Redis + Lua 脚本",[94,2825,2826],{},[97,2827,2828],{},"这是分布式限流的经典做法。利用 Redis 的单线程原子性，配合 Lua 脚本实现令牌桶逻辑，可以保证多个 Python 节点共用一套限流规则。",[97,2830,2831,2030,2834],{},[25,2832,2833],{},"FastAPI / Flask 插件",[94,2835,2836],{},[97,2837,2838,2839,2842],{},"如 ",[49,2840,2841],{},"slowapi","（基于 Python 的 limits 库），可以非常简单地通过装饰器实现接口级的限流。",[1110,2844],{},[25,2846,2847],{},"总结","：在处理缓存雪崩时，",[25,2850,2851],{},"限流是第一道关卡"," 。它像“入场安检”一样，从源头确保数据库不会因为流量过载而崩溃 。",[1110,2854],{},"你现在的业务场景中，流量是比较平稳的，还是经常会有突然爆发的情况（如整点秒杀）？这决定了你更适合选择哪种算法。",[32,2857],{},[22,2859,2860],{},[25,2861,2862],{},"总结对比",[167,2864,2865,2884],{},[170,2866,2867],{},[173,2868,2869,2874,2879],{},[176,2870,2871],{},[25,2872,2873],{},"场景",[176,2875,2876],{},[25,2877,2878],{},"问题核心",[176,2880,2881],{},[25,2882,2883],{},"核心对策",[185,2885,2886,2899,2912],{},[173,2887,2888,2893,2896],{},[190,2889,2890],{},[25,2891,2892],{},"穿透",[190,2894,2895],{},"数据不存在",[190,2897,2898],{},"布隆过滤器 / 缓存空值",[173,2900,2901,2906,2909],{},[190,2902,2903],{},[25,2904,2905],{},"击穿",[190,2907,2908],{},"热点 Key 过期",[190,2910,2911],{},"互斥锁 / 逻辑过期",[173,2913,2914,2919,2922],{},[190,2915,2916],{},[25,2917,2918],{},"雪崩",[190,2920,2921],{},"大量 Key 同时过期",[190,2923,2924],{},"随机 TTL / 高可用集群",[32,2926],{},[859,2928,2930],{"id":2929},"_2-互斥锁","2 互斥锁",[859,2932,2934],{"id":2933},"_3-缓存与数据库的一致性维护","3 缓存与数据库的一致性维护",[2936,2937,2938],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":47,"searchDepth":517,"depth":585,"links":2940},[2941,2942,2943],{"id":16,"depth":79,"text":17},{"id":376,"depth":79,"text":377},{"id":857,"depth":79,"text":857,"children":2944},[2945,2950,2951],{"id":861,"depth":517,"text":862,"children":2946},[2947,2948,2949],{"id":866,"depth":585,"text":867},{"id":1199,"depth":585,"text":1200},{"id":1589,"depth":585,"text":1590},{"id":2929,"depth":517,"text":2930},{"id":2933,"depth":517,"text":2934},"redis学习","md",{"date":2955,"image":2956,"alt":12,"tags":2957,"published":2342,"trending":2959},"2025/10/9","/blogs-img/blog5.jpg",[12,2958],"数据库",false,"/docs/redis",{"title":5,"description":2952},"docs/Redis","wM6uL4JkwqcNDaaLui5Axi2V5Hv-iz4yKncR0oxNY1s",1778575225314]