>  기사  >  백엔드 개발  >  경쟁 조건 처리: 실제 예

경쟁 조건 처리: 실제 예

PHPz
PHPz원래의
2024-07-18 14:47:221009검색

당신의 경력에서 슈뢰딩거의 고양이 문제, 때로는 효과가 있고 때로는 효과가 없는 상황에 직면하게 될 것입니다. 경쟁 조건은 이러한 과제 중 하나입니다(예, 딱 하나입니다!).

이 블로그 게시물 전반에 걸쳐 실제 사례를 제시하고 문제를 재현하는 방법을 시연하며 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 저장소를 확인하세요. 요약하면 다음이 필요합니다.

  1. 서버 시작: Yarn nx는 병원 교대 근무를 제공합니다.
  2. k6 테스트를 실행하여 경쟁 조건 시나리오 재현: Yarn nx 테스트 병원 교대

테스트는 두 명의 의사를 동시에 호출하여 서로 다른 접근 방식으로 끝점에 도달하려고 시도합니다. 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이 지원하는 두 가지 옵션을 살펴보겠습니다. 다른 데이터베이스 관리 시스템에서도 유사한 솔루션을 찾을 수 있습니다.

직렬화 가능한 트랜잭션 격리

직렬화 가능한 스냅샷 격리는 애플리케이션에서 나타나는 쓰기 왜곡과 같은 이상 현상을 자동으로 감지하고 방지합니다.

트랜잭션 격리에 대한 이론을 자세히 다루지는 않겠지만 이는 많은 인기 있는 데이터베이스 관리 시스템에서 공통적으로 사용되는 주제입니다. 트랜잭션 격리에 관한 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
    updateWithSerializedIsolation 함수에서 전체 예제를 찾을 수 있습니다.
권고 잠금

비즈니스 규칙을 시행하는 또 다른 방법은 특정 근무조에 대해 리소스를 명시적으로 잠그는 것입니다.

트랜잭션 수준에서 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;
  • You can find the complete example in the function updateWithAdvisoryLock.

Conclusion

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!


Dealing with Race Conditions: A Practical Example iamseki / dev-to

Implementations of dev.to blog posts

위 내용은 경쟁 조건 처리: 실제 예의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.