什麼是事件溯源Event Sourcing?

banq發表於2016-11-04
本文比較全面以易懂方式闡述了什麼是事件溯源以及優缺點。

什麼是Event Sourcing?
“傳統”儲存應用程式變化資料的方式是儲存當前狀態。例如,您的應用程式可能是一個日曆,所以您想儲存約會。它可能看起來像這樣:

約會ID	開始時間	結束時間	標題
1      	09:30	10:45	會議
2      	11:15	11:30	飯局

<p class="indent">

這是一張看起來很熟悉的資料表。它可能是關聯式資料庫中的一個表,或一個鍵值儲存中的一組文件,甚至一個儲存在記憶體中的物件列表。重點是,它代表了系統的狀態,因為它是現在。讓我問你一個問題:這個系統是如何進入這個狀態的?

審計日誌audit log
要回答這個問題,您可以建立一個審計日誌。除了記錄建立或更新約會,在審計日誌中您還將記錄描述所發生的事件(一個“事件”)。對於一個單一的約會,它可能看起來像這樣:

Sequence	事件Event
1	              約會被建立:
                           時間:09:30 - 10:30
                           Title: 開會
2	              約會重新被安排: 09:30 - 10:45
3              	標題被改變: 飯局
<p class="indent">

相比如果你只有系統的當前狀態,這揭示了一個大量的資訊。在約會被建立一次,約會改期一次,它的標題被改變了。10:30是預約結束時間,但它後來被改期,之前的預約資訊會消失。

這種資訊對應用程式的使用者來說是非常有用的,因為他們可以看到發生了什麼事,他們應該責怪誰。開發人員也將從中獲利最多;當你看到為什麼導致某個特定的狀態時,尋找問題的原因就變得容易得多了。

單一真理之源
除了你的當前狀態以外存在一個審計日誌是有用的,但有一個問題:衝突。如果你發現自己在目前的狀態A,而你的審計日誌說是B,你現在有兩個問題。你不僅要診斷手頭的問題,而且你也必須找出其中的差異原因。

問題是,你有兩個“真相”的來源,陳述你係統的狀態應該是什麼樣的。您的應用程式只能檢視到當前狀態,所以它沒有任何問題。你作為一個開發人員面對兩個來源的真相。因為他們衝突,也就都沒有真正值得信任,如同一個人戴兩隻表,如果時間不一樣,都無法信任了。

事件溯源
如果我們消除審計日誌,我們只有一個真相的來源,但我們失去了所有詳細的歷史資訊,這些資訊我們非常重視。如果我們消除了當前的狀態呢?

這是事件溯源的本質:它不是儲存系統的當前狀態,您只儲存導致該狀態的事件。為了獲得當前狀態,您可以在記憶體中“重播”這些事件。

當前狀態只是發生了事件的“短暫”表示。它不是恆久不變的。你可以改變你的應用程式的狀態,但你不能改變事件。他們已經成為不爭的事實。

重播
為了得到您的系統的當前狀態,您將必須重播所有的事件。所有的事件?嗯,不是真的都是。您當前的狀態通常被分為幾個邏輯的“物件”,這將是傳統上一個表中的行。如果您唯一標識每個物件,您只需要重播該物件的事件,以獲得該物件的狀態。

讓我們來看看一個非常簡單的例子,我們的日曆目前的狀態。

class Appointment  
{
    public Guid AppointmentId { get; }
    public DateTime StartTime { get; private set; }
    public DateTime EndTime { get; private set; }
    public string Title { get; private set; }
    public bool IsCanceled { get; private set; }
}
<p class="indent">


事件看上去如下:

class AppointmentCreated  
{
    public Guid AppointmentId { get; }
    public DateTimeOffset StartTime { get; }
    public DateTimeOffset EndTime { get; }
    public string Title { get; }
}

class AppointmentRescheduled  
{
    public DateTimeOffset StartTime { get; }
    public DateTimeOffset EndTime { get; }
}

class AppointmentRenamed  
{
    public string Title { get; }
}

class AppointmentCanceled  
{
}
<p class="indent">

AppointmentCanceled 事件沒有任何屬性。它的型別代表發生了一個有意義的事件。

從約會視角重播這些事件看起來像什麼呢?

void ReplayEvent(AppointmentCreated @event)  
{
    AppointmentId = @event.AppointmentId;
    StartTime = @event.StartTime;
    EndTime = @event.EndTime;
    Title = @event.Title;
}

void ReplayEvent(AppointmentRescheduled @event)  
{
    StartTime = @event.StartTime;
    EndTime = @event.EndTime;
}

