ЁЯПл The SchoolтА║ЁЯЧДя╕П DatabasesтА║ЁЯФТ рдзрдбрд╛ 07 тАФ Concurrency рдЖрдгрд┐ isolation: рджреЛрди рдХрд╛рд░рдХреВрди, рдПрдХ рдиреЛрдВрджрд╡рд╣реА
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯФТ рдзрдбрд╛ 07 тАФ Concurrency рдЖрдгрд┐ isolation: рджреЛрди рдХрд╛рд░рдХреВрди, рдПрдХ рдиреЛрдВрджрд╡рд╣реА

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 07 тАФ рднрд╛рдЧ 2 рд╕реБрд░реВ! ┬╖ рдорд╛рдЧреЗ: lesson-06-indexes ┬╖ рдкреБрдвреЗ: lesson-08-migrations


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

рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ рджреЛрди clerks рдирд╛ рдПрдХрд╛рдЪ рд╡реЗрд│реА рддреАрдЪ рдиреЛрдВрджрд╡рд╣реА рд╣рд╡реА рдЕрд╕рддреЗ рддреЗрд╡реНрд╣рд╛ рдХрд╛рдп рд╣реЛрддреЗ: locks, isolation levels, рдЖрдгрд┐ deadlock рдЪреЗ рд╣рд╕реНрддрд╛рдВрджреЛрд▓рди.

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

Clerk 2 students рдиреЛрдВрджрд╡рд╣реА рдЙрдШрдбрддреЗ, pencil рдиреЗ "Katrina тЖТ Katrina K" рд▓рд┐рд╣рд┐рддреЗ (BEGIN, UPDATE) рдЖрдгрд┐ рд╢рд╛рдИрдиреЗ рдкрдХреНрдХреЗ рдХрд░рдгреНрдпрд╛рдЖрдзреА рдХрд╛рд╣реАрддрд░реА рддрдкрд╛рд╕рд╛рдпрд▓рд╛ рдирд┐рдШреВрди рдЬрд╛рддреЗ. Clerk 1 рдпреЗрддреЗ:

рд╡рд╛рдЪрдгрд╛рд░реАрд▓рд╛ рдХрд┐рддреА pencil рджрд┐рд╕реВ рд╢рдХрддреЗ? рддреЗ рдмрдЯрдг рдореНрд╣рдгрдЬреЗ isolation level: "рдлрдХреНрдд рд╢рд╛рдИрдиреЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реНрдпрд╛ рдУрд│реА" (read committed, рдиреЗрд╣рдореАрдЪрд╛ default) рдкрд╛рд╕реВрди "рдЬрдгреВ рдПрдХрд╛ рд╡реЗрд│реА рдПрдХрдЪ clerk рдХрд╛рдо рдХрд░рдд рдЖрд╣реЗ" (serializable) рдкрд░реНрдпрдВрдд. рдЬрд╛рд╕реНрдд рдХрдбрдХ рдореНрд╣рдгрдЬреЗ рдЬрд╛рд╕реНрдд рд╕реБрд░рдХреНрд╖рд┐рдд рдЖрдгрд┐ рдЬрд╛рд╕реНрдд рдерд╛рдВрдмрдгреЗ.

рдЖрдгрд┐ рдХреНрд▓рд╛рд╕рд┐рдХ рд╕рд╛рдкрд│рд╛ тАФ deadlock: clerk 1 рдиреЗ students рдиреЛрдВрджрд╡рд╣реА рдзрд░рд▓реА рдЖрд╣реЗ рдЖрдгрд┐ рддрд┐рд▓рд╛ grades рд╣рд╡реА рдЖрд╣реЗ; clerk 2 рдиреЗ grades рдзрд░рд▓реА рдЖрд╣реЗ рдЖрдгрд┐ рддрд┐рд▓рд╛ students рд╣рд╡реА рдЖрд╣реЗ. рдХреЛрдгреАрдЪ рд╣рд▓реВ рд╢рдХрдд рдирд╛рд╣реА. рджрдкреНрддрд░рджрд╛рд░ рдПрдХреАрд▓рд╛ рдирд┐рд╡рдбрддреЛ, рддрд┐рдЪреЗ pencil рдХрд╛рдо рдлрд╛рдбреВрди рдЯрд╛рдХрддреЛ, рдЖрдгрд┐ рддрд┐рд▓рд╛ рдкреБрдиреНрд╣рд╛ рд╕реБрд░реБрд╡рд╛рдд рдХрд░рд╛рдпрд▓рд╛ рд╕рд╛рдВрдЧрддреЛ. рдЙрдкрд╛рдп рдХрдВрдЯрд╛рд│рд╡рд╛рдгрд╛ рдЖрд╣реЗ: transactions рд▓рд╣рд╛рди рдареЗрд╡рд╛, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдиреЛрдВрджрд╡рд╣реНрдпрд╛ рдПрдХрд╛рдЪ рдХреНрд░рдорд╛рдиреЗ рдШреНрдпрд╛.

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

