SQLAlchemy를 사용한 PostgreSQL의 안전한 업데이트 작업

PostgreSQL에서 기본 격리 모드를 사용하는 경우. 웹 응용 프로그램에서 데이터베이스를 업데이트하려고 할 때 ACID 트랜잭션은 경쟁 조건으로부터 사용자를 보호하지 않습니다. 둘 이상의 프로세스가 실행 중인 상태에서 아래 코드를 실행하는 경우. (웹 서버에 매우 일반적인 다중 프로세스)

from sqlalchemy import create_engine
from sqlalchemy.orm import Session
# setup
engine = create_engine(...)
session = Session(engine)
# read from the table Account
account = session.query(Account).get(1)
# modify the record, account is decimal
account.amount = account.amount + 100
session.commit()


왜 이것이 안전하지 않습니까? SELECT가 행 수준에서 잠금을 설정하지 않기 때문입니다. 행을 독점적으로 잠그는 업데이트가 발생할 때만 차단합니다( FOR UPDATE/FOR NO KEY UPDATE). 이 시나리오에서는 경쟁 조건이 발생할 수 있습니다.


거래 1(T1)
거래 2(T2)


시작

금액 선택
시작

금액 += 100
금액 선택

금액 업데이트
금액 += 100

저지르다


금액 업데이트

커밋SAME value as T1


T1은 T2가 읽기 전에 커밋하지 않으므로 T2는 T1과 동일한 값을 선택합니다. 결국 같은 일을 다시 저지릅니다.

따라서 기본 격리 모드에서 이러한 상황을 방지하는 방법은 다음과 같습니다. (읽기 커밋 모드)

해결 방법 1: 읽지 마십시오.



위의 스니펫은read-modify-write . 이를 방지하는 한 가지 방법은 열 값으로 직접 값을 읽고 변경하지 않는 것입니다. 주어진 읽기는 이 시나리오에서 꼭 필요한 것은 아닙니다.

# same session setting as above
session.query(Account).filter_by(id=1)\
     .update({"amount": Account.amount + 100})
session.commit()


해결 방법 2: 잠금 업데이트



복잡한 수정을 하고 싶고 먼저 읽기만 하면 되는 몇 가지 시나리오가 있습니다. 이 경우 데이터베이스에서 읽을 때 업데이트 잠금을 사용할 수 있습니다. SQLAlchemy에는 변경하려는 행을 with_for_update 잠금으로 잠그는 FOR UPDATE 메소드가 있습니다. FOR UPDATE 잠금이 자체 호환되지 않습니다. 다른 트랜잭션은 이 트랜잭션이 잠금을 해제할 때까지 기다려야 합니다.

# same session setting as above
# locks the row that id = 1
account = session.query(Account).filter_by(id=1)\
     .with_for_update().one()
account.amount = account.amount + 100
session.commit()  # save and release the lock


솔루션 3: 버전 추적(낙관적 잠금)



동일한 행에서 업데이트를 실행하는 다른 모든 프로세스를 롤백하려는 경우. 테이블에 버전 열을 추가하여 이 행의 업데이트를 추적할 수 있습니다. 일부 행에 버전 v가 있다고 가정해 보겠습니다. 행을 업데이트하려는 경우 버전 v와 함께 행을 검색하고 버전을 v + 1로 업데이트합니다.

update account set version = v + 1, ... where version = v ...;


이것은 우리 자신을 구현하기에는 상당히 성가신 일입니다. 다행히도 대부분의 ORM 라이브러리는 버전 추적을 지원합니다. SQLAlchemy에서는 매퍼에서 버전 열 이름을 설정할 수 있으므로 업데이트할 때 버전을 관리하고 일부 프로세스가 동일한 행에서 작동하면 예외가 발생합니다.

class Account(Base):
    __tablename__ = "account"
    ...
    version = Column(Integer, nullable=False)

    __mapper_args__ = {"version_id_col": version}



def version_tracking(change):
    try:
        account = session.query(Account).get(1)
        account.amount = account.amount + change
        print_account(account, change)
        session.commit()
    except StaleDataError:
        print("someone has changed the account, plz retry.")
        # some actions...



위에 제공된 모든 솔루션은 Postgres의 기본 격리 모델인 읽기 커밋 모드를 사용하고 있다고 가정합니다. 또한 대상 행을 선택할 때 집계를 사용하지 않는다고 가정합니다. 트랜잭션이 직렬로 작동하는 것처럼 작동하도록 더 엄격한 격리 모드를 사용하는 것과 같이 동일한 문제를 방지하는 몇 가지 방법이 더 있습니다. 그러나 이 기사에서는 이에 대해 다루지 않을 것입니다.

더 많은 정보를 위해서.



All Source Code
SQLAlchemy for update (kite)
SQLAlchemy Version Tracking
PSQL Transaction Isolation
PSQL Locking

좋은 웹페이지 즐겨찾기