这篇文章本来是继续分享 IdentityServer4 的相关文章,由于之前有博友问我关于 微服务 相关的问题,我就先跳过 IdentityServer4 的分享,进行 微服务 相关的技术学习和分享。 微服务 在我的分享目录里面是放到四月份开始系列文章分享的,这里就先穿越下,提前安排 微服务 应用的开篇文章 电商系统升级之微服务架构的应用 。 
本博客以及公众号 坚持以架构的思维来分享技术,不仅仅是单纯的分享怎么使用的Demo 。
 先来回顾下我上篇文章 Asp.Net Core 中IdentityServer4 授权中心之应用实战 中,电商架构由 单体式架构 拆分升级到 多网关架构 
  
 
  
 
 然而升级之后问题又来了,由于之前增加了代理商业务并且把 授权中心 和 支付网关 单独拆出来了,这使得公司的业务订单量翻了几十倍,这个时候整个电商系统达到了瓶颈,如果再不找解决方案系统又得宕机了。 
经过技术的调研及问题分析,导致这个瓶颈的问题主要有以下几个原因,只需要把下面问题解决就可以得到很大的性能提升
单表 、 单数据库 (未进行读写分离),以致于订单数据持续暴增。 为了一劳永逸的解决以上问题,经过技术的调研,决定对订单业务做如下升级改造:
ES 分布式缓存
经过升级后的架构图如下:
 
 
 架构图说明:
单体式架构 微服务
 微服务 的相关概念我就不多说了,以下就先简单概况下微服务带来的利和弊。 
Docker 、 Kubernets 、 Jenkins 、 Git 等。  说到 单体架构 拆分,那也不是随意拆分,是要有一定的原则,拆分的好是优势,拆分的不好是混乱。以下是我查阅资料以及我的经验总结出来的拆分原则 
领域驱动设计
 好了,到这里大家已经对微服务有了一定的理解,就不继续详细概述相关理念的东西,下面来直接撸代码,让大家熟悉微服务的应用。这里我使用 莫堇蕈 在github 上开源的 微服务框架 ,框架源代码地址 : https://github.com/overtly/core-grpc ( 我这里强烈推荐该框架,目前已经比较成熟的用于公司生产环境 ) 
core-grpc 微服务框架的优势: Jlion.NetCore.OrderService 订单微服务  我们用 vs2019 创建控制台应用程序 选择框架.Net Core 3.1 命名为 Jlion.NetCore.OrderService 后面简称 订单服务 ,创建完后我们通过 nuget 包引入 core-grpc 微服务框架,如下图: 
 
 
   目前 core-grpc 微服务框架,最新正式发布版本是 1.0.3 
 引用了 core-grpc 后我们还需要安装一个工具 VS RPC Menu ,这个工具也是大神免费提供的,图片如下: 
 
 
 由于微软官方下载比较慢,我这里共享到 百度网盘,百度网盘下载地址如下:
链接: https://pan.baidu.com/s/1twpmA4_aErrsg-m0ICmOPw 提取码: cshs
VS RPC Menu 工具说明如下:
用于客户端代码生成 支持Grpc 和Thrift
 我们再在 订单服务 项目 中创建 OrderRequest.proto 文件,这个是 Grpc 的语法,不了解该语法的同学可以 点击 gRPC 官方文档中文版_V1.0 进行学习,地址: http://doc.oschina.net/grpc?t=56831 
 OrderRequest.proto 代码如下: 
syntax = "proto3";
package Jlion.NetCore.OrderService.Service.Grpc;
//定义订单查找参数实体
message OrderSearchRequest{
    string OrderId = 1; //定义订单ID
    string Name = 2;
}
//定义订单实体
message OrderRepsonse{
    string OrderId = 1;
    string Name = 2;
    double Amount = 3;
    int32 Count = 4;
    string Time = 5;
}
//定义订单查找列表
message OrderSearchResponse{
    bool Success = 1;
    string ErrorMsg = 2;
    repeated OrderRepsonse Data = 3;
} 
 上面主要是定义了几个消息实体,
 我们再创建 JlionOrderService.proto ,代码如下: 
