[{"data":1,"prerenderedAt":1408},["ShallowReactive",2],{"article-\u002Farticles\u002Fredis-cache-penetration-breakdown-avalanche":3},{"id":4,"title":5,"body":6,"date":1394,"description":1395,"excerpt":1396,"extension":1397,"meta":1398,"navigation":231,"path":1399,"seo":1400,"stem":1401,"tags":1402,"__hash__":1407},"articles\u002Farticles\u002Fredis-cache-penetration-breakdown-avalanche.md","Redis 快取怎麼用才不會出事？Cache-Aside、資料一致性，與穿透、擊穿、雪崩三個問題",{"type":7,"value":8,"toc":1378},"minimark",[9,24,29,36,43,49,62,67,82,427,432,439,502,509,512,516,523,534,537,540,567,574,577,583,587,606,611,631,635,646,650,678,682,689,697,700,704,738,741,1278,1288,1292,1295,1330,1333,1368,1371,1374],[10,11,12,13,18,19,23],"p",{},"Redis 的資料結構與基本操作可以先參考 ",[14,15,17],"a",{"href":16},"\u002Farticles\u002Fredis-data-structures-use-cases","Redis 不只是快取","。這篇專注在最常見的用法：",[20,21,22],"strong",{},"把它放在資料庫前面當快取","。",[25,26,28],"h2",{"id":27},"cache-aside最常用的快取模式","Cache-Aside：最常用的快取模式",[10,30,31],{},[32,33],"img",{"alt":34,"src":35},"Cache-Aside 流程示意圖：讀取時先查 Redis，命中就直接回傳；沒命中就查資料庫，把結果寫回 Redis 並設定過期時間，再回傳。寫入時先更新資料庫，成功後刪除 Redis 中對應的 Key，下一次讀取時再從資料庫重新載入","\u002Fimages\u002Farticles\u002Fredis-cache-aside.svg",[10,37,38,39,42],{},"Cache-Aside（旁路快取）是由",[20,40,41],{},"應用程式自己","負責讀寫快取，Redis 和資料庫彼此不知道對方存在：",[10,44,45,48],{},[20,46,47],{},"讀取","：",[50,51,52,56,59],"ol",{},[53,54,55],"li",{},"先查 Redis，有就直接回傳（Cache Hit）",[53,57,58],{},"沒有（Cache Miss）就查資料庫",[53,60,61],{},"把查到的結果寫進 Redis，設定過期時間，再回傳",[10,63,64,48],{},[20,65,66],{},"寫入",[50,68,69,72,79],{},[53,70,71],{},"先更新資料庫",[53,73,74,75,78],{},"成功後",[20,76,77],{},"刪除"," Redis 裡對應的 Key",[53,80,81],{},"下一次有人讀取時，就會因為 Cache Miss 從資料庫載入最新的資料",[83,84,89],"pre",{"className":85,"code":86,"language":87,"meta":88,"style":88},"language-csharp shiki shiki-themes github-dark","public async Task\u003CHouse?> GetHouseAsync(int id)\n{\n    var key = $\"house:{id}\";\n    var cached = await _redis.StringGetAsync(key);\n    if (cached.HasValue)\n        return JsonSerializer.Deserialize\u003CHouse>(cached.ToString());\n\n    var house = await _db.Houses.FindAsync(id);\n    if (house is not null)\n        await _redis.StringSetAsync(key, JsonSerializer.Serialize(house), TimeSpan.FromMinutes(10));\n\n    return house;\n}\n\npublic async Task UpdateHouseAsync(House house)\n{\n    _db.Houses.Update(house);\n    await _db.SaveChangesAsync();          \u002F\u002F 1. 先更新資料庫\n    await _redis.KeyDeleteAsync($\"house:{house.Id}\");  \u002F\u002F 2. 再刪除快取\n}\n","csharp","",[90,91,92,133,139,170,192,201,226,233,254,274,305,310,319,325,330,350,355,367,386,422],"code",{"__ignoreMap":88},[93,94,97,101,104,108,112,115,118,121,124,127,130],"span",{"class":95,"line":96},"line",1,[93,98,100],{"class":99},"snl16","public",[93,102,103],{"class":99}," async",[93,105,107],{"class":106},"svObZ"," Task",[93,109,111],{"class":110},"s95oV","\u003C",[93,113,114],{"class":106},"House",[93,116,117],{"class":110},"?> ",[93,119,120],{"class":106},"GetHouseAsync",[93,122,123],{"class":110},"(",[93,125,126],{"class":99},"int",[93,128,129],{"class":106}," id",[93,131,132],{"class":110},")\n",[93,134,136],{"class":95,"line":135},2,[93,137,138],{"class":110},"{\n",[93,140,142,145,148,151,155,158,161,164,167],{"class":95,"line":141},3,[93,143,144],{"class":99},"    var",[93,146,147],{"class":106}," key",[93,149,150],{"class":99}," =",[93,152,154],{"class":153},"sU2Wk"," $\"house:",[93,156,157],{"class":153},"{",[93,159,160],{"class":110},"id",[93,162,163],{"class":153},"}",[93,165,166],{"class":153},"\"",[93,168,169],{"class":110},";\n",[93,171,173,175,178,180,183,186,189],{"class":95,"line":172},4,[93,174,144],{"class":99},[93,176,177],{"class":106}," cached",[93,179,150],{"class":99},[93,181,182],{"class":99}," await",[93,184,185],{"class":110}," _redis.",[93,187,188],{"class":106},"StringGetAsync",[93,190,191],{"class":110},"(key);\n",[93,193,195,198],{"class":95,"line":194},5,[93,196,197],{"class":99},"    if",[93,199,200],{"class":110}," (cached.HasValue)\n",[93,202,204,207,210,213,215,217,220,223],{"class":95,"line":203},6,[93,205,206],{"class":99},"        return",[93,208,209],{"class":110}," JsonSerializer.",[93,211,212],{"class":106},"Deserialize",[93,214,111],{"class":110},[93,216,114],{"class":106},[93,218,219],{"class":110},">(cached.",[93,221,222],{"class":106},"ToString",[93,224,225],{"class":110},"());\n",[93,227,229],{"class":95,"line":228},7,[93,230,232],{"emptyLinePlaceholder":231},true,"\n",[93,234,236,238,241,243,245,248,251],{"class":95,"line":235},8,[93,237,144],{"class":99},[93,239,240],{"class":106}," house",[93,242,150],{"class":99},[93,244,182],{"class":99},[93,246,247],{"class":110}," _db.Houses.",[93,249,250],{"class":106},"FindAsync",[93,252,253],{"class":110},"(id);\n",[93,255,257,259,262,265,268,272],{"class":95,"line":256},9,[93,258,197],{"class":99},[93,260,261],{"class":110}," (house ",[93,263,264],{"class":99},"is",[93,266,267],{"class":99}," not",[93,269,271],{"class":270},"sDLfK"," null",[93,273,132],{"class":110},[93,275,277,280,282,285,288,291,294,297,299,302],{"class":95,"line":276},10,[93,278,279],{"class":99},"        await",[93,281,185],{"class":110},[93,283,284],{"class":106},"StringSetAsync",[93,286,287],{"class":110},"(key, JsonSerializer.",[93,289,290],{"class":106},"Serialize",[93,292,293],{"class":110},"(house), TimeSpan.",[93,295,296],{"class":106},"FromMinutes",[93,298,123],{"class":110},[93,300,301],{"class":270},"10",[93,303,304],{"class":110},"));\n",[93,306,308],{"class":95,"line":307},11,[93,309,232],{"emptyLinePlaceholder":231},[93,311,313,316],{"class":95,"line":312},12,[93,314,315],{"class":99},"    return",[93,317,318],{"class":110}," house;\n",[93,320,322],{"class":95,"line":321},13,[93,323,324],{"class":110},"}\n",[93,326,328],{"class":95,"line":327},14,[93,329,232],{"emptyLinePlaceholder":231},[93,331,333,335,337,339,342,344,346,348],{"class":95,"line":332},15,[93,334,100],{"class":99},[93,336,103],{"class":99},[93,338,107],{"class":106},[93,340,341],{"class":106}," UpdateHouseAsync",[93,343,123],{"class":110},[93,345,114],{"class":106},[93,347,240],{"class":106},[93,349,132],{"class":110},[93,351,353],{"class":95,"line":352},16,[93,354,138],{"class":110},[93,356,358,361,364],{"class":95,"line":357},17,[93,359,360],{"class":110},"    _db.Houses.",[93,362,363],{"class":106},"Update",[93,365,366],{"class":110},"(house);\n",[93,368,370,373,376,379,382],{"class":95,"line":369},18,[93,371,372],{"class":99},"    await",[93,374,375],{"class":110}," _db.",[93,377,378],{"class":106},"SaveChangesAsync",[93,380,381],{"class":110},"();          ",[93,383,385],{"class":384},"sAwPA","\u002F\u002F 1. 先更新資料庫\n",[93,387,389,391,393,396,398,401,403,406,409,412,414,416,419],{"class":95,"line":388},19,[93,390,372],{"class":99},[93,392,185],{"class":110},[93,394,395],{"class":106},"KeyDeleteAsync",[93,397,123],{"class":110},[93,399,400],{"class":153},"$\"house:",[93,402,157],{"class":153},[93,404,405],{"class":110},"house",[93,407,408],{"class":153},".",[93,410,411],{"class":110},"Id",[93,413,163],{"class":153},[93,415,166],{"class":153},[93,417,418],{"class":110},");  ",[93,420,421],{"class":384},"\u002F\u002F 2. 再刪除快取\n",[93,423,425],{"class":95,"line":424},20,[93,426,324],{"class":110},[428,429,431],"h3",{"id":430},"為什麼是刪除快取而不是更新快取","為什麼是「刪除」快取，而不是「更新」快取？",[10,433,434,435,438],{},"直覺上，資料庫改了，順便把新值寫進 Redis 不是更好嗎？問題出在",[20,436,437],{},"兩個請求同時更新","的時候：",[440,441,442,458],"table",{},[443,444,445],"thead",{},[446,447,448,452,455],"tr",{},[449,450,451],"th",{},"時間",[449,453,454],{},"請求 A（改價為 1000 萬）",[449,456,457],{},"請求 B（改價為 1200 萬）",[459,460,461,472,482,492],"tbody",{},[446,462,463,467,470],{},[464,465,466],"td",{},"1",[464,468,469],{},"更新資料庫 → 1000 萬",[464,471],{},[446,473,474,477,479],{},[464,475,476],{},"2",[464,478],{},[464,480,481],{},"更新資料庫 → 1200 萬",[446,483,484,487,489],{},[464,485,486],{},"3",[464,488],{},[464,490,491],{},"更新快取 → 1200 萬",[446,493,494,497,500],{},[464,495,496],{},"4",[464,498,499],{},"更新快取 → 1000 萬",[464,501],{},[10,503,504,505,508],{},"資料庫最後是 1200 萬，快取卻是 1000 萬，",[20,506,507],{},"而且會一直錯到過期為止","。網路延遲、GC 停頓都可能讓 A 的第二步比 B 慢。",[10,510,511],{},"改成刪除，不管誰先誰後，結果都是「快取裡沒有這個 Key」，下一次讀取一定從資料庫拿最新值。另外，很多資料改了之後不一定馬上有人讀，每次更新都重新寫快取也是浪費。",[428,513,515],{"id":514},"為什麼是先更新資料庫再刪快取","為什麼是「先更新資料庫，再刪快取」？",[10,517,518,519,522],{},"反過來先刪快取、再更新資料庫，在這兩步之間如果剛好有讀取請求進來，它會 Cache Miss、讀到資料庫的",[20,520,521],{},"舊值","、再把舊值寫回快取。資料庫更新完之後，快取裡就是一份舊資料。",[10,524,525,526,529,530,533],{},"「先更新資料庫、再刪快取」也不是完全沒有問題，但出錯需要一個很巧的時序：讀取請求在快取過期後讀到舊值，",[20,527,528],{},"而且","它寫回快取的動作剛好落在寫入請求刪除快取",[20,531,532],{},"之後","。因為寫回快取通常比更新資料庫快得多，機率低很多。",[428,535,536],{"id":536},"刪除失敗怎麼辦",[10,538,539],{},"資料庫更新成功了，刪除快取時 Redis 剛好連不上，快取就會一直是舊的。幾個層次的保險：",[541,542,543,549,555,561],"ul",{},[53,544,545,548],{},[20,546,547],{},"一定要設過期時間","：最差的情況，也只會舊到過期為止。這是最後一道防線",[53,550,551,554],{},[20,552,553],{},"刪除失敗重試","：失敗時丟到訊息佇列，由背景重試刪除",[53,556,557,560],{},[20,558,559],{},"延遲雙刪","：更新資料庫後刪一次，過幾百毫秒再刪一次，處理前面提到那個「讀取請求把舊值寫回去」的情況",[53,562,563,566],{},[20,564,565],{},"訂閱資料庫異動","：由獨立的服務監聽資料異動（例如 CDC 或異動事件）來刪除快取，應用程式本身不用處理",[10,568,569,570,573],{},"要先接受一件事：",[20,571,572],{},"快取和資料庫之間只能做到最終一致","。像餘額、庫存這種一定要即時正確的資料，就不該從快取讀來做判斷。",[25,575,576],{"id":576},"三個常見問題",[10,578,579],{},[32,580],{"alt":581,"src":582},"快取三大問題示意圖：快取穿透是請求查詢根本不存在的資料，例如 id=-1，每次都 Cache Miss 打到資料庫，解法是快取空值或布隆過濾器；快取擊穿是一個熱門 Key 過期的瞬間，上千個請求同時 Cache Miss 一起查資料庫，解法是互斥鎖只讓一個請求重建，或邏輯過期；快取雪崩是大量 Key 在同一時間過期或 Redis 整個掛掉，所有請求湧向資料庫，解法是過期時間加隨機值、Redis 高可用、本機快取與限流降級","\u002Fimages\u002Farticles\u002Fredis-cache-problems.svg",[428,584,586],{"id":585},"快取穿透查詢根本不存在的資料","快取穿透：查詢根本不存在的資料",[10,588,589,590,593,594,597,598,601,602,605],{},"有人一直查 ",[90,591,592],{},"house:-1","、",[90,595,596],{},"house:99999999"," 這種",[20,599,600],{},"資料庫裡也沒有","的資料。因為查不到，就不會寫進快取，",[20,603,604],{},"每一次都會打到資料庫","。如果是惡意的大量請求，等於快取完全沒有作用。",[10,607,608,48],{},[20,609,610],{},"解法",[541,612,613,619,625],{},[53,614,615,618],{},[20,616,617],{},"快取空值","：查不到時也寫一筆「空」進 Redis，設定比較短的過期時間（例如 1 分鐘）。下一次同樣的查詢在 Redis 就擋掉了",[53,620,621,624],{},[20,622,623],{},"布隆過濾器（Bloom Filter）","：把所有存在的 ID 放進布隆過濾器，查詢前先問它，它說「不存在」就一定不存在，直接回傳。適合 ID 範圍很大、惡意請求可能每次都換不同 ID 的情況",[53,626,627,630],{},[20,628,629],{},"參數驗證","：ID 小於 0、格式不對的，在 API 入口就擋掉",[428,632,634],{"id":633},"快取擊穿熱門-key-過期的那一瞬間","快取擊穿：熱門 Key 過期的那一瞬間",[10,636,637,638,641,642,645],{},"首頁的熱門物件、活動頁面，每秒有上千個請求在讀",[20,639,640],{},"同一個 Key","。它一過期，這一千個請求",[20,643,644],{},"同時"," Cache Miss，同時去查資料庫、同時寫回快取。資料庫在那一秒突然多了一千個一模一樣的查詢。",[10,647,648,48],{},[20,649,610],{},[541,651,652,662,672],{},[53,653,654,657,658,661],{},[20,655,656],{},"互斥鎖","：Cache Miss 時，只讓",[20,659,660],{},"一個","請求去查資料庫並重建快取，其他請求等它完成後再讀快取",[53,663,664,667,668,671],{},[20,665,666],{},"邏輯過期","：Redis 的 Key 不設過期時間，而是在值裡放一個「邏輯過期時間」。讀到已經邏輯過期的資料時，",[20,669,670],{},"先回傳舊值","，再由一個背景工作去更新。使用者永遠不會遇到 Cache Miss，代價是短時間內會讀到舊資料",[53,673,674,677],{},[20,675,676],{},"熱門資料預先載入","：已知會很熱門的資料（例如活動開始），在活動前就先寫進快取",[428,679,681],{"id":680},"快取雪崩大量-key-同時失效","快取雪崩：大量 Key 同時失效",[10,683,684,685,688],{},"和擊穿不同，雪崩是",[20,686,687],{},"很多 Key 同時","沒了：",[541,690,691,694],{},[53,692,693],{},"批次匯入的資料全部設定了相同的過期時間，一小時後同時過期",[53,695,696],{},"Redis 本身掛了，或是重啟後記憶體是空的",[10,698,699],{},"所有請求一起湧向資料庫，資料庫被壓垮，接著依賴資料庫的服務也一起倒下。",[10,701,702,48],{},[20,703,610],{},[541,705,706,712,718,728],{},[53,707,708,711],{},[20,709,710],{},"過期時間加隨機值","：原本 10 分鐘，改成 10 分鐘加上 0～2 分鐘的隨機秒數，讓過期時間分散開來",[53,713,714,717],{},[20,715,716],{},"Redis 高可用","：用 Sentinel 或 Cluster，單一節點掛掉時自動切換",[53,719,720,723,724,727],{},[20,721,722],{},"多層快取","：應用程式內再加一層短時間的本機快取（",[90,725,726],{},"IMemoryCache","），Redis 短暫不可用時還能撐一下",[53,729,730,733,734,737],{},[20,731,732],{},"限流與降級","：資料庫前面加上限流，超過的請求直接回傳預設內容或錯誤，",[20,735,736],{},"保護資料庫不被打垮","，比全部一起倒下好",[25,739,740],{"id":740},"把三個解法放在一起",[83,742,744],{"className":85,"code":743,"language":87,"meta":88,"style":88},"private static readonly ConcurrentDictionary\u003Cstring, SemaphoreSlim> Locks = new();\nprivate const string NullMarker = \"__null__\";\n\npublic async Task\u003CHouse?> GetHouseAsync(int id)\n{\n    if (id \u003C= 0) return null;  \u002F\u002F 參數驗證，擋掉明顯不存在的 ID\n\n    var key = $\"house:{id}\";\n    var cached = await _redis.StringGetAsync(key);\n    if (cached.HasValue)\n        return cached == NullMarker ? null : JsonSerializer.Deserialize\u003CHouse>(cached.ToString());\n\n    \u002F\u002F 擊穿：同一個 Key 只讓一個請求去查資料庫（這是單一實例內的鎖）\n    var gate = Locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));\n    await gate.WaitAsync();\n    try\n    {\n        \u002F\u002F 拿到鎖之後再檢查一次，前一個請求可能已經重建好了\n        cached = await _redis.StringGetAsync(key);\n        if (cached.HasValue)\n            return cached == NullMarker ? null : JsonSerializer.Deserialize\u003CHouse>(cached.ToString());\n\n        var house = await _db.Houses.FindAsync(id);\n        if (house is null)\n        {\n            \u002F\u002F 穿透：不存在也快取，但時間短一點\n            await _redis.StringSetAsync(key, NullMarker, TimeSpan.FromMinutes(1));\n            return null;\n        }\n\n        \u002F\u002F 雪崩：過期時間加上隨機值，避免同時過期\n        var ttl = TimeSpan.FromMinutes(10) + TimeSpan.FromSeconds(Random.Shared.Next(0, 120));\n        await _redis.StringSetAsync(key, JsonSerializer.Serialize(house), ttl);\n        return house;\n    }\n    finally\n    {\n        gate.Release();\n    }\n}\n",[90,745,746,785,805,809,833,837,864,868,888,904,910,945,949,954,993,1005,1010,1015,1020,1036,1043,1075,1080,1098,1111,1117,1123,1144,1153,1159,1164,1170,1217,1233,1240,1246,1252,1257,1268,1273],{"__ignoreMap":88},[93,747,748,751,754,757,760,762,765,768,771,774,777,779,782],{"class":95,"line":96},[93,749,750],{"class":99},"private",[93,752,753],{"class":99}," static",[93,755,756],{"class":99}," readonly",[93,758,759],{"class":106}," ConcurrentDictionary",[93,761,111],{"class":110},[93,763,764],{"class":99},"string",[93,766,767],{"class":110},", ",[93,769,770],{"class":106},"SemaphoreSlim",[93,772,773],{"class":110},"> ",[93,775,776],{"class":106},"Locks",[93,778,150],{"class":99},[93,780,781],{"class":99}," new",[93,783,784],{"class":110},"();\n",[93,786,787,789,792,795,798,800,803],{"class":95,"line":135},[93,788,750],{"class":99},[93,790,791],{"class":99}," const",[93,793,794],{"class":99}," string",[93,796,797],{"class":106}," NullMarker",[93,799,150],{"class":99},[93,801,802],{"class":153}," \"__null__\"",[93,804,169],{"class":110},[93,806,807],{"class":95,"line":141},[93,808,232],{"emptyLinePlaceholder":231},[93,810,811,813,815,817,819,821,823,825,827,829,831],{"class":95,"line":172},[93,812,100],{"class":99},[93,814,103],{"class":99},[93,816,107],{"class":106},[93,818,111],{"class":110},[93,820,114],{"class":106},[93,822,117],{"class":110},[93,824,120],{"class":106},[93,826,123],{"class":110},[93,828,126],{"class":99},[93,830,129],{"class":106},[93,832,132],{"class":110},[93,834,835],{"class":95,"line":194},[93,836,138],{"class":110},[93,838,839,841,844,847,850,853,856,858,861],{"class":95,"line":203},[93,840,197],{"class":99},[93,842,843],{"class":110}," (id ",[93,845,846],{"class":99},"\u003C=",[93,848,849],{"class":270}," 0",[93,851,852],{"class":110},") ",[93,854,855],{"class":99},"return",[93,857,271],{"class":270},[93,859,860],{"class":110},";  ",[93,862,863],{"class":384},"\u002F\u002F 參數驗證，擋掉明顯不存在的 ID\n",[93,865,866],{"class":95,"line":228},[93,867,232],{"emptyLinePlaceholder":231},[93,869,870,872,874,876,878,880,882,884,886],{"class":95,"line":235},[93,871,144],{"class":99},[93,873,147],{"class":106},[93,875,150],{"class":99},[93,877,154],{"class":153},[93,879,157],{"class":153},[93,881,160],{"class":110},[93,883,163],{"class":153},[93,885,166],{"class":153},[93,887,169],{"class":110},[93,889,890,892,894,896,898,900,902],{"class":95,"line":256},[93,891,144],{"class":99},[93,893,177],{"class":106},[93,895,150],{"class":99},[93,897,182],{"class":99},[93,899,185],{"class":110},[93,901,188],{"class":106},[93,903,191],{"class":110},[93,905,906,908],{"class":95,"line":276},[93,907,197],{"class":99},[93,909,200],{"class":110},[93,911,912,914,917,920,923,926,928,931,933,935,937,939,941,943],{"class":95,"line":307},[93,913,206],{"class":99},[93,915,916],{"class":110}," cached ",[93,918,919],{"class":99},"==",[93,921,922],{"class":110}," NullMarker ",[93,924,925],{"class":99},"?",[93,927,271],{"class":270},[93,929,930],{"class":99}," :",[93,932,209],{"class":110},[93,934,212],{"class":106},[93,936,111],{"class":110},[93,938,114],{"class":106},[93,940,219],{"class":110},[93,942,222],{"class":106},[93,944,225],{"class":110},[93,946,947],{"class":95,"line":312},[93,948,232],{"emptyLinePlaceholder":231},[93,950,951],{"class":95,"line":321},[93,952,953],{"class":384},"    \u002F\u002F 擊穿：同一個 Key 只讓一個請求去查資料庫（這是單一實例內的鎖）\n",[93,955,956,958,961,963,966,969,972,975,978,980,983,985,987,989,991],{"class":95,"line":327},[93,957,144],{"class":99},[93,959,960],{"class":106}," gate",[93,962,150],{"class":99},[93,964,965],{"class":110}," Locks.",[93,967,968],{"class":106},"GetOrAdd",[93,970,971],{"class":110},"(key, ",[93,973,974],{"class":106},"_",[93,976,977],{"class":99}," =>",[93,979,781],{"class":99},[93,981,982],{"class":106}," SemaphoreSlim",[93,984,123],{"class":110},[93,986,466],{"class":270},[93,988,767],{"class":110},[93,990,466],{"class":270},[93,992,304],{"class":110},[93,994,995,997,1000,1003],{"class":95,"line":332},[93,996,372],{"class":99},[93,998,999],{"class":110}," gate.",[93,1001,1002],{"class":106},"WaitAsync",[93,1004,784],{"class":110},[93,1006,1007],{"class":95,"line":352},[93,1008,1009],{"class":99},"    try\n",[93,1011,1012],{"class":95,"line":357},[93,1013,1014],{"class":110},"    {\n",[93,1016,1017],{"class":95,"line":369},[93,1018,1019],{"class":384},"        \u002F\u002F 拿到鎖之後再檢查一次，前一個請求可能已經重建好了\n",[93,1021,1022,1025,1028,1030,1032,1034],{"class":95,"line":388},[93,1023,1024],{"class":110},"        cached ",[93,1026,1027],{"class":99},"=",[93,1029,182],{"class":99},[93,1031,185],{"class":110},[93,1033,188],{"class":106},[93,1035,191],{"class":110},[93,1037,1038,1041],{"class":95,"line":424},[93,1039,1040],{"class":99},"        if",[93,1042,200],{"class":110},[93,1044,1046,1049,1051,1053,1055,1057,1059,1061,1063,1065,1067,1069,1071,1073],{"class":95,"line":1045},21,[93,1047,1048],{"class":99},"            return",[93,1050,916],{"class":110},[93,1052,919],{"class":99},[93,1054,922],{"class":110},[93,1056,925],{"class":99},[93,1058,271],{"class":270},[93,1060,930],{"class":99},[93,1062,209],{"class":110},[93,1064,212],{"class":106},[93,1066,111],{"class":110},[93,1068,114],{"class":106},[93,1070,219],{"class":110},[93,1072,222],{"class":106},[93,1074,225],{"class":110},[93,1076,1078],{"class":95,"line":1077},22,[93,1079,232],{"emptyLinePlaceholder":231},[93,1081,1083,1086,1088,1090,1092,1094,1096],{"class":95,"line":1082},23,[93,1084,1085],{"class":99},"        var",[93,1087,240],{"class":106},[93,1089,150],{"class":99},[93,1091,182],{"class":99},[93,1093,247],{"class":110},[93,1095,250],{"class":106},[93,1097,253],{"class":110},[93,1099,1101,1103,1105,1107,1109],{"class":95,"line":1100},24,[93,1102,1040],{"class":99},[93,1104,261],{"class":110},[93,1106,264],{"class":99},[93,1108,271],{"class":270},[93,1110,132],{"class":110},[93,1112,1114],{"class":95,"line":1113},25,[93,1115,1116],{"class":110},"        {\n",[93,1118,1120],{"class":95,"line":1119},26,[93,1121,1122],{"class":384},"            \u002F\u002F 穿透：不存在也快取，但時間短一點\n",[93,1124,1126,1129,1131,1133,1136,1138,1140,1142],{"class":95,"line":1125},27,[93,1127,1128],{"class":99},"            await",[93,1130,185],{"class":110},[93,1132,284],{"class":106},[93,1134,1135],{"class":110},"(key, NullMarker, TimeSpan.",[93,1137,296],{"class":106},[93,1139,123],{"class":110},[93,1141,466],{"class":270},[93,1143,304],{"class":110},[93,1145,1147,1149,1151],{"class":95,"line":1146},28,[93,1148,1048],{"class":99},[93,1150,271],{"class":270},[93,1152,169],{"class":110},[93,1154,1156],{"class":95,"line":1155},29,[93,1157,1158],{"class":110},"        }\n",[93,1160,1162],{"class":95,"line":1161},30,[93,1163,232],{"emptyLinePlaceholder":231},[93,1165,1167],{"class":95,"line":1166},31,[93,1168,1169],{"class":384},"        \u002F\u002F 雪崩：過期時間加上隨機值，避免同時過期\n",[93,1171,1173,1175,1178,1180,1183,1185,1187,1189,1191,1194,1196,1199,1202,1205,1207,1210,1212,1215],{"class":95,"line":1172},32,[93,1174,1085],{"class":99},[93,1176,1177],{"class":106}," ttl",[93,1179,150],{"class":99},[93,1181,1182],{"class":110}," TimeSpan.",[93,1184,296],{"class":106},[93,1186,123],{"class":110},[93,1188,301],{"class":270},[93,1190,852],{"class":110},[93,1192,1193],{"class":99},"+",[93,1195,1182],{"class":110},[93,1197,1198],{"class":106},"FromSeconds",[93,1200,1201],{"class":110},"(Random.Shared.",[93,1203,1204],{"class":106},"Next",[93,1206,123],{"class":110},[93,1208,1209],{"class":270},"0",[93,1211,767],{"class":110},[93,1213,1214],{"class":270},"120",[93,1216,304],{"class":110},[93,1218,1220,1222,1224,1226,1228,1230],{"class":95,"line":1219},33,[93,1221,279],{"class":99},[93,1223,185],{"class":110},[93,1225,284],{"class":106},[93,1227,287],{"class":110},[93,1229,290],{"class":106},[93,1231,1232],{"class":110},"(house), ttl);\n",[93,1234,1236,1238],{"class":95,"line":1235},34,[93,1237,206],{"class":99},[93,1239,318],{"class":110},[93,1241,1243],{"class":95,"line":1242},35,[93,1244,1245],{"class":110},"    }\n",[93,1247,1249],{"class":95,"line":1248},36,[93,1250,1251],{"class":99},"    finally\n",[93,1253,1255],{"class":95,"line":1254},37,[93,1256,1014],{"class":110},[93,1258,1260,1263,1266],{"class":95,"line":1259},38,[93,1261,1262],{"class":110},"        gate.",[93,1264,1265],{"class":106},"Release",[93,1267,784],{"class":110},[93,1269,1271],{"class":95,"line":1270},39,[93,1272,1245],{"class":110},[93,1274,1276],{"class":95,"line":1275},40,[93,1277,324],{"class":110},[10,1279,1280,1281,1283,1284,1287],{},"這裡的 ",[90,1282,770],{}," 只在",[20,1285,1286],{},"同一個應用程式實例內","有效。部署了 10 個 Pod，最多就會有 10 個請求同時去查資料庫——通常這已經足夠，比一千個好太多了。如果連這 10 個都不能接受，才需要用 Redis 做跨實例的分散式鎖。",[25,1289,1291],{"id":1290},"使用情境物件詳細頁","使用情境：物件詳細頁",[10,1293,1294],{},"物件詳細頁是房仲網站流量最大的頁面之一：",[541,1296,1297,1302,1312,1318,1324],{},[53,1298,1299,1301],{},[20,1300,47],{},"：Cache-Aside，過期時間 10 分鐘加隨機值",[53,1303,1304,1307,1308,1311],{},[20,1305,1306],{},"物件改價、下架","：更新資料庫後刪除 ",[90,1309,1310],{},"house:{id}","；同時發出物件異動事件，由同步服務再刪一次，處理刪除失敗與延遲雙刪的情況",[53,1313,1314,1317],{},[20,1315,1316],{},"爬蟲與惡意請求","：ID 驗證加上空值快取，不存在的物件不會一直打到資料庫",[53,1319,1320,1323],{},[20,1321,1322],{},"新建案上架、廣告曝光","：預先把物件寫進快取，並用互斥鎖避免過期瞬間的大量查詢",[53,1325,1326,1329],{},[20,1327,1328],{},"Redis 維護或故障","：Pod 內有 30 秒的本機快取，資料庫前面有限流，最差的情況是部分請求變慢，而不是整個網站掛掉",[25,1331,1332],{"id":1332},"注意事項",[541,1334,1335,1341,1347,1353,1362],{},[53,1336,1337,1340],{},[20,1338,1339],{},"不是所有資料都適合快取","：讀多寫少、可以接受短暫不一致的才適合。寫入頻繁的資料，快取一直被刪，命中率很低，反而多了一次 Redis 往返",[53,1342,1343,1346],{},[20,1344,1345],{},"監控命中率","：命中率太低代表 Key 設計或過期時間有問題，快取只是多一層開銷",[53,1348,1349,1352],{},[20,1350,1351],{},"快取的值不要太大","：一個 Key 存好幾 MB 的 JSON，序列化、網路傳輸都很慢，還會變成 Big Key。只快取頁面真正需要的欄位",[53,1354,1355,1358,1359],{},[20,1356,1357],{},"序列化的格式要有版本概念","：物件類別加了欄位、改了型別，部署新版本後讀到舊格式的快取可能會反序列化失敗。改動結構時可以在 Key 加上版本號，例如 ",[90,1360,1361],{},"house:v2:{id}",[53,1363,1364,1367],{},[20,1365,1366],{},"Redis 掛掉不能讓整個服務掛掉","：連不上 Redis 時，要降級成直接查資料庫（搭配限流），而不是直接拋出例外",[25,1369,1370],{"id":1370},"一句話總結",[10,1372,1373],{},"用 Cache-Aside 時，讀取先查快取再查資料庫並寫回，寫入先更新資料庫再刪除快取，並一定要設過期時間當最後防線；查不存在的資料用空值快取或布隆過濾器防穿透，熱門 Key 過期用互斥鎖或邏輯過期防擊穿，大量 Key 同時失效用隨機過期時間、高可用與限流防雪崩。",[1375,1376,1377],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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);}",{"title":88,"searchDepth":135,"depth":135,"links":1379},[1380,1385,1390,1391,1392,1393],{"id":27,"depth":135,"text":28,"children":1381},[1382,1383,1384],{"id":430,"depth":141,"text":431},{"id":514,"depth":141,"text":515},{"id":536,"depth":141,"text":536},{"id":576,"depth":135,"text":576,"children":1386},[1387,1388,1389],{"id":585,"depth":141,"text":586},{"id":633,"depth":141,"text":634},{"id":680,"depth":141,"text":681},{"id":740,"depth":135,"text":740},{"id":1290,"depth":135,"text":1291},{"id":1332,"depth":135,"text":1332},{"id":1370,"depth":135,"text":1370},"2024-07-09","Redis 的資料結構與基本操作可以先參考 Redis 不只是快取。這篇專注在最常見的用法：把它放在資料庫前面當快取。",null,"md",{},"\u002Farticles\u002Fredis-cache-penetration-breakdown-avalanche",{"title":5,"description":1395},"articles\u002Fredis-cache-penetration-breakdown-avalanche",[1403,1404,1405,1406],"Redis","快取","高併發","系統設計","WYOD_mmruhvreq7FY3aRyNd1nayFate3CefjwdmgBmo",1792959150360]