void ReplayEvent(AppointmentRenamed @event)  
{
    Title = @event.Title;
}

void ReplayEvent(AppointmentCanceled @event)  
{
    IsCanceled = true;
}
<p class="indent">

你開始一個空白物件。然後為每個發生的事件簡單呼叫ReplayEvent,按照它們發生的順序。這是所有您需要重新建立當前狀態的步驟。

修改狀態
因為狀態是代表過去發生的事件,修改狀態就是透過新增事件實現。獲得當前狀態是物件的一種職責責任,它方便讓所有的業務邏輯放在一個地方,包括業務規則的知識,它是DDD中的哲學。

當前狀態的物件(或實體物件或領域物件,不管你怎麼稱呼它)也會對自己負責一致性;它是唯一的創造事件的角色,也負責從過去的事件重播決定是否允許或禁止某個操作:你的約會已經取消;你就不能再取消了。

class Appointment  
{
    public Appointment(
        Guid id,
        DateTimeOffset startTime,
        DateTimeOffset endTime,
        string title)
    {
        if(endTime < startTime)
            throw new EndTimeBeforeStartTimeException();

        AppendEvent(
            new AppointmentCreated(id, startTime, endTime, title));
    }

    public void Reschedule(
        DateTimeOffset startTime,
        DateTimeOffset endTime)
    {
        AppendEvent(
            new AppointmentRescheduled(startTime, endTime));
    }

    public void Cancel()
    {
        if (IsCanceled)
            throw new AppointmentAlreadyCanceledException();

        AppendEvent(
            new AppointmentCanceled());
    }
}
<p class="indent">

AppendEvent 是一個方法,該方法將為指定的唯一物件也就是約會Appointment代表的物件新增一個事件到儲存中。

在方法內完成引數的驗證。它使得一種方法最終負責資料的有效性和一致性。為簡潔起見,Reschedule 不驗證引數,但Cancel根據目前的狀態檢查操作是否允許。

當前,如果您試圖取消已被取消的約會,將引發異常。通常不會這樣。我們可以跳過檢查,並只是儲存事件。它不會改變已被取消的事實;當重播時,我們還將設定iscanceled屬性為true時。這裡的要點是:你不必總是需要維護和基於狀態驗證。有時它會忽略多餘的事件。

注意,所有的構造器和Reschedule或Cancel 的方法都是儲存事件。他們並沒有直接修改狀態。為什麼?我們已經有了一個基於事件修改狀態的方法:ReplayEvent方法。所以,除了儲存事件,AppendEvent也將立即重播的事件。

資料表
事件的規則特點:
1. 事件描述了已經發生的事情,因為我們不能改變過去,他們是不可變的。在他們持久後,事件不能改變。

2.擴充套件以前的規則:他們不能被刪除。

3.每一個事件都包含表示狀態變化所需的所有資訊,並能夠重播它。如果他們可以來自其他事件,他們不應該包括計算值。

4.事件的後設資料包含它的型別,當它發生時間

5.您不能查詢事件。它們只用於重放一個給定的時間內系統的狀態。

對於一個事件源物件,一個更改狀態的請求只會導致三個東西中的一個:
1.儲存事件;
2.丟擲一個例外(因為一個業務規則會被侵犯);
3.什麼都不做。

其他的結果,如直接修改物件的狀態或進行資料庫呼叫,都是非常令人沮喪的,因為它們是副作用。副作用通常不會被儲存的事件表示,這意味著你不能重播它們。也使測試更難。

好處
因為你只需要讀取和附加事件,儲存是非常容易的。你需要做的唯一的事情是新增一個事件和檢索事件列表。你可以儲存事件在任何地方:在一個表中,一個鍵值儲存,一個檔案目錄,一個電子郵件,或幾乎任何你可以想象的地方。由於事件是不可變的,他們是非常容易快取,這使您能夠獲得優異的效能。

您將不再需要顯式的事務,因為您只需要插入資料,而不需要更新或刪除它;您也只需要一個表、集合、set,或是您的資料儲存組資料。事務就像一把鎖,所以不需要事務是一種像無鎖程式碼:沒有死鎖和效能快得多。

您還得到一個免費的審計日誌,您可以在除錯時使用,看看到底發生了什麼,為什麼系統處於一個特定的狀態。

如果您確定實體類中的操作是無副作用得,它基本上生活在隔離中,類Class將有一個非常低的耦合度量。你已經從系統的其餘部分中分離出了業務邏輯。

如果你已經實現了這些分離,測試你的邏輯可以表示為“指定的這些事件發生了,當這個操作被請求,那麼這些事件是否被儲存?”或者,“那麼這個異常會被丟擲嗎?”