sequenceDiagram
    participant A as ЁЯзСтАНЁЯТ╝ clerk 1
    participant DB as ЁЯЧДя╕П register
    participant B as ЁЯзСтАНЁЯТ╝ clerk 2
    B->>DB: 1 BEGIN; UPDATE students SET name='Katrina K' WHERE id=2 (pencil, not committed)
    A->>DB: 2 SELECT name WHERE id=2
    DB-->>A: 'Katrina' тАФ the old, inked value (isolation)
    A->>DB: 3 UPDATE students SET name='Katrina R' WHERE id=2
    DB-->>A: тП│ waits for the lockтАж 'database is locked'
    B->>DB: 4 ROLLBACK (pencil erased) тАФ the register is free again

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рд╕рд╛рдорд╛рдпрд┐рдХ" (рдзрдбрд╛ 01 рдЪреЗ рджреБрд╕рд░реЗ рд╡рдЪрди) рд╣реЗ рддрд┐рдиреНрд╣реАрдВрдкреИрдХреА рд╕рд░реНрд╡рд╛рдд рдХрдареАрдг рдЖрд╣реЗ. рдЗрдерд▓реЗ bugs tests рдордзреНрдпреЗ рдЕрджреГрд╢реНрдп (рдПрдХ user) рдЖрдгрд┐ production рдордзреНрдпреЗ рд╡рд┐рдирд╛рд╢рдХрд╛рд░реА рдЕрд╕рддрд╛рдд (sale, ticket drop, payroll run): рджреЛрдирджрд╛ book рдЭрд╛рд▓реЗрд▓реНрдпрд╛ рдЬрд╛рдЧрд╛ рдЖрдгрд┐ рд╣рд░рд╡рд▓реЗрд▓реЗ updates рдореНрд╣рдгрдЬреЗ business рдЪрд╛ рд╡реЗрд╢ рдШрд╛рддрд▓реЗрд▓реНрдпрд╛ isolation рд╕рдорд╕реНрдпрд╛.

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

db/demo.py рдордзреАрд▓ locks() рджреБрд╕рд░реЗ connection рдЙрдШрдбрддреЗ, commit рди рдХреЗрд▓реЗрд▓реНрдпрд╛ UPDATE рд╕рд╣ BEGIN IMMEDIATE (write lock) рд╕реБрд░реВ рдХрд░рддреЗ, рдордЧ clerk 1 рдЬреБрдиреА value рд╡рд╛рдЪрддрд╛рдирд╛ рдЖрдгрд┐ timeout рдордзреНрдпреЗ рд▓рд┐рд╣рд┐рдгреНрдпрд╛рдд рдЕрдкрдпрд╢реА рд╣реЛрддрд╛рдирд╛ рджрд╛рдЦрд╡рддреЗ.

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

python3 db/demo.py locks
python3 - <<'EOF'
import sqlite3, threading
def clerk(name, delta):
    c = sqlite3.connect("db/school.db", timeout=5)
    with c:                                                       # atomic increment: no lost update
        c.execute("UPDATE homework SET title = title || ?", (delta,))
    print(name, "done")
ts = [threading.Thread(target=clerk, args=(f"clerk {i}", "!")) for i in range(5)]
[t.start() for t in ts]; [t.join() for t in ts]
c = sqlite3.connect("db/school.db"); print(c.execute("SELECT title FROM homework").fetchall())
EOF

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

locks clerk 1 рдЪреНрдпрд╛ read рд╕рд╛рдареА рдЬреБрдиреЗ рдирд╛рд╡ рдЫрд╛рдкрддреЛ рдЖрдгрд┐ рдордЧ timeout рдирдВрддрд░ database is locked. рддреБрдордЪреЗ рдкрд╛рдЪ threads рдкреНрд░рддреНрдпреЗрдХреА рдПрдХ ! рдЬреЛрдбрддрд╛рдд тАФ рдкреНрд░рддреНрдпреЗрдХ title рдЕрдЧрджреА рдкрд╛рдЪ ! рдиреЗ рд╕рдВрдкрддреЗ: lock рдиреЗ writers рдирд╛ рдПрдХрд╛рдорд╛рдЧреВрди рдПрдХ рд▓рд╛рд╡рд▓реЗ рдЖрдгрд┐ рдПрдХрд╣реА update рд╣рд░рд╡рд▓рд╛ рдирд╛рд╣реА.

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

Readers рдирд╛ рдХрдзреАрдЪ pencil рджрд┐рд╕рдд рдирд╛рд╣реА, writers lock рд╕рд╛рдареА рдерд╛рдВрдмрддрд╛рдд, рдЖрдгрд┐ рдкрд╛рдЪ рдПрдХрд╛рдЪрд╡реЗрд│рдЪреЗ updates рдкрд╛рдЪ рдореНрд╣рдгреВрдирдЪ рдЙрддрд░рд▓реЗ тАФ рдПрдХрд╛рдиреЗ рдЗрддрд░рд╛рдВрд╡рд░ overwrite рдХрд░реВрди рдирд╡реНрд╣реЗ.

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

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: "flash sale рдордзреНрдпреЗ 100 рдкреИрдХреА 101 items рд╡рд┐рдХрд▓реЗ" рд╣реА рдЧреЛрд╖реНрдЯ рдореНрд╣рдгрдЬреЗ lost update; "рд░рд╛рддреНрд░реА 9 рд▓рд╛ checkout рдЕрдбрдХрддреЛ" рд╣реА рдЧреЛрд╖реНрдЯ рдореНрд╣рдгрдЬреЗ lock рдзрд░реВрди рдмрд╕рд▓реЗрд▓реЗ рд▓рд╛рдВрдм transaction. рдЗрдереЗ рдПрдХрджрд╛ рдЕрдиреБрднрд╡рд▓реНрдпрд╛рд╡рд░ рджреЛрдиреНрд╣реАрдВрдЪреЗ рдирд┐рджрд╛рди рдзрдбрд╛ 07 рдЪреНрдпрд╛ рдПрдХрд╛ рд╡рд╛рдХреНрдпрд╛рдд рд╣реЛрддреЗ.

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

