Net Core 重要的技術點
1、中間件概念
Asp.Net Core作為控制台應用程序啟動,在Program的Main方法是入口,通過調用CreateWebHostBuilder創建WebHost的,WebHost會利用WebHostBuilder提供的服務器和中間件構建一個請求處理管道;這就是ASP.NET Core框架的核心(一個服務器和若干中間件構成的管道)。
那么中間件就應用程序管道里面的一個組件,也是AOP的一種實現,用來攔截請求進行其他處理和響應,每一個組件都可以對管道中的請求進行攔截,也可以選擇是否將請求傳遞給下一個中間件。
中間件通過RequestDelegat進行構建和處理,最后一個管道或者中斷管道的中間件被稱為終端中間件。
HttPContext表示Http請求上下文,可以獲取請求信息
是處理Http請求和響應的組件(代碼段,一段處理邏輯),每個組件
- 選擇是否將請求傳遞給管道中的下一個組件。
- 可以在調用管道中的下一個組件之前和之后執行一些邏輯。
這樣的機制使得HTTP請求能夠很好的被層層處理和控制,並且層次清晰處理起來甚是方便。
最后一個管道或者中斷管道的中間件叫終端中間件;
請求委托(Request delegates)用於構建請求管道,處理每個HTTP請求
管道就是http請求抵達服務器到響應結果返回的中間的一系列的處理過程
2、中間件常用的方法
中間件中定義了Run、Use、Map、MapWhen幾種方法,我們下面一一講解這幾種方法。
1、Run()
Run()方法中只有一個RequestDelegate委托類型的參數,沒有Next參數,所以Run()方法也叫終端中間件,不會將請求傳遞給下一個中間件,也就是發生了“短路”
// Run方法向應用程序的請求管道中添加一個RequestDelegate委托
// 放在管道最后面,終端中間件
app.Run(handler: async context =>
{
await context.Response.WriteAsync(text: "Hello World1\r\n");
});
app.Run(handler: async context =>
{
await context.Response.WriteAsync(text: "Hello World2\r\n");
});
2、Use()方法
Use方法的參數是一個Func委托,輸入參數是一個RequestDelegate類型的委托,返回參數也是一個RequestDelegate類型的委托,這里表示調用下一個中間件
Public delegate Task RequestDelegate(HttpContext context)
RequestDelegate是一個委托,有一個HttpContext類型的參數,HttPContext表示Http請求上下文,可以獲取請求信息,返回值是Task類型
// 向應用程序的請求管道中添加一個Func委托,這個委托其實就是所謂的中間件。
// context參數是HttpContext,表示HTTP請求的上下文對象
// next參數表示管道中的下一個中間件委托,如果不調用next,則會使管道短路
// 用Use可以將多個中間件鏈接在一起
app.Use(async (context, next) =>
{
await context.Response.WriteAsync(text: "hello Use1\r\n");
// 調用下一個委托
await next();
});
app.Use(async (context, next) =>
{
await context.Response.WriteAsync(text: "hello Use2\r\n");
// 調用下一個委托
await next();
});
3、自定義中間件
中間件遵循顯示依賴原則,並在其構造函數中暴露所有依賴項。中間件能夠利用UseMiddleware
ASP.NET Core約定中間件類必須包括以下內容:
- 具有類型為RequestDelegate參數的公共構造函數。
- 必須有名為Invoke或InvokeAsync的公共方法,此方法必須滿足兩個條件:方法返回類型是Task、方法的第一個參數必須是HttpContext類型。
我們自定義一個記錄IP的中間件,新建一個類RequestIPMiddleware,代碼如下:
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace MiddlewareDemo.Middleware
{
/// <summary>
/// 記錄IP地址的中間件
/// </summary>
public class RequestIPMiddleware
{
// 私有字段
private readonly RequestDelegate _next;
/// <summary>
/// 公共構造函數,參數是RequestDelegate類型
/// 通過構造函數進行注入,依賴注入服務會自動完成注入
/// </summary>
/// <param name="next"></param>
public RequestIPMiddleware(RequestDelegate next)
{
_next = next;
}
/// <summary>
/// Invoke方法
/// 返回值是Task,參數類型是HttpContext
/// </summary>
/// <param name="context">Http上下文</param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
await context.Response.WriteAsync($"User IP:{context.Connection.RemoteIpAddress.ToString()}\r\n");
// 調用管道中的下一個委托
await _next.Invoke(context);
}
}
}
然后創建一個擴展方法,對IApplicationBuilder進行擴展:
using Microsoft.AspNetCore.Builder;
namespace MiddlewareDemo.Middleware
{
public static class RequestIPExtensions
{
/// <summary>
/// 擴展方法,對IApplicationBuilder進行擴展
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseRequestIP(this IApplicationBuilder builder)
{
// UseMiddleware<T>
return builder.UseMiddleware<RequestIPMiddleware>();
}
}
}
最后在Startup類的Configure方法中使用自定義中間件:
// 使用自定義中間件
app.UseRequestIP();
4、中間件和過濾器的區別
中間件和過濾器都是一種AOP的思想,他們的功能類似,那么他們有什么區別呢?
- 過濾器更加貼合業務,它關注於應用程序本身,關注的是如何實現業務,比如對輸出結果進行格式化,對請求的ViewModel進行數據校驗,這時就肯定要使用過濾器了。過濾器是MVC的一部分,它可以攔截到你Action上下文的一些信息,而中間件是沒有這個能力的。可以認為過濾器是附加性的一種功能,它只是中間件附帶表現出來的特征。
- 中間件是管道模型里重要的組成部分,不可或缺,而過濾器可以沒有。
5、Asp.Net Core異常處理
- 使用開發人員異常頁面(The developer exception page)
- 配置HTTP錯誤代碼頁 Configuring status code pages
- 使用MVC過濾器 ExceptionFilter
- 自定義異常捕獲中間件 Middleware
1、使用開發人員異常頁面(The developer exception page)
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//判斷是否是開發環境
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
}
2、配置HTTP錯誤代碼頁 Configuring status code pages
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
//開發環境異常處理
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
//生產環境異常處理
app.UseExceptionHandler("/Home/Error");
}
app.UseStatusCodePages();//使用HTTP錯誤代碼頁
}
app.UseStatusCodePages支持多種擴展方法。其中一個方法接受一個lambda表達式:
app.UseStatusCodePages(async context =>
{
context.HttpContext.Response.ContentType = "text/plain";
await context.HttpContext.Response.WriteAsync(
"Status code page, status code: " +
context.HttpContext.Response.StatusCode);
});
還可以跳轉到指定頁面,並附加Response.StatusCode
app.UseStatusCodePagesWithReExecute("/Home/Error/{0}");
3、使用MVC過濾器
/// <summary>
/// 自定義全局異常過濾器
/// </summary>
public class GlobalExceptionFilter : IExceptionFilter
{
readonly ILoggerFactory _loggerFactory;//采用內置日志記錄
readonly IHostingEnvironment _env;//環境變量
public GlobalExceptionFilter(ILoggerFactory loggerFactory, IHostingEnvironment env)
{
_loggerFactory = loggerFactory;
_env = env;
}
public void OnException(ExceptionContext context)
{
var controller = context.ActionDescriptor;
ILog log = LogManager.GetLogger(Startup.Repository.Name, controller.ToString());//初始化Log4net日志
#region 記錄到內置日志
//var logger = _loggerFactory.CreateLogger(context.Exception.TargetSite.ReflectedType);
//logger.LogError(new EventId(context.Exception.HResult),
//context.Exception,
//context.Exception.Message);
#endregion
if (_env.IsDevelopment())
{
log.Error(context.Exception.ToString());
//var JsonMessage = new ErrorResponse("未知錯誤,請重試");
//JsonMessage.DeveloperMessage = context.Exception;
//context.Result = new ApplicationErrorResult(JsonMessage);
//context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
//context.ExceptionHandled = true;
}
else
{
log.Error(context.Exception.ToString());
context.ExceptionHandled = true;
context.Result=new RedirectResult("/home/Error");
}
}
public class ApplicationErrorResult : ObjectResult
{
public ApplicationErrorResult(object value) : base(value)
{
StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
public class ErrorResponse
{
public ErrorResponse(string msg)
{
Message = msg;
}
public string Message { get; set; }
public object DeveloperMessage { get; set; }
}
}
}
4、四自定義異常捕獲中間件 Middleware
/// <summary>
/// 自定義異常處理中間件
/// </summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
var statusCode = context.Response.StatusCode;
await HandleExceptionAsync(context, ex.ToString());
}
}
private Task HandleExceptionAsync(HttpContext context, string msg)
{
HandleExceptionHelper hannd = new HandleExceptionHelper();
hannd.log.Error(msg);//記錄到日志文件
return context.Response.WriteAsync("ERROR");
}
}
6、依賴注入
ASP.NET Core的核心是通過一個Server和若干注冊的Middleware構成的管道,不論是管道自身的構建,還是Server和Middleware自身的實現,以及構建在這個管道的應用,都需要相應的服務提供支持,ASP.NET Core自身提供了一個DI容器來實現針對服務的注冊和消費。
DI框架具有兩個核心的功能,即服務的注冊和提供,這兩個功能分別由對應的對象來承載, 它們分別是ServiceCollection和ServiceProvider
1、依賴注入的三種方式
在ASP.Net Core 依賴注入有三種:
- Transient :每次請求時都會創建,並且永遠不會被共享。
- Scoped : 在同一個Scope內只初始化一個實例 ,可以理解為( 每一個request級別只創建一個實例,同一個http request會在一個 scope內)
- Singleton :只會創建一個實例。該實例在需要它的所有組件之間共享。因此總是使用相同的實例。
DI容器跟蹤所有已解析的組件, 組件在其生命周期結束時被釋放和處理:
- 如果組件具有依賴關系,則它們也會自動釋放和處理。
- 如果組件實現IDisposable接口,則在組件釋放時自動調用Dispose方法。
重要的是要理解,如果將組件A注冊為單例,則它不能依賴於使用Scoped或Transient生命周期注冊的組件。更一般地說:
服務不能依賴於生命周期小於其自身的服務。
通常你希望將應用范圍的配置注冊為單例,數據庫訪問類,比如Entity Framework上下文被推薦以Scoped方式注入,以便可以重用連接。如果要並行運行的話,請記住Entity Framework上下文不能由兩個線程共享,如果需要,最好將上下文注冊為Transient,然后每個服務都獲得自己的上下文實例,並且可以並行運行。
建議的做法:
盡可能將您的服務注冊為瞬態服務。 因為設計瞬態服務很簡單。 您通常不用關心多線程和內存泄漏,並且您知道該服務的壽命很短。
1、請謹慎使用Scoped,因為如果您創建子服務作用域或從非Web應用程序使用這些服務,則可能會非常棘手。
2、謹慎使用singleton ,因為您需要處理多線程和潛在的內存泄漏問題。
3、在singleton 服務中不要依賴transient 或者scoped 服務,因為如果當一個singleton 服務注入transient服務,這個 transient服務就會變成一個singleton服務,並且如果transient服務不是為支持這種情況而設計的,則可能導致問題。 在這種情況下,ASP.NET Core的默認DI容器已經拋出異常。
2、DI在ASP.NET Core中的應用
1在Startup類中初始化
ASP.NET Core可以在Startup.cs的 ConfigureService中配置DI,大家看到 IServiceCollection這個參數應該就比較熟悉了。
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ILoginService<ApplicationUser>,
EFLoginService>();
services.AddMvc();
)
ASP.NET Core的一些組件已經提供了一些實例的綁定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的擴展方法。
public static IMvcBuilder AddMvc(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
var builder = services.AddMvcCore();
builder.AddApiExplorer();
builder.AddAuthorization();
AddDefaultFrameworkParts(builder.PartManager);
...
}
2 Controller中使用
一般可以通過構造函數或者屬性來實現注入,但是官方推薦是通過構造函數。這也是所謂的顯式依賴。
private ILoginService<ApplicationUser> _loginService;
public AccountController(
ILoginService<ApplicationUser> loginService)
{
_loginService = loginService;
}
我們只要在控制器的構造函數里面寫了這個參數,ServiceProvider就會幫我們注入進來。這一步是在Mvc初始化控制器的時候完成的,我們后面再介紹到Mvc的時候會往細里講。
3 View中使用
在View中需要用@inject 再聲明一下,起一個別名。
@using MilkStone.Services;
@model MilkStone.Models.AccountViewModel.LoginViewModel
@inject ILoginService<ApplicationUser> loginService
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head></head>
<body>
@loginService.GetUserName()
</body>
</html>
4 通過 HttpContext來獲取實例
HttpContext下有一個RequestedService同樣可以用來獲取實例對象,不過這種方法一般不推薦。同時要注意GetService<>這是個范型方法,默認如果沒有添加Microsoft.Extension.DependencyInjection的using,是不用調用這個方法的。
HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();
3、替換其它的Ioc容器
使用Autofac可實現批量注入,Autofac 原來的一個生命周期InstancePerRequest,將不再有效。正如我們前面所說的,整個request的生命周期被ASP.NET Core管理了,所以Autofac的這個將不再有效。我們可以使用 InstancePerLifetimeScope ,同樣是有用的,對應了我們ASP.NET Core DI 里面的Scoped
這會給我們的初始化帶來一些便利性,我們來看看如何替換Autofac到ASP.NET Core。我們只需要把Startup類里面的 ConfigureService的 返回值從 void改為 IServiceProvider即可。而返回的則是一個AutoServiceProvider。
public IServiceProvider ConfigureServices(
IServiceCollection services){
services.AddMvc();
// Add other framework services
// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<DefaultModule>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
Autofac 批量注入
//自動注冊接口
builder.RegisterAssemblyTypes(assemblies).Where(b => b.GetInterfaces().
Any(c => c == baseType && b != baseType)).AsImplementedInterfaces(). InstancePerLifetimeScope();
//定義可批量注入的接口 需要繼承
public interface IAutoInject { }
protected void Application_Start()
{
var builder = new ContainerBuilder();
//獲取IAutoInject的Type
var baseType = typeof(IAutoInject);
//獲取所有程序集
var assemblies = System.Web.Compilation.BuildManager.GetReferencedAssemblies().Cast<Assembly>().ToArray();
//自動注冊接口
builder.RegisterAssemblyTypes(assemblies).Where(b => b.GetInterfaces().
Any(c => c == baseType && b != baseType)).AsImplementedInterfaces(). InstancePerLifetimeScope();
//自動注冊控制器
builder.RegisterControllers(assemblies);
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
7、提高ASP.NET Web應用性能
參考
https://developer.aliyun.com/article/298431
1、運行環境優化 做負載均衡和服務器加成
服務器配置升級 CPU 處理器 磁盤陣列
橫向擴展 使用負載均衡、反向代理服務實現服務器集群
2、緩存應用
緩存是一種用空間換取時間的技術,通俗點也就是說把你得到的數據存放在內存 中一段時間,在這短時間內服務器不去讀取數據庫、或是真實的數據源,而是讀取你存放在內存中的數據。 緩存是網站性能優化不可缺少的一種數據處理機制,他能有效的緩解數據庫壓力。 ASP.NET 中的緩存主要分為:
頁面緩存
數據源緩存
自定義數據緩存
redis分布式緩存
增加緩存命中率
全文搜索使用ES
緩存系統
緩存分為文件緩存、內存緩存、數據庫緩存。在大型Web應用中使用最多且效率最高的是內存緩存。最常用的內存緩存工具是Memcachd。使用正確的緩存系統可以達到實現以下目標:
1、使用緩存系統可以提高訪問效率,提高服務器吞吐能力,改善用戶體驗。
2、減輕對數據庫及存儲集服務器的訪問壓力。
3、Memcached服務器有多台,避免單點故障,提供高可靠性和可擴展性,提高性能。
3、數據庫優化
搭建數據庫集群 使用主從策略、讀寫分離
1、讀寫分離 主從策略 主從庫,主負責寫,從是只讀的;搭建集群
2、分庫分表
3、合理使用索引,避免使用全表掃描
4、搜索引擎ES(文檔和索引結合,快的原因是分詞 like "%word%",es只需要查"word"這個詞包含的文檔id ) ELK
5、使用ETL工具(Kettle)
由於Web前端采用了負載均衡集群結構提高了服務的有效性和擴展性,因此數據庫必須也是高可靠的才能保證整個服務體系的高可靠性,如何構建一個高可靠的、可以提供大規模並發處理的數據庫體系?
我們可以采用如上圖所示的方案:
1)使用SQL數據庫,考慮到Web應用的數據庫讀多寫少的特點,我們主要對讀數據庫做了優化,提供專用的讀數據庫和寫數據庫,在應用程序中實現讀操作和寫操作分別訪問不同的數據庫。
2)使用同步機制實現快速將主庫(寫庫)的數據庫復制到從庫(讀庫)。一個主庫對應多個從庫,主庫數據實時同步到從庫。
3)寫數據庫有多台,每台都可以提供多個應用共同使用,這樣可以解決寫庫的性能瓶頸問題和單點故障問題。
4)讀數據庫有多台,通過負載均衡設備實現負載均衡,從而達到讀數據庫的高性能、高可靠和高可擴展性。
5)數據庫服務器和應用服務器分離。
4、代碼層面
1、資源合理利用,合理釋放資源數據庫連接 的關閉 使用using()
2、避免拋出異常最小化異常 盡量不要拋出異常 異常應極少。 相對於其他代碼流模式,引發和捕獲異常的速度很慢。 因此,不應使用異常來控制正常的程序流。
3、使用異步 async/await 多個請求過來時,線程池分配足夠的線程來處理多個請求,提高線程池的利用率 !
4、返回多個數據源進行讀取,減少數據庫的連接 比方分頁中返回 所有當前頁數據 和數據總條數 一條sql 返回多個數據源 進行讀取
5、較少裝箱拆箱操作,使用泛型,string 和stringbuilder
8、Redis分布式緩存
主從模式:讀寫分離
哨兵模式:
心跳機制(每隔幾分鍾發送一個固定信息給服務端,服務端收到后回復一個固定信息如果服務端幾分鍾內沒有收到客戶端信息則視客戶端斷開)+哨兵裁決。主從切換,故障轉移。
Cluster集群模式:無中心架構
面試題:redis內存操作速度快,缺點是受物理內存限制。持久化:RDB(定時,二進制,適合備份),AOF(日志方式,寫,刪,沒有查詢),緩存命中率,通過緩存取到數據,不需要去數據庫查詢,預熱可以提高。
9、IIS經典模式和集成模式的區別
經典模式 兼容IIS 6,服務器通過 ISAPI 托管代碼請求,在經典模式中,IIS擁有自身的管道,ASP.NET作為一個ISAPI擴展運行,只是IIS管道中的一項組成部分。
集成模式 將使用 IIS 和 ASP.NET 的集成請求處理管道來處理請求,可以通過Module自定義擴展。
10、MySql 引擎
Innodb 支持事務;支持行級鎖和外鍵約束 ;高並發時可降低內存,全變掃描也會鎖表;表空間大;沒有保存行數 count() 掃描全表;沒有全文索引
支持事務,對數據完整性要求高,中大型項目使用
MyISAM 不支持事務和行級鎖和外鍵約束,操作時會鎖表;表空間小;存儲了標的行數,count()不會掃描全表;有全文索引
關注效率空間和內存使用比較低,非事務安全的,在小型項目上可以使用
11、NET core 如何實現 動態擴展 如何 承載 高並發
1、動態擴展
使用AOP思想的中間件和過濾器
使用依賴注入
2、承載高並發 緩存 存儲 負載
1、緩解web服務器壓力,使用多台服務器負載均衡,頁面緩存技術
2、緩解數據庫讀取壓力,使用緩存機制 內存緩存、redis緩存 、增加緩存命中率(粒度、緩存預熱、有效期、更新緩存、多級緩存 服務器內存和nosql緩存配合)
3、緩解數據庫壓力 數據庫分庫分表、讀寫分離、數據庫集群
4、使用消息隊列,建立多個消費端,進行流量削峰