कैश सरल दिखता है जब तक ट्रैफ़िक असमान न हो, कुंजियाँ एक साथ न मरें, और एक मशहूर प्रोडक्ट आईडी एक ही रेडिस शार्ड न जला दे। ज़्यादातर आउटेज जो मैंने देखे, वे "रेडिस धीमा है" नहीं थे। वे स्टैंपीड थे, हमेशा-बासी डेटा, या एक हॉट की जिसे किसी ने मापा ही नहीं।

यह पोस्ट वही छोटी सूची है जो असली सर्विस पर बार-बार आती है: कैश-असाइड, स्टैंपीड नियंत्रण, जिटर वाला टीटीएल, इनवैलिडेशन, और हॉट कीज़। हर रेडिस कमांड की सूची नहीं। सिर्फ वे पैटर्न जो रीड पथ पर कैश रखने से पहले चाहिए।


कैश-असाइड असल में क्या करता है

कैश-असाइड (आलसी लोड) ऐप-स्वामित्व वाले कैश का डिफ़ॉल्ट है:

१. key के लिए रेडिस पढ़ो। २. हिट पर मान लौटाओ। ३. मिस पर स्रोत-सत्य (अक्सर पोस्टग्रेस) से लाओ, टीटीएल के साथ रेडिस लिखो, लौटाओ। ४. लेखन पहले डेटाबेस में जाते हैं। फिर कैश कुंजी मिटाते हो या ओवरराइट करते हो।

value = GET cache:user:{id}
if value is nil:
  value = db.query("SELECT ... WHERE id = ?")
  if value is nil:
    SET cache:user:{id} "null" EX 60   # negative cache; see below
  else:
    SET cache:user:{id} value EX 300
return value

टीमें क्यों चुनती हैं:

  • ऐप कुंजी स्कीमा और टीटीएल नीति का मालिक है।
  • डेटाबेस स्रोत-सत्य रहता है।
  • एक एंडपॉइंट से शुरू करके बढ़ा सकते हो।

जो कीमत स्वीकार करनी पड़ती है:

  • मिस के बाद पहला अनुरोध पूरा डेटाबेस विलंब चुकाता है।
  • एक ही कुंजी पर समवर्ती मिस डेटाबेस पर स्टैंपीड कर सकते हैं (अगला खंड)।
  • लेखन के बाद क्या होता है, सोचना ज़रूरी है।

रीड-थ्रू और राइट-थ्रू ज़्यादा काम कैश परत या लाइब्रेरी में डालते हैं। हो तो ठीक। ज़्यादातर माइक्रोसर्विस कोड अब भी कैश-असाइड हाथ से करता है।


कैश स्टैंपीड (थंडरिंग हर्ड)

स्टैंपीड: लोकप्रिय कुंजी खत्म होती है (या बेदखल होती है), फिर सैकड़ों समवर्ती अनुरोध मिस करते हैं और एक ही क्वेरी से डेटाबेस पर गिरते हैं। डेटाबेस उछलता है। विलंब चढ़ता है। टाइमआउट रिट्राई बनाते हैं। झुंड बढ़ता है।

क्लासिक ट्रिगर:

  • गरम कुंजी पर स्थिर टीटीएल ताकि हर इंस्टेंस एक ही सेकंड में एक्सपायरी देखे।
  • डिप्लॉय जो कैश खाली कर दे।
  • कुंजी के मरते ही ट्रैफ़िक स्पाइक।
  • नेगेटिव कैश न होना: गायब पंक्ति हमेशा फिर पूछी जाए।

बचाव १: सिंगल-फ्लाइट / अनुरोध जोड़ना

सिर्फ एक अनुरोध मान बनाता है। बाकी प्रतीक्षा करते हैं (या थोड़ा बासी डेटा देते हैं)।

value = GET key
if hit: return value

if SETNX lock:key "1" EX 10:
  value = load_from_db()
  SET key value EX ttl
  DEL lock:key
  return value
else:
  sleep briefly and retry GET
  # or return last-known if you keep a soft TTL copy

SETNX (या SET key NX EX) मोटा लॉक है। मल्टी-पॉड सिस्टम में स्टैंपीड के लिए अक्सर काफी है। सख्त नियंत्रण के लिए छोटा लॉक + हार्ड टाइमआउट वाला वेट लूप, फिर सिर्फ तब डेटाबेस जब लॉक धारक मर चुका हो।