你不必處理建立在代數上的關聯式資料庫,因為你的物件不是。事件溯源迴避了物件關係阻抗匹配。

你的真理之源是一個資料庫中的一堆事件。在你儲存它們之後,你也可以透過一個訊息系統廣播到世界各地。它很容易讓其他應用程式知道在你的系統中發生了什麼。您不再需要到另一個系統儲存中查詢資料,然後複製以找出發生了什麼變化。所有您需要儲存的是:一個唯一物件識別符號的列表和他們的最新已知的版本號。

缺點
當然,每一個方法的缺點,事件溯源也不例外。最重要的實際缺點是:沒有簡單的方法來檢視您的應用程式的持久狀態是什麼,你不能透過查詢得知。只有當你只能看到執行時發生了什麼時。一個解決方案,是使用一個單獨的模型讀取發生在您的系統中的事件,由此需要CQRS

事件溯源是一個完全不同於大多數人都習慣的方法。您需要定義可以在您的系統中發生的每一個事件,所以新增新的功能可能會比過去習慣的慢。為了彌補這一差距,你的資料變得更珍貴。

事件溯源聽起來像是一個非常低效的機制來儲存狀態。它比當前狀態直接儲存需要更多的儲存空間。它還需要更多的處理時間,僅僅是因為需要檢索更多的資料才能到達當前狀態。因為你現在擁有幾乎無限量的儲存和處理能力,克服這些不足是很容易的。其中之一是使用快照。

快照
你可能會認為,當一個事件流包含數千或數萬的事件時,您的系統必須變得緩慢,因為每一個操作需要重播所有這些事件。嗯,不一定。

記住,唯一做回放事件是由於要改變有狀態。它應該是冪等;無論你重播事件一次或一百次,它應該有相同的結局。

我們重播以後,比如一萬事件,我們可以得到結果狀態的快照並儲存,以最後一個重播事件為標識。現在,當我們想要當前狀態時,我們只需載入快照和回放快照以後任何發生的事件,如果你已經正確建立了你的快照,為了重放所有的事件載入快照和重放新的事件應該是相同。

測試
前面提到過測試變得容易多了。讓我們更具體一點,展示如何測試一個事件溯源物件。

private AppointmentCreated CreateAppointmentCreatedEvent()  
{
    return new AppointmentCreated(
        appointmentId: Guid.NewGuid(),
        startTime: DateTimeOffset.Now.AddHours(3),
        endTime: DateTimeOffset.Now.AddHours(4),
        title: "Appointment");
}

private AppointmentRenamed CreateAppointmentRenamedEvent()  
{
    return new AppointmentRenamed(title: "Renamed appointment");
}

private Appointment CreateSut(IEnumerable<IEvent> events)  
{
    var sut = new Appointment();

    foreach (var @event in events)
        sut.ReplayEvent(@event);

    return sut;
}

<p class="indent">[Fact]
public void Reschedule_AppendsAppointmentRescheduled()  
{
    // GIVEN these events have happened
    var events = new IEvent[]
                 {
                     CreateAppointmentCreatedEvent(),
                     CreateAppointmentRenamedEvent()
                 };
    var sut = CreateSut(events);

    // WHEN we ask to reschedule
    var newStartTime = DateTimeOffset.Now.AddHours(5);
    var newEndTime = DateTimeOffset.Now.AddHours(6);
    sut.Reschedule(newStartTime, newEndTime);

    // THEN does the AppointmentRescheduledEvent get published?
    sut.AppendedEvents.ShouldAllBeEquivalentTo(
        new[]
        {
            new AppointmentRescheduled(newStartTime, newEndTime)
        },
        config => config.RespectingRuntimeTypes());
}
<p class="indent">

請注意,測試可以分為三個邏輯部分:給定的一些狀態,什麼時候一個動作執行,那麼我們可以觀察到這種行為嗎?這種測試正式的語言為:Cucumber 。

類比
事件溯源是非常強大的思維方式,但它不真的是新的。我們大多數人都是熟悉的系統中都存在它。一些例子:
1.您的銀行帳戶可能儲存使用事件溯源。當前賬戶餘額只是你所做的所有存款和取款的最終結果。

2.版本控制系統。每個提交或更改到檔案都是一個事件。如果您重播所有的事件,您將得到原始碼的當前狀態。

3.大多數大型RDBM關聯式資料庫內部使用事件溯源。簡單地說,只有三個事件:插入,更新和刪除。RDBMS儲存事件在事務日誌中並將其運用到資料表操作上。


Event Sourcing: Awesome, powerful & different:

相關文章