중요 참고 사항: 이제 Couchbase에서 다중 문서 ACID 트랜잭션을 사용할 수 있습니다. 참조: NoSQL 애플리케이션을 위한 ACID 트랜잭션 에서 자세한 정보를 확인하세요!

이 시리즈의 이전 게시물에서는 다중 문서 트랜잭션에 대해 다루지 않았습니다: ACID 속성 및 카우치베이스. 이 블로그 게시물에서는 Couchbase가 지원하는 ACID의 빌딩 블록에 대해 설명했습니다. 단일 문서가 있습니다. 이 블로그 게시물에서는 이러한 기반을 사용하여 다음과 같은 것을 구축하려고 합니다. 같은 원자적인 분산형 다중 문서 트랜잭션입니다.

면책 조항: 이 블로그 게시물의 코드는 프로덕션용으로 권장되지 않습니다. 이 예제는 다음과 같은 기능을 제거한 예제입니다. might 있는 그대로 유용하게 사용할 수 있지만 프로덕션에 사용할 준비가 되려면 더 다듬고 다듬어야 합니다. 이 글의 의도는 카우치베이스로 다중 문서 트랜잭션이 필요한 (드물겠지만) 상황에 대비해 어떤 것이 필요한지 알려드리기 위함입니다.

간략한 요약

1부에서는 실제로 Couchbase에서 단일 문서 수준에서 ACID 속성을 사용할 수 있다는 것을 살펴보았습니다. 문서가 비정규화된 방식으로 데이터를 함께 저장할 수 있는 사용 사례의 경우, 이는 적절합니다. 어떤 경우에는 단일 문서로의 비정규화만으로는 요구 사항을 충족하기에 충분하지 않습니다. 이러한 소수의 사용 사례의 경우 이 블로그 게시물의 예를 고려해 볼 수 있습니다.

주의 사항: 이 블로그 게시물은 시작 포인트가 될 수 있습니다. 사용 사례, 기술적 요구 사항, Couchbase Server의 기능 및 관심 있는 에지 케이스는 모두 다를 수 있습니다. 현재로서는 모든 경우에 적합한 접근 방식은 없습니다.

다중 문서 트랜잭션 예시

여기서는 코드를 단순하게 유지하기 위해 간단한 작업에 중점을 두겠습니다. 좀 더 고급의 경우에는 이 코드를 기반으로 일반화하여 적절하다고 생각되는 대로 조정할 수 있습니다.

게임을 개발 중이라고 가정해 봅시다. 이 게임에는 농장을 만들고 운영하는 것이 포함됩니다(미친 소리처럼 들리겠지만). 이 게임에서 닭이 몇 마리 있는 헛간이 있다고 가정해 보겠습니다. 친구에게도 몇 마리의 닭이 있는 헛간이 있습니다. 어느 시점에 여러분은 자신의 헛간에서 친구의 헛간으로 닭을 옮기고 싶을 수 있습니다.

이 경우 데이터 정규화는 도움이 되지 않을 것입니다. 왜냐하면:

  • 모든 헛간을 포함하는 단일 문서는 규모가 큰 게임에는 적합하지 않습니다.
  • 내 헛간 문서에 친구의 헛간 문서가 포함되는 것은 말이 안 됩니다(또는 그 반대의 경우도 마찬가지).
  • 나머지 게임 로직은 단일 문서 원자성에서 잘 작동합니다. 까다로운 것은 치킨 전송뿐입니다.

우선, 우리가 가진 것은 두 개의 '헛간' 문서(Grant Barn 및 Miller Barn)뿐입니다:

Initial barn documents

닭을 전송하는 데 사용할 이 방법을 "2단계 커밋"이라고 합니다. 총 6단계가 있습니다. 전체 소스 코드는 GitHub에서 확인할 수 있습니다..

모든 스크린샷을 찍고 코드 샘플을 작성하면서 닭은 축사가 아닌 닭장에 산다는 생각이 들었나요? 하지만 저와 함께 가보세요.

0) 거래 문서

첫 번째 단계는 트랜잭션 문서를 만드는 것입니다. 이 문서는 여러 문서로 이루어진 트랜잭션을 추적하고 상태 의 트랜잭션을 생성합니다. C#를 만들었습니다. Enum 를 트랜잭션에 사용되는 가능한 상태와 함께 설정합니다. Couchbase에 저장할 때는 숫자가 되지만 원하는 경우 문자열이나 다른 표현을 사용할 수 있습니다.

트랜잭션은 "초기" 상태로 시작됩니다. 이 트랜잭션을 살펴보면 "소스" 닭장, "대상" 닭장, 그리고 전송할 닭의 수가 있습니다.

