[cqrs] RDBMS를 이벤트 소싱 스토리지로 사용

RDBMS (예 : SQL Server)를 사용하여 이벤트 소싱 데이터를 저장하는 경우 스키마는 어떻게 생겼을까 요?

나는 추상적 인 의미에서 몇 가지 변이를 보았지만 구체적인 것은 아니다.

예를 들어 “제품”엔터티가 있고 해당 제품에 대한 변경 사항은 가격, 비용 및 설명의 형태로 제공 될 수 있습니다. 내가 다음과 같은지에 대해 혼란 스럽습니다.

  1. 제품에 대한 모든 필드가 포함 된 “ProductEvent”테이블이 있습니다. 여기서 각 변경은 해당 테이블의 새 레코드를 의미하고 “누가, 무엇을, 어디서, 왜, 언제, 어떻게”(WWWWWH)를 적절하게 의미합니다. 비용, 가격 또는 설명이 변경되면 제품을 나타내는 완전히 새로운 행이 추가됩니다.
  2. 외래 키 관계를 사용하여 제품 테이블에 조인 된 별도의 테이블에 제품 비용, 가격 및 설명을 저장합니다. 이러한 속성이 변경되면 적절하게 WWWWWH로 새 행을 작성합니다.
  3. WWWWWH와 이벤트를 나타내는 직렬화 된 개체를 “ProductEvent”테이블에 저장합니다. 즉, 지정된 제품에 대한 응용 프로그램 상태를 다시 빌드하려면 이벤트 자체를 내 응용 프로그램 코드에서로드, 역 직렬화 및 재생해야합니다. .

특히 위의 옵션 2에 대해 걱정합니다. 극단적으로 생각해 보면 제품 테이블은 속성 당 거의 하나의 테이블이 될 것입니다. 여기서 주어진 제품에 대한 애플리케이션 상태를로드하려면 각 제품 이벤트 테이블에서 해당 제품에 대한 모든 이벤트를로드해야합니다. 이 테이블 폭발은 나에게 잘못된 냄새가 난다.

나는 “상황에 따라 다르다”고 확신하고 “정답”은 하나도 없지만 받아 들일 수있는 것과 완전히 받아 들일 수없는 것에 대한 느낌을 얻으려고 노력하고 있습니다. 또한 NoSQL이 여기에서 도움이 될 수 있음을 알고 있습니다. 여기서 이벤트는 집계 루트에 대해 저장 될 수 있습니다. 즉, 개체를 다시 빌드 할 이벤트를 가져 오기 위해 데이터베이스에 대한 단일 요청 만 수행 할 수 있습니다. 순간 그래서 나는 대안을 찾고 있습니다.



답변

이벤트 저장소는 이벤트의 특정 필드 또는 속성에 대해 알 필요가 없습니다. 그렇지 않으면 모델을 수정할 때마다 데이터베이스를 마이그레이션해야합니다 (좋은 구식 상태 기반 지속성에서와 마찬가지로). 따라서 옵션 1과 2는 전혀 권장하지 않습니다.

다음은 Ncqrs 에서 사용되는 스키마 입니다. 보시다시피 “Events”테이블은 관련 데이터를 CLOB (예 : JSON 또는 XML)로 저장합니다. 이는 옵션 3에 해당합니다 (일반 “이벤트”테이블이 하나만 필요하기 때문에 “ProductEvents”테이블이 없습니다. Ncqrs에서 집계 루트에 대한 매핑은 “EventSources”테이블을 통해 발생합니다. 여기서 각 EventSource는 실제 집계 루트.)

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint],

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL,
    Type [nvarchar](255) NOT NULL,
    Version [int] NOT NULL

Jonathan Oliver의 Event Store 구현의 SQL 지속성 메커니즘 은 기본적으로 BLOB 필드 “Payload”가있는 “커밋”이라는 하나의 테이블로 구성됩니다. 이것은 Ncqrs에서와 거의 동일하지만 이벤트의 속성을 바이너리 형식으로 직렬화한다는 점만 있습니다 (예를 들어 암호화 지원을 추가 함).

Greg Young 은 Greg의 웹 사이트에 광범위하게 문서화 된 유사한 접근 방식을 권장합니다 .

그의 프로토 타입 “이벤트”테이블의 스키마는 다음과 같습니다.

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]


답변