इन-प्रोसेस सिंगल-फ्लाइट (प्रति कुंजी प्रति पॉड एक गोरूटीन/प्रॉमिस) प्रोसेस के अंदर मदद करता है। यह नहीं रोकता कि एन पॉड में से हर एक एक रीबिल्ड चलाए। गरम कुंजियों पर दोनों मिलाओ।

बचाव २: संभाव्य जल्दी रिफ्रेश

कठिन एक्सपायरी से पहले अनुरोधों का एक अंश जल्दी रिफ्रेश करता है। लोकप्रिय कुंजियाँ चट्टान से पहले बन जाती हैं। कम लोकप्रिय अक्सर स्वाभाविक मिस का इंतज़ार करती हैं।

एक्स-फेच शैली का स्केच: अगर शेष टीटीएल कुल के मुकाबले छोटा है, और रैंडम ड्रॉ जीतता है, रीबिल्ड और फिर लिखो। सूत्र से ज़्यादा विचार मायने रखता है: रिफ्रेश काम को समय पर फैलाओ, एक सिंक्रनाइज़्ड मिस की जगह।

बचाव ३: स्टेल-वाइल-रीवैलिडेट

दो समय रखो: सॉफ्ट टीटीएल (सेवा करो लेकिन रिफ्रेश) और हार्ड टीटीएल (फिर लोड ज़रूरी)। या पेलोड में stale_after फ़ील्ड और लंबा रेडिस EX

सॉफ्ट मिस पर: पुराना मान तुरंत दो, एसिंक रिफ्रेश चलाओ। यूज़र तेज़ रहते हैं। बैकग्राउंड वर्कर रीबिल्ड सोखते हैं। सख्त ताज़गी के बदले स्थिरता।

बचाव ४: नेगेटिव कैशिंग

अगर डेटाबेस कहे "नहीं मिला," उस तथ्य को छोटे टीटीएल (३०से-२मि) पर कैश करो। बिना इसके बॉट और टूटे क्लाइंट गायब आईडी हमेशा पीटते हैं। टीटीएल छोटा रखो ताकि नई पंक्ति घंटों अदृश्य न रहे।


टीटीएल: हर चीज़ के लिए एक संख्या नहीं

टीटीएल एक बासीपन बजट है, ३६०० का यादृच्छिक डिफ़ॉल्ट नहीं।

डेटा आकार सामान्य टीटीएल नोट
यूज़र सेशन / अधिकृत स्नैपशॉट मिनट सुरक्षा संवेदनशील; लॉगआउट/भूमिका बदलने पर इनवैलिड
प्रोडक्ट कैटलॉग पंक्ति ५-३० मि एडमिन संपादन पर इनवैलिड
फ़ीड रैंकिंग / होम कार्ड ३०से-५ मि थोड़ा बासी चल सकता है
फ़ीचर फ़्लैग १०-६०से पुश इनवैलिडेशन हो तो बेहतर
रेट काउंटर / आइडेम्पोटेंसी विंडो लंबाई अक्सर सटीक टीटीएल, "हमेशा" नहीं
नेगेटिव कैश ("नहीं मिला") ३०से-२ मि जानबूझकर छोटा

जिटर ताकि कुंजियाँ कतार में न मरें

अगर ५०,००० प्रोडक्ट कुंजियाँ सब EX 300 लें, कोल्ड स्टार्ट या सामूहिक इनसर्ट सिंक्रनाइज़्ड एक्सपायरी लहरें बना सकता है। जिटर जोड़ो:

ttl = base_ttl + random(0, base_ttl * 0.1)
# e.g. 300 + random(0, 30) seconds

जिटर स्टैंपीड लॉक की जगह नहीं लेता। यह घटाता है कि कई अलग कुंजियाँ एक साथ मरें और रेडिस व डेटाबेस दोनों ओवरलोड हों।

मेमोरी और बेदखली