데이터를 다시 자세히 살펴봅시다. 이제 세 개의 문서가 있습니다. 트랜잭션은 새 문서이고, 헛간 문서는 처음과 동일합니다.

Initial multi-document transactions document

1) 보류 중으로 전환

다음으로 트랜잭션 문서를 '보류 중' 상태로 설정해 보겠습니다. 트랜잭션의 '상태'가 중요한 이유는 나중에 살펴보겠습니다.

저는 여기서 약간의 속임수를 썼습니다. UpdateWithCas 함수를 사용해야 합니다. .NET에서 Cas 연산을 사용하여 문서를 업데이트하는 것은 다소 장황할 수 있기 때문에 이 작업을 많이 할 것입니다. 그래서 작은 도우미 함수를 만들었습니다:

이는 중요한 도우미 방법입니다. 이 방법은 낙관적 잠금 를 사용하여 문서를 업데이트할 수 있지만 재시도나 오류 처리는 수행하지 않습니다.

데이터로 돌아가 보겠습니다. 아직 3개의 문서가 있지만 트랜잭션 문서 '상태'가 업데이트되었습니다.

Multi-document Transaction pending

2) 문서 변경

다음으로, 실제로 축사 문서에 필요한 변형을 수행합니다. 소스 헛간에서 닭 한 마리를 빼고 대상 헛간에 닭 한 마리를 추가합니다. 동시에 이러한 헛간 문서에 트랜잭션 문서 ID를 '태그'하려고 합니다. 이것이 왜 중요한지 나중에 다시 설명하겠습니다. 또한 나중에 이러한 문서를 추가로 변경할 때 필요하기 때문에 이러한 돌연변이의 Cas 값도 저장하겠습니다.

이 시점에서 코드는 닭을 헛간 사이로 이동시켰습니다. 또한 헛간에 있는 트랜잭션 '태그'도 주목하세요.

Barns tagged with transaction

3) 커밋으로 전환

여기까지입니다. 변경이 완료되었으니 이제 트랜잭션을 "커밋됨"으로 표시할 차례입니다.

변경된 것은 트랜잭션의 '상태'뿐입니다.

Transaction committed

4) 트랜잭션 태그 제거

이제 다중 문서 트랜잭션이 "커밋된" 상태가 되었으므로 저장소는 더 이상 트랜잭션의 일부라는 사실을 알 필요가 없습니다. 저장소에서 이러한 "태그"를 제거하세요.

이제 헛간은 거래에서 자유로워졌습니다.

Multi-document Transactions tags removed

5) 거래 완료

마지막 단계는 트랜잭션 상태를 '완료'로 변경하는 것입니다.

여기까지 왔다면 다중 문서 트랜잭션이 완료된 것입니다. 전송 후 계사에는 정확한 수의 닭이 있습니다.

Transaction done

롤백: 문제가 발생하면 어떻게 하나요?

다중 문서 트랜잭션 중에 문제가 발생할 가능성은 전적으로 있습니다. 그게 바로 트랜잭션의 핵심입니다. 모든 작업이 성공할 수도 있고 실패할 수도 있습니다.

위의 1~5단계의 코드를 하나의 시도/캐치 블록 안에 넣었습니다. 도중에 예외가 발생할 수 있지만 두 가지 중요한 사항에 집중해 보겠습니다.

"보류 중" 예외 - 2단계 중간에 오류가 발생하면 어떻게 처리해야 하나요? 즉, 소스 헛간에서 닭 한 마리를 뺀 후 대상 헛간에 닭 한 마리를 추가하기 전입니다. 이 상황을 처리하지 않으면 닭이 에테르 속으로 사라지고 게임 플레이어는 닭 울음소리를 낼 것입니다!

트랜잭션 "커밋" 후 예외 - 트랜잭션의 상태가 "커밋됨"이지만 트랜잭션 태그가 더 이상 저장소에 없기 전에 오류가 발생합니다. 이 문제를 처리하지 않으면 다른 프로세스에서 여전히 트랜잭션 내부에 있는 것으로 보일 수 있습니다. 트랜잭션의 먼저 닭 전송은 성공하지만 더 이상 닭을 전송할 수 없습니다.

코드는 이러한 문제를 내부에서 처리할 수 있습니다. catch 블록으로 이동합니다. 트랜잭션의 '상태'(트랜잭션 '태그'와 함께)가 중요한 역할을 하는 곳입니다.

"보류 중" 예외

닭을 잃어버린 상황은 게이머를 화나게 할 수 있는 상황입니다. 목표는 잃어버린 닭을 모두 교체하고 축사를 거래 전 상태로 되돌리기 위한 것입니다.

