ЁЯПл The SchoolтА║ЁЯУИ ScalingтА║ЁЯУМ рдзрдбрд╛ 09 тАФ Database рдЪреЗ caching: рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рд╕рдореЛрд░рдЪрд╛ рд╕реВрдЪрдирд╛ рдлрд▓рдХ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУМ рдзрдбрд╛ 09 тАФ Database рдЪреЗ caching: рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рд╕рдореЛрд░рдЪрд╛ рд╕реВрдЪрдирд╛ рдлрд▓рдХ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 13 рдкреИрдХреА рдзрдбрд╛ 09 ┬╖ рдорд╛рдЧреАрд▓: lesson-08-serverless-scaling ┬╖ рдкреБрдвреАрд▓: lesson-10-replicas-and-pools


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ08, рдЖрдгрд┐ database рд╕реНрддрд░рд╛рд╕рд╛рдареАрдЪреЗ рдкрд╣рд┐рд▓реЗ рд╕рд╛рдзрди: рддреНрдпрд╛рдЪреНрдпрд╛рд╕рдореЛрд░ рдПрдХ cache (Redis рдХрд┐рдВрд╡рд╛ Valkey рд╕рд╛рдареА ElastiCache). рддреБрдореНрд╣реА cache-aside, TTLs, write рдЭрд╛рд▓реНрдпрд╛рд╡рд░ рдХрд╛рдп рдХрд░рд╛рдпрдЪреЗ, рдЖрдгрд┐ рд▓реЛрдХрдкреНрд░рд┐рдп key expire рдЭрд╛рд▓реНрдпрд╛рд╡рд░ stampede рдХрд╕рд╛ рдерд╛рдВрдмрд╡рд╛рдпрдЪрд╛ рд╣реЗ рд╢рд┐рдХрддрд╛. scale/demo.py рдордзрд▓реЗ cache() рдпрд╛рддрд▓реЗ рдкреНрд░рддреНрдпреЗрдХ рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рд▓рдХ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рд▓рд╛ рдПрдХрдЪ рдкреНрд░рд╢реНрди рд╡рд┐рдЪрд╛рд░рддреЛ: "3A рд╡рд░реНрдЧрд╛рдЪреЗ рдирд┐рдХрд╛рд▓ рдХрд╛рдп рдЖрд╣реЗрдд?" рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рддрд▓рд╛ рдХрд╛рд░рдХреВрди рдорд╛рдЧрдЪреНрдпрд╛ рдЦреЛрд▓реАрддрд▓реНрдпрд╛ рдореЛрдареНрдпрд╛ рдиреЛрдВрджрд╡рд╣реАрдХрдбреЗ рдЬрд╛рддреЛ, 3A рд╢реЛрдзрддреЛ, рдкрд░рдд рдпреЗрддреЛ рдЖрдгрд┐ рдЙрддреНрддрд░ рджреЗрддреЛ. рдкреБрдиреНрд╣рд╛. рдЖрдгрд┐ рдкреБрдиреНрд╣рд╛. рдПрдХрд╛ рдорд┐рдирд┐рдЯрд╛рдд 6,000 рд╡реЗрд│рд╛.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдмрд╛рд╣реЗрд░ рдПрдХ рд╕реВрдЪрдирд╛ рдлрд▓рдХ ЁЯУМ рд▓рд╛рд╡рддреЗ. рдкрд╣рд┐рд▓реНрдпрд╛рдВрджрд╛ рдХреЛрдгреА рд╡рд┐рдЪрд╛рд░рд▓реЗ рдХреА, рдХрд╛рд░рдХреВрди рдиреЛрдВрджрд╡рд╣реА рдПрдХрджрд╛рдЪ рд╡рд╛рдЪрддреЛ рдЖрдгрд┐ рдЙрддреНрддрд░ рдлрд▓рдХрд╛рд╡рд░ рдПрдХрд╛ рдЪрд┐рдареНрдареАрд╕рд╣ рд▓рд╛рд╡рддреЛ: "30 рд╕реЗрдХрдВрдж рдЪрд╛рд▓реЗрд▓". рдмрд╛рдХреА рд╕рдЧрд│реЗ рдлрд▓рдХ рд╡рд╛рдЪрддрд╛рдд.