रेडिस अनंत नहीं। maxmemory लगने पर नीति मायने रखती है:

  • allkeys-lru / allkeys-lfu: शुद्ध कैश के लिए ठीक जहाँ हर कुंजी मर सकती है।
  • volatile-lru: सिर्फ टीटीएल वाली कुंजियाँ। खतरनाक अगर कुछ बिना टीटीएल हों और मेमोरी पिन करें।
  • प्रोडक्शन कैश लगभग हर कुंजी पर टीटीएल और साफ़ maxmemory-policy के बिना न चलाओ।

अगर कुंजी दबाव में गायब नहीं होनी चाहिए (लॉक, कतार), अलग रेडिस अलग नीति पर रखो, या मान लो कि कैश इंस्टेंस टिकाऊपन के लिए गलत स्टोर है।


इनवैलिडेशन: कठिन हिस्सा

कंप्यूटर विज्ञान में कठिन समस्याएँ कम हैं, और कैश इनवैलिडेशन मज़ाक इसलिए है। विफलताएँ ठोस हैं:

  • डिलीट-फिर-राइट दौड़: अनुरोध ए कैश मिटाता है, बी पुरानी डेटाबेस पंक्ति कैश में डालता है, सी नया लेखन कमिट करता है। कैश टीटीएल तक बासी रहता है।
  • राइट-फिर-भूल: ऐप डेटाबेस अपडेट करता है, रेडिस नहीं छूता। टीटीएल तक बासी।
  • मल्टी-की ऑब्जेक्ट: प्रोफ़ाइल user:42 पर, और team:9:members में भी एम्बेड। एक कुंजी अपडेट की, डिनॉर्मलाइज़्ड कॉपी छोड़ दी।

पैटर्न जो काम करते हैं

१. डेटाबेस लिखो, फिर कैश मिटाओ (आलसी रीबिल्ड)

BEGIN; UPDATE users SET name = ? WHERE id = ?; COMMIT;
DEL cache:user:{id}

अगला रीड बनाता है। ओवरराइट से डिलीट तब पसंद करो जब लेखन पथ पर पूरा कैश ऑब्जेक्ट न हो, या समवर्ती लेखक उलझे हों।

२. डेटाबेस लिखो, फिर कैश सेट (अगर पूरा पेलोड हो)

जब रिस्पॉन्स आकार कैश ब्लॉब से मेल खाए। समवर्ती लेखकों पर फिर भी दौड़ संभव। वर्शन फ़ील्ड या "वर्शन बढ़े तभी लिखो" मदद करते हैं।

३. वर्शन वाली कुंजियाँ

cache:user:{id}:v{version} या कुंजी हैश में updated_at। लेखन पर वर्शन बढ़ाओ; पुरानी टीटीएल से मरें। रीडर हमेशा वर्तमान वर्शन डेटाबेस या छोटी पॉइंटर कुंजी से माँगे। ज़्यादा हिस्से, जटिल ऑब्जेक्ट पर कम "चुपचाप बासी" बग।

४. पब/सब या स्ट्रीम इनवैलिडेशन

लेखक invalidate user:42 प्रकाशित करता है। ऐप इंस्टेंस लोकल एल१ छोड़ते हैं। साझा एल२ के लिए रेडिस कुंजी डिलीट फिर भी चाहिए। बिना इनवैलिडेशन का लोकल एल१ "मेरे पॉड में ठीक है" को घटना बना देता है।

५. टीटीएल बैकस्टॉप, एकमात्र योजना नहीं

परफेक्ट डिलीट पर भी वर्कर संदेश खो सकता है। टीटीएल सबसे खराब बासीपन बाँधता है। सीमा प्रोडक्ट के साथ चुनो, अंधविश्वास से नहीं।

समवर्तीता में क्रम

कई टीमों का व्यावहारिक नियम:

१. डेटाबेस अपडेट (ट्रांज़ैक्शन कमिट)। २. कैश कुंजी मिटाओ (या वर्शन बढ़ाओ)। ३. अगला रीड फिर भरे।

अगर लेखन पर कैश सेट ज़रूरी हो, कमिट के बाद कमिटेड पंक्ति से सेट करो, और टीटीएल रखो। ऊँची प्रतिस्पर्धा वाली पंक्तियों पर वर्शन कॉलम, और नई के ऊपर पुरानी वर्शन कैश न करो।


हॉट कीज़: जब एक कुंजी ही आउटेज हो