рд╢рд╛рд│рд╛ рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛рдЪ рдиреЛрдВрджрд╡рд╣реНрдпрд╛ рдмрджрд▓рд╛рд╡реНрдпрд╛ рд▓рд╛рдЧрддрд╛рдд: migrations тАФ versioned scripts, logbook, рдЖрдгрд┐ expand тЖТ migrate тЖТ contract.

git checkout lesson-08-migrations

ЁЯФТ Lesson 07 тАФ Concurrency & isolation: two clerks, one register

ЁЯУН You are here: Lesson 07 of 18 тАФ Part 2 begins! ┬╖ Previous: lesson-06-indexes ┬╖ Next: lesson-08-migrations


ЁЯУж What's in this branch

Lessons 01тАУ06, plus what happens when two clerks want the same register at the same time: locks, isolation levels, and the deadlock handshake.

ЁЯзТ Explain like I'm 5

Clerk 2 opens the students register, writes in pencil "Katrina тЖТ Katrina K" (BEGIN, UPDATE) and walks off to check something before inking it. Clerk 1 arrives:

How much pencil may a reader see? That dial is the isolation level: from "only inked lines" (read committed, the usual default) to "as if one clerk worked at a time" (serializable). Stricter means safer and means more waiting.

And the classic trap тАФ the deadlock: clerk 1 holds the students register and wants grades; clerk 2 holds grades and wants students. Neither can move. The archivist picks one, tears up their pencil work, and tells them to start over. The cure is boring: keep transactions short, and take registers in the same order every time.

ЁЯЧ║я╕П Diagram

sequenceDiagram
    participant A as ЁЯзСтАНЁЯТ╝ clerk 1
    participant DB as ЁЯЧДя╕П register
    participant B as ЁЯзСтАНЁЯТ╝ clerk 2
    B->>DB: 1 BEGIN; UPDATE students SET name='Katrina K' WHERE id=2 (pencil, not committed)
    A->>DB: 2 SELECT name WHERE id=2
    DB-->>A: 'Katrina' тАФ the old, inked value (isolation)
    A->>DB: 3 UPDATE students SET name='Katrina R' WHERE id=2
    DB-->>A: тП│ waits for the lockтАж 'database is locked'
    B->>DB: 4 ROLLBACK (pencil erased) тАФ the register is free again

тЭУ What

ЁЯдФ Why

Because "shared" (lesson 01's second promise) is the hardest of the three. The bugs here are invisible in tests (one user) and catastrophic in production (a sale, a ticket drop, a payroll run): double-booked seats and lost updates are isolation problems wearing a business costume.

ЁЯФз How (in this repo)

locks() in db/demo.py opens a second connection, starts BEGIN IMMEDIATE (the write lock) with an uncommitted UPDATE, then shows clerk 1 reading the old value and failing to write within the timeout.

ЁЯзк Try it

python3 db/demo.py locks
python3 - <<'EOF'
import sqlite3, threading
def clerk(name, delta):
    c = sqlite3.connect("db/school.db", timeout=5)
    with c:                                                       # atomic increment: no lost update
        c.execute("UPDATE homework SET title = title || ?", (delta,))
    print(name, "done")
ts = [threading.Thread(target=clerk, args=(f"clerk {i}", "!")) for i in range(5)]
[t.start() for t in ts]; [t.join() for t in ts]
c = sqlite3.connect("db/school.db"); print(c.execute("SELECT title FROM homework").fetchall())
EOF

тЬЕ Verify тАФ what you should see

locks prints the old name for clerk 1's read and then database is locked after the timeout. Your five threads each append one ! тАФ every title ends with exactly five !: the lock serialised the writers and no update was lost.

ЁЯПБ What you just proved

Readers never see pencil, writers wait for the lock, and five concurrent updates landed as five тАФ not as one overwriting the others.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: the "flash sale sold 101 of 100 items" story is a lost update; the "checkout hangs at 9 pm" story is a long transaction holding a lock. Both are a lesson-07 sentence to diagnose once you have felt them here.

тПня╕П Next

The registers must change while school is open: migrations тАФ versioned scripts, the logbook, and expand тЖТ migrate тЖТ contract.

git checkout lesson-08-migrations
тЖР PreviousindexesNext тЖТmigrations

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