рдПрдХреЗ рджрд┐рд╡рд╢реА 500 рдкрд╛рд▓рдХ рдлрд▓рдХрд╛рд╕рдореЛрд░ рдЙрднреЗ рдЕрд╕рддрд╛рдирд╛рдЪ рдЪрд┐рдареНрдареАрдЪреА рдореБрджрдд рд╕рдВрдкрддреЗ. рд╕рдЧрд│реЗ 500 рдПрдХрд╛рдЪ рдХреНрд╖рдгреА рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдд рдШреБрд╕рддрд╛рдд, рдЖрдгрд┐ рдХрд╛рд░рдХреВрди рдкреБрд░рд╛ рдкрдбрдд рдирд╛рд╣реА. рдпрд╛рд▓рд╛рдЪ stampede рдореНрд╣рдгрддрд╛рдд.

рдкреБрдврдЪреНрдпрд╛ рд╡реЗрд│реА, рдЪрд┐рдареНрдареАрдЪреА рдореБрджрдд рд╕рдВрдкрд▓реЗрд▓реА рдкрд╛рд╣рдгрд╛рд░рд╛ рдкрд╣рд┐рд▓рд╛ рдкрд╛рд▓рдХ рдореНрд╣рдгрддреЛ: "рдореА рдЖрдгрддреЛ тАФ рдмрд╛рдХреАрдЪреНрдпрд╛рдВрдиреА рдПрдХ рдХреНрд╖рдг рдерд╛рдВрдмрд╛." рдиреЛрдВрджрд╡рд╣реАрдХрдбреЗ рдПрдХрдЪ рдлреЗрд░реА, рдЖрдгрд┐ рдордЧ 500 рдЖрдирдВрджреА рдкрд╛рд▓рдХ. рдпрд╛рд▓рд╛рдЪ single-flight рдореНрд╣рдгрддрд╛рдд.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    app["ЁЯН│ API"] -->|"1 GET results:3A"| rc["ЁЯУМ Redis / Valkey<br/>ElastiCache"]
    rc -->|"hit"| app
    app -->|"2 miss: SELECT"| db["ЁЯУТ database"]
    app -->|"3 SET results:3A EX 30"| rc
    w["тЬПя╕П teacher saves a grade"] -->|"UPDATE, then DEL results:3A"| db
    st["ЁЯРШ 500 waiting on one expired key<br/>single-flight off тЖТ 500 DB reads<br/>on тЖТ 1"]

ЁЯЧ║я╕П рд░реЗрдЦрд╛рдЯрд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l09

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг database рд╣рд╛ scale рдХрд░рд╛рдпрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рдХрдареАрдг рд╕реНрддрд░ рдЖрд╣реЗ тАФ рдПрдХрд╛ рдорд┐рдирд┐рдЯрд╛рдд 20 рдкреНрд░рддреА рдЬреЛрдбрддрд╛ рдпреЗрдд рдирд╛рд╣реАрдд тАФ рдЖрдгрд┐ рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╢реА рддреНрдпрд╛рдЪреЗ рдмрд╣реБрддреЗрдХ reads рдПрдХрдЪ рдкреНрд░рд╢реНрди рдЕрд╕рддрд╛рдд. Cache рддреА рдЙрддреНрддрд░реЗ memory рдордзреВрди рдПрдХрд╛ millisecond рдкреЗрдХреНрд╖рд╛ рдЦреВрдк рдХрдореА рд╡реЗрд│рд╛рдд рджреЗрддреЛ, рдЖрдгрд┐ database рд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ key рд╕рд╛рдареА рдкреНрд░рддреНрдпреЗрдХ TTL рдордзреНрдпреЗ рдПрдХрдЪ read рджрд┐рд╕рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

scale/sim.py рдордзрд▓реЗ CacheAside(ttl, single_flight=False) рдкреНрд░рддреНрдпреЗрдХ key рд╕рд╛рдареА рдПрдХ timestamp рдареЗрд╡рддреЗ. entry ttl рдкреЗрдХреНрд╖рд╛ рд▓рд╣рд╛рди рд╡рдпрд╛рдЪреА рдЕрд╕реЗрдкрд░реНрдпрдВрдд read(key, t, concurrent=1) рдПрдХ hit рдореЛрдЬрддреЗ; рдирд╛рд╣реАрддрд░ рддреЗ database reads рдореЛрдЬрддреЗ тАФ single-flight рдмрдВрдж рдЕрд╕реЗрд▓ рддрд░ concurrent рдЗрддрдХреЗ (рдерд╛рдВрдмрд▓реЗрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ caller database рдХрдбреЗ рдЬрд╛рддреЛ), рдЪрд╛рд▓реВ рдЕрд╕реЗрд▓ рддрд░ рдПрдХ тАФ рдЖрдгрд┐ key refresh рдХрд░рддреЗ. cache() 60 рд╕реЗрдХрдВрдж рджрд░ рд╕реЗрдХрдВрджрд╛рд▓рд╛ 100 рд╡реЗрд│рд╛ results:3A рд╡рд╛рдЪрддреЗ, рдЖрдгрд┐ рдордЧ 500 рдерд╛рдВрдмрд▓реЗрд▓реНрдпрд╛ callers рдЕрд╕рддрд╛рдирд╛ key expire рдХрд░рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 scale/demo.py cache
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import CacheAside
for ttl in (1, 5, 30, 300):
    c = CacheAside(ttl=ttl)
    for t in range(60):
        for _ in range(100): c.read("results:3A", t)
    print(f"TTL {ttl:>3} s тЖТ database reads {c.db_reads:>3} ┬╖ hit ratio {c.hits / 6000:.1%}")