GitHub 프로젝트 CQRS.NET 에는 몇 가지 다른 기술로 EventStores를 수행 할 수있는 방법에 대한 몇 가지 구체적인 예가 있습니다. 글을 쓰는 시점에 Linq2SQL 을 사용하는 SQL 과 함께 사용할 SQL 스키마 가 있습니다. 하나는 MongoDB , 하나는 DocumentDB (Azure에있는 경우 CosmosDB) 및 EventStore를 사용하는 하나 (위에서 언급 한대로)입니다. 플랫 파일 스토리지와 매우 유사한 Table Storage 및 Blob Storage와 같은 Azure에는 더 많은 것이 있습니다.

여기서 요점은 모두 동일한 원칙 / 계약을 준수한다는 것입니다. 이들은 모두 단일 장소 / 컨테이너 / 테이블에 정보를 저장하고, 메타 데이터를 사용하여 한 이벤트를 다른 이벤트와 식별하고 전체 이벤트를 그대로 ‘그냥’저장합니다. 일부 경우에는 지원 기술에서 그대로 유지됩니다. 따라서 문서 데이터베이스, 관계형 데이터베이스 또는 플랫 파일을 선택하는지 여부에 따라 이벤트 저장소의 동일한 의도에 모두 도달하는 여러 가지 방법이 있습니다 (언제든지 마음이 바뀌고 마이그레이션하거나 지원해야하는 경우 유용합니다. 둘 이상의 스토리지 기술).

프로젝트 개발자로서 우리가 선택한 몇 가지 통찰력을 공유 할 수 있습니다.

첫째로 우리는 여러 가지 이유로 (정수 대신 고유 한 UUID / GUID를 사용하더라도) 전략적인 이유로 순차 ID가 발생하므로 ID가 키에 대해 충분히 고유하지 않았으므로 기본 ID 키 열을 데이터 / 개체 유형을 사용하여 실제 (응용 프로그램의 의미에서) 고유 한 키 여야합니다. 나는 어떤 사람들이 그것을 저장할 필요가 없다고 말하는 것을 알고 있지만 그것은 당신이 그린 필드인지 또는 기존 시스템과 공존 해야하는지에 달려 있습니다.

유지 관리상의 이유로 단일 컨테이너 / 테이블 / 컬렉션을 사용했지만 엔티티 / 객체별로 별도의 테이블을 사용했습니다. 실제로 응용 프로그램에 “CREATE”권한이 필요하거나 (일반적으로 좋은 생각이 아닙니다 … 일반적으로 항상 예외 / 제외가 있음) 새 엔티티 / 개체가 존재하거나 배포 될 때마다 새로운 저장 용기 / 테이블 / 수집을 만들어야했습니다. 로컬 개발에는 고통스럽고 느리고 프로덕션 배포에는 문제가 있음을 발견했습니다. 그렇지 않을 수도 있지만 그것은 우리의 실제 경험이었습니다.

기억해야 할 또 다른 사항은 액션 X가 발생하도록 요청하면 다양한 이벤트가 발생할 수 있으므로 명령 / 이벤트에 의해 생성 된 모든 이벤트 / 유용한 내용을 알 수 있습니다. 또한 다른 개체 유형에 걸쳐있을 수 있습니다. 예를 들어 장바구니에서 “구매”를 푸시하면 계정 및 창고 이벤트가 실행될 수 있습니다. 소비 애플리케이션은이 모든 것을 알고 싶어 할 수 있으므로 CorrelationId를 추가했습니다. 이는 소비자가 요청의 결과로 발생한 모든 이벤트를 요청할 수 있음을 의미합니다. 당신은스키마 있습니다.

특히 SQL의 경우 인덱스와 파티션이 적절하게 사용되지 않으면 성능이 실제로 병목 현상이된다는 것을 발견했습니다. 스냅 샷을 사용하는 경우 이벤트를 역순으로 스트리밍해야합니다. 우리는 몇 가지 다른 인덱스를 시도해 보았고 실제로 프로덕션 환경에서 실제 응용 프로그램을 디버깅하는 데 몇 가지 추가 인덱스가 필요하다는 것을 발견했습니다. 다시 당신은 스키마 .

다른 프로덕션 내 메타 데이터는 프로덕션 기반 조사 중에 유용했으며 타임 스탬프를 통해 이벤트가 지속되고 발생하는 순서에 대한 통찰력을 얻을 수있었습니다. 이는 방대한 양의 이벤트를 발생시킨 특히 이벤트 중심 시스템에 대한 지원을 제공하여 네트워크 및 네트워크를 통한 시스템 배포와 같은 성능에 대한 정보를 제공했습니다.