syntax = "proto3";
package Jlion.NetCore.OrderService.Service.Grpc;
import "OrderRequest.proto";
service JlionOrderService{
    rpc Order_Search(OrderSearchRequest) returns (OrderSearchResponse){} 
} 
  上面的代码中都可以看到最上面有 package Jlion.NetCore.OrderService.Service.Grpc 代码,这是声明包名也就是后面生成代码后的命名空间, 这个很重要 。 
 同时定义了 JlionOrderService 服务入口,并且定义了一个订单搜索的方法 Order_Search ,到这里我们已经完成了一小部分了。 
 再在 JlionOrderService.proto 文件里面右键 》选择Grpc代码生成》Grpc 代码 会自动生存微服务客户端代码 。 
生存工具中具有如下功能:
Jlion.NetCore.OrderService.Grpc 类库  把刚刚通过工具生成的 Grpc 客户端代码直接copy到 Jlion.NetCore.OrderService.Grpc 这个类库中(必须和上面Grpc 的代码声明的package 一致)以下简称 订单服务客户端 ,并且需要通过 Nuget 包添加 Overt.Core.Grpc 的依赖,代码结构如下: 
 
 
   Jlion.NetCore.OrderService.Grpc 类库已经构建完成,现在让 Jlion.NetCore.OrderService 服务引用 Jlion.NetCore.OrderService.Grpc 类库 
订单服务 中 实现自己的 IHostedService  创建 HostService 类,继承 IHostedService 代码如下: 
public class HostedService : IHostedService
{
    readonly ILogger _logger;
    readonly JlionOrderServiceBase _grpcServImpl;
    public HostedService(
        ILogger<HostedService> logger,
        JlionOrderServiceBase grpcService)
    {
        _logger = logger;
        _grpcServImpl = grpcService;
    }
    //服务的启动机相关配置
    public Task StartAsync(CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(() =>
        {
            var channelOptions = new List<ChannelOption>()
            {
                 new ChannelOption(ChannelOptions.MaxReceiveMessageLength, int.MaxValue),
                 new ChannelOption(ChannelOptions.MaxSendMessageLength, int.MaxValue),
            };
            GrpcServiceManager.Start(BindService(_grpcServImpl), channelOptions: channelOptions, whenException: (ex) =>
            {
                _logger.LogError(ex, $"{typeof(HostedService).Namespace.Replace(".", "")}开启失败");
                throw ex;
            });
            System.Console.WriteLine("服务已经启动");
            _logger.LogInformation($"{nameof(Jlion.NetCore.OrderService.Service).Replace(".", "")}开启成功");
        }, cancellationToken);
    }
    //服务的停止
    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.Factory.StartNew(() =>
        {
            GrpcServiceManager.Stop();
            _logger.LogInformation($"{typeof(HostedService).Namespace.Replace(".", "")}停止成功");
        }, cancellationToken);
    }
 } 
  上面代码主要是创建宿主机并且实现了 StartAsync 服务启动及 StopAsync 服务停止方法。 
 我们创建完 HostedServicce 代码再来创建之前定义的 Grpc 服务的方法实现类 JlionOrderServiceImpl ,代码如下: 
public partial class JlionOrderServiceImpl : JlionOrderServiceBase
{
    private readonly ILogger _logger;
    private readonly IServiceProvider _serviceProvider;
    public JlionOrderServiceImpl(ILogger<JlionOrderServiceImpl> logger, IServiceProvider provider)
    {
        _logger = logger;
        _serviceProvider = provider;
    }
    public override async Task<OrderSearchResponse> Order_Search(OrderSearchRequest request, ServerCallContext context)
    {
        //TODO 从底层ES中查找订单数据,
        //可以设计成DDD 方式来进行ES的操作,这里我就为了演示直接硬编码了
        var response = new OrderSearchResponse();
        try
        {
            response.Data.Add(new OrderRepsonse()
            {
                Amount = 100.00,
                Count = 10,
                Name = "订单名称测试",
                OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"),
                Time = DateTime.Now.ToString()
            });
            response.Data.Add(new OrderRepsonse()
            {
                Amount = 200.00,
                Count = 10,
                Name = "订单名称测试2",
                OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"),
                Time = DateTime.Now.ToString()
            });
            response.Data.Add(new OrderRepsonse()
            {
                Amount = 300.00,
                Count = 10,
                Name = "订单名称测试2",
                OrderId = DateTime.Now.ToString("yyyyMMddHHmmss"),
                Time = DateTime.Now.ToString()
            });
            response.Success = true;
        }
        catch (Exception ex)
        {
            response.ErrorMsg = ex.Message;
            _logger.LogWarning("异常");
        }
        return response;
    }
 } 
  再修改 Program 代码,并把 HostedService 和 JlionOrderServiceImpl 注入到容器中,代码如下: 
