这篇文章本来是继续分享 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