加快ASP。NET Core WEB API应用程序。第2部分

时间:2023-12-29 16:58:20

使用各种方法来增加ASP。NET Core WEB API应用程序的生产力 介绍 第1部分。创建测试RESTful WEB API应用程序第2部分。增加了ASP。NET Core WEB API应用程序的生产力。第3部分。对ASP进行深度重构和优化。NET Core WEB API应用程序代码 在第2部分中,我们将回顾以下内容: 应用生产力;异步设计模式;数据规范化vs SQL查询效率NCHAR vs NVARCHAR数据类型;使用MSSQL server全文引擎;存储过程;优化存储过程;预编译和重用存储过程执行计划;利用实体框架核心进行全文搜索;实体框架核心性能;价格表的全文搜索;数值的全文搜索;改变计算的列公式;缓存数据处理结果;复述,缓存;在Windows上安装Redis;复述,桌面管理器;复述,NuGet包;缓存过期控制;在哪里应用缓存?缓存的实现;提前准备数据概念;在实施前准备数据;微服务体系结构的思考;为价格准备创建API。使用HttpClientFactory管理httpclient 生产力应用程序 有一些步骤,可以执行,以提高我们的应用程序的生产力: 异步设计模式;Denormalizing数据;全文搜索;优化实体框架核心;缓存数据处理结果;提前准备数据。 异步设计模式 异步工作是提高应用程序生产率的第一步。 异步设计模式已在第1部分中实现。它需要一些额外的编码,并且通常比同步的要慢一些,因为它需要系统的某些后台活动来提供异步。因此,在没有长I/O操作的小型应用程序中,异步工作甚至会降低应用程序的性能。 但是在负载严重的应用程序中,异步可以通过更有效地使用资源来提高生产率和弹性。让我们观察一下请求是如何在ASP中处理的。NET核心: 每个请求都在从线程池中获取的单个线程中处理。如果同步工作并且发生了一个长I/O操作,那么线程将等待直到操作结束,并在操作完成后返回池。但在此等待期间,该线程被阻塞,不能被其他请求使用。因此,对于一个新请求,如果在胎面池中没有找到可用的线程,就会创建一个新线程来处理该请求。创建一个新线程需要时间,而且对于每个阻塞的线程,也有一些分配给线程的阻塞内存。在负载严重的应用程序中,大量创建线程和阻塞内存可能导致资源缺乏,从而显著降低应用程序和整个系统的生产率。它甚至会导致应用程序崩溃。 但是,如果异步工作,就在I/O操作启动之后,处理该操作的线程返回到线程池,并可用来处理另一个请求。 因此,异步设计模式通过更有效地使用资源来提高应用程序的可伸缩性,从而使应用程序更快、更有弹性。 数据规范化vs SQL查询效率 您可能已经注意到,SpeedUpCoreAPIExampleDB数据库结构几乎完全对应于预期的输出结果。这意味着从数据库获取数据并将其发送给用户不需要任何数据转换,因此可以提供最快的结果。我们通过改变价格表的规格化,使用供应商名称而不是供应商id来实现这一点。 我们目前的数据库结构是: 价格表中的所有价格都可以通过请求获得: 隐藏,复制Code

SELECT PriceId, ProductId, Value, Supplier FROM Prices

有执行计划: 我们的数据库结构在完全规范化时是什么样子的? 但在一个完全规范化的数据库中,价格和供应商表应该在SQL查询中加入,这可能是这样的: 隐藏,复制Code

SELECT Prices.PriceId, Prices.ProductId, Prices.Value, Suppliers.Name AS Supplier
FROM Prices INNER JOIN
Suppliers ON Prices.SupplierId = Suppliers.SupplierId

有执行计划: 第一个查询显然要快得多,因为价格表已经为读取进行了优化。但是对于一个为存储复杂对象而不是为快速读取而优化的完全规范化的数据模型,情况就不是这样了。因此,对于完全规范化的数据,我们可能会遇到SQL查询效率方面的问题。 请注意,价格表不仅用于阅读,而且用于填充数据。例如,现在很多价格表都带有Excel文件或.csv文件,这些文件可以很容易地从Excel、任何MS SQL表或视图和其他来源获得。通常这些文件有以下列:代码;SKU;产品;供应商;价格;其*应商是一个名称,而不是一个代码。如果一个文件中的代码值对应于产品t中的ProductId能够,填充价格表的数据从这样一个文件与数百万记录可以执行在几秒钟的一行T-SQL代码: 隐藏,复制Code

