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.
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대다. 둘이 거의 동시에 그 칸을 읽으면, 둘 다 똑같은 숫자(예: 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…
왜 자바 락(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…
핵심은 '읽고 → +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…
해결: 그 번호표 '한 줄'을 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…
비관적 락 대신 낙관적 락(버전 비교)을 쓸 수도 있다. 락을 안 잡고, 쓸 때 '내가 읽은 버전 그대로냐?' 확인하고, 다르면 다시 시도한다.
⚖️ 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단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.
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.
synchronized long next() { return ++seq; } // one JVM onlyUse when: Single process. Never across servers.
Cost / limit: Two servers each have their own seq → duplicates.
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.
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.
// 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.
-- 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.
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…