for waiting in (50, 500, 2000):
    off, on = CacheAside(30), CacheAside(30, single_flight=True)
    off.read("results:3A", 0, concurrent=waiting); on.read("results:3A", 0, concurrent=waiting)
    print(f"{waiting:>4} waiting on an expired key тЖТ DB reads {off.db_reads:>4} (single-flight off), {on.db_reads} (on)")
EOF
python3 scale/test_scale.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

cache рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА 6,000 reads of results:3A in a minute, cache TTL 30 s тЖТ database reads: 2, hits: 5,998
   the key expires while 500 parents are waiting тАФ single-flight off тЖТ database reads: 500
   the key expires while 500 parents are waiting тАФ single-flight on  тЖТ database reads: 1

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

TTL   1 s тЖТ database reads  60 ┬╖ hit ratio 99.0%
TTL   5 s тЖТ database reads  12 ┬╖ hit ratio 99.8%
TTL  30 s тЖТ database reads   2 ┬╖ hit ratio 100.0%
TTL 300 s тЖТ database reads   1 ┬╖ hit ratio 100.0%
  50 waiting on an expired key тЖТ DB reads   50 (single-flight off), 1 (on)
 500 waiting on an expired key тЖТ DB reads  500 (single-flight off), 1 (on)
2000 waiting on an expired key тЖТ DB reads 2000 (single-flight off), 1 (on)

(100.0% рдореНрд╣рдгрдЬреЗ 6,000 рдкреИрдХреА 5,998, рдкреВрд░реНрдгрд╛рдВрдХрд┐рдд.) Tests рдордзреНрдпреЗ тЬЕ L09 single-flight stops a stampede рдЖрд╣реЗ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

1 рд╕реЗрдХрдВрджрд╛рдЪрд╛ TTL рд╕реБрджреНрдзрд╛ 6,000 database reads рдЪреЗ 60 рдХрд░рддреЛ. рдкрдг expire рд╣реЛрдгреНрдпрд╛рдЪрд╛ рдХреНрд╖рдг рдзреЛрдХрд╛рджрд╛рдпрдХ рдЕрд╕рддреЛ: single-flight рдирд╕реЗрд▓ рддрд░ database рд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рдерд╛рдВрдмрд▓реЗрд▓реНрдпрд╛ caller рдорд╛рдЧреЗ рдПрдХ read рдорд┐рд│рддреЛ тАФ рдПрдХрджрдо 2,000. Single-flight рдЕрд╕реЗрд▓ рддрд░ рдПрдХ тАФ рдЧрд░реНрджреА рдХрд┐рддреАрд╣реА рдореЛрдареА рдЕрд╕реЛ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдПрдХ serverless Valkey cache:

aws elasticache create-serverless-cache --serverless-cache-name school-cache --engine valkey \
    --subnet-ids subnet-0aaa1111bbbb2222c subnet-0ddd3333eeee4444f \
    --security-group-ids sg-0123456789abcdef0
aws elasticache describe-serverless-caches --serverless-cache-name school-cache \
    --query 'ServerlessCaches[0].Endpoint'

Jittered TTL рдЖрдгрд┐ lock рд╕рд╣ cache-aside (Python, redis client тАФ Valkey рддреЛрдЪ protocol рдмреЛрд▓рддреЛ):

import json, random, time, redis
r = redis.Redis(host="school-cache-abc123.serverless.aps1.cache.amazonaws.com", port=6379, ssl=True)

