加快ASP。NET Core WEB API应用程序。第2部分
使用各种方法来增加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
加快ASP。NET Core WEB API应用程序。第2部分的更多相关文章
- Docker容器环境下ASP.NET Core Web API应用程序的调试
本文主要介绍通过Visual Studio 2015 Tools for Docker – Preview插件,在Docker容器环境下,对ASP.NET Core Web API应用程序进行调试.在 ...
- 在docker中运行ASP.NET Core Web API应用程序
本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Cor ...
- 在docker中运行ASP.NET Core Web API应用程序(附AWS Windows Server 2016 widt Container实战案例)
环境准备 1.亚马逊EC2 Windows Server 2016 with Container 2.Visual Studio 2015 Enterprise(Profresianal要装Updat ...
- Docker容器环境下ASP.NET Core Web API
Docker容器环境下ASP.NET Core Web API应用程序的调试 本文主要介绍通过Visual Studio 2015 Tools for Docker – Preview插件,在Dock ...
- docker中运行ASP.NET Core Web API
在docker中运行ASP.NET Core Web API应用程序 本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过 ...
- 支持多个版本的ASP.NET Core Web API
基本配置及说明 版本控制有助于及时推出功能,而不会破坏现有系统. 它还可以帮助为选定的客户提供额外的功能. API版本可以通过不同的方式完成,例如在URL中添加版本或通过自定义标头和通过Accept- ...
- 在ASP.NET Core Web API中为RESTful服务增加对HAL的支持
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务 ...
- [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了
[译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 本文首发自:博客园 文章地址: https://www.cnblogs.com/yilezhu/p/ ...
- C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志
C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...
- ASP.NET Core Web API 教程 - Project Configuration
ASP.NET Core Web API 教程 本系列文章主要参考了<Ultimate ASP.NET Core 3 Web API>一书,我对原文进行了翻译,同时适当删减.修改了一部分内 ...
随机推荐
- Java实现IO通信(服务器篇)
Java实现IO通信(服务器篇) 如何利用java实现我们的通信呢?首先我们了解一下什么是通信?通信的机制是怎样的? 首先来讨论一下什么是通信?通信,指人与人或人与自然之间通过某种行为或媒介进行的信息 ...
- Android studio Debug 源码
原来有的地方打不了断点 会提示no executable code at line xxx 源码sdk里有,sdkManager下好对应版本,然后使用对应版本的模拟器debug就行了 如果要debug ...
- deepin20 安装英伟达闭源驱动
第一步.安装深度的"显卡驱动器" 在deepin v20 中默认没有显卡驱动管理器,需要命令行安装,命令如下(刚开始一直出错,当我第一次打开应用商店,就可以安装了,好神奇): su ...
- 解决 Mac 上 Docker 无法直接 ping 通的问题
解决 Mac 上 Docker 无法直接 ping 通的问题 原文连接 一.背景 Mac os Mojave 10.14.3 Docker Desktop community 2.3.0.4 二.问题 ...
- python应用 处理excel数据
实现功能 excel表格中有4列数,分别为RMF计算得到的 β,γ,势能面及组态,需要挑选出相同 β 值下势能面最低时的组态.为了减小数据量,先将 β 值保留两位小数. 代码 import xlrd ...
- selenium常用webdriver api汇总
1.driver.current_url:用于获得当前页面的URL 2.driver.title:用于获取当前页面的标题 3.driver.page_source:用于获取页面html源代码 4.dr ...
- delphi DBgrid应用全书
在一个Dbgrid中显示多数据库 在数据库编程中,不必要也不可能将应用程序操作的所有数据库字段放入一个数据库文件中.正确的数据库结构应是:将数据库字段放入多个数据库文件,相关的数据库都包含一个唯 ...
- 谈谈 mysql和oracle的使用感受 -- 差异
之前一直使用mysql作为存储数据库,虽然中间偶尔使用sqlite作为本地数据库存储,但没有感觉多少差别. 后来遇上了oracle,且以其作为主要存储,这下就不得不好好了解其东西了.oracle作为商 ...
- adb无线连接android手机进行调式,无需获得root权限
利用adb无线连接android手机进行调式 无需获得root权限 转载来自CSDN https://blog.csdn.net/lnking1992/article/details/5346518 ...
- range如何倒序
for j in range(3,-2,-1): 表示对3进行每次加-1的操作,直到-2,但不包括-2 print(j) 打印出3 2 1 0 -1都换行展示的