हॉट की वह कुंजी है जो ऑप्स का अनुपातहीन हिस्सा सोखती है: होमपेज कॉन्फ़िग ब्लॉब, मशहूर प्रोफ़ाइल, ग्लोबल फ़ीचर-फ़्लैग दस्तावेज़, फ़्लैश-सेल प्रोडक्ट।

लक्षण:

  • एक रेडिस सीपीयू कोर चिपका (खासकर क्लस्टर: एक हैश स्लॉट)।
  • असंबंधित कुंजियों का विलंब बढ़ता है क्योंकि वह नोड व्यस्त है।
  • क्लाइंट टाइमआउट और रीकनेक्ट तूफ़ान।

उपाय

ऐप में लोकल एल१ कैश

इन-प्रोसेस एलआरयू (कैफ़ीन, रिस्ट्रेटो, गूआवा आदि) ज्ञात गरम कुंजियों पर छोटे टीटीएल (१से-३०से) के साथ। ज़्यादातर रीड पॉड नहीं छोड़ते। पब/सब से इनवैलिड करो या छोटी बासी स्वीकारो।

कुंजी विभाजन / मान शार्डिंग

अगर मान बड़ा हैश है, product:{id}:core, product:{id}:stats आदि में बाँटो, सिर्फ अगर एक्सेस पैटर्न अलग हों। एक तार्किक काउंटर को N शार्ड में बाँटना (रीड योग, राइट यादृच्छिक शार्ड) राइट-हॉट काउंटर पर रीड-हॉट ब्लॉब से ज़्यादा मदद करता है।

रीड रेप्लिका

रेडिस रेप्लिका कैश-असाइड रीड ले सकती हैं अगर रेप्लिकेशन लैग स्वीकार हो। सेशन-महत्वपूर्ण डेटा प्राइमरी पर। होमपेज जेएसओएन जो १००मिसे पीछे चल सकता है, रेप्लिका मदद करती हैं।

हॉट की कॉपी (एज पर कैश)

सचमुच सार्वजनिक, लगभग स्थिर ब्लॉब के लिए सीडीएन या एज। अनाम ट्रैफ़िक के लिए रेडिस अकेला बचाव न हो जिसे प्रति-यूज़र ताज़गी की ज़रूरत ही नहीं।

सबसे गरम नेमस्पेस के लिए समर्पित इंस्टेंस

कभी ईमानदार सुधार अलगाव है: सिर्फ config:* और flags:* के लिए छोटा रेडिस, ताकि वहाँ तूफ़ान कार्ट सेशन को भूखा न छोड़े।


जोड़कर: एक उबाऊ, मज़बूत डिफ़ॉल्ट

पोस्टग्रेस और रेडिस वाली सामान्य सीआरयूडी एपीआई के लिए:

चिंता डिफ़ॉल्ट चुनाव
रीड पथ कैश-असाइड
मिस तूफ़ान लॉक (SET NX) + वैकल्पिक इन-प्रोसेस सिंगल-फ्लाइट
टीटीएल डोमेन अनुसार आधार + १०% जिटर
लेखन के बाद डेटाबेस कमिट, फिर कैश कुंजी DEL
नहीं मिला नेगेटिव कैश, छोटा टीटीएल
अति-गरम कुंजियाँ प्रोसेस में एल१ + छोटा टीटीएल + मेट्रिक्स
रेडिस डाउन टाइमआउट और सर्किट ब्रेकर के साथ डेटाबेस पर फेल-ओपन; अलर्ट

न्यूनतम मेट्रिक्स जो काम आते हैं

  • कुंजी प्रीफ़िक्स के अनुसार हिट अनुपात (एक ग्लोबल संख्या नहीं)।
  • मिस विलंब बनाम हिट विलंब।
  • स्टैंपीड लॉक अक्वायर विफलता / प्रतीक्षा समय।
  • रेडिस सीपीयू, बेदखल कुंजियाँ, अस्वीकृत कनेक्शन।
  • ऑप्स के अनुसार शीर्ष कुंजियाँ (रेडिस HOTKEYS / प्रॉक्सी मेट्रिक्स / सैंपलिंग)।

९९% हिट अनुपात एक प्रीफ़िक्स को २०% पर छिपा सकता है जो डेटाबेस मार रहा हो। संख्याएँ अलग करो।