def results_for(cls):
    key = f"results:{cls}"
    for _ in range(50):                                   # wait up to ~2.5 s for another refill
        hit = r.get(key)
        if hit is not None:
            return json.loads(hit)
        if r.set(f"{key}:lock", "1", nx=True, ex=5):       # single-flight: only one caller refills
            try:
                rows = db_query("SELECT pupil, grade FROM results WHERE class = %s", cls)
                r.set(key, json.dumps(rows), ex=30 + random.randint(0, 5))   # jittered TTL
                return rows
            finally:
                r.delete(f"{key}:lock")
        time.sleep(0.05)
    return db_query("SELECT pupil, grade FROM results WHERE class = %s", cls)  # give up waiting

def save_grade(pupil, cls, grade):
    db_execute("UPDATE results SET grade = %s WHERE pupil = %s", grade, pupil)
    r.delete(f"results:{cls}")                            # on a write: delete the key

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: cache рдЪрд╛ hit rate рдЖрдгрд┐ database рдЪреЗ read IOPS рдПрдХрддреНрд░ рдкрд╛рд╣рд╛. Hit rate рдШрд╕рд░рд▓рд╛ рдХреА рддреЛ рдПрдХрд╛ рд╕реЗрдХрдВрджрд╛рдиреЗ database рд╡рд░ рджрд┐рд╕рддреЛ.

тПня╕П рдкреБрдвреЗ

рд╕реВрдЪрдирд╛ рдлрд▓рдХ рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рдпреЗрдгрд╛рд░реЗ рдкреНрд░рд╢реНрди рд╣рд╛рддрд╛рд│рддреЛ. рдмрд╛рдХреАрдЪреЗ рдЕрдЬреВрдирд╣реА рдПрдХрд╛рдЪ database рдХрдбреЗ рдЬрд╛рддрд╛рдд. рдкреБрдвреЗ: рд╡рд╛рдЪрдгреНрдпрд╛рд╕рд╛рдареА рдиреЛрдВрджрд╡рд╣реАрдЪреНрдпрд╛ рдкреНрд░рддреА, рдЖрдгрд┐ connections рд╡рд╛рдЯреВрди рджреЗрдгрд╛рд░рд╛ proxy.

git checkout lesson-10-replicas-and-pools

ЁЯУМ Lesson 09 тАФ Caching the database: the notice board in front of the office

ЁЯУН You are here: Lesson 09 of 13 ┬╖ Previous: lesson-08-serverless-scaling ┬╖ Next: lesson-10-replicas-and-pools


ЁЯУж What's in this branch

Lessons 01тАУ08, plus the first tool for the database tier: a cache in front of it (ElastiCache for Redis or Valkey). You learn cache-aside, TTLs, what to do on a write, and how to stop a stampede when a popular key expires. cache() in scale/demo.py shows each one.

ЁЯзТ Explain like I'm 5

Every parent asks the office the same question: "What are the results for class 3A?" The office clerk walks to the big register in the back room, finds 3A, walks back and answers. Again. And again. 6,000 times in a minute.

So Dipika puts up a notice board ЁЯУМ outside the office. The first time someone asks, the clerk reads the register once and pins the answer on the board with a note: "good for 30 seconds". Everyone else reads the board.

One day the note runs out while 500 parents are standing at the board. All 500 rush into the office at the same moment, and the clerk cannot keep up. That is a stampede.

Next time, the first parent who finds the note expired says: "I will fetch it тАФ everyone else, wait one moment." One trip to the register, then 500 happy parents. That is single-flight.

ЁЯЧ║я╕П Diagram

