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