eShopOnContainers 看微服务④:Catalog Service
服务简介
Catalog service(目录服务)维护着所有产品信息,包括库存、价格。所以该微服务的核心业务为:
- 产品信息的维护
- 库存的更新
- 价格的维护
架构模式
先看代码结构(下图)。
主要依赖:
1、HealthCheck 健康检查
2、WebHost
3、Entity Framework
4、Autofac
5、BuildingBlocks文件夹下的EventBus,RabbitMq
其中前四项在Identity Service里面都已经用到了。事件总线EventBus是第一次用到,我们后面会详细讲到。
这个服务采用简单的数据驱动的CRUD微服务架构,来执行产品信息的创建、读取、更新和删除(CRUD)操作。
这种类型的服务在单个 ASP.NET Core Web API 项目中即可实现所有功能,该项目包括数据模型类、业务逻辑类及其数据访问类。
启动流程
我们还是从程序启动处开始看,跟identit.API差别不大。
Program.cs
Main函数,用到两个dbcontext。IntegrationEventLogContext负责记录事件日志,CatalogContext负责产品。最终数据库如下:
BuildWebHost函数:
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>()//使用startup类 .UseApplicationInsights() .UseHealthChecks("/hc")//健康检查 .UseContentRoot(Directory.GetCurrentDirectory()) .UseWebRoot("Pics")//Web 根 string ... ... ... ... 此处忽略N行代码 .Build();
这里有一个UseWebRoot,用来设置web根: webroot
。
默认情况下如果不指定,是 (Content Root Path)\wwwroot
,前提是该路径存在。如果这个路径不存在,则使用一个没有文件操作的提供器。
startup.cs
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddAppInsight(Configuration) .AddCustomMVC(Configuration)//健康检查,跨域等 .AddCustomDbContext(Configuration)//两个dbcontext的连接字符串等属性 .AddCustomOptions(Configuration) .AddIntegrationServices(Configuration) .AddEventBus(Configuration)//添加事件总线 .AddSwagger(); var container = new ContainerBuilder(); container.Populate(services); return new AutofacServiceProvider(container.Build()); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //Configure logs loggerFactory.AddAzureWebAppDiagnostics(); loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); var pathBase = Configuration["PATH_BASE"]; if (!string.IsNullOrEmpty(pathBase)) { loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); app.UsePathBase(pathBase); } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously app.UseCors("CorsPolicy");//跨域 app.UseMvcWithDefaultRoute();//路由 app.UseSwagger()//Swagger生成API文档 .UseSwaggerUI(c => { c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Catalog.API V1"); }); ConfigureEventBus(app);//配置事件总线 } protected virtual void ConfigureEventBus(IApplicationBuilder app) { var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); } }
这里有个app.UseCors("CorsPolicy"),实际上services.AddCors是写在AddCustomMVC扩展函数里面的。
services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); });
需要注意的是UseCors必须放在 UseMvc
之前,且策略名称(CorsPolicy)必须是已经定义的。
业务实体
该服务的主要实体是商品CatalogItem,其中包含两个辅助类CatalogBrand,CatalogType:
我们在看CatalogItem.cs的时候会发现两个函数AddStock,RemoveStock
对于实体这一块:
- 进行数据库字段映射时,主键都使用了
ForSqlServerUseSequenceHiLo
指定使用HI-LO
高低位序列进行主键生成。 - 使用NoTracking提升查询速度
在CatalogController
的构造方法中,明确指定以下代码来进行查询优化,这一点也是我们值得学习的地方。((DbContext)context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
- 在进行种子数据的预置时,使用了
Polly
开启了Retry机制。
private Policy CreatePolicy( ILogger<CatalogContextSeed> logger, string prefix,int retries = 3) { return Policy.Handle<SqlException>(). WaitAndRetryAsync( retryCount: retries, sleepDurationProvider: retry => TimeSpan.FromSeconds(5), onRetry: (exception, timeSpan, retry, ctx) => { logger.LogTrace($"[{prefix}] Exception {exception.GetType().Name} with message ${exception.Message} detected on attempt {retry} of {retries}"); } ); }
public async Task SeedAsync(CatalogContext context,IHostingEnvironment env,IOptions<CatalogSettings> settings,ILogger<CatalogContextSeed> logger) { var policy = CreatePolicy(logger, nameof(CatalogContextSeed)); await policy.ExecuteAsync(async () => { ... ... }); }
业务处理
运行起来后,我们浏览器输入 http://localhost:5101
展开catalog
对应CatalogController.cs代码
[Route("api/v1/[controller]")]//标记版本 [ApiController] public class CatalogController : ControllerBase { private readonly CatalogContext _catalogContext; private readonly CatalogSettings _settings; private readonly ICatalogIntegrationEventService _catalogIntegrationEventService; public CatalogController(CatalogContext context, IOptionsSnapshot<CatalogSettings> settings, ICatalogIntegrationEventService catalogIntegrationEventService) { _catalogContext = context ?? throw new ArgumentNullException(nameof(context)); _catalogIntegrationEventService = catalogIntegrationEventService ?? throw new ArgumentNullException(nameof(catalogIntegrationEventService)); _settings = settings.Value; ((DbContext)context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; }
... .... ...
}
通过构造函数注入了3个对象
context,settings,catalogIntegrationEventService
他们分别在startup类的AddCustomDbContext,AddCustomOptions,AddIntegrationServices中被注册到了DI容器。
再看具体的action
通过ProducesResponseType描述HttpStatusCode的返回状态,200,404
UpdateProduct函数
[Route("items")] [HttpPut] [ProducesResponseType((int)HttpStatusCode.NotFound)] [ProducesResponseType((int)HttpStatusCode.Created)] public async Task<IActionResult> UpdateProduct([FromBody]CatalogItem productToUpdate) { var catalogItem = await _catalogContext.CatalogItems .SingleOrDefaultAsync(i => i.Id == productToUpdate.Id); if (catalogItem == null) { return NotFound(new { Message = $"Item with id {productToUpdate.Id} not found." }); } var oldPrice = catalogItem.Price; var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price; // Update current product catalogItem = productToUpdate; _catalogContext.CatalogItems.Update(catalogItem); if (raiseProductPriceChangedEvent) // 保存产品数据,如果价格发生变化,通过事件总线发布集成事件 { //创建要通过事件总线发布的集成事件 var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); // 通过本地事务实现原始目录数据库操作和IntegrationEventLog之间的原子性 await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); // 通过事件总线发布,并将保存的事件标记为已发布 await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); } else //保存更新后的产品,因为产品价格没有变化。 { await _catalogContext.SaveChangesAsync(); } return CreatedAtAction(nameof(GetItemById), new { id = productToUpdate.Id }, null); }
使用 PublishThroughEventBusAsync函数,通过事件总线发布事件
public async Task PublishThroughEventBusAsync(IntegrationEvent evt)
{
try
{
await _eventLogService.MarkEventAsInProgressAsync(evt.Id);//标记事件,进行中
_eventBus.Publish(evt);
await _eventLogService.MarkEventAsPublishedAsync(evt.Id);//标记事件,发布
}
catch (Exception)
{
await _eventLogService.MarkEventAsFailedAsync(evt.Id);//标记事件,失败
}
}
通过这个事件,修改产品价格时,同步更新购物车中保存的产品信息的价格。
我们先看看eshop如何实现多个context之间的原子性的 _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent)的实现代码:
public async Task SaveEventAndCatalogContextChangesAsync(IntegrationEvent evt) { //在显式BeginTransaction()中使用多个dbcontext时,使用EF核心弹性策略: //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency await ResilientTransaction.New(_catalogContext) .ExecuteAsync(async () => { // 通过本地事务实现原始目录数据库操作和IntegrationEventLog之间的原子性 await _catalogContext.SaveChangesAsync(); await _eventLogService.SaveEventAsync(evt, _catalogContext.Database.CurrentTransaction.GetDbTransaction()); }); }
然后ResilientTransaction.cs
public class ResilientTransaction { private DbContext _context; private ResilientTransaction(DbContext context) => _context = context ?? throw new ArgumentNullException(nameof(context)); public static ResilientTransaction New (DbContext context) => new ResilientTransaction(context); public async Task ExecuteAsync(Func<Task> action) { //在显式BeginTransaction()中使用多个dbcontext时,使用EF核心弹性策略: //See: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency var strategy = _context.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { using (var transaction = _context.Database.BeginTransaction()) { await action(); transaction.Commit(); } }); } }
我们这样把Catalog service梳理了一遍,肯定有些地方还是不明不白的,我们后面会继续讨论。