바로 중간에 일어난다고 가정해 보겠습니다. 이 예제에서는 새 트랜잭션이 발생하여 Burrows 헛간(닭 12마리)에서 White 헛간(닭 13마리)으로 닭 1마리를 전송합니다.

Barns before rollback

중간에 오류가 발생했습니다. 소스 축사에는 닭 한 마리가 부족하지만 목적지 축사에는 닭이 들어가지 않았습니다.

Barns inconsistent and transaction

복구하는 3단계는 다음과 같습니다:

1) 거래 취소

트랜잭션 상태를 "취소"로 변경합니다. 나중에 "취소됨"으로 변경합니다.

지금까지 변경된 것은 거래 문서뿐입니다:

Transaction now cancelling

2) 변경 사항 되돌리기

다음으로, 헛간의 상태를 이전 상태로 되돌려야 합니다. 이 작업은 헛간에 트랜잭션 태그가 있는 경우에만 필요하다는 점에 유의하세요. 태그가 없으면 이미 트랜잭션 이전 상태인 것입니다. 태그가 있으면 제거하세요.

이제 헛간은 예전으로 돌아갔습니다.

Barns rolled back

3) 취소된 거래

마지막으로 할 일은 거래를 '취소됨'으로 설정하는 것입니다.

이제 거래가 '취소'됩니다.

Transaction cancelled

이렇게 하면 게임의 총 닭 수가 유지됩니다. 이 시점에서 롤백이 필요한 오류를 처리해야 합니다. 다시 시도하거나, 플레이어에게 알리거나, 오류를 기록하거나, 위의 모든 작업을 수행할 수 있습니다.

"커밋" 중 예외

다음으로, 헛간에 대한 변경이 완료되었지만 아직 트랜잭션 태그가 제거되지 않은 다른 경우를 살펴봅시다. 게임 로직이 이러한 태그를 신경 쓴다고 가정하면 향후 다중 문서 트랜잭션이 불가능할 수 있습니다.

이 상황도 똑같은 롤백 로직으로 처리합니다.

문제 및 에지 사례

이 간단한 예는 애플리케이션의 요령에 불과할 수 있지만 고려해야 할 엣지 사례는 매우 많습니다.

프로세스가 도중에 중단되면 어떻게 되나요? 즉, 코드가 코드의 catch 블록으로 이동합니다. 애플리케이션 시작 시 완료되지 않은 다중 문서 트랜잭션이 있는지 확인하고 거기서 복구를 수행해야 할 수도 있습니다. 또는 불완전한 다중 문서 트랜잭션을 찾는 다른 감시 프로세스가 필요할 수도 있습니다.

거래 중에 읽기가 발생하면 어떻게 하나요? 업데이트 사이에 헛간을 "가져온다"고 가정해 봅시다. 이는 '더티' 읽기가 되어 문제가 될 수 있습니다.

모든 것이 어떤 상태로 남아 있나요? 보류 중인 다중 문서 트랜잭션을 완료/롤백하는 것은 누구의 책임인가요?

동일한 문서가 동시에 두 개의 다중 문서 트랜잭션에 포함되면 어떻게 되나요? 이러한 일이 발생하지 않도록 로직을 구축해야 합니다.

샘플에는 롤백을 위한 모든 상태가 포함되어 있습니다. 하지만 더 많은 트랜잭션 유형을 원한다면(소를 전송하고 싶을 수도 있습니다)? 트랜잭션 유형 식별자가 필요하거나, 위의 예에서 사용된 '금액'을 추상화하고 대신 문서의 업데이트된 버전을 지정할 수 있도록 트랜잭션 코드를 일반화해야 합니다.

기타 엣지 사례. 클러스터에 트랜잭션 중간에 실패하는 노드가 있으면 어떻게 되나요? 원하는 잠금을 얻지 못하면 어떻게 되나요? 얼마나 오랫동안 계속 재시도해야 하나요? 실패한 트랜잭션(시간 초과)을 어떻게 식별할 수 있나요? 처리해야 할 엣지 케이스는 매우 많습니다. 프로덕션 환경에서 발생할 것으로 예상되는 모든 조건을 철저히 테스트해야 합니다. 그리고 결국에는 일종의 완화 전략을 고려해야 할 수도 있습니다. 문제를 감지하거나 버그를 발견하면 버그를 수정한 후 관련된 모든 당사자에게 무료 치킨을 제공할 수 있습니다.

기타 옵션

저희 엔지니어링 팀은 다음과 같은 실험을 해왔습니다. RAMP 클라이언트 측 트랜잭션. RAMP(읽기 원자 다중 파티션)는 다음에서 원자 가시성을 보장하는 방법입니다. 분산 데이터베이스. 자세한 내용은 다음을 확인하세요. 간편한 RAM 존 하다드 또는 RAMP 트랜잭션을 통한 확장 가능한 원자 가시성 작성자: 피터 베일리스

카우치베이스에서 가장 성숙한 예제는 다음과 같습니다. Java SDK를 사용하는 Graham 사람들. 이 라이브러리 역시 프로덕션에 사용할 수 있는 라이브러리가 아닙니다. 하지만 그레이엄은 클라이언트 측 다중 문서 트랜잭션으로 흥미로운 작업을 하고 있습니다. 계속 지켜봐 주세요!

또 다른 옵션은 오픈 소스입니다. NDescribe 라이브러리 이언 카트리지( 카우치베이스 커뮤니티 챔피언).

마지막으로 사가 패턴를 사용하면 마이크로 서비스 간의 다중 문서 트랜잭션에 특히 유용합니다.

결론

이 블로그 게시물에서는 분산 데이터베이스를 위한 일종의 원자적 다중 문서 트랜잭션을 생성하기 위해 Couchbase에서 사용할 수 있는 ACID 프리미티브를 사용하는 방법에 대해 설명했습니다. 이것은 여전히 ACID를 완전히 대체할 수는 없지만, 대부분의 최신 마이크로서비스 기반 애플리케이션에 필요한 것에는 충분합니다. 추가적인 트랜잭션 보장이 필요한 소수의 사용 사례를 위해 Couchbase는 계속해서 혁신을 거듭할 것입니다.

이 블로그 게시물을 검토하는 데 도움을 주신 마이크 골드스미스, 그레이엄 포플, 시바니 굽타에게 감사드립니다.

Couchbase와 같은 분산 데이터베이스의 이점을 활용하고 싶지만 여전히 다중 문서 트랜잭션에 대한 우려가 있으시다면 저희에게 연락주세요! 다음에서 질문하실 수 있습니다. 카우치베이스 포럼 또는 다음 연락처로 문의하실 수 있습니다. 트위터 @mgroves.

작성자

게시자 매튜 그로브스

Matthew D. Groves는 코딩을 좋아하는 사람입니다. C#, jQuery, PHP 등 무엇이든 풀 리퀘스트를 제출할 정도로 코딩을 좋아합니다. 90년대에 부모님의 피자 가게를 위해 QuickBASIC POS 앱을 만든 이후로 전문적으로 코딩을 해왔습니다. 현재 Couchbase의 선임 제품 마케팅 관리자로 일하고 있습니다. 여가 시간에는 가족과 함께 축구 경기를 관람하고 개발자 커뮤니티에 참여하며 시간을 보냅니다. 그는 .NET의 AOP, .NET의 프로 마이크로서비스, Pluralsight 저자, Microsoft MVP의 저자이기도 합니다.

댓글 하나

  1. 마이크로서비스의 강점 중 하나는 쓰기 중에 이벤트를 토픽으로 전송한 다음 보기와 같은 다른 항목에서 토픽의 이벤트를 사용하도록 하는 것입니다. 이벤트가 토픽으로 전송되도록 하기 위해 간단하면서도 효과적인 해결책 중 하나는 이벤트를 DB(예를 들어 EventsToBeSent라는 테이블에)에 집계 루트와 함께 쓰고, 이 테이블을 폴링하여 이벤트를 (순서대로) 토픽으로 푸시하는 워커(예: Kafka)를 만드는 것입니다.
    AR에 이벤트를 저장하는 것은 거의 불가능하기 때문에 이벤트를 단독으로 저장해야 한다는 것을 즉시 이해할 수 있습니다. 이를 달성하기 위해 카우치베이스를 사용하면 즉시 사용이 불가능하고, 접근 방식을 사용할 가능성도 극히 낮아 보입니다.
    또 다른 가능한 해결책은 AR 문서 내에 이벤트를 저장하고 폴러가 모든 AR을 폴링하도록 하는 것입니다. SELECT flat_array(ar.events) FROM bucket ar where type in ("AR1", "AR2", "AR3") WHERE count(ar.events) > 0 sort by ar.timeStamp. 그러나 이 접근 방식에는 세 가지 주요 문제가 있습니다. 1) Couchbase에서 쿼리가 일관성이 없고(강력한 일관성을 위한 트릭을 사용하지 않는 한), 2) 이 쿼리 자체의 성능이 오래되고 전투에서 검증된 RDBMS에 비해 떨어지며, 3) AR에서 이벤트를 제거하는 성능이 떨어집니다. RDBMS에서는 간단한 UPDATE EventsToBeSent e SET e.IsSent = true WHERE e.Id in (,,,)만 수행하면 됩니다.

댓글 남기기