답변

Datomic을 살펴보고 싶을 것입니다.

Datomic은 유연한 시간 기반 사실 의 데이터베이스입니다. 탄력적 인 확장 성과 ACID 트랜잭션을 통해 쿼리 및 조인을 지원하는 .

여기에 자세한 답변을 썼습니다

여기서 Datomic의 디자인을 설명하는 Stuart Halloway의 강연을 볼 수 있습니다.

Datomic은 사실을 적시에 저장하므로 이벤트 소싱 사용 사례 등에 사용할 수 있습니다.


답변

도메인 모델이 발전함에 따라 솔루션 (1 & 2)이 매우 빠르게 문제가 될 수 있다고 생각합니다. 새 필드가 생성되고 일부는 의미가 변경되며 일부는 더 이상 사용되지 않을 수 있습니다. 결국 테이블에는 수십 개의 nullable 필드가 있으며 이벤트로드가 엉망이됩니다.

또한 이벤트 저장소는 쓰기에만 사용해야하며 집계 속성이 아닌 이벤트를로드하기 위해 쿼리 만해야합니다. 그것들은 별개의 것입니다 (즉, CQRS의 본질입니다).

해결 방법 3 사람들이 일반적으로하는 일,이를 달성하는 데는 여러 가지 방법이 있습니다.

예를 들어 EventFlow CQRS 를 SQL Server와 함께 사용하면 다음 스키마가있는 테이블이 생성됩니다.

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED
(
    [GlobalSequenceNumber] ASC
)

어디:

  • GlobalSequenceNumber : 간단한 전역 식별로, 프로젝션 (readmodel)을 만들 때 누락 된 이벤트를 정렬하거나 식별하는 데 사용할 수 있습니다.
  • BatchId : 원자 적으로 삽입 된 이벤트 그룹 식별 (TBH, 이것이 왜 유용한 지 알 수 없음)
  • AggregateId : 집계의 식별
  • 데이터 : 직렬화 된 이벤트
  • 메타 데이터 : 이벤트의 기타 유용한 정보 (예 : 역 직렬화에 사용되는 이벤트 유형, 타임 스탬프, 명령의 발신자 ID 등)
  • AggregateSequenceNumber : 동일한 집합 내의 시퀀스 번호 (순서대로 쓰기가 발생할 수없는 경우 유용하므로 낙관적 동시성을 위해이 필드를 사용)

그러나 처음부터 생성하는 경우 YAGNI 원칙을 따르고 사용 사례에 필요한 최소한의 필드로 생성하는 것이 좋습니다.


답변

가능한 힌트는 디자인에 이어 “천천히 변경되는 치수”(유형 = 2)가 다음을 다루는 데 도움이됩니다.

  • 발생하는 이벤트 순서 (대리 키를 통해)
  • 각 상태의 내구성 (유효 기간-유효 기간)

왼쪽 접기 기능도 구현할 수 있어야하지만 향후 쿼리 복잡성을 고려해야합니다.


답변

나는 이것이 늦은 대답이라고 생각하지만 처리량 요구 사항이 높지 않으면 RDBMS를 이벤트 소싱 저장소로 사용하는 것이 완전히 가능하다는 점을 지적하고 싶습니다. 설명하기 위해 작성한 이벤트 소싱 원장의 예를 보여 드리겠습니다.

https://github.com/andrewkkchan/client-ledger-service
위는 이벤트 소싱 원장 웹 서비스입니다.
https://github.com/andrewkkchan/client-ledger-core-db
위의 내용은 RDBMS를 사용하여 상태를 계산하므로 트랜잭션 지원과 같은 RDBMS와 함께 제공되는 모든 이점을 누릴 수 있습니다.
https://github.com/andrewkkchan/client-ledger-core-memory
그리고 버스트를 처리하기 위해 메모리에서 처리 할 다른 소비자가 있습니다.

RDBMS는 특히 삽입이 항상 추가 될 때 삽입 속도가 느리기 때문에 위의 실제 이벤트 저장소가 여전히 Kafka에 있다고 주장 할 수 있습니다.

이 질문에 대해 이미 제공된 아주 좋은 이론적 답변과는 별도로 코드가 그림을 제공하는 데 도움이되기를 바랍니다.


답변