一、背景前端
最近,一個工做了一個月的同事離職了,所作的東西懟了過來。一看代碼,慘不忍睹,一個方法六七百行,啥也不說了吧,實在無法兒說。介紹下業務場景吧,一個公共操做A,業務中各個地方都會作A操做,正常人正常思惟應該是把A操做提取出來封裝,其餘地方調用,可這哥們兒恰恰不這麼幹,代碼處處複製。仔細分析了整個業務以後,發現是一個典型的事件/消息驅動型,或者叫發佈/訂閱型的業務邏輯。鑑於系統是單體的,因此想到利用進程內發佈/訂閱的解決方案。記得好久以前,作WPF時候,用過Prism的EventAggregator(是否是暴露年齡了。。。),那玩意兒不知道如今還在不在,支不支持core,目前流行的是MediatR,跟core的集成也好,因而決定採用MediatR。後端
2.Demo代碼async
Startup服務註冊:ide
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddScoped<IService1, Service1>(); services.AddScoped<IService2, Service2>(); services.AddScoped<IContext, Context>(); services.AddMediatR(typeof(SomeEventHandler).Assembly); }
服務1:ui
public class Service1 : IService1 { private readonly ILogger _logger; private readonly IMediator _mediator; private readonly IContext _context; private readonly IService2 _service2; public Service1(ILogger<Service1> logger, IMediator mediator, IContext context) { _logger = logger; _mediator = mediator; _context = context; //_service2 = service2; } public async Task Method() { _context.CurrentUser = "test"; //await _service2.Method(); //_service2.Method(); await _mediator.Publish(new SomeEvent()); //_mediator.Publish(new SomeEvent()); await Task.CompletedTask; } }
能夠看到,在服務1的method方法中,發佈了SomeEvent事件消息。spa
服務2代碼:3d
public class Service2 : IService2 { private readonly ILogger _logger; private readonly IContext _context; public Service2(ILogger<Service2> logger, IContext context) { _logger = logger; _context = context; } public async Task Method() { _logger.LogDebug("當前用戶:{0}", _context.CurrentUser); await Task.Delay(5000); //_logger.LogDebug("當前用戶:{0}", _context.CurrentUser); _logger.LogDebug("Service2 Method at :{0}", DateTime.Now); } }
解釋下,爲啥服務2 Method方法中,要等待5秒,由於實際項目中,有這麼一個操做,把一個壓縮程序包傳遞到遠端,而後在遠端代碼操做IIS建立站點,這玩意兒很是耗時,大概要1分多鐘,這裏我用5s模擬,意思意思。這個5s相當重要,待會兒會詳述。日誌
再看事件訂閱Handler:code
public class SomeEventHandler : INotificationHandler<SomeEvent>, IDisposable { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly IService2 _service2; public SomeEventHandler(ILogger<SomeEventHandler> logger, IServiceProvider serviceProvider, IService2 service2) { _logger = logger; _serviceProvider = serviceProvider; _service2 = service2; } public void Dispose() { _logger.LogDebug("Handler disposed at :{0}", DateTime.Now); } public async Task Handle(SomeEvent notification, CancellationToken cancellationToken) { await _service2.Method(); //using (var scope = _serviceProvider.CreateScope()) //{ // var service2 = scope.ServiceProvider.GetService<IService2>(); // await service2.Method(); //} } }
而後,咱們的入口Action:orm
[HttpGet("test")] public async Task<ActionResult<string>> Test() { StringBuilder sb = new StringBuilder(); sb.AppendFormat("開始時間:{0}", DateTime.Now); sb.AppendLine(); await _service1.Method(); sb.AppendFormat("結束時間:{0}", DateTime.Now); sb.AppendLine(); return sb.ToString(); }
至此,Demo要乾的事情,脈絡應該很清晰了:控制器接收HTTP請求,而後調用Service1的Method,service1的Method又發佈消息,消息處理器接收到消息,調用Service2的Method完成後續操做。咱們運行起來看下:
http請求開始到結束,耗時5s,看似沒問題。咱們看系統輸出日誌:
Service2的Method方法也確實被訂閱執行了。
3.問題
上述一切的一切,看似沒問題。運行成功沒?成功了。對不對?好像也對。有沒問題?大大的問題!HTTP從開始到結束,要耗時5s,實際項目中,那是一分鐘,這整整一分鐘,你要前端掛起等待麼一直?理論上,這種耗時的後端操做,合理作法是HTTP迅速響應前端,並返給前端業務ID,前端根據此業務ID長輪詢後端查詢操做結果狀態,直至此操做完成,決不能一直卡死的,不然交互效果不說,超過必定時間,HTTP請求會直接超時的!這就必須動刀子了,將Service2操做後臺任務化且不等待。Service1的Method代碼調整以下:
public async Task Method() { _context.CurrentUser = "test"; //await _service2.Method(); //_service2.Method(); //await _mediator.Publish(new SomeEvent()); _mediator.Publish(new SomeEvent()); await Task.CompletedTask; }
見註釋先後,改進地方只有一處,發佈事件代碼去掉了await,這樣系統發佈事件以後,便不會等待Service2而是繼續運行並馬上響應HTTP請求。好,咱們再來運行看下效果:
咱們看到,系統當即響應了HTTP請求(22:40:15),5s以後,Service2才執行完成(22:40:20)。看似又沒問題了。那是否是真的沒問題呢?咱們注意,Service1和Service2中,都注入了一個Context上下文對象,這個對象是我用來模擬一些Scope類型對象,例如DBContext的,代碼以下:
public class Context : IContext, IDisposable { private bool _isDisposed = false; private string _currentUser; public string CurrentUser { get { if (_isDisposed) { throw new Exception("Context disposed"); } return _currentUser; } set { if (_isDisposed) { throw new Exception("Context disposed"); } _currentUser = value; } } public void Dispose() { _isDisposed = true; } }
裏邊就一個屬性,當前上下文用戶,並實現了Dispose模式,而且當前上下文被釋放時,對該上下文對象任何操做將引起異常。從上文的Service1及Service2截圖中,咱們看到了,兩個服務均注入了這個context對象,Service1設置,Service2中獲取。如今咱們將Service2的Method方法稍做調整,以下:
public async Task Method() { //_logger.LogDebug("當前用戶:{0}", _context.CurrentUser); await Task.Delay(5000); _logger.LogDebug("當前用戶:{0}", _context.CurrentUser); _logger.LogDebug("Service2 Method at :{0}", DateTime.Now); }
調整隻有一處,就是獲取當前上下文用戶的操做,從5s延時以前,放到了5s延時以後。咱們再來看看效果:
http請求上看,貌似沒問題,當即響應了,是吧。咱們再看看程序日誌輸出:
WFT!Service2 Method沒成功執行,給了我一個異常。咱們看看這個異常:
Context dispose異常,就是說上下文這時候已經被釋放掉,對它任何操做都無效並引起異常。很容易想到,這裏就是爲了模擬DBContext這種一般爲Scope類型的對象生命週期,這種吊毛它就這樣。爲啥會釋放?由於HTTP請求結束那會兒,core運行時就會Dispose相應scope類型對象(注意,釋放,不必定是銷燬,具體銷燬時間不肯定)。那麼,怎麼解決?若是對基於DI生命週期比較熟悉,就會知道,這兒應該基於HTTP 的Scope以外,單獨起一個Scope了,兩個scope互補影響,HTTP對應的scope結束,另外的照常運行。咱們將Handler處調整以下:
public async Task Handle(SomeEvent notification, CancellationToken cancellationToken) { //await _service2.Method(); using (var scope = _serviceProvider.CreateScope()) { var service2 = scope.ServiceProvider.GetService<IService2>(); await service2.Method(); } }
無非就是Handle中單獨起了一個Scope。咱們再看運行效果:
OK,HTTP請求23:02:58響應,Service2 Method 23:03:03執行完成。至此,問題纔算獲得解決。
順便提一下,你們注意看截圖,當前用戶null,由於scope以後,原來的設置過CurrentUser的context已經釋放掉了,新開的scope中注入的context是另外的,因此沒任何信息。這裏你可能會問了,那我確實須要傳遞上下文怎麼辦?答案是,訂閱事件,本文中SomeEvent未定義任何信息,若是你須要傳遞,作對應調整便可,比較簡單,也不是重點,不作贅述。
四、總結
感受,沒什麼好總結的。紮實,細心,實踐,沒什麼解決不了的。