• eShopOnContainers 看微服务④:Catalog Service


    服务简介

    Catalog service(目录服务)维护着所有产品信息,包括库存、价格。所以该微服务的核心业务为:

    1. 产品信息的维护
    2. 库存的更新
    3. 价格的维护

    架构模式

    先看代码结构(下图)。

    主要依赖:

    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>();
            }
        }
    View Code

    这里有个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

    对于实体这一块:

    1. 进行数据库字段映射时,主键都使用了ForSqlServerUseSequenceHiLo指定使用HI-LO高低位序列进行主键生成。
    2. 使用NoTracking提升查询速度
      CatalogController的构造方法中,明确指定以下代码来进行查询优化,这一点也是我们值得学习的地方。((DbContext)context).ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    3. 在进行种子数据的预置时,使用了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梳理了一遍,肯定有些地方还是不明不白的,我们后面会继续讨论。

  • 相关阅读:
    angular中transclude的理解
    node express中使用static的错误
    待研究———node中使用session时的id不断更改问题
    node 中mongoose使用validate和密码加密的问题
    exports 和 module.exports 的区别
    node.js开发错误——DeprecationWarning: Mongoose: mpromise
    Mongoose全面理解
    【js】JSON.stringify 语法实例讲解
    springboot+mybatis使用PageHelper分页
    连接mysql提示Establishing SSL connection without server's identity verification is not recommended错误
  • 原文地址:https://www.cnblogs.com/tianyamoon/p/10141221.html
Copyright © 2020-2023  润新知