class Program
 {
    static void Main(string[] args)
    {
        var host = new HostBuilder()
           .UseConsoleLifetime() //使用控制台生命周期
           .ConfigureAppConfiguration((context, configuration) =>
           {
               configuration
               .AddJsonFile("appsettings.json", optional: true)
               .AddEnvironmentVariables();
           })
           .ConfigureLogging(logger =>
           {
               logger.AddFilter("Microsoft", LogLevel.Critical)
                     .AddFilter("System", LogLevel.Critical);
           })
           .ConfigureServices(ConfigureServices)
           .Build();
        AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
        {
            var logFactory = host.Services.GetService<ILoggerFactory>();
            var logger = logFactory.CreateLogger<Program>();
            logger.LogError(e.ExceptionObject as Exception, $"UnhandledException");
        };
        host.Run();
    }
    /// <summary>
    /// 通用DI注入
    /// </summary>
    /// <param name="context"></param>
    /// <param name="services"></param>
    private static void ConfigureServices(HostBuilderContext context, IServiceCollection services)
    {
        //HostedService 单例注入到DI 中
        services.AddSingleton<IHostedService, HostedService>();
        services.AddTransient<JlionOrderServiceBase, JlionOrderServiceImpl>();
    }
 } 
  到了这里简单的 微服务 已经编码完成,但是还缺少两个配置文件,我们创建 appsettings.json 配置文件和 consulsettings.json 服务注册发现的配置文件 
 consulsettings.json 配置文件如下: 
{
  "ConsulServer": {
    "Service": {
      "Address": "127.0.0.1:8500"// 你的Consul 服务注册及发现配置地址
    }
  }
} 
  上面的地址配置只是简单的例子,我这里假定我的 Consul 服务地址是 127.0.0.1:8500 等下服务启动是会通过这个地址进行注册。 
 appsettings.json 配置文件如下: 
{
  "GrpcServer": {
    "Service": {
      "Name": "JlionOrderService",
      "Port": 10001,
      "HostEnv": "serviceaddress",
      "Consul": {
        "Path": "dllconfigs/consulsettings.json"
      }
    }
  }
} 
  我这里服务监听了10001 端口,后面注册到 Consul 中也会看到该端口 
官方完整的配置文件如下:
{
  "GrpcServer": {
    "Service": {
      "Name": "OvertGrpcServiceApp",                    // 服务名称使用服务名称去除点:OvertGrpcServiceApp
      "Host": "service.g.lan",                          // 专用注册的域名 (可选)格式:ip[:port=default]
      "HostEnv": "serviceaddress",                      // 获取注册地址的环境变量名字(可选,优先)环境变量值格式:ip[:port=default]
      "Port": 10001,                                    // 端口自定义
      "Consul": {
        "Path": "dllconfigs/consulsettings.json"        // Consul路径
      }
    }
  }
} 
  好了, 订单服务 已经全部完成了, 订单服务 服务整体结构图如下: 
 
 
  好了,我们这里通过命令行启动下 JlionOrderService 服务,生产环境你们可以搭建在 Docker 容器里面 
  
 
 我们可以来看下我之前搭建好的 Consul 服务 ,打开管理界面,如图: 
 
 
   图片中可以发现刚刚启动的服务已经注册进去了,但是里面有一个健康检查未通过,主要是由于服务端不能访问我本地的 订单服务 ,所有健康检查不能通过。你可以在你本地搭建 Consul 服务用于测试。 
 我本地再来开启一个服务,配置中的的端口号由10001 改成10002,再查看下 Consul 的管理界面,如下图: 
 
 
  发现已经注册了两个服务,端口号分别是10001 和10002,这样可以通过自定化工具自动添加服务及下架服务,分布式服务也即完成。
 到这里 订单服务 的启动已经完全成功了,我们接下来是需要客户端也就是上面架构图中的 电商业务网关 或者 支付网关 等等要跟 订单服务 进行通讯了。 
 创建订单网关之前我先把上面的 订单服务客户端 类库发布到我的nuget包上,这里就不演示了。我发布的测试包名称 JlionOrderServiceDemo nuget官方可以搜索找到。你们也可以直接搜索添加到你们的Demo中进行测试。 