flowchart LR
    app["ЁЯН│ API"] -->|"1 GET results:3A"| rc["ЁЯУМ Redis / Valkey<br/>ElastiCache"]
    rc -->|"hit"| app
    app -->|"2 miss: SELECT"| db["ЁЯУТ database"]
    app -->|"3 SET results:3A EX 30"| rc
    w["тЬПя╕П teacher saves a grade"] -->|"UPDATE, then DEL results:3A"| db
    st["ЁЯРШ 500 waiting on one expired key<br/>single-flight off тЖТ 500 DB reads<br/>on тЖТ 1"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l09

тЭУ What

ЁЯдФ Why

Because a database is the hardest tier to scale тАФ you cannot add 20 copies in a minute тАФ and most of its reads on results day are the same question. A cache answers those from memory in well under a millisecond, and the database sees one read per key per TTL.

ЁЯФз How (in this repo)

CacheAside(ttl, single_flight=False) in scale/sim.py keeps a timestamp per key. read(key, t, concurrent=1) counts a hit while the entry is younger than ttl; otherwise it counts database reads тАФ concurrent of them with single-flight off (every waiting caller goes to the database), one with it on тАФ and refreshes the key. cache() reads results:3A 100 times a second for 60 seconds, then expires the key under 500 waiting callers.

ЁЯзк Try it

python3 scale/demo.py cache
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import CacheAside
for ttl in (1, 5, 30, 300):
    c = CacheAside(ttl=ttl)
    for t in range(60):
        for _ in range(100): c.read("results:3A", t)
    print(f"TTL {ttl:>3} s тЖТ database reads {c.db_reads:>3} ┬╖ hit ratio {c.hits / 6000:.1%}")
for waiting in (50, 500, 2000):
    off, on = CacheAside(30), CacheAside(30, single_flight=True)
    off.read("results:3A", 0, concurrent=waiting); on.read("results:3A", 0, concurrent=waiting)
    print(f"{waiting:>4} waiting on an expired key тЖТ DB reads {off.db_reads:>4} (single-flight off), {on.db_reads} (on)")
EOF
python3 scale/test_scale.py

тЬЕ Verify тАФ what you should see

cache prints:

тФАтФА 6,000 reads of results:3A in a minute, cache TTL 30 s тЖТ database reads: 2, hits: 5,998
   the key expires while 500 parents are waiting тАФ single-flight off тЖТ database reads: 500
   the key expires while 500 parents are waiting тАФ single-flight on  тЖТ database reads: 1

Your snippet prints:

TTL   1 s тЖТ database reads  60 ┬╖ hit ratio 99.0%
TTL   5 s тЖТ database reads  12 ┬╖ hit ratio 99.8%
TTL  30 s тЖТ database reads   2 ┬╖ hit ratio 100.0%
TTL 300 s тЖТ database reads   1 ┬╖ hit ratio 100.0%
  50 waiting on an expired key тЖТ DB reads   50 (single-flight off), 1 (on)
 500 waiting on an expired key тЖТ DB reads  500 (single-flight off), 1 (on)
2000 waiting on an expired key тЖТ DB reads 2000 (single-flight off), 1 (on)

(100.0% is 5,998 of 6,000, rounded.) The tests include тЬЕ L09 single-flight stops a stampede.

ЁЯПБ What you just proved

Even a 1-second TTL turns 6,000 database reads into 60. But the moment of expiry is dangerous: without single-flight, the database gets one read per waiting caller тАФ 2,000 at once. With it, one тАФ however big the crowd.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ a serverless Valkey cache:

aws elasticache create-serverless-cache --serverless-cache-name school-cache --engine valkey \
    --subnet-ids subnet-0aaa1111bbbb2222c subnet-0ddd3333eeee4444f \
    --security-group-ids sg-0123456789abcdef0
aws elasticache describe-serverless-caches --serverless-cache-name school-cache \
    --query 'ServerlessCaches[0].Endpoint'

Cache-aside with a jittered TTL and a lock (Python, redis client тАФ Valkey speaks the same protocol):

import json, random, time, redis
r = redis.Redis(host="school-cache-abc123.serverless.aps1.cache.amazonaws.com", port=6379, ssl=True)

def results_for(cls):
    key = f"results:{cls}"
    for _ in range(50):                                   # wait up to ~2.5 s for another refill
        hit = r.get(key)
        if hit is not None:
            return json.loads(hit)
        if r.set(f"{key}:lock", "1", nx=True, ex=5):       # single-flight: only one caller refills
            try:
                rows = db_query("SELECT pupil, grade FROM results WHERE class = %s", cls)
                r.set(key, json.dumps(rows), ex=30 + random.randint(0, 5))   # jittered TTL
                return rows
            finally:
                r.delete(f"{key}:lock")
        time.sleep(0.05)
    return db_query("SELECT pupil, grade FROM results WHERE class = %s", cls)  # give up waiting

def save_grade(pupil, cls, grade):
    db_execute("UPDATE results SET grade = %s WHERE pupil = %s", grade, pupil)
    r.delete(f"results:{cls}")                            # on a write: delete the key

ЁЯПн Why this matters in production: watch the cache's hit rate and the database's read IOPS together. A drop in hit rate shows up on the database a second later.

тПня╕П Next

The notice board handles the repeated questions. The rest still go to one database. Next: copies of the register for reading, and a proxy that shares connections.

git checkout lesson-10-replicas-and-pools
тЖР Previousserverless scalingNext тЖТreplicas and pools

This page is the lesson's README from the lesson-09-caching-the-database branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.