最近,一个工作了一个月的同事离职了,所做的东西怼了过来。一看代码,惨不忍睹,一个方法六七百行,啥也不说了吧,实在没法儿说。介绍下业务场景吧,一个公共操作A,业务中各个地方都会做A操作,正常人正常思维应该是把A操作提取出来封装,其他地方调用,可这哥们儿偏偏不这么干,代码到处复制。仔细分析了整个业务之后,发现是一个典型的事件/消息驱动型,或者叫发布/订阅型的业务逻辑。鉴于系统是单体的,所以想到利用进程内发布/订阅的解决方案。记得很久之前,做WPF时候,用过Prism的EventAggregator(是不是暴露年龄了。。。),那玩意儿不知道现在还在不在,支不支持core,目前流行的是MediatR,跟core的集成也好,于是决定采用MediatR。
2.Demo代码
Startup服务注册:
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:
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事件消息。
服务2代码:
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:
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:
[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代码调整如下: