당신의 경력에서 슈뢰딩거의 고양이 문제, 때로는 효과가 있고 때로는 효과가 없는 상황에 직면하게 될 것입니다. 경쟁 조건은 이러한 과제 중 하나입니다(예, 딱 하나입니다!).
이 블로그 게시물 전반에 걸쳐 실제 사례를 제시하고 문제를 재현하는 방법을 시연하며 PostgreSQL의 직렬화 가능 트랜잭션 격리 및 권고 잠금을 사용하여 경합 상태를 처리하기 위한 전략을 논의하겠습니다.
"데이터 집약적 애플리케이션 설계", 7장 - 트랜잭션 "약한 격리 수준"에서 영감을 얻음
실제 사례가 포함된 Github 저장소
병원 의사들의 당직 근무를 관리하는 애플리케이션입니다. 경쟁 조건 문제에 초점을 맞추기 위해 시나리오를 단순화해 보겠습니다. 우리 앱은 다음 단일 테이블을 중심으로 해결합니다.
CREATE TABLE shifts ( id SERIAL PRIMARY KEY, doctor_name TEXT NOT NULL, shift_id INTEGER NOT NULL, on_call BOOLEAN NOT NULL DEFAULT FALSE );
우리에게는 중요한 비즈니스 규칙이 있습니다:
짐작하셨겠지만 단순한 API를 구현하면 경쟁 조건 시나리오가 발생할 수 있습니다. 다음과 같은 가상 상황을 고려해보세요.
Jack과 John은 같은 근무 시간에 병원에 당직 중입니다. 거의 동시에 그들은 휴가를 요청하기로 결정했습니다. 하나는 성공했지만 다른 하나는 교대근무 중인 의사 수에 대한 오래된 정보에 의존합니다. 그 결과 두 사람 모두 업무 규칙을 어기고 담당 의사 없이 특정 교대 근무를 떠나게 되었습니다.
John --BEGIN------doctors on call: 2-------leave on call-----COMMIT--------> (t) \ \ \ \ \ \ \ \ Database ------------------------------------------------------------------> (t) / / / / / / / / Jack ------BEGIN------doctors on call: 2-----leave on call----COMMIT-------> (t)
이 애플리케이션은 Golang에서 구현된 간단한 API입니다. 이 경쟁 조건 시나리오를 재현하기 위해 스크립트를 실행하고 실행하는 방법에 대한 지침은 GitHub 저장소를 확인하세요. 요약하면 다음이 필요합니다.
테스트는 두 명의 의사를 동시에 호출하여 서로 다른 접근 방식으로 끝점에 도달하려고 시도합니다. shiftId=1은 자문 잠금을 사용하고, shiftId=2은 직렬화 가능한 트랜잭션 격리이며 shiftId=3은 동시성 제어가 없는 순진한 구현입니다.
k6 결과는 비즈니스 규칙을 위반한 ShiftId를 나타내는 사용자 정의 측정항목을 출력합니다.
✓ at least one doctor on call for shiftId=1 ✓ at least one doctor on call for shiftId=2 ✗ at least one doctor on call for shiftId=3 ↳ 36% — ✓ 123 / ✗ 217
Yarn, Go, K6, Docker와 같은 도구가 필요하거나 DevBox를 사용하여 저장소 종속성을 더 쉽게 설정할 수 있습니다.경쟁 상황 해결
직렬화 가능한 트랜잭션 격리
트랜잭션 격리에 대한 이론을 자세히 다루지는 않겠지만 이는 많은 인기 있는 데이터베이스 관리 시스템에서 공통적으로 사용되는 주제입니다. 트랜잭션 격리에 관한 PostgreSQL 공식 문서에서 이와 같은 스냅샷 격리를 검색하면 좋은 자료를 찾을 수 있습니다. 또한 몇 년 전에 이 솔루션을 제안한 논문이 있습니다. Talk가 저렴하니 코드를 살펴보겠습니다:
먼저 트랜잭션을 시작하고 격리 수준을 직렬화 가능으로 설정합니다.
// Init transaction with serializable isolation level tx, err := db.BeginTxx(c.Request().Context(), &sql.TxOptions{ Isolation: sql.LevelSerializable, })
CREATE OR REPLACE FUNCTION update_on_call_status_with_serializable_isolation(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN) RETURNS VOID AS $$ DECLARE on_call_count INT; BEGIN -- Check the current number of doctors on call for this shift SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE; IF on_call_to_update = FALSE AND on_call_count = 1 THEN RAISE EXCEPTION '[SerializableIsolation] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update; ELSE UPDATE shifts s SET on_call = on_call_to_update WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update; END IF; END; $$ LANGUAGE plpgsql;
ERROR: could not serialize access due to read/write dependencies among transactions
트랜잭션 수준에서 Advisory Lock을 사용하여 이를 달성할 수 있습니다. 이 유형의 잠금은 애플리케이션에 의해 완전히 제어됩니다. 자세한 내용은 여기에서 확인하실 수 있습니다.
세션 수준과 트랜잭션 수준 모두에서 잠금을 적용할 수 있다는 점에 유의하는 것이 중요합니다. 여기에서 사용 가능한 다양한 기능을 탐색할 수 있습니다. 우리의 경우 pg_try_advisory_xact_lock(key bigint) → boolean을 사용하여 커밋 또는 롤백 후 자동으로 잠금을 해제합니다.
BEGIN; -- Attempt to acquire advisory lock and handle failure with EXCEPTION IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update; END IF; -- Perform necessary operations -- Commit will automatically release the lock COMMIT;
-- Function to Manage On Call Status with Advisory Locks, automatic release when the trx commits CREATE OR REPLACE FUNCTION update_on_call_status_with_advisory_lock(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN) RETURNS VOID AS $$ DECLARE on_call_count INT; BEGIN -- Attempt to acquire advisory lock and handle failure with NOTICE IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update; END IF; -- Check the current number of doctors on call for this shift SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE; IF on_call_to_update = FALSE AND on_call_count = 1 THEN RAISE EXCEPTION '[AdvisoryLock] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update; ELSE UPDATE shifts s SET on_call = on_call_to_update WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update; END IF; END; $$ LANGUAGE plpgsql;
Dealing with race conditions, like the write skew scenario we talked about, can be pretty tricky. There's a ton of research and different ways to solve these problems, so definitely check out some papers and articles if you're curious.
These issues can pop up in real-life situations, like when multiple people try to book the same seat at an event or buy the same spot in a theater. They tend to appear randomly and can be hard to figure out, especially if it's your first time dealing with them.
When you run into race conditions, it's important to look into what solution works best for your specific situation. I might do a benchmark in the future to compare different approaches and give you more insights.
I hope this post has been helpful. Remember, there are tools out there to help with these problems, and you're not alone in facing them!
위 내용은 경쟁 조건 처리: 실제 예의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!