WebApi 取名为 
  Jlion.NetCore.OrderApiService 下面简称 
  订单网关服务 
   现在我把前面发布的 微服务 客户端依赖包 JlionOrderServiceDemo 添加到 订单网关服务 中,如下图: 
 
 
  现在在 订单网关服务 中添加 OrderController api控制器,代码如下: 
namespace Jlion.NetCore.OrderApiService.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly IGrpcClient<OrderService.Service.Grpc.JlionOrderService.JlionOrderServiceClient> _orderService;
        public OrderController (IGrpcClient<OrderService.Service.Grpc.JlionOrderService.JlionOrderServiceClient> orderService)
        {
            _orderService = orderService;
        }
     
        [HttpGet("getlist")]
        public async Task<List<OrderRepsonse>> GetList()
        {
            var respData =await _orderService.Client.Order_SearchAsync(new OrderService.Service.Grpc.OrderSearchRequest()
            {
                Name = "test",
                OrderId = "",
            });
            if ((respData?.Data?.Count ?? 0) <= 0)
            {
                return new List<OrderRepsonse>();
            }
            return respData.Data.ToList();
        }
    }
} 
  代码中通过构造函数注入 OrderService 并且提供了一个 GetList 的接口方法。接下来我们还需要把 OrderService.Service.Grpc.JlionOrderService 注入到容器中,代码如下: 
public void ConfigureServices(IServiceCollection services)
{
     services.AddControllers();
     //注册Grpc 客户端,具体可以查看源代码
     services.AddGrpcClient();
} 
  现在整个 订单网关服务 项目结构如下图: 
 
 
  项目中有两个最重要的配置 dllconfig//Jlion.NetCore.OrderService.Grpc.dll.json 和 consulsettings.json 他们分别是干什么的呢?我们先分别来看我本地这两个配置的内容 
 Jlion.NetCore.OrderService.Grpc.dll.json 配置如下: 
{
   "GrpcClient": {
       "Service": {
        "Name": "JlionOrderService",  // 服务名称与服务端保持一致
        "MaxRetry": 0,                // 最大可重试次数,默认不重试
        "Discovery": {
            "Consul": {              // Consul集群,集群优先原则
                "Path": "dllconfigs/consulsettings.json"
            },
            "EndPoints": [           // 单点模式
                {
                    "Host": "127.0.0.1",
                    "Port": 10001
             }]
            }
        }
    }
} 
  Jlion.NetCore.OrderService.Grpc.dll.json 配置主要是告诉 订单网关服务 和 订单服务 应该怎样进行通信,以及通信当中的一些参数配置。我为了测试,本地使用单点模式,不使用Consul模式 
 consulsettings.json 配置如下: 
{
  "ConsulServer": {
    "Service": {
      "Address": "127.0.0.1:8500"
    }
  }
} 
  有没有发现这个配置和之前服务端的配置一样,主要是告诉 订单网关服务 (客户端调用者)和 订单服务 服务端服务发现的集群地址,如果上面的配置是单点模式则这个配置不会起作用。 
 到这里 订单网关服务 (客户调用端)编码完成,我们开始启动它: 
 
 
  我这里固定5003端口,现在完美的启动了,我们访问下订单接口,看下是否成功。访问结果如下图:
 
 
 微服务完美的运行成功。
上面的构建微服务还是比较麻烦,官方提供了比较快速构建你需要的微服务方式,不需要写上面的那些代码,那些代码全部通过模板的方式进行构建你的微服务,有需要学习的可以到点击
微服务项目构建模板使用教程
教程地址: https://www.cnblogs.com/jlion/p/12494525.html
文章中的Demo 代码已经提交到github 上,代码地址: https://github.com/a312586670/IdentityServerDemo
微服务框架开源项目地址: https://github.com/overtly/core-grpc