[{"data":1,"prerenderedAt":3501},["ShallowReactive",2],{"content:\u002F09-redis\u002F03-go-client-caching":3},{"title":4,"description":5,"path":6,"body":7},"Go и Redis: go-redis, cache-aside и защита от stampede","В Go Redis чаще всего используют через клиент github.com\u002Fredis\u002Fgo-redis\u002Fv9. Он даёт connection pool, context-aware API, pipeline, transactions, cluster\u002Fsentinel clients и нормальную интеграцию с типами Go.","\u002F09-redis\u002F03-go-client-caching",{"type":8,"value":9,"toc":3484},"minimark",[10,14,23,34,37,42,45,662,665,690,692,696,702,710,713,734,736,740,743,749,752,1503,1509,1514,1520,1523,1601,1604,1606,1610,1613,1649,1652,1791,1794,1796,1800,1803,1809,1812,1815,2058,2060,2064,2067,2128,2131,2137,2140,2142,2146,2149,2155,2158,2178,2180,2184,2190,2530,2533,2535,2539,2546,2564,2942,2945,2947,2951,2957,2990,2992,2996,3031,3033,3037,3069,3071,3075,3112,3311,3480],[11,12,4],"h1",{"id":13},"go-и-redis-go-redis-cache-aside-и-защита-от-stampede",[15,16,17,18,22],"p",{},"В Go Redis чаще всего используют через клиент ",[19,20,21],"code",{},"github.com\u002Fredis\u002Fgo-redis\u002Fv9",". Он даёт connection pool, context-aware API, pipeline, transactions, cluster\u002Fsentinel clients и нормальную интеграцию с типами Go.",[15,24,25,26,29,30,33],{},"Redis-код в backend должен быть таким же аккуратным, как код для PostgreSQL: timeout'ы, обработка ошибок, метрики, graceful shutdown и понятные границы ответственности. Кеш - это не место для хаотичных ",[19,27,28],{},"GET"," и ",[19,31,32],{},"SET"," из любого слоя приложения.",[35,36],"hr",{},[38,39,41],"h2",{"id":40},"подключение-и-graceful-shutdown","Подключение и graceful shutdown",[15,43,44],{},"Минимальный пример подключения:",[46,47,53],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":52},"language-go shiki shiki-themes github-dark","package main\n\nimport (\n    \"context\"\n    \"errors\"\n    \"log\"\n    \"os\"\n    \"os\u002Fsignal\"\n    \"syscall\"\n    \"time\"\n\n    \"github.com\u002Fredis\u002Fgo-redis\u002Fv9\"\n)\n\nfunc main() {\n    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)\n    defer stop()\n\n    rdb := redis.NewClient(&redis.Options{\n        Addr:         \"localhost:6379\",\n        Username:     \"\",\n        Password:     \"\",\n        DB:           0,\n        PoolSize:     20,\n        MinIdleConns: 5,\n        DialTimeout:  2 * time.Second,\n        ReadTimeout:  500 * time.Millisecond,\n        WriteTimeout: 500 * time.Millisecond,\n    })\n    defer func() {\n        if err := rdb.Close(); err != nil {\n            log.Printf(\"close redis: %v\", err)\n        }\n    }()\n\n    pingCtx, cancel := context.WithTimeout(ctx, time.Second)\n    defer cancel()\n\n    if err := rdb.Ping(pingCtx).Err(); err != nil {\n        log.Fatalf(\"ping redis: %v\", err)\n    }\n\n    log.Println(\"redis connected\")\n\n    \u003C-ctx.Done()\n    if !errors.Is(ctx.Err(), context.Canceled) {\n        log.Printf(\"shutdown: %v\", ctx.Err())\n    }\n    _ = os.Stdout.Sync()\n}\n","go","no-run","",[19,54,55,68,75,85,98,108,118,128,138,148,158,163,172,178,183,195,219,231,236,268,280,291,301,313,324,335,350,364,376,382,392,421,444,450,456,461,478,488,493,522,542,548,553,569,574,588,610,634,639,656],{"__ignoreMap":52},[56,57,60,64],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"snl16","package",[56,65,67],{"class":66},"svObZ"," main\n",[56,69,71],{"class":58,"line":70},2,[56,72,74],{"emptyLinePlaceholder":73},true,"\n",[56,76,78,81],{"class":58,"line":77},3,[56,79,80],{"class":62},"import",[56,82,84],{"class":83},"s95oV"," (\n",[56,86,88,92,95],{"class":58,"line":87},4,[56,89,91],{"class":90},"sU2Wk","    \"",[56,93,94],{"class":66},"context",[56,96,97],{"class":90},"\"\n",[56,99,101,103,106],{"class":58,"line":100},5,[56,102,91],{"class":90},[56,104,105],{"class":66},"errors",[56,107,97],{"class":90},[56,109,111,113,116],{"class":58,"line":110},6,[56,112,91],{"class":90},[56,114,115],{"class":66},"log",[56,117,97],{"class":90},[56,119,121,123,126],{"class":58,"line":120},7,[56,122,91],{"class":90},[56,124,125],{"class":66},"os",[56,127,97],{"class":90},[56,129,131,133,136],{"class":58,"line":130},8,[56,132,91],{"class":90},[56,134,135],{"class":66},"os\u002Fsignal",[56,137,97],{"class":90},[56,139,141,143,146],{"class":58,"line":140},9,[56,142,91],{"class":90},[56,144,145],{"class":66},"syscall",[56,147,97],{"class":90},[56,149,151,153,156],{"class":58,"line":150},10,[56,152,91],{"class":90},[56,154,155],{"class":66},"time",[56,157,97],{"class":90},[56,159,161],{"class":58,"line":160},11,[56,162,74],{"emptyLinePlaceholder":73},[56,164,166,168,170],{"class":58,"line":165},12,[56,167,91],{"class":90},[56,169,21],{"class":66},[56,171,97],{"class":90},[56,173,175],{"class":58,"line":174},13,[56,176,177],{"class":83},")\n",[56,179,181],{"class":58,"line":180},14,[56,182,74],{"emptyLinePlaceholder":73},[56,184,186,189,192],{"class":58,"line":185},15,[56,187,188],{"class":62},"func",[56,190,191],{"class":66}," main",[56,193,194],{"class":83},"() {\n",[56,196,198,201,204,207,210,213,216],{"class":58,"line":197},16,[56,199,200],{"class":83},"    ctx, stop ",[56,202,203],{"class":62},":=",[56,205,206],{"class":83}," signal.",[56,208,209],{"class":66},"NotifyContext",[56,211,212],{"class":83},"(context.",[56,214,215],{"class":66},"Background",[56,217,218],{"class":83},"(), syscall.SIGINT, syscall.SIGTERM)\n",[56,220,222,225,228],{"class":58,"line":221},17,[56,223,224],{"class":62},"    defer",[56,226,227],{"class":66}," stop",[56,229,230],{"class":83},"()\n",[56,232,234],{"class":58,"line":233},18,[56,235,74],{"emptyLinePlaceholder":73},[56,237,239,242,244,247,250,253,256,259,262,265],{"class":58,"line":238},19,[56,240,241],{"class":83},"    rdb ",[56,243,203],{"class":62},[56,245,246],{"class":83}," redis.",[56,248,249],{"class":66},"NewClient",[56,251,252],{"class":83},"(",[56,254,255],{"class":62},"&",[56,257,258],{"class":66},"redis",[56,260,261],{"class":83},".",[56,263,264],{"class":66},"Options",[56,266,267],{"class":83},"{\n",[56,269,271,274,277],{"class":58,"line":270},20,[56,272,273],{"class":83},"        Addr:         ",[56,275,276],{"class":90},"\"localhost:6379\"",[56,278,279],{"class":83},",\n",[56,281,283,286,289],{"class":58,"line":282},21,[56,284,285],{"class":83},"        Username:     ",[56,287,288],{"class":90},"\"\"",[56,290,279],{"class":83},[56,292,294,297,299],{"class":58,"line":293},22,[56,295,296],{"class":83},"        Password:     ",[56,298,288],{"class":90},[56,300,279],{"class":83},[56,302,304,307,311],{"class":58,"line":303},23,[56,305,306],{"class":83},"        DB:           ",[56,308,310],{"class":309},"sDLfK","0",[56,312,279],{"class":83},[56,314,316,319,322],{"class":58,"line":315},24,[56,317,318],{"class":83},"        PoolSize:     ",[56,320,321],{"class":309},"20",[56,323,279],{"class":83},[56,325,327,330,333],{"class":58,"line":326},25,[56,328,329],{"class":83},"        MinIdleConns: ",[56,331,332],{"class":309},"5",[56,334,279],{"class":83},[56,336,338,341,344,347],{"class":58,"line":337},26,[56,339,340],{"class":83},"        DialTimeout:  ",[56,342,343],{"class":309},"2",[56,345,346],{"class":62}," *",[56,348,349],{"class":83}," time.Second,\n",[56,351,353,356,359,361],{"class":58,"line":352},27,[56,354,355],{"class":83},"        ReadTimeout:  ",[56,357,358],{"class":309},"500",[56,360,346],{"class":62},[56,362,363],{"class":83}," time.Millisecond,\n",[56,365,367,370,372,374],{"class":58,"line":366},28,[56,368,369],{"class":83},"        WriteTimeout: ",[56,371,358],{"class":309},[56,373,346],{"class":62},[56,375,363],{"class":83},[56,377,379],{"class":58,"line":378},29,[56,380,381],{"class":83},"    })\n",[56,383,385,387,390],{"class":58,"line":384},30,[56,386,224],{"class":62},[56,388,389],{"class":62}," func",[56,391,194],{"class":83},[56,393,395,398,401,403,406,409,412,415,418],{"class":58,"line":394},31,[56,396,397],{"class":62},"        if",[56,399,400],{"class":83}," err ",[56,402,203],{"class":62},[56,404,405],{"class":83}," rdb.",[56,407,408],{"class":66},"Close",[56,410,411],{"class":83},"(); err ",[56,413,414],{"class":62},"!=",[56,416,417],{"class":309}," nil",[56,419,420],{"class":83}," {\n",[56,422,424,427,430,432,435,438,441],{"class":58,"line":423},32,[56,425,426],{"class":83},"            log.",[56,428,429],{"class":66},"Printf",[56,431,252],{"class":83},[56,433,434],{"class":90},"\"close redis: ",[56,436,437],{"class":309},"%v",[56,439,440],{"class":90},"\"",[56,442,443],{"class":83},", err)\n",[56,445,447],{"class":58,"line":446},33,[56,448,449],{"class":83},"        }\n",[56,451,453],{"class":58,"line":452},34,[56,454,455],{"class":83},"    }()\n",[56,457,459],{"class":58,"line":458},35,[56,460,74],{"emptyLinePlaceholder":73},[56,462,464,467,469,472,475],{"class":58,"line":463},36,[56,465,466],{"class":83},"    pingCtx, cancel ",[56,468,203],{"class":62},[56,470,471],{"class":83}," context.",[56,473,474],{"class":66},"WithTimeout",[56,476,477],{"class":83},"(ctx, time.Second)\n",[56,479,481,483,486],{"class":58,"line":480},37,[56,482,224],{"class":62},[56,484,485],{"class":66}," cancel",[56,487,230],{"class":83},[56,489,491],{"class":58,"line":490},38,[56,492,74],{"emptyLinePlaceholder":73},[56,494,496,499,501,503,505,508,511,514,516,518,520],{"class":58,"line":495},39,[56,497,498],{"class":62},"    if",[56,500,400],{"class":83},[56,502,203],{"class":62},[56,504,405],{"class":83},[56,506,507],{"class":66},"Ping",[56,509,510],{"class":83},"(pingCtx).",[56,512,513],{"class":66},"Err",[56,515,411],{"class":83},[56,517,414],{"class":62},[56,519,417],{"class":309},[56,521,420],{"class":83},[56,523,525,528,531,533,536,538,540],{"class":58,"line":524},40,[56,526,527],{"class":83},"        log.",[56,529,530],{"class":66},"Fatalf",[56,532,252],{"class":83},[56,534,535],{"class":90},"\"ping redis: ",[56,537,437],{"class":309},[56,539,440],{"class":90},[56,541,443],{"class":83},[56,543,545],{"class":58,"line":544},41,[56,546,547],{"class":83},"    }\n",[56,549,551],{"class":58,"line":550},42,[56,552,74],{"emptyLinePlaceholder":73},[56,554,556,559,562,564,567],{"class":58,"line":555},43,[56,557,558],{"class":83},"    log.",[56,560,561],{"class":66},"Println",[56,563,252],{"class":83},[56,565,566],{"class":90},"\"redis connected\"",[56,568,177],{"class":83},[56,570,572],{"class":58,"line":571},44,[56,573,74],{"emptyLinePlaceholder":73},[56,575,577,580,583,586],{"class":58,"line":576},45,[56,578,579],{"class":62},"    \u003C-",[56,581,582],{"class":83},"ctx.",[56,584,585],{"class":66},"Done",[56,587,230],{"class":83},[56,589,591,593,596,599,602,605,607],{"class":58,"line":590},46,[56,592,498],{"class":62},[56,594,595],{"class":62}," !",[56,597,598],{"class":83},"errors.",[56,600,601],{"class":66},"Is",[56,603,604],{"class":83},"(ctx.",[56,606,513],{"class":66},[56,608,609],{"class":83},"(), context.Canceled) {\n",[56,611,613,615,617,619,622,624,626,629,631],{"class":58,"line":612},47,[56,614,527],{"class":83},[56,616,429],{"class":66},[56,618,252],{"class":83},[56,620,621],{"class":90},"\"shutdown: ",[56,623,437],{"class":309},[56,625,440],{"class":90},[56,627,628],{"class":83},", ctx.",[56,630,513],{"class":66},[56,632,633],{"class":83},"())\n",[56,635,637],{"class":58,"line":636},48,[56,638,547],{"class":83},[56,640,642,645,648,651,654],{"class":58,"line":641},49,[56,643,644],{"class":83},"    _ ",[56,646,647],{"class":62},"=",[56,649,650],{"class":83}," os.Stdout.",[56,652,653],{"class":66},"Sync",[56,655,230],{"class":83},[56,657,659],{"class":58,"line":658},50,[56,660,661],{"class":83},"}\n",[15,663,664],{},"Что важно:",[666,667,668,675,678,684],"ul",{},[669,670,671,674],"li",{},[19,672,673],{},"context.Context"," должен приходить из request\u002Fjob lifecycle;",[669,676,677],{},"у операций должны быть deadline или timeout;",[669,679,680,683],{},[19,681,682],{},"Close()"," нужен, чтобы корректно закрыть соединения pool;",[669,685,686,689],{},[19,687,688],{},"PoolSize"," не надо ставить \"чем больше, тем лучше\".",[35,691],{},[38,693,695],{"id":694},"connection-pool","Connection pool",[15,697,698,701],{},[19,699,700],{},"go-redis"," держит pool TCP-соединений. Одна Redis-команда занимает соединение на время round-trip'а. Если pool слишком маленький, goroutine ждут свободное соединение. Если слишком большой, Redis и сеть получают лишнюю нагрузку.",[46,703,708],{"className":704,"code":706,"language":707,"meta":52},[705],"language-text","goroutines ─► go-redis pool ─► TCP connections ─► Redis\n                │\n                ├─ idle conn\n                ├─ busy conn\n                └─ wait queue\n","text",[19,709,706],{"__ignoreMap":52},[15,711,712],{},"Практический подход:",[666,714,715,725,728,731],{},[669,716,717,718,720,721,724],{},"начинайте с умеренного ",[19,719,688],{},", например ",[19,722,723],{},"10 * CPU"," или меньше для небольшого сервиса;",[669,726,727],{},"измеряйте pool stats: hits, misses, timeouts;",[669,729,730],{},"избегайте длинных блокирующих команд на общем клиенте;",[669,732,733],{},"для Pub\u002FSub и blocking operations часто нужен отдельный client.",[35,735],{},[38,737,739],{"id":738},"cache-aside-repository","Cache-aside repository",[15,741,742],{},"Cache-aside означает: приложение само решает, когда читать кеш, когда идти в БД и когда сохранять результат в кеш.",[46,744,747],{"className":745,"code":746,"language":707,"meta":52},[705],"GetCourse(id)\n  ├─ Redis GET course:{id}\n  │    └─ hit -> return\n  ├─ PostgreSQL SELECT\n  ├─ Redis SET course:{id} EX 10m\n  └─ return\n",[19,748,746],{"__ignoreMap":52},[15,750,751],{},"Пример почти реального repository:",[46,753,755],{"className":48,"code":754,"language":50,"meta":51,"style":52},"package cache\n\nimport (\n    \"context\"\n    \"encoding\u002Fjson\"\n    \"errors\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com\u002Fredis\u002Fgo-redis\u002Fv9\"\n)\n\ntype Course struct {\n    ID          int64  `json:\"id\"`\n    Slug        string `json:\"slug\"`\n    Title       string `json:\"title\"`\n    Description string `json:\"description\"`\n}\n\ntype CourseStore interface {\n    GetCourse(ctx context.Context, id int64) (Course, error)\n}\n\ntype CachedCourseStore struct {\n    redis *redis.Client\n    next  CourseStore\n    ttl   time.Duration\n}\n\nfunc NewCachedCourseStore(rdb *redis.Client, next CourseStore, ttl time.Duration) *CachedCourseStore {\n    return &CachedCourseStore{redis: rdb, next: next, ttl: ttl}\n}\n\nfunc (s *CachedCourseStore) GetCourse(ctx context.Context, id int64) (Course, error) {\n    key := fmt.Sprintf(\"course:%d\", id)\n\n    raw, err := s.redis.Get(ctx, key).Bytes()\n    if err == nil {\n        var course Course\n        if err := json.Unmarshal(raw, &course); err == nil {\n            return course, nil\n        }\n        \u002F\u002F Битый кеш не должен ломать основной сценарий.\n        delCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)\n        defer cancel()\n        _ = s.redis.Del(delCtx, key).Err()\n    } else if !errors.Is(err, redis.Nil) {\n        \u002F\u002F Redis недоступен: логируем в реальном коде и идём в основное хранилище.\n    }\n\n    course, err := s.next.GetCourse(ctx, id)\n    if err != nil {\n        return Course{}, err\n    }\n\n    payload, err := json.Marshal(course)\n    if err != nil {\n        return course, nil\n    }\n\n    if err := s.redis.Set(ctx, key, payload, s.ttl).Err(); err != nil {\n        \u002F\u002F Ошибка записи в кеш обычно не должна ломать read path.\n        return course, nil\n    }\n\n    return course, nil\n}\n",[19,756,757,764,768,774,782,791,799,808,816,820,828,832,836,849,860,871,881,891,895,899,911,952,956,960,971,986,994,1006,1010,1014,1065,1078,1082,1086,1132,1158,1162,1183,1196,1207,1235,1246,1250,1256,1278,1287,1306,1326,1331,1335,1339,1355,1368,1379,1384,1389,1405,1418,1427,1432,1437,1464,1470,1479,1484,1489,1498],{"__ignoreMap":52},[56,758,759,761],{"class":58,"line":59},[56,760,63],{"class":62},[56,762,763],{"class":66}," cache\n",[56,765,766],{"class":58,"line":70},[56,767,74],{"emptyLinePlaceholder":73},[56,769,770,772],{"class":58,"line":77},[56,771,80],{"class":62},[56,773,84],{"class":83},[56,775,776,778,780],{"class":58,"line":87},[56,777,91],{"class":90},[56,779,94],{"class":66},[56,781,97],{"class":90},[56,783,784,786,789],{"class":58,"line":100},[56,785,91],{"class":90},[56,787,788],{"class":66},"encoding\u002Fjson",[56,790,97],{"class":90},[56,792,793,795,797],{"class":58,"line":110},[56,794,91],{"class":90},[56,796,105],{"class":66},[56,798,97],{"class":90},[56,800,801,803,806],{"class":58,"line":120},[56,802,91],{"class":90},[56,804,805],{"class":66},"fmt",[56,807,97],{"class":90},[56,809,810,812,814],{"class":58,"line":130},[56,811,91],{"class":90},[56,813,155],{"class":66},[56,815,97],{"class":90},[56,817,818],{"class":58,"line":140},[56,819,74],{"emptyLinePlaceholder":73},[56,821,822,824,826],{"class":58,"line":150},[56,823,91],{"class":90},[56,825,21],{"class":66},[56,827,97],{"class":90},[56,829,830],{"class":58,"line":160},[56,831,177],{"class":83},[56,833,834],{"class":58,"line":165},[56,835,74],{"emptyLinePlaceholder":73},[56,837,838,841,844,847],{"class":58,"line":174},[56,839,840],{"class":62},"type",[56,842,843],{"class":66}," Course",[56,845,846],{"class":62}," struct",[56,848,420],{"class":83},[56,850,851,854,857],{"class":58,"line":180},[56,852,853],{"class":83},"    ID          ",[56,855,856],{"class":62},"int64",[56,858,859],{"class":90},"  `json:\"id\"`\n",[56,861,862,865,868],{"class":58,"line":185},[56,863,864],{"class":83},"    Slug        ",[56,866,867],{"class":62},"string",[56,869,870],{"class":90}," `json:\"slug\"`\n",[56,872,873,876,878],{"class":58,"line":197},[56,874,875],{"class":83},"    Title       ",[56,877,867],{"class":62},[56,879,880],{"class":90}," `json:\"title\"`\n",[56,882,883,886,888],{"class":58,"line":221},[56,884,885],{"class":83},"    Description ",[56,887,867],{"class":62},[56,889,890],{"class":90}," `json:\"description\"`\n",[56,892,893],{"class":58,"line":233},[56,894,661],{"class":83},[56,896,897],{"class":58,"line":238},[56,898,74],{"emptyLinePlaceholder":73},[56,900,901,903,906,909],{"class":58,"line":270},[56,902,840],{"class":62},[56,904,905],{"class":66}," CourseStore",[56,907,908],{"class":62}," interface",[56,910,420],{"class":83},[56,912,913,916,918,922,925,927,930,933,936,939,942,945,947,950],{"class":58,"line":282},[56,914,915],{"class":66},"    GetCourse",[56,917,252],{"class":83},[56,919,921],{"class":920},"s9osk","ctx",[56,923,924],{"class":66}," context",[56,926,261],{"class":83},[56,928,929],{"class":66},"Context",[56,931,932],{"class":83},", ",[56,934,935],{"class":920},"id",[56,937,938],{"class":62}," int64",[56,940,941],{"class":83},") (",[56,943,944],{"class":66},"Course",[56,946,932],{"class":83},[56,948,949],{"class":62},"error",[56,951,177],{"class":83},[56,953,954],{"class":58,"line":293},[56,955,661],{"class":83},[56,957,958],{"class":58,"line":303},[56,959,74],{"emptyLinePlaceholder":73},[56,961,962,964,967,969],{"class":58,"line":315},[56,963,840],{"class":62},[56,965,966],{"class":66}," CachedCourseStore",[56,968,846],{"class":62},[56,970,420],{"class":83},[56,972,973,976,979,981,983],{"class":58,"line":326},[56,974,975],{"class":83},"    redis ",[56,977,978],{"class":62},"*",[56,980,258],{"class":66},[56,982,261],{"class":83},[56,984,985],{"class":66},"Client\n",[56,987,988,991],{"class":58,"line":337},[56,989,990],{"class":83},"    next  ",[56,992,993],{"class":66},"CourseStore\n",[56,995,996,999,1001,1003],{"class":58,"line":352},[56,997,998],{"class":83},"    ttl   ",[56,1000,155],{"class":66},[56,1002,261],{"class":83},[56,1004,1005],{"class":66},"Duration\n",[56,1007,1008],{"class":58,"line":366},[56,1009,661],{"class":83},[56,1011,1012],{"class":58,"line":378},[56,1013,74],{"emptyLinePlaceholder":73},[56,1015,1016,1018,1021,1023,1026,1028,1030,1032,1035,1037,1040,1042,1044,1047,1050,1052,1055,1058,1060,1063],{"class":58,"line":384},[56,1017,188],{"class":62},[56,1019,1020],{"class":66}," NewCachedCourseStore",[56,1022,252],{"class":83},[56,1024,1025],{"class":920},"rdb",[56,1027,346],{"class":62},[56,1029,258],{"class":66},[56,1031,261],{"class":83},[56,1033,1034],{"class":66},"Client",[56,1036,932],{"class":83},[56,1038,1039],{"class":920},"next",[56,1041,905],{"class":66},[56,1043,932],{"class":83},[56,1045,1046],{"class":920},"ttl",[56,1048,1049],{"class":66}," time",[56,1051,261],{"class":83},[56,1053,1054],{"class":66},"Duration",[56,1056,1057],{"class":83},") ",[56,1059,978],{"class":62},[56,1061,1062],{"class":66},"CachedCourseStore",[56,1064,420],{"class":83},[56,1066,1067,1070,1073,1075],{"class":58,"line":394},[56,1068,1069],{"class":62},"    return",[56,1071,1072],{"class":62}," &",[56,1074,1062],{"class":66},[56,1076,1077],{"class":83},"{redis: rdb, next: next, ttl: ttl}\n",[56,1079,1080],{"class":58,"line":423},[56,1081,661],{"class":83},[56,1083,1084],{"class":58,"line":446},[56,1085,74],{"emptyLinePlaceholder":73},[56,1087,1088,1090,1093,1096,1098,1100,1102,1105,1107,1109,1111,1113,1115,1117,1119,1121,1123,1125,1127,1129],{"class":58,"line":452},[56,1089,188],{"class":62},[56,1091,1092],{"class":83}," (",[56,1094,1095],{"class":920},"s ",[56,1097,978],{"class":62},[56,1099,1062],{"class":66},[56,1101,1057],{"class":83},[56,1103,1104],{"class":66},"GetCourse",[56,1106,252],{"class":83},[56,1108,921],{"class":920},[56,1110,924],{"class":66},[56,1112,261],{"class":83},[56,1114,929],{"class":66},[56,1116,932],{"class":83},[56,1118,935],{"class":920},[56,1120,938],{"class":62},[56,1122,941],{"class":83},[56,1124,944],{"class":66},[56,1126,932],{"class":83},[56,1128,949],{"class":62},[56,1130,1131],{"class":83},") {\n",[56,1133,1134,1137,1139,1142,1145,1147,1150,1153,1155],{"class":58,"line":458},[56,1135,1136],{"class":83},"    key ",[56,1138,203],{"class":62},[56,1140,1141],{"class":83}," fmt.",[56,1143,1144],{"class":66},"Sprintf",[56,1146,252],{"class":83},[56,1148,1149],{"class":90},"\"course:",[56,1151,1152],{"class":309},"%d",[56,1154,440],{"class":90},[56,1156,1157],{"class":83},", id)\n",[56,1159,1160],{"class":58,"line":463},[56,1161,74],{"emptyLinePlaceholder":73},[56,1163,1164,1167,1169,1172,1175,1178,1181],{"class":58,"line":480},[56,1165,1166],{"class":83},"    raw, err ",[56,1168,203],{"class":62},[56,1170,1171],{"class":83}," s.redis.",[56,1173,1174],{"class":66},"Get",[56,1176,1177],{"class":83},"(ctx, key).",[56,1179,1180],{"class":66},"Bytes",[56,1182,230],{"class":83},[56,1184,1185,1187,1189,1192,1194],{"class":58,"line":490},[56,1186,498],{"class":62},[56,1188,400],{"class":83},[56,1190,1191],{"class":62},"==",[56,1193,417],{"class":309},[56,1195,420],{"class":83},[56,1197,1198,1201,1204],{"class":58,"line":495},[56,1199,1200],{"class":62},"        var",[56,1202,1203],{"class":83}," course ",[56,1205,1206],{"class":66},"Course\n",[56,1208,1209,1211,1213,1215,1218,1221,1224,1226,1229,1231,1233],{"class":58,"line":524},[56,1210,397],{"class":62},[56,1212,400],{"class":83},[56,1214,203],{"class":62},[56,1216,1217],{"class":83}," json.",[56,1219,1220],{"class":66},"Unmarshal",[56,1222,1223],{"class":83},"(raw, ",[56,1225,255],{"class":62},[56,1227,1228],{"class":83},"course); err ",[56,1230,1191],{"class":62},[56,1232,417],{"class":309},[56,1234,420],{"class":83},[56,1236,1237,1240,1243],{"class":58,"line":544},[56,1238,1239],{"class":62},"            return",[56,1241,1242],{"class":83}," course, ",[56,1244,1245],{"class":309},"nil\n",[56,1247,1248],{"class":58,"line":550},[56,1249,449],{"class":83},[56,1251,1252],{"class":58,"line":555},[56,1253,1255],{"class":1254},"sAwPA","        \u002F\u002F Битый кеш не должен ломать основной сценарий.\n",[56,1257,1258,1261,1263,1265,1267,1270,1273,1275],{"class":58,"line":571},[56,1259,1260],{"class":83},"        delCtx, cancel ",[56,1262,203],{"class":62},[56,1264,471],{"class":83},[56,1266,474],{"class":66},[56,1268,1269],{"class":83},"(ctx, ",[56,1271,1272],{"class":309},"200",[56,1274,978],{"class":62},[56,1276,1277],{"class":83},"time.Millisecond)\n",[56,1279,1280,1283,1285],{"class":58,"line":576},[56,1281,1282],{"class":62},"        defer",[56,1284,485],{"class":66},[56,1286,230],{"class":83},[56,1288,1289,1292,1294,1296,1299,1302,1304],{"class":58,"line":590},[56,1290,1291],{"class":83},"        _ ",[56,1293,647],{"class":62},[56,1295,1171],{"class":83},[56,1297,1298],{"class":66},"Del",[56,1300,1301],{"class":83},"(delCtx, key).",[56,1303,513],{"class":66},[56,1305,230],{"class":83},[56,1307,1308,1311,1314,1317,1319,1321,1323],{"class":58,"line":612},[56,1309,1310],{"class":83},"    } ",[56,1312,1313],{"class":62},"else",[56,1315,1316],{"class":62}," if",[56,1318,595],{"class":62},[56,1320,598],{"class":83},[56,1322,601],{"class":66},[56,1324,1325],{"class":83},"(err, redis.Nil) {\n",[56,1327,1328],{"class":58,"line":636},[56,1329,1330],{"class":1254},"        \u002F\u002F Redis недоступен: логируем в реальном коде и идём в основное хранилище.\n",[56,1332,1333],{"class":58,"line":641},[56,1334,547],{"class":83},[56,1336,1337],{"class":58,"line":658},[56,1338,74],{"emptyLinePlaceholder":73},[56,1340,1342,1345,1347,1350,1352],{"class":58,"line":1341},51,[56,1343,1344],{"class":83},"    course, err ",[56,1346,203],{"class":62},[56,1348,1349],{"class":83}," s.next.",[56,1351,1104],{"class":66},[56,1353,1354],{"class":83},"(ctx, id)\n",[56,1356,1358,1360,1362,1364,1366],{"class":58,"line":1357},52,[56,1359,498],{"class":62},[56,1361,400],{"class":83},[56,1363,414],{"class":62},[56,1365,417],{"class":309},[56,1367,420],{"class":83},[56,1369,1371,1374,1376],{"class":58,"line":1370},53,[56,1372,1373],{"class":62},"        return",[56,1375,843],{"class":66},[56,1377,1378],{"class":83},"{}, err\n",[56,1380,1382],{"class":58,"line":1381},54,[56,1383,547],{"class":83},[56,1385,1387],{"class":58,"line":1386},55,[56,1388,74],{"emptyLinePlaceholder":73},[56,1390,1392,1395,1397,1399,1402],{"class":58,"line":1391},56,[56,1393,1394],{"class":83},"    payload, err ",[56,1396,203],{"class":62},[56,1398,1217],{"class":83},[56,1400,1401],{"class":66},"Marshal",[56,1403,1404],{"class":83},"(course)\n",[56,1406,1408,1410,1412,1414,1416],{"class":58,"line":1407},57,[56,1409,498],{"class":62},[56,1411,400],{"class":83},[56,1413,414],{"class":62},[56,1415,417],{"class":309},[56,1417,420],{"class":83},[56,1419,1421,1423,1425],{"class":58,"line":1420},58,[56,1422,1373],{"class":62},[56,1424,1242],{"class":83},[56,1426,1245],{"class":309},[56,1428,1430],{"class":58,"line":1429},59,[56,1431,547],{"class":83},[56,1433,1435],{"class":58,"line":1434},60,[56,1436,74],{"emptyLinePlaceholder":73},[56,1438,1440,1442,1444,1446,1448,1451,1454,1456,1458,1460,1462],{"class":58,"line":1439},61,[56,1441,498],{"class":62},[56,1443,400],{"class":83},[56,1445,203],{"class":62},[56,1447,1171],{"class":83},[56,1449,1450],{"class":66},"Set",[56,1452,1453],{"class":83},"(ctx, key, payload, s.ttl).",[56,1455,513],{"class":66},[56,1457,411],{"class":83},[56,1459,414],{"class":62},[56,1461,417],{"class":309},[56,1463,420],{"class":83},[56,1465,1467],{"class":58,"line":1466},62,[56,1468,1469],{"class":1254},"        \u002F\u002F Ошибка записи в кеш обычно не должна ломать read path.\n",[56,1471,1473,1475,1477],{"class":58,"line":1472},63,[56,1474,1373],{"class":62},[56,1476,1242],{"class":83},[56,1478,1245],{"class":309},[56,1480,1482],{"class":58,"line":1481},64,[56,1483,547],{"class":83},[56,1485,1487],{"class":58,"line":1486},65,[56,1488,74],{"emptyLinePlaceholder":73},[56,1490,1492,1494,1496],{"class":58,"line":1491},66,[56,1493,1069],{"class":62},[56,1495,1242],{"class":83},[56,1497,1245],{"class":309},[56,1499,1501],{"class":58,"line":1500},67,[56,1502,661],{"class":83},[15,1504,1505,1506,1508],{},"Ключевая мысль: если Redis используется как кеш, ошибка Redis обычно не должна превращаться в ",[19,1507,358],{},", если основная БД доступна.",[1510,1511,1513],"h3",{"id":1512},"invalidation-и-stale-policy","Invalidation и stale policy",[15,1515,1516,1517,1519],{},"Cache-aside ломается не на ",[19,1518,28],{},", а на обновлениях. Если source of truth изменился, старый ключ в Redis может жить до TTL и отдавать устаревший ответ.",[15,1521,1522],{},"Типичные варианты:",[1524,1525,1526,1542],"table",{},[1527,1528,1529],"thead",{},[1530,1531,1532,1536,1539],"tr",{},[1533,1534,1535],"th",{},"Подход",[1533,1537,1538],{},"Когда подходит",[1533,1540,1541],{},"Риск",[1543,1544,1545,1557,1568,1579,1590],"tbody",{},[1530,1546,1547,1551,1554],{},[1548,1549,1550],"td",{},"TTL only",[1548,1552,1553],{},"данные редко меняются, stale допустим",[1548,1555,1556],{},"пользователь видит старое до конца TTL",[1530,1558,1559,1562,1565],{},[1548,1560,1561],{},"delete-on-write",[1548,1563,1564],{},"после commit удалить cache key",[1548,1566,1567],{},"race: параллельный reader может снова записать старое значение",[1530,1569,1570,1573,1576],{},[1548,1571,1572],{},"write-through",[1548,1574,1575],{},"запись сразу обновляет БД и кеш",[1548,1577,1578],{},"сложнее error handling и rollback story",[1530,1580,1581,1584,1587],{},[1548,1582,1583],{},"versioned keys",[1548,1585,1586],{},"новый формат или версия данных получает новый prefix",[1548,1588,1589],{},"старые ключи нужно чистить по TTL\u002Fretention",[1530,1591,1592,1595,1598],{},[1548,1593,1594],{},"stale-while-revalidate",[1548,1596,1597],{},"отдаём старое и обновляем в фоне",[1548,1599,1600],{},"нужно явно показывать\u002Fограничивать максимальную старость",[15,1602,1603],{},"Для RateDesk это должно быть частью cache contract: например, кеш конверсии инвалидируется при обновлении курса валют, а если курс старше freshness threshold, use case возвращает доменную ошибку вместо молчаливого stale результата.",[35,1605],{},[38,1607,1609],{"id":1608},"ttl-и-jitter","TTL и jitter",[15,1611,1612],{},"TTL задаёт срок жизни ключа:",[46,1614,1616],{"className":48,"code":1615,"language":50,"meta":51,"style":52},"err := rdb.Set(ctx, \"course:42\", payload, 10*time.Minute).Err()\n",[19,1617,1618],{"__ignoreMap":52},[56,1619,1620,1623,1625,1627,1629,1631,1634,1637,1640,1642,1645,1647],{"class":58,"line":59},[56,1621,1622],{"class":83},"err ",[56,1624,203],{"class":62},[56,1626,405],{"class":83},[56,1628,1450],{"class":66},[56,1630,1269],{"class":83},[56,1632,1633],{"class":90},"\"course:42\"",[56,1635,1636],{"class":83},", payload, ",[56,1638,1639],{"class":309},"10",[56,1641,978],{"class":62},[56,1643,1644],{"class":83},"time.Minute).",[56,1646,513],{"class":66},[56,1648,230],{"class":83},[15,1650,1651],{},"Но если много ключей создано одновременно с одинаковым TTL, они могут истечь одновременно. Это создаёт всплеск запросов в БД. Добавляют jitter:",[46,1653,1655],{"className":48,"code":1654,"language":50,"meta":51,"style":52},"func ttlWithJitter(base time.Duration) time.Duration {\n    if base \u003C= 0 {\n        return base\n    }\n    maxJitter := int64(base \u002F 10)\n    if maxJitter \u003C= 0 {\n        return base\n    }\n    jitter := time.Duration(rand.Int63n(maxJitter))\n    return base + jitter\n}\n",[19,1656,1657,1685,1700,1707,1711,1731,1744,1750,1754,1775,1787],{"__ignoreMap":52},[56,1658,1659,1661,1664,1666,1669,1671,1673,1675,1677,1679,1681,1683],{"class":58,"line":59},[56,1660,188],{"class":62},[56,1662,1663],{"class":66}," ttlWithJitter",[56,1665,252],{"class":83},[56,1667,1668],{"class":920},"base",[56,1670,1049],{"class":66},[56,1672,261],{"class":83},[56,1674,1054],{"class":66},[56,1676,1057],{"class":83},[56,1678,155],{"class":66},[56,1680,261],{"class":83},[56,1682,1054],{"class":66},[56,1684,420],{"class":83},[56,1686,1687,1689,1692,1695,1698],{"class":58,"line":70},[56,1688,498],{"class":62},[56,1690,1691],{"class":83}," base ",[56,1693,1694],{"class":62},"\u003C=",[56,1696,1697],{"class":309}," 0",[56,1699,420],{"class":83},[56,1701,1702,1704],{"class":58,"line":77},[56,1703,1373],{"class":62},[56,1705,1706],{"class":83}," base\n",[56,1708,1709],{"class":58,"line":87},[56,1710,547],{"class":83},[56,1712,1713,1716,1718,1720,1723,1726,1729],{"class":58,"line":100},[56,1714,1715],{"class":83},"    maxJitter ",[56,1717,203],{"class":62},[56,1719,938],{"class":62},[56,1721,1722],{"class":83},"(base ",[56,1724,1725],{"class":62},"\u002F",[56,1727,1728],{"class":309}," 10",[56,1730,177],{"class":83},[56,1732,1733,1735,1738,1740,1742],{"class":58,"line":110},[56,1734,498],{"class":62},[56,1736,1737],{"class":83}," maxJitter ",[56,1739,1694],{"class":62},[56,1741,1697],{"class":309},[56,1743,420],{"class":83},[56,1745,1746,1748],{"class":58,"line":120},[56,1747,1373],{"class":62},[56,1749,1706],{"class":83},[56,1751,1752],{"class":58,"line":130},[56,1753,547],{"class":83},[56,1755,1756,1759,1761,1764,1766,1769,1772],{"class":58,"line":140},[56,1757,1758],{"class":83},"    jitter ",[56,1760,203],{"class":62},[56,1762,1763],{"class":83}," time.",[56,1765,1054],{"class":66},[56,1767,1768],{"class":83},"(rand.",[56,1770,1771],{"class":66},"Int63n",[56,1773,1774],{"class":83},"(maxJitter))\n",[56,1776,1777,1779,1781,1784],{"class":58,"line":150},[56,1778,1069],{"class":62},[56,1780,1691],{"class":83},[56,1782,1783],{"class":62},"+",[56,1785,1786],{"class":83}," jitter\n",[56,1788,1789],{"class":58,"line":160},[56,1790,661],{"class":83},[15,1792,1793],{},"Идея простая: TTL должен быть немного размазан. В production random source лучше инжектировать или централизовать, чтобы код было удобно тестировать и чтобы все ключи не получили одинаковый jitter после рестарта. Для jitter достаточно обычного pseudo-random source; для lock token ниже нужен криптографически стойкий random.",[35,1795],{},[38,1797,1799],{"id":1798},"negative-caching","Negative caching",[15,1801,1802],{},"Если сущность не найдена в БД, можно ненадолго кешировать сам факт отсутствия.",[46,1804,1807],{"className":1805,"code":1806,"language":707,"meta":52},[705],"GET course:404 -> miss\nSELECT ... WHERE id=404 -> not found\nSET course:404:missing 1 EX 30\n",[19,1808,1806],{"__ignoreMap":52},[15,1810,1811],{},"Это защищает БД от повторяющихся запросов к несуществующим объектам. TTL должен быть коротким, иначе можно получить неприятный эффект: объект создали, а кеш всё ещё говорит \"not found\".",[15,1813,1814],{},"Пример:",[46,1816,1818],{"className":48,"code":1817,"language":50,"meta":51,"style":52},"var ErrNotFound = errors.New(\"not found\")\n\nfunc (s *CachedCourseStore) GetCourseWithNegativeCache(ctx context.Context, id int64) (Course, error) {\n    key := fmt.Sprintf(\"course:%d\", id)\n    missingKey := key + \":missing\"\n\n    exists, err := s.redis.Exists(ctx, missingKey).Result()\n    if err == nil && exists == 1 {\n        return Course{}, ErrNotFound\n    }\n\n    course, err := s.GetCourse(ctx, id)\n    if errors.Is(err, ErrNotFound) {\n        _ = s.redis.Set(ctx, missingKey, \"1\", 30*time.Second).Err()\n    }\n    return course, err\n}\n",[19,1819,1820,1843,1847,1890,1910,1925,1929,1949,1972,1981,1985,1989,2002,2013,2043,2047,2054],{"__ignoreMap":52},[56,1821,1822,1825,1828,1830,1833,1836,1838,1841],{"class":58,"line":59},[56,1823,1824],{"class":62},"var",[56,1826,1827],{"class":83}," ErrNotFound ",[56,1829,647],{"class":62},[56,1831,1832],{"class":83}," errors.",[56,1834,1835],{"class":66},"New",[56,1837,252],{"class":83},[56,1839,1840],{"class":90},"\"not found\"",[56,1842,177],{"class":83},[56,1844,1845],{"class":58,"line":70},[56,1846,74],{"emptyLinePlaceholder":73},[56,1848,1849,1851,1853,1855,1857,1859,1861,1864,1866,1868,1870,1872,1874,1876,1878,1880,1882,1884,1886,1888],{"class":58,"line":77},[56,1850,188],{"class":62},[56,1852,1092],{"class":83},[56,1854,1095],{"class":920},[56,1856,978],{"class":62},[56,1858,1062],{"class":66},[56,1860,1057],{"class":83},[56,1862,1863],{"class":66},"GetCourseWithNegativeCache",[56,1865,252],{"class":83},[56,1867,921],{"class":920},[56,1869,924],{"class":66},[56,1871,261],{"class":83},[56,1873,929],{"class":66},[56,1875,932],{"class":83},[56,1877,935],{"class":920},[56,1879,938],{"class":62},[56,1881,941],{"class":83},[56,1883,944],{"class":66},[56,1885,932],{"class":83},[56,1887,949],{"class":62},[56,1889,1131],{"class":83},[56,1891,1892,1894,1896,1898,1900,1902,1904,1906,1908],{"class":58,"line":87},[56,1893,1136],{"class":83},[56,1895,203],{"class":62},[56,1897,1141],{"class":83},[56,1899,1144],{"class":66},[56,1901,252],{"class":83},[56,1903,1149],{"class":90},[56,1905,1152],{"class":309},[56,1907,440],{"class":90},[56,1909,1157],{"class":83},[56,1911,1912,1915,1917,1920,1922],{"class":58,"line":100},[56,1913,1914],{"class":83},"    missingKey ",[56,1916,203],{"class":62},[56,1918,1919],{"class":83}," key ",[56,1921,1783],{"class":62},[56,1923,1924],{"class":90}," \":missing\"\n",[56,1926,1927],{"class":58,"line":110},[56,1928,74],{"emptyLinePlaceholder":73},[56,1930,1931,1934,1936,1938,1941,1944,1947],{"class":58,"line":120},[56,1932,1933],{"class":83},"    exists, err ",[56,1935,203],{"class":62},[56,1937,1171],{"class":83},[56,1939,1940],{"class":66},"Exists",[56,1942,1943],{"class":83},"(ctx, missingKey).",[56,1945,1946],{"class":66},"Result",[56,1948,230],{"class":83},[56,1950,1951,1953,1955,1957,1959,1962,1965,1967,1970],{"class":58,"line":130},[56,1952,498],{"class":62},[56,1954,400],{"class":83},[56,1956,1191],{"class":62},[56,1958,417],{"class":309},[56,1960,1961],{"class":62}," &&",[56,1963,1964],{"class":83}," exists ",[56,1966,1191],{"class":62},[56,1968,1969],{"class":309}," 1",[56,1971,420],{"class":83},[56,1973,1974,1976,1978],{"class":58,"line":140},[56,1975,1373],{"class":62},[56,1977,843],{"class":66},[56,1979,1980],{"class":83},"{}, ErrNotFound\n",[56,1982,1983],{"class":58,"line":150},[56,1984,547],{"class":83},[56,1986,1987],{"class":58,"line":160},[56,1988,74],{"emptyLinePlaceholder":73},[56,1990,1991,1993,1995,1998,2000],{"class":58,"line":165},[56,1992,1344],{"class":83},[56,1994,203],{"class":62},[56,1996,1997],{"class":83}," s.",[56,1999,1104],{"class":66},[56,2001,1354],{"class":83},[56,2003,2004,2006,2008,2010],{"class":58,"line":174},[56,2005,498],{"class":62},[56,2007,1832],{"class":83},[56,2009,601],{"class":66},[56,2011,2012],{"class":83},"(err, ErrNotFound) {\n",[56,2014,2015,2017,2019,2021,2023,2026,2029,2031,2034,2036,2039,2041],{"class":58,"line":180},[56,2016,1291],{"class":83},[56,2018,647],{"class":62},[56,2020,1171],{"class":83},[56,2022,1450],{"class":66},[56,2024,2025],{"class":83},"(ctx, missingKey, ",[56,2027,2028],{"class":90},"\"1\"",[56,2030,932],{"class":83},[56,2032,2033],{"class":309},"30",[56,2035,978],{"class":62},[56,2037,2038],{"class":83},"time.Second).",[56,2040,513],{"class":66},[56,2042,230],{"class":83},[56,2044,2045],{"class":58,"line":185},[56,2046,547],{"class":83},[56,2048,2049,2051],{"class":58,"line":197},[56,2050,1069],{"class":62},[56,2052,2053],{"class":83}," course, err\n",[56,2055,2056],{"class":58,"line":221},[56,2057,661],{"class":83},[35,2059],{},[38,2061,2063],{"id":2062},"serialization","Serialization",[15,2065,2066],{},"Для кеша часто используют JSON: он читаемый, простой и удобный. Но у него есть цена по CPU и размеру payload. Для горячих путей можно рассмотреть msgpack, protobuf или хранение отдельных полей в Hash.",[1524,2068,2069,2082],{},[1527,2070,2071],{},[1530,2072,2073,2076,2079],{},[1533,2074,2075],{},"Формат",[1533,2077,2078],{},"Плюсы",[1533,2080,2081],{},"Минусы",[1543,2083,2084,2095,2106,2117],{},[1530,2085,2086,2089,2092],{},[1548,2087,2088],{},"JSON",[1548,2090,2091],{},"просто читать, легко отлаживать",[1548,2093,2094],{},"больше размер, медленнее",[1530,2096,2097,2100,2103],{},[1548,2098,2099],{},"Protobuf",[1548,2101,2102],{},"компактно, строгий контракт",[1548,2104,2105],{},"нужен schema\u002Fcodegen",[1530,2107,2108,2111,2114],{},[1548,2109,2110],{},"Msgpack",[1548,2112,2113],{},"компактнее JSON",[1548,2115,2116],{},"хуже читается руками",[1530,2118,2119,2122,2125],{},[1548,2120,2121],{},"Redis Hash",[1548,2123,2124],{},"частичные обновления",[1548,2126,2127],{},"сложнее версионировать объект",[15,2129,2130],{},"Версионирование payload тоже важно:",[46,2132,2135],{"className":2133,"code":2134,"language":707,"meta":52},[705],"course:v1:42\ncourse:v2:42\n",[19,2136,2134],{"__ignoreMap":52},[15,2138,2139],{},"Если формат изменился, новый prefix позволяет не пытаться читать старые данные новым кодом.",[35,2141],{},[38,2143,2145],{"id":2144},"cache-stampede","Cache stampede",[15,2147,2148],{},"Cache stampede происходит, когда популярный ключ истёк, и много запросов одновременно пошли в БД.",[46,2150,2153],{"className":2151,"code":2152,"language":707,"meta":52},[705],"100 requests -> Redis miss -> 100 SELECT в PostgreSQL -> 100 SET в Redis\n",[19,2154,2152],{"__ignoreMap":52},[15,2156,2157],{},"Решения:",[666,2159,2160,2163,2169,2172,2175],{},[669,2161,2162],{},"TTL jitter;",[669,2164,2165,2168],{},[19,2166,2167],{},"singleflight"," внутри одного процесса;",[669,2170,2171],{},"distributed mutex через Redis;",[669,2173,2174],{},"stale-while-revalidate;",[669,2176,2177],{},"background refresh для самых горячих ключей.",[35,2179],{},[38,2181,2183],{"id":2182},"singleflight-в-go","singleflight в Go",[15,2185,2186,2189],{},[19,2187,2188],{},"singleflight.Group"," объединяет одинаковые конкурентные вызовы внутри одного процесса.",[46,2191,2193],{"className":48,"code":2192,"language":50,"meta":51,"style":52},"package cache\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"golang.org\u002Fx\u002Fsync\u002Fsingleflight\"\n)\n\ntype StampedeProtectedStore struct {\n    cache *CachedCourseStore\n    group singleflight.Group\n}\n\nfunc (s *StampedeProtectedStore) GetCourse(ctx context.Context, id int64) (Course, error) {\n    key := fmt.Sprintf(\"course:%d\", id)\n\n    value, err, _ := s.group.Do(key, func() (any, error) {\n        loadCtx, cancel := context.WithTimeout(ctx, 2*time.Second)\n        defer cancel()\n        return s.cache.GetCourse(loadCtx, id)\n    })\n    if err != nil {\n        return Course{}, err\n    }\n\n    course, ok := value.(Course)\n    if !ok {\n        return Course{}, fmt.Errorf(\"unexpected cache value type %T\", value)\n    }\n    return course, nil\n}\n",[19,2194,2195,2201,2205,2211,2219,2227,2235,2239,2248,2252,2256,2267,2277,2289,2293,2297,2340,2360,2364,2394,2414,2422,2434,2438,2450,2458,2462,2466,2480,2489,2514,2518,2526],{"__ignoreMap":52},[56,2196,2197,2199],{"class":58,"line":59},[56,2198,63],{"class":62},[56,2200,763],{"class":66},[56,2202,2203],{"class":58,"line":70},[56,2204,74],{"emptyLinePlaceholder":73},[56,2206,2207,2209],{"class":58,"line":77},[56,2208,80],{"class":62},[56,2210,84],{"class":83},[56,2212,2213,2215,2217],{"class":58,"line":87},[56,2214,91],{"class":90},[56,2216,94],{"class":66},[56,2218,97],{"class":90},[56,2220,2221,2223,2225],{"class":58,"line":100},[56,2222,91],{"class":90},[56,2224,805],{"class":66},[56,2226,97],{"class":90},[56,2228,2229,2231,2233],{"class":58,"line":110},[56,2230,91],{"class":90},[56,2232,155],{"class":66},[56,2234,97],{"class":90},[56,2236,2237],{"class":58,"line":120},[56,2238,74],{"emptyLinePlaceholder":73},[56,2240,2241,2243,2246],{"class":58,"line":130},[56,2242,91],{"class":90},[56,2244,2245],{"class":66},"golang.org\u002Fx\u002Fsync\u002Fsingleflight",[56,2247,97],{"class":90},[56,2249,2250],{"class":58,"line":140},[56,2251,177],{"class":83},[56,2253,2254],{"class":58,"line":150},[56,2255,74],{"emptyLinePlaceholder":73},[56,2257,2258,2260,2263,2265],{"class":58,"line":160},[56,2259,840],{"class":62},[56,2261,2262],{"class":66}," StampedeProtectedStore",[56,2264,846],{"class":62},[56,2266,420],{"class":83},[56,2268,2269,2272,2274],{"class":58,"line":165},[56,2270,2271],{"class":83},"    cache ",[56,2273,978],{"class":62},[56,2275,2276],{"class":66},"CachedCourseStore\n",[56,2278,2279,2282,2284,2286],{"class":58,"line":174},[56,2280,2281],{"class":83},"    group ",[56,2283,2167],{"class":66},[56,2285,261],{"class":83},[56,2287,2288],{"class":66},"Group\n",[56,2290,2291],{"class":58,"line":180},[56,2292,661],{"class":83},[56,2294,2295],{"class":58,"line":185},[56,2296,74],{"emptyLinePlaceholder":73},[56,2298,2299,2301,2303,2305,2307,2310,2312,2314,2316,2318,2320,2322,2324,2326,2328,2330,2332,2334,2336,2338],{"class":58,"line":197},[56,2300,188],{"class":62},[56,2302,1092],{"class":83},[56,2304,1095],{"class":920},[56,2306,978],{"class":62},[56,2308,2309],{"class":66},"StampedeProtectedStore",[56,2311,1057],{"class":83},[56,2313,1104],{"class":66},[56,2315,252],{"class":83},[56,2317,921],{"class":920},[56,2319,924],{"class":66},[56,2321,261],{"class":83},[56,2323,929],{"class":66},[56,2325,932],{"class":83},[56,2327,935],{"class":920},[56,2329,938],{"class":62},[56,2331,941],{"class":83},[56,2333,944],{"class":66},[56,2335,932],{"class":83},[56,2337,949],{"class":62},[56,2339,1131],{"class":83},[56,2341,2342,2344,2346,2348,2350,2352,2354,2356,2358],{"class":58,"line":221},[56,2343,1136],{"class":83},[56,2345,203],{"class":62},[56,2347,1141],{"class":83},[56,2349,1144],{"class":66},[56,2351,252],{"class":83},[56,2353,1149],{"class":90},[56,2355,1152],{"class":309},[56,2357,440],{"class":90},[56,2359,1157],{"class":83},[56,2361,2362],{"class":58,"line":233},[56,2363,74],{"emptyLinePlaceholder":73},[56,2365,2366,2369,2371,2374,2377,2380,2382,2385,2388,2390,2392],{"class":58,"line":238},[56,2367,2368],{"class":83},"    value, err, _ ",[56,2370,203],{"class":62},[56,2372,2373],{"class":83}," s.group.",[56,2375,2376],{"class":66},"Do",[56,2378,2379],{"class":83},"(key, ",[56,2381,188],{"class":62},[56,2383,2384],{"class":83},"() (",[56,2386,2387],{"class":66},"any",[56,2389,932],{"class":83},[56,2391,949],{"class":62},[56,2393,1131],{"class":83},[56,2395,2396,2399,2401,2403,2405,2407,2409,2411],{"class":58,"line":270},[56,2397,2398],{"class":83},"        loadCtx, cancel ",[56,2400,203],{"class":62},[56,2402,471],{"class":83},[56,2404,474],{"class":66},[56,2406,1269],{"class":83},[56,2408,343],{"class":309},[56,2410,978],{"class":62},[56,2412,2413],{"class":83},"time.Second)\n",[56,2415,2416,2418,2420],{"class":58,"line":282},[56,2417,1282],{"class":62},[56,2419,485],{"class":66},[56,2421,230],{"class":83},[56,2423,2424,2426,2429,2431],{"class":58,"line":293},[56,2425,1373],{"class":62},[56,2427,2428],{"class":83}," s.cache.",[56,2430,1104],{"class":66},[56,2432,2433],{"class":83},"(loadCtx, id)\n",[56,2435,2436],{"class":58,"line":303},[56,2437,381],{"class":83},[56,2439,2440,2442,2444,2446,2448],{"class":58,"line":315},[56,2441,498],{"class":62},[56,2443,400],{"class":83},[56,2445,414],{"class":62},[56,2447,417],{"class":309},[56,2449,420],{"class":83},[56,2451,2452,2454,2456],{"class":58,"line":326},[56,2453,1373],{"class":62},[56,2455,843],{"class":66},[56,2457,1378],{"class":83},[56,2459,2460],{"class":58,"line":337},[56,2461,547],{"class":83},[56,2463,2464],{"class":58,"line":352},[56,2465,74],{"emptyLinePlaceholder":73},[56,2467,2468,2471,2473,2476,2478],{"class":58,"line":366},[56,2469,2470],{"class":83},"    course, ok ",[56,2472,203],{"class":62},[56,2474,2475],{"class":83}," value.(",[56,2477,944],{"class":66},[56,2479,177],{"class":83},[56,2481,2482,2484,2486],{"class":58,"line":378},[56,2483,498],{"class":62},[56,2485,595],{"class":62},[56,2487,2488],{"class":83},"ok {\n",[56,2490,2491,2493,2495,2498,2501,2503,2506,2509,2511],{"class":58,"line":384},[56,2492,1373],{"class":62},[56,2494,843],{"class":66},[56,2496,2497],{"class":83},"{}, fmt.",[56,2499,2500],{"class":66},"Errorf",[56,2502,252],{"class":83},[56,2504,2505],{"class":90},"\"unexpected cache value type ",[56,2507,2508],{"class":309},"%T",[56,2510,440],{"class":90},[56,2512,2513],{"class":83},", value)\n",[56,2515,2516],{"class":58,"line":394},[56,2517,547],{"class":83},[56,2519,2520,2522,2524],{"class":58,"line":423},[56,2521,1069],{"class":62},[56,2523,1242],{"class":83},[56,2525,1245],{"class":309},[56,2527,2528],{"class":58,"line":446},[56,2529,661],{"class":83},[15,2531,2532],{},"Ограничение: это работает только внутри одного процесса. Если у вас 20 replicas backend'а, каждая replica может сделать свой запрос в БД. Для глобальной защиты используют Redis lock или другой координатор, но с caveats.",[35,2534],{},[38,2536,2538],{"id":2537},"простая-mutex-защита-через-redis","Простая mutex-защита через Redis",[15,2540,2541,2542,2545],{},"Для cache rebuild можно использовать ",[19,2543,2544],{},"SET key token NX PX ttl",". Важно хранить случайный token и удалять lock только если token совпал.",[15,2547,2548,2549,2552,2553,2556,2557,2560,2561,261],{},"В примере ",[19,2550,2551],{},"crand"," - это alias для ",[19,2554,2555],{},"crypto\u002Frand",", а ",[19,2558,2559],{},"hex"," - ",[19,2562,2563],{},"encoding\u002Fhex",[46,2565,2567],{"className":48,"code":2566,"language":50,"meta":51,"style":52},"func newLockToken() (string, error) {\n    var b [16]byte\n    if _, err := crand.Read(b[:]); err != nil {\n        return \"\", err\n    }\n    return hex.EncodeToString(b[:]), nil\n}\n\nfunc acquireLock(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (string, bool, error) {\n    token, err := newLockToken()\n    if err != nil {\n        return \"\", false, err\n    }\n    ok, err := rdb.SetNX(ctx, key, token, ttl).Result()\n    if err != nil || !ok {\n        return \"\", ok, err\n    }\n    return token, true, nil\n}\n\nfunc releaseLock(ctx context.Context, rdb *redis.Client, key, token string) error {\n    const script = `\nif redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n  return redis.call(\"DEL\", KEYS[1])\nend\nreturn 0`\n    return rdb.Eval(ctx, script, []string{key}, token).Err()\n}\n",[19,2568,2569,2586,2603,2627,2637,2641,2656,2660,2664,2726,2737,2749,2762,2766,2785,2802,2811,2815,2829,2833,2837,2883,2897,2902,2907,2912,2917,2938],{"__ignoreMap":52},[56,2570,2571,2573,2576,2578,2580,2582,2584],{"class":58,"line":59},[56,2572,188],{"class":62},[56,2574,2575],{"class":66}," newLockToken",[56,2577,2384],{"class":83},[56,2579,867],{"class":62},[56,2581,932],{"class":83},[56,2583,949],{"class":62},[56,2585,1131],{"class":83},[56,2587,2588,2591,2594,2597,2600],{"class":58,"line":70},[56,2589,2590],{"class":62},"    var",[56,2592,2593],{"class":83}," b [",[56,2595,2596],{"class":309},"16",[56,2598,2599],{"class":83},"]",[56,2601,2602],{"class":62},"byte\n",[56,2604,2605,2607,2610,2612,2615,2618,2621,2623,2625],{"class":58,"line":77},[56,2606,498],{"class":62},[56,2608,2609],{"class":83}," _, err ",[56,2611,203],{"class":62},[56,2613,2614],{"class":83}," crand.",[56,2616,2617],{"class":66},"Read",[56,2619,2620],{"class":83},"(b[:]); err ",[56,2622,414],{"class":62},[56,2624,417],{"class":309},[56,2626,420],{"class":83},[56,2628,2629,2631,2634],{"class":58,"line":87},[56,2630,1373],{"class":62},[56,2632,2633],{"class":90}," \"\"",[56,2635,2636],{"class":83},", err\n",[56,2638,2639],{"class":58,"line":100},[56,2640,547],{"class":83},[56,2642,2643,2645,2648,2651,2654],{"class":58,"line":110},[56,2644,1069],{"class":62},[56,2646,2647],{"class":83}," hex.",[56,2649,2650],{"class":66},"EncodeToString",[56,2652,2653],{"class":83},"(b[:]), ",[56,2655,1245],{"class":309},[56,2657,2658],{"class":58,"line":120},[56,2659,661],{"class":83},[56,2661,2662],{"class":58,"line":130},[56,2663,74],{"emptyLinePlaceholder":73},[56,2665,2666,2668,2671,2673,2675,2677,2679,2681,2683,2685,2687,2689,2691,2693,2695,2698,2701,2703,2705,2707,2709,2711,2713,2715,2717,2720,2722,2724],{"class":58,"line":140},[56,2667,188],{"class":62},[56,2669,2670],{"class":66}," acquireLock",[56,2672,252],{"class":83},[56,2674,921],{"class":920},[56,2676,924],{"class":66},[56,2678,261],{"class":83},[56,2680,929],{"class":66},[56,2682,932],{"class":83},[56,2684,1025],{"class":920},[56,2686,346],{"class":62},[56,2688,258],{"class":66},[56,2690,261],{"class":83},[56,2692,1034],{"class":66},[56,2694,932],{"class":83},[56,2696,2697],{"class":920},"key",[56,2699,2700],{"class":62}," string",[56,2702,932],{"class":83},[56,2704,1046],{"class":920},[56,2706,1049],{"class":66},[56,2708,261],{"class":83},[56,2710,1054],{"class":66},[56,2712,941],{"class":83},[56,2714,867],{"class":62},[56,2716,932],{"class":83},[56,2718,2719],{"class":62},"bool",[56,2721,932],{"class":83},[56,2723,949],{"class":62},[56,2725,1131],{"class":83},[56,2727,2728,2731,2733,2735],{"class":58,"line":150},[56,2729,2730],{"class":83},"    token, err ",[56,2732,203],{"class":62},[56,2734,2575],{"class":66},[56,2736,230],{"class":83},[56,2738,2739,2741,2743,2745,2747],{"class":58,"line":160},[56,2740,498],{"class":62},[56,2742,400],{"class":83},[56,2744,414],{"class":62},[56,2746,417],{"class":309},[56,2748,420],{"class":83},[56,2750,2751,2753,2755,2757,2760],{"class":58,"line":165},[56,2752,1373],{"class":62},[56,2754,2633],{"class":90},[56,2756,932],{"class":83},[56,2758,2759],{"class":309},"false",[56,2761,2636],{"class":83},[56,2763,2764],{"class":58,"line":174},[56,2765,547],{"class":83},[56,2767,2768,2771,2773,2775,2778,2781,2783],{"class":58,"line":180},[56,2769,2770],{"class":83},"    ok, err ",[56,2772,203],{"class":62},[56,2774,405],{"class":83},[56,2776,2777],{"class":66},"SetNX",[56,2779,2780],{"class":83},"(ctx, key, token, ttl).",[56,2782,1946],{"class":66},[56,2784,230],{"class":83},[56,2786,2787,2789,2791,2793,2795,2798,2800],{"class":58,"line":185},[56,2788,498],{"class":62},[56,2790,400],{"class":83},[56,2792,414],{"class":62},[56,2794,417],{"class":309},[56,2796,2797],{"class":62}," ||",[56,2799,595],{"class":62},[56,2801,2488],{"class":83},[56,2803,2804,2806,2808],{"class":58,"line":197},[56,2805,1373],{"class":62},[56,2807,2633],{"class":90},[56,2809,2810],{"class":83},", ok, err\n",[56,2812,2813],{"class":58,"line":221},[56,2814,547],{"class":83},[56,2816,2817,2819,2822,2825,2827],{"class":58,"line":233},[56,2818,1069],{"class":62},[56,2820,2821],{"class":83}," token, ",[56,2823,2824],{"class":309},"true",[56,2826,932],{"class":83},[56,2828,1245],{"class":309},[56,2830,2831],{"class":58,"line":238},[56,2832,661],{"class":83},[56,2834,2835],{"class":58,"line":270},[56,2836,74],{"emptyLinePlaceholder":73},[56,2838,2839,2841,2844,2846,2848,2850,2852,2854,2856,2858,2860,2862,2864,2866,2868,2870,2872,2875,2877,2879,2881],{"class":58,"line":282},[56,2840,188],{"class":62},[56,2842,2843],{"class":66}," releaseLock",[56,2845,252],{"class":83},[56,2847,921],{"class":920},[56,2849,924],{"class":66},[56,2851,261],{"class":83},[56,2853,929],{"class":66},[56,2855,932],{"class":83},[56,2857,1025],{"class":920},[56,2859,346],{"class":62},[56,2861,258],{"class":66},[56,2863,261],{"class":83},[56,2865,1034],{"class":66},[56,2867,932],{"class":83},[56,2869,2697],{"class":920},[56,2871,932],{"class":83},[56,2873,2874],{"class":920},"token",[56,2876,2700],{"class":62},[56,2878,1057],{"class":83},[56,2880,949],{"class":62},[56,2882,420],{"class":83},[56,2884,2885,2888,2891,2894],{"class":58,"line":293},[56,2886,2887],{"class":62},"    const",[56,2889,2890],{"class":309}," script",[56,2892,2893],{"class":62}," =",[56,2895,2896],{"class":90}," `\n",[56,2898,2899],{"class":58,"line":303},[56,2900,2901],{"class":90},"if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n",[56,2903,2904],{"class":58,"line":315},[56,2905,2906],{"class":90},"  return redis.call(\"DEL\", KEYS[1])\n",[56,2908,2909],{"class":58,"line":326},[56,2910,2911],{"class":90},"end\n",[56,2913,2914],{"class":58,"line":337},[56,2915,2916],{"class":90},"return 0`\n",[56,2918,2919,2921,2923,2926,2929,2931,2934,2936],{"class":58,"line":352},[56,2920,1069],{"class":62},[56,2922,405],{"class":83},[56,2924,2925],{"class":66},"Eval",[56,2927,2928],{"class":83},"(ctx, script, []",[56,2930,867],{"class":62},[56,2932,2933],{"class":83},"{key}, token).",[56,2935,513],{"class":66},[56,2937,230],{"class":83},[56,2939,2940],{"class":58,"line":366},[56,2941,661],{"class":83},[15,2943,2944],{},"Для критичных distributed locks этого мало: нужны clock assumptions, fencing tokens, продуманная обработка пауз, failover и повторов. Но для защиты кеша от stampede такой lock часто достаточно хорош, потому что ошибка не нарушает деньги или инварианты домена.",[35,2946],{},[38,2948,2950],{"id":2949},"go-client-pitfalls-на-ревью","Go client pitfalls на ревью",[15,2952,2953,2954,2956],{},"Что часто стоит проверять в MR с ",[19,2955,700],{},":",[666,2958,2959,2969,2972,2978,2981,2984,2987],{},[669,2960,2961,2962,2965,2966,2968],{},"reusable adapter не создаёт ",[19,2963,2964],{},"context.Background()"," внутри операций вместо caller ",[19,2967,921],{},";",[669,2970,2971],{},"Redis timeout короче пользовательского request timeout и не зависает до отмены HTTP-клиента;",[669,2973,2974,2977],{},[19,2975,2976],{},"redis.Nil"," обрабатывается как cache miss, а не как infrastructure outage;",[669,2979,2980],{},"метрики не используют полный Redis key как label;",[669,2982,2983],{},"Pub\u002FSub, blocking pop и Streams worker не делят один маленький pool с request path;",[669,2985,2986],{},"retries включены только там, где операция идемпотентна или повтор безопасен;",[669,2988,2989],{},"cache payload имеет версию, ограничение размера и понятный fallback при ошибке decode.",[35,2991],{},[38,2993,2995],{"id":2994},"вопросы-на-собеседовании","Вопросы на собеседовании",[666,2997,2998,3004,3010,3013,3016,3019,3022,3028],{},[669,2999,3000,3001,3003],{},"Что делает ",[19,3002,2976],{}," в go-redis и почему это не обычная ошибка сервера?",[669,3005,3006,3007,3009],{},"Почему операции с Redis должны принимать ",[19,3008,673],{},"?",[669,3011,3012],{},"Что такое cache-aside?",[669,3014,3015],{},"Когда ошибка Redis должна приводить к ошибке HTTP-запроса, а когда нет?",[669,3017,3018],{},"Зачем TTL jitter?",[669,3020,3021],{},"Что такое negative caching и какой у него риск?",[669,3023,3024,3025,3027],{},"Чем ",[19,3026,2167],{}," отличается от distributed lock?",[669,3029,3030],{},"Почему lock нужно удалять через Lua-скрипт с проверкой token?",[35,3032],{},[38,3034,3036],{"id":3035},"практика","Практика",[3038,3039,3040,3047,3050,3056],"ol",{},[669,3041,3042,3043,3046],{},"Напишите ",[19,3044,3045],{},"CachedLessonRepository",", который кеширует урок по slug на 5 минут и использует negative caching для отсутствующих slug на 30 секунд.",[669,3048,3049],{},"Добавьте TTL jitter к записи кеша и объясните, как это влияет на нагрузку на PostgreSQL.",[669,3051,3052,3053,3055],{},"Оберните загрузку урока через ",[19,3054,2188],{},", чтобы 50 одновременных miss'ов внутри одного процесса дали один запрос в БД.",[669,3057,3058,3059,932,3062,932,3065,3068],{},"Добавьте метрики ",[19,3060,3061],{},"cache_hit",[19,3063,3064],{},"cache_miss",[19,3066,3067],{},"cache_error"," на уровне repository.",[35,3070],{},[38,3072,3074],{"id":3073},"интерактивная-практика","Интерактивная практика",[3076,3077,3079,3088,3105],"quiz",{"answer":343,"id":3078,"xp":1639},"redis-go-cache-q1",[15,3080,3081,3082,3084,3085,3087],{},"Как обычно нужно трактовать ",[19,3083,2976],{}," при ",[19,3086,28],{}," в go-redis?",[3089,3090,3091],"template",{"v-slot:options":52},[666,3092,3093,3096,3099,3102],{},[669,3094,3095],{},"Как падение Redis-сервера",[669,3097,3098],{},"Как cache miss или отсутствие ключа",[669,3100,3101],{},"Как ошибку JSON-декодирования",[669,3103,3104],{},"Как сигнал немедленно остановить процесс",[3089,3106,3107],{"v-slot:explanation":52},[15,3108,3109,3111],{},[19,3110,2976],{}," означает, что ключ не найден. Это нормальная ветка cache-aside, а не infrastructure outage.",[3113,3114,3118,3121,3306],"predict",{"answer":3115,"id":3116,"xp":3117},"hit\\nload-db\\nload-db","redis-go-cache-p1","15",[15,3119,3120],{},"Что выведет программа?",[3089,3122,3123],{"v-slot:code":52},[46,3124,3126],{"className":48,"code":3125,"language":50,"meta":52,"style":52},"package main\n\nimport \"fmt\"\n\nfunc cachePath(found bool, redisDown bool) string {\n    if redisDown {\n        return \"load-db\"\n    }\n    if found {\n        return \"hit\"\n    }\n    return \"load-db\"\n}\n\nfunc main() {\n    fmt.Println(cachePath(true, false))\n    fmt.Println(cachePath(false, false))\n    fmt.Println(cachePath(false, true))\n}\n",[19,3127,3128,3134,3138,3149,3153,3181,3188,3195,3199,3206,3213,3217,3223,3227,3231,3239,3262,3282,3302],{"__ignoreMap":52},[56,3129,3130,3132],{"class":58,"line":59},[56,3131,63],{"class":62},[56,3133,67],{"class":66},[56,3135,3136],{"class":58,"line":70},[56,3137,74],{"emptyLinePlaceholder":73},[56,3139,3140,3142,3145,3147],{"class":58,"line":77},[56,3141,80],{"class":62},[56,3143,3144],{"class":90}," \"",[56,3146,805],{"class":66},[56,3148,97],{"class":90},[56,3150,3151],{"class":58,"line":87},[56,3152,74],{"emptyLinePlaceholder":73},[56,3154,3155,3157,3160,3162,3165,3168,3170,3173,3175,3177,3179],{"class":58,"line":100},[56,3156,188],{"class":62},[56,3158,3159],{"class":66}," cachePath",[56,3161,252],{"class":83},[56,3163,3164],{"class":920},"found",[56,3166,3167],{"class":62}," bool",[56,3169,932],{"class":83},[56,3171,3172],{"class":920},"redisDown",[56,3174,3167],{"class":62},[56,3176,1057],{"class":83},[56,3178,867],{"class":62},[56,3180,420],{"class":83},[56,3182,3183,3185],{"class":58,"line":110},[56,3184,498],{"class":62},[56,3186,3187],{"class":83}," redisDown {\n",[56,3189,3190,3192],{"class":58,"line":120},[56,3191,1373],{"class":62},[56,3193,3194],{"class":90}," \"load-db\"\n",[56,3196,3197],{"class":58,"line":130},[56,3198,547],{"class":83},[56,3200,3201,3203],{"class":58,"line":140},[56,3202,498],{"class":62},[56,3204,3205],{"class":83}," found {\n",[56,3207,3208,3210],{"class":58,"line":150},[56,3209,1373],{"class":62},[56,3211,3212],{"class":90}," \"hit\"\n",[56,3214,3215],{"class":58,"line":160},[56,3216,547],{"class":83},[56,3218,3219,3221],{"class":58,"line":165},[56,3220,1069],{"class":62},[56,3222,3194],{"class":90},[56,3224,3225],{"class":58,"line":174},[56,3226,661],{"class":83},[56,3228,3229],{"class":58,"line":180},[56,3230,74],{"emptyLinePlaceholder":73},[56,3232,3233,3235,3237],{"class":58,"line":185},[56,3234,188],{"class":62},[56,3236,191],{"class":66},[56,3238,194],{"class":83},[56,3240,3241,3244,3246,3248,3251,3253,3255,3257,3259],{"class":58,"line":197},[56,3242,3243],{"class":83},"    fmt.",[56,3245,561],{"class":66},[56,3247,252],{"class":83},[56,3249,3250],{"class":66},"cachePath",[56,3252,252],{"class":83},[56,3254,2824],{"class":309},[56,3256,932],{"class":83},[56,3258,2759],{"class":309},[56,3260,3261],{"class":83},"))\n",[56,3263,3264,3266,3268,3270,3272,3274,3276,3278,3280],{"class":58,"line":221},[56,3265,3243],{"class":83},[56,3267,561],{"class":66},[56,3269,252],{"class":83},[56,3271,3250],{"class":66},[56,3273,252],{"class":83},[56,3275,2759],{"class":309},[56,3277,932],{"class":83},[56,3279,2759],{"class":309},[56,3281,3261],{"class":83},[56,3283,3284,3286,3288,3290,3292,3294,3296,3298,3300],{"class":58,"line":233},[56,3285,3243],{"class":83},[56,3287,561],{"class":66},[56,3289,252],{"class":83},[56,3291,3250],{"class":66},[56,3293,252],{"class":83},[56,3295,2759],{"class":309},[56,3297,932],{"class":83},[56,3299,2824],{"class":309},[56,3301,3261],{"class":83},[56,3303,3304],{"class":58,"line":238},[56,3305,661],{"class":83},[3089,3307,3308],{"v-slot:hint":52},[15,3309,3310],{},"В cache-aside read path Redis miss и Redis outage обычно ведут к загрузке из source of truth.",[3312,3313,3316,3323,3467],"code-task",{"expected":3314,"id":3315,"xp":321},"return-cache\\ndelete-and-load\\nload-db","redis-go-cache-ct1",[15,3317,3318,3319,3322],{},"Реализуй ",[19,3320,3321],{},"CacheDecision",": валидный cached payload возвращаем, битый payload удаляем и грузим заново, отсутствие payload грузим из БД.",[3089,3324,3325],{"v-slot:template":52},[46,3326,3328],{"className":48,"code":3327,"language":50,"meta":52,"style":52},"package main\n\nimport \"fmt\"\n\nfunc CacheDecision(found bool, decodeOK bool) string {\n    return \"todo\"\n}\n\nfunc main() {\n    fmt.Println(CacheDecision(true, true))\n    fmt.Println(CacheDecision(true, false))\n    fmt.Println(CacheDecision(false, false))\n}\n",[19,3329,3330,3336,3340,3350,3354,3380,3387,3391,3395,3403,3423,3443,3463],{"__ignoreMap":52},[56,3331,3332,3334],{"class":58,"line":59},[56,3333,63],{"class":62},[56,3335,67],{"class":66},[56,3337,3338],{"class":58,"line":70},[56,3339,74],{"emptyLinePlaceholder":73},[56,3341,3342,3344,3346,3348],{"class":58,"line":77},[56,3343,80],{"class":62},[56,3345,3144],{"class":90},[56,3347,805],{"class":66},[56,3349,97],{"class":90},[56,3351,3352],{"class":58,"line":87},[56,3353,74],{"emptyLinePlaceholder":73},[56,3355,3356,3358,3361,3363,3365,3367,3369,3372,3374,3376,3378],{"class":58,"line":100},[56,3357,188],{"class":62},[56,3359,3360],{"class":66}," CacheDecision",[56,3362,252],{"class":83},[56,3364,3164],{"class":920},[56,3366,3167],{"class":62},[56,3368,932],{"class":83},[56,3370,3371],{"class":920},"decodeOK",[56,3373,3167],{"class":62},[56,3375,1057],{"class":83},[56,3377,867],{"class":62},[56,3379,420],{"class":83},[56,3381,3382,3384],{"class":58,"line":110},[56,3383,1069],{"class":62},[56,3385,3386],{"class":90}," \"todo\"\n",[56,3388,3389],{"class":58,"line":120},[56,3390,661],{"class":83},[56,3392,3393],{"class":58,"line":130},[56,3394,74],{"emptyLinePlaceholder":73},[56,3396,3397,3399,3401],{"class":58,"line":140},[56,3398,188],{"class":62},[56,3400,191],{"class":66},[56,3402,194],{"class":83},[56,3404,3405,3407,3409,3411,3413,3415,3417,3419,3421],{"class":58,"line":150},[56,3406,3243],{"class":83},[56,3408,561],{"class":66},[56,3410,252],{"class":83},[56,3412,3321],{"class":66},[56,3414,252],{"class":83},[56,3416,2824],{"class":309},[56,3418,932],{"class":83},[56,3420,2824],{"class":309},[56,3422,3261],{"class":83},[56,3424,3425,3427,3429,3431,3433,3435,3437,3439,3441],{"class":58,"line":160},[56,3426,3243],{"class":83},[56,3428,561],{"class":66},[56,3430,252],{"class":83},[56,3432,3321],{"class":66},[56,3434,252],{"class":83},[56,3436,2824],{"class":309},[56,3438,932],{"class":83},[56,3440,2759],{"class":309},[56,3442,3261],{"class":83},[56,3444,3445,3447,3449,3451,3453,3455,3457,3459,3461],{"class":58,"line":165},[56,3446,3243],{"class":83},[56,3448,561],{"class":66},[56,3450,252],{"class":83},[56,3452,3321],{"class":66},[56,3454,252],{"class":83},[56,3456,2759],{"class":309},[56,3458,932],{"class":83},[56,3460,2759],{"class":309},[56,3462,3261],{"class":83},[56,3464,3465],{"class":58,"line":174},[56,3466,661],{"class":83},[3089,3468,3469],{"v-slot:hints":52},[666,3470,3471,3474,3477],{},[669,3472,3473],{},"Битый кеш не должен ломать основной read path.",[669,3475,3476],{},"Если payload не найден, это обычный miss.",[669,3478,3479],{},"Если найден и декодируется, можно вернуть кеш.",[3481,3482,3483],"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 .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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":52,"searchDepth":70,"depth":70,"links":3485},[3486,3487,3488,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500],{"id":40,"depth":70,"text":41},{"id":694,"depth":70,"text":695},{"id":738,"depth":70,"text":739,"children":3489},[3490],{"id":1512,"depth":77,"text":1513},{"id":1608,"depth":70,"text":1609},{"id":1798,"depth":70,"text":1799},{"id":2062,"depth":70,"text":2063},{"id":2144,"depth":70,"text":2145},{"id":2182,"depth":70,"text":2183},{"id":2537,"depth":70,"text":2538},{"id":2949,"depth":70,"text":2950},{"id":2994,"depth":70,"text":2995},{"id":3035,"depth":70,"text":3036},{"id":3073,"depth":70,"text":3074},1781022064377]