事件總線解決了微服務間如何基於集成事件進行異步通訊的問題。然而只有事件總線正常運行,微服務之間基於事件的通訊才得以運轉。
而現實狀況是,總有這樣或那樣的問題,致使事件總線不穩定或不可用,好比:網絡中斷,系統斷電等等,這均可能致使微服務間的不一致性問題。
那如何解決事件總線故障致使的不一致問題呢?sql
發件箱模式數據庫
var oldPrice = item.Price; item.Price = product.Price; _context.CatalogItems.Update(item); var @event = new ProductPriceChangedIntegrationEvent(item.Id, item.Price, oldPrice); // Commit changes in original transaction await _context.SaveChangesAsync(); // Publish integration event to the event bus // (RabbitMQ or a service bus underneath) _eventBus.Publish(@event);
當產品價格更改後,代碼將數據提交給數據庫,而後發佈ProductPriceChangedIntegrationEvent
事件。
若是服務在數據庫更新後崩潰(奔潰發生在_context.SaveChangesAsync()
代碼執行以後,但又發生在集成事件成功發佈前),就會致使本地微服務價格已成功更新,但集成事件未發佈的問題。就會致使目錄微服務中定義的價格和顧客購物車中緩存的價格不一致。緩存
以上問題的關鍵在因而如何確保兩個獨立的操做的原子性。若是單從單體應用的角度來處理的話,咱們徹底是能夠將他們放到同一個事務中去保證。然而在微服務中,就違背了其高可用的基本要求。由於一旦事件總線處於癱瘓狀態,那麼整個目錄微服務就不可用了。這種強制經過事務保證的一致性,就引入了太多的問題依賴。網絡
若是從微服務的角度來看,每一個微服務負責各自的業務邏輯,對於目錄微服務來講,它的關注點是產品的更新是否成功。至於藉助事件總線經過異步事件實現微服務間的通訊,並非其關注點。這也就是關注點分離。換句話說,產品的更新不該該依賴外部狀態。在這裏,外部狀態就是事件總線的可用性。異步
你可能會說了,既然不容許經過強事務保證一致性,那麼如何解決一致性問題呢(好像繞了半天又回到了原點)?微服務
這裏就要引入強一致性和最終一致性的概念了。
強一致性:也就是事務一致性,將多個操做放到單一事務處理。要麼所有成功,要麼所有失敗。
最終一致性:經過將某些操做的執行延遲到稍後的時間來執行。若前面的操做執行成功,後續操做將延後執行。若前面的操做失敗,後續的操做就不會執行。
線程
到這裏,咱們實際要解決的問題就明確了:如何確保事件總線可以正確進行事件轉發?日誌
換句話說:事件總線掛了,可是事件消息不能丟失。只要事件消息不丟,後面咱們還有機會挽救(從新發布消息)。code
如何保證事件消息不丟失呢?固然是持久化了。blog
eShopOnContainers已經考慮了這一點,集成了事件日誌用於持久化。咱們直接來看類圖:
從類圖中看其實現邏輯也很簡單,主要是定義了一個IntegrationEventLogEntry
實體、EventStateEnum
事件狀態枚舉和IntegrationEventLogContext
EF上下文用於事件日誌持久化。暴露IIntegrationEventLogService
用於事件狀態的更新。
其餘微服務經過在啓動類中註冊IntegrationEventLogContext
便可完成事件日誌的集成。
services.AddDbContext<IntegrationEventLogContext>(options => { options.UseSqlServer(configuration["ConnectionString"], sqlServerOptionsAction: sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup) .GetTypeInfo().Assembly.GetName().Name); sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); }); });
使用EF進行數據庫遷移後,就會生成IntergrationEventLog
表。以下圖所示:
主要分兩步走:
第一步毋庸置疑,第二步發佈事件,咱們又有多種實現方式:
這裏很顯然第二種方式更爲穩妥。而eShopOnContainers出於簡單考慮,採用了第一種方案,具體代碼以下:
using (var transaction = _catalogContext.Database.BeginTransaction()) { _catalogContext.CatalogItems.Update(catalogItem); await _catalogContext.SaveChangesAsync(); // Save to EventLog only if product price changed if(raiseProductPriceChangedEvent) await _integrationEventLogService.SaveEventAsync(priceChangedEvent); transaction.Commit(); } // Publish the intergation event through the event bus _eventBus.Publish(priceChangedEvent); integrationEventLogService.MarkEventAsPublishedAsync( priceChangedEvent);
至此,eShopOnContainers確保事件總線可以正確轉發消息的解決方案闡述完畢。你可能會問,這對應的是引言中的哪種方案?都不是,你能夠看做其是基於事件日誌的簡化版的事件溯源。
經過持久化事件日誌來避免事件發佈失敗致使的一致性問題,是一種有效措施。然而消息從發送到接收再到正常消費的過程當中,每個環節均可能故障,因此僅僅在消息發送端使用事件日誌只是確保最終一致性的一小步。還有不少問題有待完善:
而這些問題就留給你們思考吧。