Row-level lock: the multi-instance ID generator

pessimistic-lock
Mechanism lab

Row-level lock: the multi-instance ID generator

Two service instances issue IDs from one shared sequence row: read the current value, add one, write it back. If that read-modify-write isn't mutually exclusive, both instances read the same value and hand out the SAME id. The fix is a pessimistic row lock — SELECT … FOR UPDATE — so the second instance waits for the first to finish.

résumé · SK AX — resolved duplicate ID generation across multi-instance manufacturing execution services with Oracle row-level locks (FOR UPDATE).

You'll predict the failure first, then write the fix and watch your own code drive the behavior.

Understand it step by step (한국어로 이해 → 영어로 말하기)

  1. 1

    ID를 만드는 방법은 간단하다. 공유하는 한 칸(번호표)에서 지금 숫자를 읽고, +1 하고, 다시 써넣는다. 이게 다음 ID가 된다.

    int cur = db.read("seq");
    int id = cur + 1;
    db.write("seq", id);   // emit id as the next ID
    🗣 영어로 말해

    Each instance reads the current number, adds one, and writes it back as the next ID.

    checking microphone…

  2. 2

    그런데 서버가 2대다. 둘이 거의 동시에 그 칸을 읽으면, 둘 다 똑같은 숫자(예: 10)를 본다. 그리고 둘 다 11을 만든다.

    ⚖️ Trade-off: 그냥 두면 같은 ID가 두 번 나온다 → 중복 ID. PK 충돌나서 라인이 멈춘다.

    // I1: read 100 -> emit 101
    // I2: read 100 -> emit 101   <-- same value, DUPLICATE id
    🗣 영어로 말해

    With two instances, both read the same value and hand out the same id.

    checking microphone…

  3. 3

    왜 자바 락(synchronized)으로는 못 막나? 그건 한 서버(JVM) 안에서만 통한다. 서버가 2대라 서로의 락을 못 본다.

    ⚖️ Trade-off: 서버끼리는 메모리가 따로라, 앱 단 락은 옆 서버한테 안 보인다 → 무용지물.

    🔧 도구:synchronizedReentrantLock (한 JVM 전용 — 여기선 X)

    synchronized (this) {        // only guards ONE JVM
        int id = db.read("seq") + 1;
        db.write("seq", id);
    }                            // the other instance never sees this monitor
    🗣 영어로 말해

    A Java lock only works inside one server, so the other instance never sees it.

    checking microphone…

  4. 4

    핵심은 '읽고 → +1 → 쓰기' 이 세 동작이 하나처럼 묶여야 한다는 거다. 중간에 옆 서버가 끼어들면 안 된다.

    ✅ Fix: 둘 다 보는 곳, 즉 DB에서 잠가야 한다. DB는 두 서버 모두가 보는 공통 장소다.

    // read + (+1) + write must be ONE atomic step
    int id = db.read("seq") + 1;
    db.write("seq", id);   // no other instance may interleave between these
    🗣 영어로 말해

    The read, the add, and the write have to happen as one atomic step.

    checking microphone…

  5. 5

    해결: 그 번호표 '한 줄'을 DB에서 잠근다 — SELECT … FOR UPDATE. 먼저 잡은 서버가 다 끝낼 때까지, 두 번째 서버는 그 줄 앞에서 기다린다.

    ⚖️ Trade-off: 기다리는 동안 그 줄에 대한 쓰기가 한 줄로 줄 세워진다 (직렬화) → 그만큼 느려질 수 있다.

    ✅ Fix: ID 뽑기는 잠깐이고 자주 일어나는 일도 아니라, 줄 세워도 비용이 거의 없다. 그래서 비관적 락이면 충분하다.

    🔧 도구:SELECT … FOR UPDATErow-level lock (pessimistic lock)

    -- lock the one row; second instance blocks here until commit
    SELECT seq FROM counters WHERE name = 'seq' FOR UPDATE;
    UPDATE counters SET seq = seq + 1 WHERE name = 'seq';
    
    // in code: db.lock("I1", "seq") blocks I2 until db.unlock("I1", "seq")
    🗣 영어로 말해

    I lock that one row with SELECT FOR UPDATE, so the second instance waits its turn.

    checking microphone…

  6. 6

    비관적 락 대신 낙관적 락(버전 비교)을 쓸 수도 있다. 락을 안 잡고, 쓸 때 '내가 읽은 버전 그대로냐?' 확인하고, 다르면 다시 시도한다.

    ⚖️ Trade-off: 여기선 두 서버가 거의 항상 동시에 같은 줄을 친다(경합이 잦다) → 충돌이 계속 나서 재시도만 반복한다.

    ✅ Fix: 경합이 잦은 이 상황엔 낙관적 락이 더 나쁘다. 그래서 처음부터 줄 세우는 비관적 락을 골랐다.

    🔧 도구:optimistic lock (version compare-and-set)

    // optimistic alt: retries forever under steady contention
    UPDATE counters SET seq = :next, version = version + 1
      WHERE name = 'seq' AND version = :expected;
    // rows=0 -> conflict -> re-read and retry (always collides here)
    🗣 영어로 말해

    Optimistic locking would just keep retrying here, because the two instances always collide.

    checking microphone…