विफलता मोड: रेडिस अनुपलब्ध

लिखित में तय करो:

  • डेटाबेस पर फेल-ओपन: ज़्यादा विलंब, डेटाबेस ओवरलोड जोखिम। प्रोडक्ट रीड में आम।
  • फेल-क्लोज़ड: त्रुटि लौटाओ। ऑथ सेशन स्टोर में आम (जो शुद्ध कैश न हों)।
  • डिस्क/एल१ से बासी सेवा: सिर्फ अगर सेवा करने को कुछ बचा हो।

फेल-ओपन के साथ टाइमआउट, बल्कहेड, लोड शेडिंग जोड़ो। रेडिस आउटेज में अनंत डेटाबेस फ़ॉलबैक कैश घटना को डेटाबेस घटना बना देता है।


छोटा कोड आकार (पायथन स्केच)

import json
import random
import time
from typing import Any, Callable, Optional

def cache_aside(
    redis,
    key: str,
    loader: Callable[[], Optional[Any]],
    base_ttl: int = 300,
    neg_ttl: int = 60,
    lock_ttl: int = 10,
) -> Optional[Any]:
    raw = redis.get(key)
    if raw is not None:
        return json.loads(raw)

    lock_key = f"lock:{key}"
    if redis.set(lock_key, "1", nx=True, ex=lock_ttl):
        try:
            value = loader()
            ttl = base_ttl + random.randint(0, max(1, base_ttl // 10))
            if value is None:
                redis.set(key, json.dumps(None), ex=neg_ttl)
            else:
                redis.set(key, json.dumps(value), ex=ttl)
            return value
        finally:
            redis.delete(lock_key)

    # Someone else is loading; brief wait then one more get
    time.sleep(0.02)
    raw = redis.get(key)
    if raw is not None:
        return json.loads(raw)
    # Last resort: load without holding the lock (rare)
    return loader()

यह जानबूझकर सादा है। प्रोडक्शन मेट्रिक्स, सर्किट ब्रेकर, टाइप्ड कोडेक, और अक्सर सॉफ्ट-टीटीएल रैपर जोड़ता है। संरचना मायने रखती है: गेट, लॉक, लोड, जिटर के साथ सेट, अनलॉक, रिट्राई


कैश को "पूरा" कहने से पहले चेकलिस्ट

  • हर कैश कुंजी पर टीटीएल (या दस्तावेज़ी कारण कि क्यों नहीं)।
  • गरम प्रीफ़िक्स पर जिटर और स्टैंपीड सुरक्षा।
  • लेखन कैश डिलीट/अपडेट से पहले डेटाबेस कमिट।
  • ऊँचे ट्रैफ़िक लुकअप पर नेगेटिव कैशिंग जो मिस हो सकते हैं।
  • हिट अनुपात और विलंब कुंजी प्रीफ़िक्स से अलग।
  • रेडिस डाउन होने पर व्यवहार दस्तावेज़।
  • हॉट की उम्मीदवार सूची (कॉन्फ़िग, होमपेज, फ़्लैश एसकेयू) एल१ या एज योजना के साथ।
  • maxmemory और बेदखली नीति जानबूझकर, इमेज डिफ़ॉल्ट से नहीं।

समापन

रेडिस कैश "क्वेरी के चारों ओर गेट/सेट लगाना" नहीं है। यह ताज़गी, मिस पर लोड, और लेखन के बाद कुंजी का मालिक के अनुबंधों का सेट है। कैश-असाइड ज़्यादातर ऐप कोड ढकता है। स्टैंपीड नियंत्रण और टीटीएल जिटर लोकप्रिय कुंजियों के मरने पर डेटाबेस जिलाए रखते हैं। इनवैलिडेशन प्रोडक्ट सत्य ईमानदार रखता है। हॉट-की योजनाएँ एक मशहूर ब्लॉब को क्लस्टर का मालिक बनने से रोकती हैं।

एक पथ से शुरू करो, उस पथ पर हिट अनुपात और डेटाबेस क्यूपीएस मापो, फिर जहाँ ग्राफ़ माँगें लॉक और एल१ जोड़ो। ऊपर के पैटर्न जानबूझकर उबाऊ हैं। उबाऊ ही ऑन-कॉल में बचता है।