基本概念
事件溯源(Event Sourcing)是一種設計模式,它記錄並儲存了應用程式狀態變化的所有事件。
其核心思想是將系統中的每次狀態變化都視為一個事件,並將這些事件以時間順序的方式持久化儲存。
這樣,透過重放這些事件,我們可以重建系統在任何特定時間點的狀態。
每個事件通常都包含了描述狀態變化的必要資訊,以及發生狀態變化的原因和時間戳。
工作原理
工作原理方面,事件溯源主要依賴於兩個關鍵部分:事件生成和事件儲存。
當系統中發生狀態變化時,會生成一個或多個事件,這些事件隨後被儲存到事件儲存中。
事件儲存需要設計成高可用、高一致且可伸縮的,以支援大規模的系統操作。
之後,當需要重建系統狀態時,只需從事件儲存中按順序讀取事件,並依次應用這些事件到系統狀態即可。
使用場景
在Orleans7中,事件溯源主要應用在以下幾個場景:
-
分散式系統狀態同步:在分散式系統中,各個節點之間的狀態同步是一個重要問題。透過事件溯源,每個節點都可以記錄併傳送自己的狀態變化事件,其他節點則可以透過訂閱這些事件來同步自己的狀態。
-
歷史資料追蹤和審計:在某些業務場景下,需要追蹤系統的歷史操作記錄,以進行審計或分析。事件溯源提供了完整的操作歷史,可以方便地查詢和回放歷史事件。
-
容錯和恢復:當系統發生故障時,透過事件溯源可以方便地恢復到故障發生前的狀態,或者根據事件日誌進行故障排查。
優勢
事件溯源在Orleans7中帶來了以下優勢:
-
資料完整性和一致性:由於事件溯源記錄了所有狀態變化的歷史,因此可以確保資料的完整性和一致性。
-
靈活性和可擴充套件性:事件溯源的設計使得系統可以很容易地新增新的狀態變化事件,同時也支援大規模的系統擴充套件。
-
容錯和恢復能力:透過事件溯源,可以輕鬆地恢復到系統的任何歷史狀態,大大提高了系統的容錯和恢復能力。
-
清晰的業務邏輯:每個事件都代表了一個具體的業務操作,因此透過檢視事件日誌,可以清晰地瞭解系統的業務邏輯和操作流程。
總的來說,事件溯源是一種強大而靈活的設計模式,它在Orleans7中的應用為分散式系統帶來了諸多優勢。對於軟體開發者來說,理解和掌握事件溯源機制,將有助於構建更加健壯、可靠和可擴充套件的分散式系統。
示例
下面使用事件溯源,來跟蹤一個賬戶的變更記錄。
首先需要安裝必須的nuget包
<PackageReference Include="Microsoft.Orleans.EventSourcing" Version="8.0.0" /> <PackageReference Include="Microsoft.Orleans.Clustering.AdoNet" Version="8.0.0" /> <PackageReference Include="Microsoft.Orleans.Persistence.AdoNet" Version="8.0.0" /> <PackageReference Include="Microsoft.Orleans.Server" Version="8.0.0" />
然後設定Orleans,除了Orleans的常規設定外,還需要 siloHostBuilder.AddLogStorageBasedLogConsistencyProvider("LogStorage") 來設定LogConsistencyProvider
builder.Host.UseOrleans(static siloHostBuilder => { var invariant = "System.Data.SqlClient"; var connectionString = "Data Source=localhost\\SQLEXPRESS;Initial Catalog=orleanstest;User Id=sa;Password=12334;"; siloHostBuilder.AddLogStorageBasedLogConsistencyProvider("LogStorage"); // Use ADO.NET for clustering siloHostBuilder.UseAdoNetClustering(options => { options.Invariant = invariant; options.ConnectionString = connectionString; }).ConfigureLogging(logging => logging.AddConsole()); siloHostBuilder.Configure<ClusterOptions>(options => { options.ClusterId = "my-first-cluster"; options.ServiceId = "SampleApp"; }); // Use ADO.NET for persistence siloHostBuilder.AddAdoNetGrainStorage("GrainStorageForTest", options => { options.Invariant = invariant; options.ConnectionString = connectionString; //options.GrainStorageSerializer = new JsonGrainStorageSerializer() }); });
定義賬戶的儲存和提取事件類
// the classes below represent events/transactions on the account // all fields are user-defined (none have a special meaning), // so these can be any type of object you like, as long as they are serializable // (so they can be sent over the wire and persisted in a log). [Serializable] [GenerateSerializer] public abstract class Transaction { /// <summary> A unique identifier for this transaction </summary> [Id(0)] public Guid Guid { get; set; } /// <summary> A description for this transaction </summary> [Id(1)] public string Description { get; set; } /// <summary> time on which the request entered the system </summary> [Id(2)] public DateTime IssueTime { get; set; } } [Serializable] [GenerateSerializer] public class DepositTransaction : Transaction { [Id(0)] public uint DepositAmount { get; set; } } [Serializable] [GenerateSerializer] public class WithdrawalTransaction : Transaction { [Id(0)] public uint WithdrawalAmount { get; set; } }
再定義賬戶的Grain,其中有存錢,取錢,獲取餘額,與變更記錄操作
Grain類必須具有 LogConsistencyProviderAttribute 才能指定日誌一致性提供程式。 還需要 StorageProviderAttribute設定儲存。
/// <summary> /// An example of a journaled grain that models a bank account. /// /// Configured to use the default storage provider. /// Configured to use the LogStorage consistency provider. /// /// This provider persists all events, and allows us to retrieve them all. /// </summary> /// <summary> /// A grain that models a bank account /// </summary> public interface IAccountGrain : IGrainWithStringKey { Task<uint> Balance(); Task Deposit(uint amount, Guid guid, string desc); Task<bool> Withdraw(uint amount, Guid guid, string desc); Task<IReadOnlyList<Transaction>> GetTransactionLog(); } [StorageProvider(ProviderName = "GrainStorageForTest")] [LogConsistencyProvider(ProviderName = "LogStorage")] public class AccountGrain : JournaledGrain<AccountGrain.GrainState, Transaction>, IAccountGrain { /// <summary> /// The state of this grain is just the current balance. /// </summary> [Serializable] [Orleans.GenerateSerializer] public class GrainState { [Orleans.Id(0)] public uint Balance { get; set; } public void Apply(DepositTransaction d) { Balance = Balance + d.DepositAmount; } public void Apply(WithdrawalTransaction d) { if (d.WithdrawalAmount > Balance) throw new InvalidOperationException("we make sure this never happens"); Balance = Balance - d.WithdrawalAmount; } } public Task<uint> Balance() { return Task.FromResult(State.Balance); } public Task Deposit(uint amount, Guid guid, string description) { RaiseEvent(new DepositTransaction() { Guid = guid, IssueTime = DateTime.UtcNow, DepositAmount = amount, Description = description }); // we wait for storage ack return ConfirmEvents(); } public Task<bool> Withdraw(uint amount, Guid guid, string description) { // if the balance is too low, can't withdraw // reject it immediately if (State.Balance < amount) return Task.FromResult(false); // use a conditional event for withdrawal // (conditional events commit only if the version hasn't already changed in the meantime) // this is important so we can guarantee that we never overdraw // even if racing with other clusters, of in transient duplicate grain situations return RaiseConditionalEvent(new WithdrawalTransaction() { Guid = guid, IssueTime = DateTime.UtcNow, WithdrawalAmount = amount, Description = description }); } public Task<IReadOnlyList<Transaction>> GetTransactionLog() { return RetrieveConfirmedEvents(0, Version); } }
最後即可透過client生成grain,並獲取賬戶變動記錄
var palyer = client.GetGrain<IAccountGrain>("zhangsan"); await palyer.Deposit(1000, Guid.NewGuid(), "aaa"); var logs = await palyer.GetTransactionLog(); return Results.Ok(logs);