EXEC('BULK INSERT Prices FROM ''' + @CsvSourceFileName + ''' WITH ( FORMATFILE = ''' + @FormatFileName + ''')');    

当然,非正规化会带来价格翻倍的数据,而且有必要解决价格和供应商表中的数据一致性问题。但是,如果目标是提高生产力,这样做是值得的。 注意!在第1部分的最后,我们测试了DELETE API。您的数据可能与我们的示例不同。如果是,请从第1部分的脚本重新创建数据库 NCHAR vs NVARCHAR 在我们的数据库中,所有字符串字段都有NCHAR数据类型,这显然不是最好的解决方案。事实上,NCHAR是一种固定长度的数据类型。这意味着,SQL server为每个字段保留一个固定大小的位置(我们已经为一个字段声明了),独立于字段内容的实际长度。例如,价格表中的“供应商”字段声明为: 隐藏,复制Code

[Supplier]  NCHAR (50)  NOT NULL    

这就是为什么当我们从价格表中收到价格时,结果是这样的: 隐藏,复制Code

[
{
"PriceId": 7,
"ProductId": 3,
"Value": 160.00,
"Supplier": "Bosch "
},
{
"PriceId": 8,
"ProductId": 3,
"Value": 165.00,
"Supplier": "LG "
},
{
"PriceId": 9,
"ProductId": 3,
"Value": 170.00,
"Supplier": "Garmin "
}
]

为了删除供应商值后面的空白,我们必须在PricesService中应用Trim()方法。产品服务中的SKU和名称也是如此。因此,我们在数据库大小和应用程序性能方面都有损失。 为了解决这个问题,我们可以将NCHAR字段的数据类型改为NVARCHAR,它是可变长度的字符串数据类型。对于NVARCHAR字段,SQL server只分配存储字段上下文所需的内存,而不向字段数据添加尾随空格。 我们可以改变字段数据类型的T-SQL脚本: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO ALTER TABLE [Products]
ALTER COLUMN SKU nvarchar(50) NOT NULL ALTER TABLE [Products]
ALTER COLUMN [Name] nvarchar(150) NOT NULL ALTER TABLE [Prices]
ALTER COLUMN Supplier nvarchar(50) NOT NULL

但是后面的空格仍然保留,因为SQL server没有为了不丢失数据而对它们进行修剪。所以,我们应该有意识地进行修剪: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO UPDATE Products SET SKU = RTRIM(SKU), Name = RTRIM(Name)
GO UPDATE Prices SET Supplier = RTRIM(Supplier)
GO

现在我们可以删除ProductsService和PricesService中的所有. trim()方法,输出结果将没有尾随空格。 使用MSSQL server全文引擎 如果Products表的大小很大,可以利用MSSQL server的全文搜索引擎的强大功能,显著提高SQL查询的执行速度。FTS在MSSQL server的全文搜索中只有一个限制-文本只能通过字段的前缀进行搜索。换句话说,如果对SKU列应用全文搜索并尝试查找SKU包含“ab”的记录,则只能找到“abc”,而不能找到“aab”记录。如果此搜索结果适合应用程序业务逻辑,则可以实现全文搜索。 因此,将在Products表的sku列中搜索sku或它的开始部分。为此,在我们的SpeedUpCoreAPIExampleDB数据库中,我们应该创建全文目录: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO CREATE FULLTEXT CATALOG [ProductsFTS] WITH ACCENT_SENSITIVITY = ON
AS DEFAULT
GO

然后在ProductsFTS目录中建立全文索引 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO CREATE FULLTEXT INDEX ON [dbo].[Products]
(SKU LANGUAGE 1033)
KEY INDEX PK_Products
ON ProductsFTS
GO

SKU列将包含在全文索引中。索引将自动填充。但如果你想手动操作,只需右键单击产品表,选择全文索引>开始完整的人口。 结果应该是: 让我们创建一个存储过程来研究全文搜索是如何工作的。 存储过程 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO CREATE PROCEDURE [dbo].[GetProductsBySKU]
@sku [varchar] (50)
AS
BEGIN
SET NOCOUNT ON; Select @sku = '"' + @sku + '*"' -- Insert statements for procedure here
SELECT ProductId, SKU, Name FROM [dbo].Products WHERE CONTAINS(SKU, @sku)
END
GO

关于@sku格式的一些解释——为了通过单词的前缀进行全文搜索,搜索参数应该有闭合的*通配符:'"aa*"'。因此,Select @sku = '"' + @sku + '*"'只是格式化@sku值。 让我们来看看这个程序是如何工作的: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO EXEC [dbo].[GetProductsBySKU] 'aa'
GO

结果将是: 正如预期的那样。 优化存储过程 不要忘记“设置NOCOUNT ON”,以防止处理记录的不必要计数。 注意,查询: 隐藏,复制Code

SELECT ProductId, SKU, [Name] FROM [dbo].Products WHERE CONTAINS(SKU, @sku)    

被使用,但不 隐藏,复制Code

SELECT * FROM Products WHERE CONTAINS(SKU, @sku)

虽然两个查询的结果是相同的,但是第一个查询的速度更快。因为如果使用*通配符代替列名,SQL server首先搜索表的所有列名,然后用这些名称替换*通配符。如果显式地声明列名,则省略此额外作业。在我们的例子中,如果没有指明表模式[dbo], SQL server将在所有模式中搜索一个表。但是如果模式是显式声明的,SQL server只会在该模式中更快地搜索表。 预编译和重用存储过程执行计划 使用存储过程的一个重要好处是,在第一次执行过程之前,会对过程进行编译,创建其执行计划并将其放入缓存中。然后,当该过程下一次执行时,将省略编译行为,并从缓存中获取一个就绪的执行计划。所有这些都使请求过程快得多。 让我们确保SQL server重用过程执行计划和预编译代码。为此,首先在Micros中清除所有缓存执行计划中的SQL服务器内存创建新的查询: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO --clear cache
DBCC FREEPROCCACHE

通过新查询检查缓存状态: 隐藏,复制Code

SELECT cplan.usecounts, cplan.objtype, qtext.text, qplan.query_plan
FROM sys.dm_exec_cached_plans AS cplan
CROSS APPLY sys.dm_exec_sql_text(plan_handle) AS qtext
CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qplan
ORDER BY cplan.usecounts DESC

结果将是: 再次执行存储过程 隐藏,复制Code

EXEC [dbo].[GetProductsBySKU] 'aa'

然后检查缓存: 我们可以看到缓存了一个过程执行计划。执行过程并再次检查当前缓存计划的信息: 在“usecounts”字段中,我们可以看到该计划被重用了多少次。您可以在“usecounts”字段中看到计划被重用了两次,这证明了执行计划缓存确实适用于我们的过程。 使用带有全文搜索的实体框架核心 全文搜索的最后一个问题是如何在实体框架核心中使用它。EFC自己生成对数据库的查询,并不考虑全文索引。有一些方法可以解决这个问题。最简单的方法是调用我们的存储过程GetProductsBySKU,它已经实现了全文搜索。 要执行存储过程,我们将使用FromSql方法。这个方法在Entity Framework Core中用于执行返回数据集的存储过程和原始SQL查询。 在productsrepository.c改变代码的FindProductsAsync方法: 隐藏,复制Code

public async Task<IEnumerable<Product>> FindProductsAsync(string sku)
{
return await _context.Products.FromSql("[dbo].GetProductsBySKU @sku = {0}", sku).ToListAsync();
}

注意,为了加速过程的开始,我们使用了它的完全限定名[dbo]。GetProductsBySKU,其中包含[dbo]模式。 使用存储过程的一个问题是其代码不受源代码控制。要解决这个问题,可以使用相同的脚本调用原始SQL查询,而不是使用存储过程。 注意!只使用参数化的原始SQL查询来利用执行计划重用和防止SQL注入攻击。 但是存储过程仍然更快,因为在调用过程时,我们只将其名称传递给SQL Server,而不是调用原始SQL查询时的完整脚本文本。 让我们检查一下存储过程和FTS在应用程序中的工作方式。启动应用程序并测试/api/products/find/ http://localhost:49858/api/products/find/aa 结果将是相同的没有全文搜索: 实体框架核心性能 由于我们的存储过程返回一个预期的实体类型产品的列表,所以EFC自动进行跟踪,以分析哪些记录被更改只为更新这些记录。但是当我们获得产品列表时,我们不会改变任何数据。因此,使用AsNoTracking()方法关闭跟踪是合理的,它禁用了EF的额外活动,显著提高了EF的工作效率。 没有跟踪的FindProductsAsync方法的最终版本是: 隐藏,复制Code

public async Task<IEnumerable<Product>> FindProductsAsync(string sku)
{
return await _context.Products.AsNoTracking().FromSql("[dbo.GetProductsBySKU @sku = {0}", sku).ToListAsync();
}

我们也可以在GetAllProductsAsync方法中应用AsNoTracking: 隐藏,复制Code

public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
return await _context.Products.AsNoTracking().ToListAsync();
}

在GetProductAsync方法中: 隐藏,复制Code

public async Task<Product> GetProductAsync(int productId)
{
return await _context.Products.AsNoTracking().Where(p => p.ProductId == productId).FirstOrDefaultAsync();
}

注意,对于AsNoTracking()方法,EFC不执行对已更改实体的跟踪,并且如果没有附加到_context,您将无法在GetProductAsync方法发现的实体中保存更改。但是EFC仍然执行身份解析,所以我们可以很容易地删除产品,由GetProductAsync方法找到。这就是为什么,我们的DeleteProductAsync方法在GetProductAsync方法的新版本中工作得很好。 在价格表上进行全文搜索 如果ProductId数据类型为NVARCHAR,那么在获取价格时,我们可以显著提高SQL查询性能,因为我们可以在ProductId列上应用全文搜索。但是它的类型是整数,因为它是Products表的ProductId主键的外键,后者是带有自动增量标识的整数。 这个问题的一种可能的解决方案是在price表中创建一个计算列,该列将由ProductId字段的NVARCHAR表示组成,并将该列添加到全文索引中。 让我们创建一个新的计算列xProductId: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO ALTER TABLE [Prices]
ADD xProductId AS convert(nvarchar(10), ProductId) PERSISTED NOT NULL
GO

我们已经将xProductId列标记为persistent,以便它的值被物理地存储在表中。如果不持久化,xProductId列值将在每次访问时重新计算。这些重新计算还会影响SQL server的性能。 xProductId字段中的值将作为字符串ProductId: 表的新内容 然后在xProductId字段上创建带有全文索引的新的PricesFTS全文目录: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO CREATE FULLTEXT CATALOG [PricesFTS] WITH ACCENT_SENSITIVITY = ON
AS DEFAULT
GO CREATE FULLTEXT INDEX ON [dbo].[Prices]
(xProductId LANGUAGE 1033)
KEY INDEX PK_Prices
ON PricesFTS
GO

最后,创建一个存储过程来测试结果: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO CREATE PROCEDURE [dbo].[GetPricesByProductId]
@productId [int]
AS
BEGIN
SET NOCOUNT ON; DECLARE @xProductId [NVARCHAR] (10)
Select @xProductId = '"' + CONVERT([nvarchar](10),@productId) + '"' -- Insert statements for procedure here
SELECT PriceId, ProductId, [Value], Supplier FROM [dbo].Prices WHERE CONTAINS(xProductId, @xProductId)
END
GO

在存储过程中,我们声明了@xProductId变量,将@productId转换为NVARCHAR,并执行了全文搜索。 执行GetPricesByProductId过程: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO DECLARE @return_value int EXEC @return_value = [dbo].[GetPricesByProductId]
@productId = 1 SELECT 'Return Value' = @return_value GO

但是什么也没有发现: 数值的全文搜索 对包含数字的字符串列进行全文搜索的问题值出现在Microsoft SQL Server中,从SQL Server 2012开始,因为它的新版本的断词器。让我们看看全文搜索引擎是如何解析xProductId值的(“1”、“2”,…)。执行: 隐藏,复制Code

SELECT display_term FROM sys.dm_fts_parser (' "1" ', 1033, 0, 0)

您可以看到,解析器将值“1”识别为第1行中的字符串和第2行中的数字。这种模糊性不允许将xProductId列值包含在全文索引中。解决这个问题的一种可能的方法是“将搜索使用的断字符恢复到以前的版本”。但是我们应用了另一种方法—用字符(例如“x”)启动xProductId列中的每个值,从而强制全文解析器将值识别为字符串。让我们确保这一点: 隐藏,复制Code

SELECT display_term FROM sys.dm_fts_parser (' "x1" ', 1033, 0, 0)

结果不再含糊不清。 更改计算列公式 更改计算列的唯一可能是删除该列,然后使用其他条件重新创建它。 由于ProductId列已启用全文搜索,我们将无法在删除全文索引之前删除该列: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO DROP FULLTEXT INDEX ON [Prices]
GO

然后删除列: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO ALTER TABLE [Prices]
DROP COLUMN xProductId
GO

然后用一个新的公式重新创建这个列: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO ALTER TABLE [Prices]
ADD xProductId AS 'x' + convert(nvarchar(10), ProductId) PERSISTED NOT NULL
GO

检查结果: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO SELECT * FROM [Prices]
GO

重新创建全文索引: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO CREATE FULLTEXT INDEX ON [dbo].[Prices]
(xProductId LANGUAGE 1033)
KEY INDEX PK_Prices
ON PricesFTS
GO

改变我们的GetPricesByProductId存储过程,将' x '添加到搜索模式: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO ALTER PROCEDURE [dbo].[GetPricesByProductId]
@productId [int]
AS
BEGIN
SET NOCOUNT ON; DECLARE @xProductId [NVARCHAR] (10)
Select @xProductId = '"x' + CONVERT([nvarchar](10),@productId) + '"' -- Insert statements for procedure here
SELECT PriceId, ProductId, [Value], Supplier FROM [dbo].Prices WHERE CONTAINS(xProductId, @xProductId)
END

最后,检查程序工作结果: 隐藏,复制Code

USE [SpeedUpCoreAPIExampleDB]
GO DECLARE @return_value int EXEC @return_value = [dbo].[GetPricesByProductId]
@productId = 1 SELECT 'Return Value' = @return_value GO

它将正常工作。现在让我们更改PricesRepository中的GetPricesAsync方法。改变: 隐藏,复制Code

return await _context.Prices.Where(p => p.ProductId == productId).ToListAsync();

: 隐藏,复制Code

return await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync();

启动应用程序并检查http://localhost:49858/api/prices/1结果。结果将是相同的没有全文搜索: 缓存数据处理的结果。 再看一遍上面的图片。在我们的示例中,可以在一段时间内兑现http://localhost:49858/api/prices/1请求的结果。在下一次尝试获取Porduct1的价格时,准备好的价格表将从缓存中获取并发送给用户。如果在缓存中仍然没有Id=1的结果,那么价格将从数据库中提取并放到缓存中。这种方法将减少相对缓慢的数据库访问次数,有利于从内存中的缓存中快速检索数据。 复述,缓存 对于缓存,将使用Redis缓存服务。Redis缓存的优点是: Redis缓存是在内存中存储数据,所以它有一个比数据库存储数据在磁盘上有更高的性能; Redis缓存实现IDistributedCache接口。这意味着我们可以很容易地将一个缓存提供程序更改为另一个IDistributedCache提供程序,例如MS SQL Server,而无需更改缓存管理逻辑; 在将服务迁移到Azure云的情况下,可以很容易地为Azure切换到Redis缓存。 在Windows上安装Redis Windows版Redis的最新版本可以从https://github.com/MicrosoftArchive/redis/releases下载 目前是3.2.100 保存并运行Redis-x64-3.2.100.msi 安装相当标准。出于测试目的,您可以默认保留所有选项。安装后,打开任务管理器,检查Redis服务运行。 另外,请确保服务是自动启动的。为此,打开:Windows >开始菜单的在管理工具在服务。 复述,桌面管理器 为了调试的目的,它是方便的一些客户端应用程序的Redis服务器,以观察缓存的值。为此,Redis桌面管理器可以使用。您可以从https://redisdesktop.com/download下载 安装Redis桌面管理器也非常简单-一切都是默认的。 打开Redis桌面管理器,点击连接到Redis服务器按钮,选择名称:Redis和地址:localhost 然后点击确定按钮,你会看到内容的Redis缓存服务器。 复述,NuGet包 添加Redis NuGet包到我们的应用程序: 主菜单在工具比;NuGet包管理器>管理解决方案的NuGet包 输入Microsoft.Extensions.Caching。Redis在浏览字段中选择包: 注意!一定要选择正确的官方微软包Microsoft. extension . caching。Redis(但不是微软。extension . cachies . redisk . core)。 在这个阶段,你必须安装以下软件包: 在启动类的配置服务方法的存储库之前声明AddDistributedRedisCache 隐藏,复制Code

//Cache
services.AddDistributedRedisCache(options =>
{
options.InstanceName = Configuration.GetValue<string>("Redis:Name");
options.Configuration = Configuration.GetValue<string>("Redis:Host");
});

在配置文件appsettings中添加Redis连接设置。json (appsettings.Development.json) 隐藏,复制Code

"Redis": {
"Name": "Redis",
"Host": "localhost"
}

缓存过期控制 对于缓存,可以应用滑动或绝对过期模型。 当你有一个巨大的李时,滑动到期将对价格有用产品的需求量很大,但只有一小部分产品需求量很大。因此,只有这一套的价格将始终缓存。所有其他价格都将自动从缓存中删除,因为它们很少被请求,而滑动到期模型将继续缓存仅在指定期限内重新请求的项目。这使内存不受不重要数据的影响。这种方法的缺点是,当数据库中的价格发生变化时,我们必须实现某种机制来从缓存中删除条目。 应用程序中使用的是绝对过期模型。在这种情况下,所有项都将在指定的时间段内平均缓存,然后自动从缓存中删除。保持缓存中的实际价格的问题将自己解决,尽管可能会有一点延迟。 在appsettings中添加缓存设置部分。json(和appsettings.Development.json)文件。 隐藏,复制Code

"Caching": {
"PricesExpirationPeriod": 15
}

价格将缓存15分钟。 在哪里应用缓存? 由于在应用程序体系结构中,服务不知道数据存储的方式,缓存的合适位置是存储库,它负责基础结构层。对于缓存价格,RedisCache将通过IConfiguration注入到PricesRepository中,它提供对缓存设置的访问。 缓存的实现 在这个阶段,PricesRepository类的最后一个版本将是: 隐藏,收缩,复制Code

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using SpeedUpCoreAPIExample.Contexts;
using SpeedUpCoreAPIExample.Interfaces;
using SpeedUpCoreAPIExample.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Repositories
{
public class PricesRepository : IPricesRepository
{
private readonly Settings _settings;
private readonly DefaultContext _context;
private readonly IDistributedCache _distributedCache; public PricesRepository(DefaultContext context, IConfiguration configuration, IDistributedCache distributedCache)
{
_settings = new Settings(configuration); _context = context;
_distributedCache = distributedCache;
} public async Task<IEnumerable<Price>> GetPricesAsync(int productId)
{
IEnumerable<Price> prices = null; string cacheKey = "Prices: " + productId; var pricesTemp = await _distributedCache.GetStringAsync(cacheKey);
if (pricesTemp != null)
{
//Deserialize
prices = JsonConvert.DeserializeObject<IEnumerable<Price>>(pricesTemp);
}
else
{
prices = await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync(); //cache prices for PricesExpirationPeriod minutes
DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_settings.PricesExpirationPeriod));
await _distributedCache.SetStringAsync(cacheKey, JsonConvert.SerializeObject(prices), cacheOptions);
} return prices;
} private class Settings
{
public int PricesExpirationPeriod = 15; //15 minutes by default public Settings(IConfiguration configuration)
{
int pricesExpirationPeriod;
if (Int32.TryParse(configuration["Caching:PricesExpirationPeriod"], NumberStyles.Any,
NumberFormatInfo.InvariantInfo, out pricesExpirationPeriod))
{
PricesExpirationPeriod = pricesExpirationPeriod;
}
}
}
}
}

守则的一些解释: 在类DefaultContext的构造函数中,注入了IConfiguration和IDistributedCache。然后创建一个新的类设置实例(在类PricesRepository的底部实现)。设置用于达到配置“缓存”一节中的“PricesExpirationPeriod”的值。在设置中,类类型检查PricesExpirationPeriod参数也被引入。如果周期不是整数,则使用默认值(15分钟)。 在GetPricessAsync方法中,我们首先尝试从Redis缓存中获取ProductId的价格列表,该缓存作为IDistributedCache注入。如果一个值存在,我们反序列化它并返回一个价格列表。如果它不存在,我们从数据库获取该列表,并通过设置的PricesExpirationPeriod参数将其缓存数分钟。 让我们检查一下所有的工作情况 在Firefox或Chrome浏览器中,启动Swagger Inspector扩展(之前安装的)并调用API http://localhost:49858/api/prices/1 API响应状态:200 OK和产品价格列表t1: 打开桌面管理器,连接到Redis服务器。现在我们可以看到一个组RedisPrices和关键价格的缓存值:1 Product1的价格被缓存,15分钟内对API API / Prices /1的下一个调用将从缓存中取出它们,而不是从数据库中取出。 提前准备数据概念 情况下当我们有一个巨大的数据库,或者只价格是基本的,必须另外重新计算为一个特定的用户,响应速度的增加可能更高之前,如果我们准备价格用户申请和缓存请求后预先计算的价格。 让我们用参数“aa”分析api/产品/查找api结果 http://localhost:49858/api/products/find/aa 我们可以找到两个sku由“aa”组成的位置。在这个阶段,我们不知道用户可以要求哪一个的价格。 但是如果参数是“abc”,我们将只获得一个产品的响应。 用户最有可能的下一步是要求这种特殊产品的价格。如果我们在这个阶段获得产品的价格并缓存结果,那么API http://localhost:49858/api/prices/3的下一个调用将从缓存中获取现成的价格,从而节省大量时间和SQL Server活动。 在实施前准备数据 为了实现这个想法,我们在PricesRepository和PricesService中创建PreparePricessAsync方法 首先在接口IPricesRepository和IPricesService中声明这些方法。在这两种情况下,该方法都不会返回任何内容。 隐藏,复制Code

using SpeedUpCoreAPIExample.Models;
using System.Collections.Generic;
using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Repositories
{
public interface IPricesRepository
{
Task<IEnumerable<Price>> GetPricesAsync(int productId);
Task PreparePricesAsync(int productId);
}
}

和 隐藏,复制Code

using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks; namespace SpeedUpCoreAPIExample.Interfaces
{
public interface IPricesService
{
Task<IActionResult> GetPricesAsync(int productId);
Task PreparePricesAsync(int productId);
}
}

PricesService的PreparePricessAsync方法只在try-catch构造中调用PricesRepository的PreparePricessAsync。注意,在PreparePricessAsync过程中没有任何异常处理,我们只是完全忽略了可能的错误。这是因为我们不想在这个地方中断程序的流程,因为仍然有可能用户永远不会要求这个产品的价格,并且错误消息可能会成为他工作中不希望看到的障碍。 隐藏,复制Code

public async Task PreparePricesAsync(int productId)
{
IEnumerable<Price> prices = null; string cacheKey = "Prices: " + productId; var pricesTemp = await _distributedCache.GetStringAsync(cacheKey);
if (pricesTemp != null)
{
//already cached
return;
}
else
{
prices = await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync(); //cache prices for PricesExpirationPeriod minutes
DistributedCacheEntryOptions cacheOptions = new DistributedCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_settings.PricesExpirationPeriod));
await _distributedCache.SetStringAsync(cacheKey, JsonConvert.SerializeObject(prices), cacheOptions);
}
return;
}

在PricesService.cs 隐藏,复制Code

using System;

public async Task PreparePricesAsync(int productId)
{
try
{
await _pricesRepository.PreparePricesAsync(productId);
}
catch (Exception ex)
{
}
}

让我们检查一下PreparePricesAsync方法是如何工作的。首先将价格服务注入产品服务: 隐藏,复制Code

private readonly IProductsRepository _productsRepository;
private readonly IPricesService _pricesService; public ProductsService(IProductsRepository productsRepository, IPricesService pricesService)
{
_productsRepository = productsRepository;
_pricesService = pricesService;
}

注意!我们已经在产品中注入了价格服务只有。以这种方式耦合服务不是一个好的实践,因为如果我们决定实现微服务体系结构,它将使事情变得困难。在理想的微观服务世界中,服务不应该相互依赖。 但是让我们进一步在产品服务类中创建PreparePricessAsync方法。该方法将是私有的,因此不需要在IProductsRepository接口中声明它。 隐藏,复制Code

private async Task PreparePricesAsync(int productId)
{
await _pricesService.PreparePricesAsync(productId);
}

该方法只调用PricesService的PreparePricessAsync方法。 然后,在FindProductsAsync中,检查产品列表的搜索结果中是否只有一项。如果只有一个,我们调用PricesService的PreparePricessAsync来获得这个单一商品的产品Id。注意,我们调用了_pricesservice。PreparePricessAsync在我们返回产品列表给用户之前-原因将在后面解释。 隐藏,收缩,复制Code

public async Task<IActionResult> FindProductsAsync(string sku)
{
try
{
IEnumerable<Product> products = await _productsRepository.FindProductsAsync(sku); if (products != null)
{
if (products.Count() == 1)
{
//only one record found - prepare prices beforehand
await PreparePricesAsync(products.FirstOrDefault().ProductId);
}; return new OkObjectResult(products.Select(p => new ProductViewModel()
{
Id = p.ProductId,
Sku = p.Sku,
Name = p.Name
}
));
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}

我们还可以在GetProductAsync方法中添加PreparePricessAsync。 隐藏,收缩,复制Code

public async Task<IActionResult> GetProductAsync(int productId)
{
try
{
Product product = await _productsRepository.GetProductAsync(productId); if (product != null)
{
await PreparePricesAsync(productId); return new OkObjectResult(new ProductViewModel()
{
Id = product.ProductId,
Sku = product.Sku,
Name = product.Name
});
}
else
{
return new NotFoundResult();
}
}
catch
{
return new ConflictResult();
}
}

从Redis缓存中删除缓存值,启动应用程序并调用http://localhost:49858/api/products/find/abc 打开Redis桌面管理器和检查缓存的值。你可以在这里找到“ProductId”的价格表 隐藏,复制Code

[
{
"PriceId": 7,
"ProductId": 3,
"Value": 160.00,
"Supplier": "Bosch"
},
{
"PriceId": 8,
"ProductId": 3,
"Value": 165.00,
"Supplier": "LG"
},
{
"PriceId": 9,
"ProductId": 3,
"Value": 170.00,
"Supplier": "Garmin"
}
]

然后检查/api/products/3 api。从缓存中删除数据并调用http://localhost:49858/api/products/3 Check in Redis桌面管理器,你会发现这个API也缓存价格正确。 但是我们没有在速度上取得任何进展,因为我们同步地调用了异步方法GetProductAsync——应用程序工作流一直等到GetProductAsync准备了价格表。我们的API完成了两次调用。 要解决这个问题,我们应该在单独的线程中执行GetProductAsync。在这种情况下,api/产品的结果将立即交付给用户。同时,GetProductAsync方法将继续工作,直到它准备价格并缓存结果。 为此,我们必须稍微改变PreparePricesAsync方法的声明—让它返回void。 在ProductsService: 隐藏,复制Code

private async void PreparePricesAsync(int productId)
{
await _pricesService.PreparePricesAsync(productId);
}

添加系统。将名称空间线程化到ProductsService类中。 隐藏,复制Code

using System.Threading

现在我们可以更改线程对该方法的调用。 在FindProductsAsync方法: 隐藏,复制Code


if (products.Count() == 1)
{
//only one record found - prepare prices beforehand
ThreadPool.QueueUserWorkItem(delegate
{
PreparePricesAsync(products.FirstOrDefault().ProductId);
});
};

GetProductAsync方法: 隐藏,复制Code


ThreadPool.QueueUserWorkItem(delegate
{
PreparePricesAsync(productId);
});

一切似乎都很好。从Redis缓存中删除缓存值,启动应用程序并调用http://localhost:49858/api/products/find/abc 结果状态为status: 200 OK,但是缓存仍然是空的。因此,发生了一些错误,但我们无法看到它,因为我们没有在PricesService中对PreparePricessAsync方法执行错误处理。 让我们在PricesService的PreparePricesAsync方法中在catch语句之后设置一个断点: 然后再次调用API http://localhost:49858/api/products/find/abc ones。 现在我们有一个异常,可以检查细节: 系统。无法访问已释放的对象。此错误的一个常见原因是处置从依赖项注入中解决的上下文,然后尝试在应用程序的其他地方使用相同的上下文实例。如果在上下文上调用Dispose(),或在using语句中包装上下文,可能会发生这种情况。如果你在使用依赖注入,你应该让依赖注入容器处理上下文实例。 这意味着,当结果发送给用户时,我们不能再使用通过依赖注入注入的DbContext,因为DbContext此时已经被释放了。DbContext在依赖注入链中注入的深度并不重要。 让我们来看看是否可以在不注入DbContext依赖项的情况下完成这项工作。在PricesRepository。PreparePricessAsync,我们将动态创建DbContext,并在内部使用构造。 添加EntityFrameworkCore名称空间 隐藏,复制Code

using Microsoft.EntityFrameworkCore

获得价格的方块是这样的: 隐藏,复制Code

using Microsoft.EntityFrameworkCore
… public async Task PreparePricessAsync(int productId)
{
… var optionsBuilder = new DbContextOptionsBuilder<DefaultContext>();
optionsBuilder.UseSqlServer(_settings.DefaultDatabase); using (var _context = new DefaultContext(optionsBuilder.Options))
{
prices = await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync();
}

并在Settings类中添加两行: 隐藏,复制Code

    public string DefaultDatabase;
… DefaultDatabase = configuration["ConnectionStrings:DefaultDatabase"];

然后启动应用程序,再次尝试http://localhost:49858/api/products/find/abc ones。 现在没有错误,价格缓存在Redis缓存。如果我们在PricesRepository中设置断点。PreparePricessAsync方法并再次调用API,我们可以看到,在结果发送给用户后,程序会在这个断点停止。所以,我们实现了我们的目标——价格是在后台预先准备的,这个过程不会阻碍应用程序的流程。 但这种解决方案并不理想。一些问题: 通过将价格服务注入到产品服务中,我们将服务结合在一起,如果我们想应用微服务架构,就会变得很困难; 我们无法从DbContext中获得依赖注入的好处; 混合的方法使我们的代码不那么统一,因此更加混乱。 考虑微服务体系结构 在本文中,我们描述的是一个整体应用程序,但是在完成了所有生产力改进之后,提高高负载应用程序性能的一种可能方法是水平扩展。为此,应用程序可能被分成两个微服务:ProductsMicroservice和PricesMicroservice。如果ProductsMicroservice希望提前准备价格,它将调用PricesMicroservice的适当方法。这个方法应该通过API访问。 我们将遵循这个想法,但是在我们的整体应用程序中实现它。首先,我们将在PricesController中创建一个API API /prices/prepare,然后通过Http请求从ProductsServive调用这个API。这应该可以解决我们在对DbContext进行依赖注入时遇到的所有问题,并为微服务体系结构准备应用程序。即使在一个整体中使用Http请求的另一个好处是,在负载均衡器背后的多租户应用程序中,此请求可能由该应用程序的另一个实例处理,因此,我们将获得水平扩展的好处。 首先,让我们将PricesRepository返回到在PricesRepository中开始测试PreparePricessAsync方法之前的状态。PreparePricessAsync方法,我们删除“using”语句,只留下一行: 隐藏,复制Code

public async Task PreparePricessAsync(int productId)
{
… prices = await _context.Prices.AsNoTracking().FromSql("[dbo].GetPricesByProductId @productId = {0}", productId).ToListAsync(); …

还要从PricesRepository中删除DefaultDatabase变量。设置类。 为价格准备创建API 在PricesController中添加方法: 隐藏,复制Code

// POST api/prices/prepare/5
[HttpPost("prepare/{id}")]
public async Task<IActionResult> PreparePricessAsync(int id)
{
await _pricesService.PreparePricesAsync(id); return Ok();
}

注意,调用方法是POST,因为我们不打算用这个API获取任何数据。并且API总是返回OK—如果在API执行过程中发生错误,它将被忽略,因为它在这个阶段不重要。 清除Redis缓存,启动我们的应用程序,调用POST http://localhost:49858/api/prices/prepare/3 API工作的很好-我们有状态:200 OK和Product3的价格表缓存。 因此,我们的意图是从ProductsService的代码中调用这个新的API。PreparePricessAsync方法。为此,我们必须决定如何获取API的URL。我们将在GetFullyQualifiedApiUrl方法中获得URL。但是,如果我们不能访问当前的Http上下文来查找主机和工作协议和端口,我们如何获得服务类中的URL呢? 至少有三种可能性我们可以使用: 将完全限定的API URL放到配置文件中。这是最简单的方法,但在将来如果我们决定将应用程序转移到另一个基础设施,可能会造成一些问题——我们必须关心配置文件中的实际URL; 当前Http上下文在控制器级别可用。因此,我们可以确定URL并将其作为参数传递给ProductsService。方法PreparePricessAsync,或者甚至传递Http上下文本身。这两个选项都不是很好,因为我们不想在控制器中实现任何业务逻辑,而且从服务类的角度来看,它将依赖于控制器,因此,服务的测试将更加难以建立; 使用HttpContextAccessor服务。它提供对应用程序中任何地方的HTTP上下文的访问。它可以通过依赖注入来注入。当然,我们选择这种方法作为通用的和原生的ASP。净的核心。 为了实现这个,我们在启动类的ConfigureServices方法中注册HttpContextAccessor: 隐藏,复制Code

using Microsoft.AspNetCore.Http;

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

服务的范围应该是单例的。 现在我们可以在ProductService中使用HttpContextAccessor it。注入HttpContextAccessor而不是PriceServive: 隐藏,复制Code

using Microsoft.AspNetCore.Http;
… public class ProductsService : IProductsService
{
private readonly IProductsRepository _productsRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly string _apiUrl; public ProductsService(IProductsRepository productsRepository, IHttpContextAccessor httpContextAccessor)
{
_productsRepository = productsRepository;
_httpContextAccessor = httpContextAccessor; _apiUrl = GetFullyQualifiedApiUrl("/api/prices/prepare/");
}

添加一个方法ProductsService。GetFullyQualifiedApiUrl与代码: 隐藏,复制Code

private string GetFullyQualifiedApiUrl(string apiRout)
{
string apiUrl = string.Format("{0}://{1}{2}",
_httpContextAccessor.HttpContext.Request.Scheme,
_httpContextAccessor.HttpContext.Request.Host,
apiRout); return apiUrl;
}

注意,我们在类构造函数中设置了the_apiUrl变量的值。我们通过消除对PricesService的依赖注入并改变ProductService来分离产品服务和价格服务。PreparePricessAsync方法——调用新的API,而不是调用PriceServive。PreparePricessAsync方法: 隐藏,复制Code

using System.Net.Http;

private async void PreparePricesAsync(int productId)
{
using (HttpClient client = new HttpClient())
{
var parameters = new Dictionary<string, string>();
var encodedContent = new FormUrlEncodedContent(parameters); try
{
var result = await client.PostAsync(_apiUrl + productId, encodedContent).ConfigureAwait(false);
}
catch
{
}
}
}

在这个方法中,我们调用try-catch内部的API,而不进行错误处理。 清除Redis缓存,启动我们的应用程序,调用http://localhost:49858/api/products/find/abc或http://localhost:49858/api/products/3 API工作得很好-我们有状态:200 OK和Product3缓存的价格表。 httpclient问题 在“Using”构造中使用HttpClient并不是最好的解决方案,我们使用它只是作为概念的证明。有两点会让我们失去生产力: 每个HttpClient都有自己的用于存储和重用连接的连接池。但是如果您为每个请求创建一个新的HttpClient,那么以前创建的HttpClient的连接池将不能被新的HttpClient重用。所以,它必须浪费时间建立建立到同一服务器的新连接; 在“Using”构造结束时处置HttpClient之后,它的连接不会立即释放。相反,它们以TIME_WAIT状态等待一段时间,阻塞分配给它们的端口。在一个负载很重的应用程序中,会在短时间内创建大量连接,但仍然不可用以供重用(默认情况下为4分钟)。这种资源的低效率使用会导致生产力的严重损失,甚至导致“套接字耗尽”问题和应用程序崩溃。 此问题的一种可能的解决方案是为每个服务使用一个HttpClient,并将服务作为单例添加。但是我们将应用另一种方法——使用HttpClientFactory以正确的方式管理httpclient。 使用HttpClientFactory管理httpclient HttpClientFactory控制httpclient处理程序的生命周期,使其可重用,从而防止应用程序无效地使用资源。 HttpClientFactory从ASP开始就可用了。2.1网络核心。要将其添加到我们的应用程序中,我们应该安装Microsoft.Extensions。Http NuGet包: 通过AddHttpClient()方法在应用程序的Startup.cs文件中注册默认的HttpClientFactory: 隐藏,复制Code


public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddHttpClient();

在ProductsService类中,通过依赖注入注入HttpClientFactory: 隐藏,复制Code


private readonly IProductsRepository _productsRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHttpClientFactory _httpClientFactory; private readonly string _apiUrl; public ProductsService(IProductsRepository productsRepository, IHttpContextAccessor httpContextAccessor, IHttpClientFactory httpClientFactory)
{
_productsRepository = productsRepository;
_httpContextAccessor = httpContextAccessor;
_httpClientFactory = httpClientFactory; _apiUrl = GetFullyQualifiedApiUrl("/api/prices/prepare/");
}

纠正PreparePricesAsync方法——删除“Using”构造,通过注入HttpClientFactory的.Create Client()方法创建HttpClient: 隐藏,复制Code


private async void PreparePricesAsync(int productId)
{
var parameters = new Dictionary<string, string>();
var encodedContent = new FormUrlEncodedContent(parameters); try
{
HttpClient client = _httpClientFactory.CreateClient();
var result = await client.PostAsync(_apiUrl + productId, encodedContent).ConfigureAwait(false);
}
catch
{
}
}

. createclient()方法通过从池中获取一个并将其传递给新创建的HttpClient来重用HttpClientHandlers。 通过了最后一个阶段,我们的应用程序提前准备价格,并以一种有效且有弹性的方式遵循。net核心范例。 总结 最后,应用了各种提高生产率的方法的应用程序。 与第1部分中的测试应用程序相比,最新版本更快,使用基础设施也更有效。 的兴趣点 在第1部分和第2部分中,我们一步一步地开发应用程序,主要关注于应用和检查不同方法的方便性,以修改代码和检查结果。但是现在,在我们选择并实现了这些方法之后,我们可以将应用程序作为一个整体来考虑。显然,代码需要进行一些重构。 因此,在第3部分中,将asp.net的深度重构和优化命名为。NET Core WEB API应用程序代码,我们将专注于简洁的代码,全局错误处理,输入参数验证,文档化和其他重要的功能,一个好的编写应用程序必须具备。 本文转载于:http://www.diyabc.com/frontweb/news19257.html