트랜잭션 가이드라인
이 문서는 응용프로그램 코드에서 데이터베이스 트랜잭션의 사용 예제를 몇 가지 제공합니다.
자세한 내용은 트랜잭션에 대한 PostgreSQL 문서를 확인하세요.
데이터베이스 분해 및 샤딩
Pods 그룹은 기존 GitLab 데이터베이스를 분할하고 데이터베이스 테이블 중 일부를 다른 데이터베이스 서버로 이동하기로 계획하고 있습니다.
우리는 먼저 ci_*
관련 데이터베이스 테이블을 분해합니다. 현재의 응용프로그램 개발 경험을 유지하기 위해 정확한
데이터 접근 및 데이터 수정 방법을 보장하기 위해 코드베이스에 도구 및 정적 분석기를 추가합니다. 올바른 형식을 사용하여 데이터베이스 트랜잭션을 정의함으로써,
미래에 중요한 리팩터링 작업을 줄일 수 있습니다.
트랜잭션 블록
ActiveRecord
라이브러리는 데이터베이스 문을 편리하게 트랜잭션으로 그룹화하는 방법을 제공합니다:
issue = Issue.find(10)
project = issue.project
ApplicationRecord.transaction do
issue.update!(title: '수정된 제목')
project.update!(last_update_at: Time.now)
end
이 트랜잭션은 두 개의 데이터베이스 테이블을 포함합니다. 오류가 발생하는 경우 각 UPDATE
문은 이전과 일관된 상태로 롤백됩니다.
참고:
ActiveRecord::Base
클래스를 참조하지 말고, 대신 ApplicationRecord
를 사용하세요.
트랜잭션 및 데이터베이스 잠금
트랜잭션 블록이 열리면 데이터베이스는 필요한 리소스에 대한 잠금을 획득하려고 시도합니다. 잠금의 유형은 실제 데이터베이스 문에 따라 달라집니다.
다음 코드가 두 개의 다른 프로세스에서 동시에 실행되는 동시 업데이트 시나리오를 고려해 보십시오:
issue = Issue.find(10)
project = issue.project
ApplicationRecord.transaction do
issue.update!(title: '수정된 제목')
project.update!(last_update_at: Time.now)
end
데이터베이스는 참조된 issue
와 project
레코드에 FOR UPDATE
잠금을 획득하려고 합니다. 우리의 경우, 이러한 잠금에 대해 두 개의 경쟁하는 트랜잭션이 있으며,
그 중 하나만이 성공적으로 그것들을 획득합니다. 다른 트랜잭션은 첫 번째 트랜잭션이 완료될 때까지 잠금 대기열에서 기다려야 합니다. 이 시점에서 두 번째 트랜잭션의 실행이 차단됩니다.
트랜잭션 속도
잠금 경합을 방지하고 안정적인 응용프로그램 성능을 유지하기 위해 트랜잭션 블록은 가능한 빨리 완료되어야 합니다. 트랜잭션이 잠금을 획득하면, 트랜잭션이 완료될 때까지 해당 잠금을 보유합니다.
응용프로그램 성능 외에도 장기간 실행되는 트랜잭션은 데이터베이스 마이그레이션을 차단하여 응용프로그램 업그레이드 프로세스에도 영향을 줄 수 있습니다.
위험한 예: 제3자 API 호출
다음 예를 고려해 보십시오:
member = Member.find(5)
Member.transaction do
member.update!(notification_email_sent: true)
member.send_notification_email
end
여기서는 send_notification_email
메서드가 성공한 경우에만 notification_email_sent
열이 업데이트되도록 보장합니다. send_notification_email
메서드는
이메일 전송 서비스로 네트워크 요청을 실행합니다. 기본 인프라가 타임아웃을 지정하지 않거나 네트워크 호출이 너무 많은 시간이 걸린다면, 데이터베이스 트랜잭션이
열려 있는 상태로 유지됩니다.
이상적으로 트랜잭션은 데이터베이스 문만 포함해야 합니다.
transaction
블록에서 다음을 수행하지 마세요:
- Sidekiq 작업을 트리거하는 것과 같은 외부 네트워크 요청.
- 이메일 전송.
- HTTP API 호출.
- 다른 연결을 사용하여 데이터베이스 문 실행.
- 파일 시스템 작업.
- 긴 CPU 집약적 계산.
-
sleep(n)
호출하기.
명시적 모델 참조
트랜잭션이 동일한 데이터베이스 테이블 레코드를 수정하는 경우, Model.transaction
블록을 사용하는 것이 좋습니다:
build_1 = Ci::Build.find(1)
build_2 = Ci::Build.find(2)
Ci::Build.transaction do
build_1.touch
build_2.touch
end
위의 트랜잭션은 transaction
블록 내의 모델과 동일한 데이터베이스 연결을 사용합니다. 다중 데이터베이스 환경에서는 다음 예제가 위험할 수 있습니다:
# `ci_builds` 테이블은 다른 데이터베이스에 있음
class Ci::Build < CiDatabase
end
build_1 = Ci::Build.find(1)
build_2 = Ci::Build.find(2)
ApplicationRecord.transaction do
build_1.touch
build_2.touch
end
ApplicationRecord
클래스는 Ci::Build
레코드보다 다른 데이터베이스 연결을 사용합니다.
트랜잭션 블록의 두 문은 트랜잭션의 일부가 아니며, 문제가 발생할 경우 롤백되지 않습니다. 이는 제3자 호출처럼 작용합니다.