6단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.

How it evolves — toy → production

The problem

Many instances ask for the next ID at the same millisecond. Without coordination they read the same value and collide (duplicate PK). The format is custom (per-plant prefix + daily reset), so a bare sequence may not fit.

  1. 1synchronized countertoy — single JVM only
    synchronized long next() { return ++seq; }   // one JVM only

    Use when: Single process. Never across servers.

    Cost / limit: Two servers each have their own seq → duplicates.

  2. 2Pessimistic row lock (FOR UPDATE)correct, holds lock across round-trip
    SELECT current_seq FROM lot_seq
      WHERE target_date = :today FOR UPDATE;   -- lock the row now
    -- app: n = current_seq + 1
    UPDATE lot_seq SET current_seq = :n WHERE target_date = :today;
    COMMIT;

    Use when: Custom format, low volume (a few/sec), you need it correct now.

    Cost / limit: Holds the lock across the app round-trip (read → +1 → write). Waiters block.

  3. 3Atomic UPDATEshorter lock
    UPDATE lot_seq SET current_seq = current_seq + 1
      WHERE target_date = :today
      RETURNING current_seq INTO :n;          -- DB does the +1, lock held only to commit

    Use when: Same as above but cut the lock duration — no app round-trip under lock.

    Cost / limit: Still serializes on the row; under heavy contention it's a hot row.

  4. 4Hi/Lo block allocationcut lock frequency 1/N
    // grab a BLOCK of N ids under one lock, then hand them out from memory
    long lo = allocateBlock(N);   // one atomic UPDATE: seq += N
    long hi = lo + N;
    // next() returns lo, lo+1, ... hi-1 with NO DB call until the block runs out

    Use when: High throughput; you can tolerate gaps in the IDs (a restart skips the rest of a block).

    Cost / limit: IDs aren't gap-free; a crash wastes the rest of the block.

  5. 5DB sequence / Snowflakeatomic, non-blocking / no coordination
    -- if the format allows: a native sequence is atomic + non-blocking
    CREATE SEQUENCE lot_seq;            SELECT lot_seq.NEXTVAL FROM dual;
    
    // Snowflake-style: 41-bit time | 10-bit nodeId | 12-bit per-ms counter
    //   -> globally unique with ZERO cross-server coordination

    Use when: Sequence: when no custom format needed. Snowflake: distributed, very high rate, no DB hop.

    Cost / limit: Sequence can't do custom formats. Snowflake needs unique node ids + clock-skew handling.

Know the ladder — start at the simplest that meets the need, and say what you'd reach for next and why.

Explain it in English

Say it out loud in your own words — hit these terms — then check against the gold answer.

Say it in your own words — but hit these: concurrency · pessimistic lock · for update

checking microphone…