ToB企服应用市场:ToB评测及商务社交产业平台
标题:
全栈开发:使用.NET Core WebAPI构建前后端分离的核心本领(一)
[打印本页]
作者:
大连全瓷种植牙齿制作中心
时间:
2025-2-13 18:00
标题:
全栈开发:使用.NET Core WebAPI构建前后端分离的核心本领(一)
目次
cors解决跨域
依靠注入使用
分层服务注册
缓存方法使用
内存缓存使用
缓存过期清理
缓存存在问题
分布式的缓存
cors解决跨域
前后端分离已经成为一种越来越流行的架构模式,由于跨域资源共享(cors)是欣赏器的一种安全机制,它会制止前端应用向不同域的服务器发起哀求,保护用户的隐私和数据安全。为了在前后端分离的应用中确保前端可以安全地访问后端的接口,不会受到欣赏器的跨域限制,这里我们可以通过后端进行相应的cors配置。
起首我们先搭建一下.net core webapi的框架,不相识的可以参考我之前的文章:地址 ,然后我们配置了一个登录的接口,返回的结果是记录的范例,然后固定了一下登录成功的用户和暗码,如下所示:
/// <summary>
/// 登录验证
/// </summary>
/// <param name="res"></param>
/// <returns></returns>
public record LoginRequest(string UserName, string Password);
public record ProcessInfo(long Id, string Name, long WorkingSet); // 记录类型
public record LoginResponse(bool OK, ProcessInfo[]? ProcessInfos);
[HttpPost]
[Route("login/user")] // 特性路由
public LoginResponse Login(LoginRequest res)
{
if (res.UserName == "admin" && res.Password == "123456")
{
var items = Process.GetProcesses().Select(x => new ProcessInfo(x.Id, x.ProcessName, x.WorkingSet64));
return new LoginResponse(true, items.ToArray()); // 返回记录类型
}
else
{
return new LoginResponse(false, null);
}
}
复制代码
然后我们就须要在入口文件Program.cs中配置一下我们答应要跨域的源,这里我们直接输入前端运行服务器的域名和端口即可,然后设置答应规则,这里我们正常就都答应,如果想配置部分答应的话,通过With函数进行筛选即可,如下:
// 配置跨域策略
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.WithOrigins("http://localhost:3000") // 允许跨域的源
.AllowAnyHeader() // 允许任何头
.AllowAnyMethod() // 允许任何方法
.AllowCredentials() // 允许携带凭证
.WithExposedHeaders("X-Custom-Header"); // 暴露自定义头信息
});
});
app.UseCors(); // 使用跨域策略
复制代码
然后前端的话,这里我们就使用react框架通过axios发起哀求,不相识react的朋侪,可参加我之前的文章:地址 ,然后我们通过如下的一个示例代码进行哀求的发起:
import axios from "axios"
import { useState } from "react"
const WebApi = () => {
const [userName, setUserName] = useState<string>('')
const [password, setPassword] = useState<string>('')
const [processInfo, setProcessInfo] = useState<any>([])
const reqPost = () => {
axios.post('http://localhost:5263/First/login/user', { userName: userName, password: password }).then(res => {
if (res.data.ok) {
setProcessInfo(res.data.processInfos)
} else {
alert('登录失败, 请重新登录!')
}
})
}
return (
<div>
账户: <input type="text" onChange={(e: any) => setUserName(e.target.value)} /> <br />
密码: <input type="password" onChange={(e: any) => setPassword(e.target.value)} /> <br />
<button onClick={() => reqPost()}>发起请求</button>
{processInfo.map((item: any) => <div key={item.id}>{item.name}</div>)}
</div>
)
}
export default WebApi
复制代码
最终出现的结果如下所示:
依靠注入使用
依靠注入通过将对象的创建和管理交给框架,而不是在类内部直接创建,可以有效地解耦各个模块,使得每个组件都可以大概独立地进行测试和维护。这对于实现前后端分离的架构至关重要,因为它答应开发者更灵活地控制和管理后端服务,使得前端与后端的交互更加清晰、可靠。具体可以参考我之前的文章:地址 ,这里不再赘述,然后接下来我们开始演示在WebAPI中如何使用依靠注入:
构造函数注入服务操作:传统且经典的创建依靠注入
创建服务
:这里我们直接可以创建一个两数相加的服务函数,如下所示:
namespace netCoreWebApi
{
public class Calculator
{
public int Add(int i1, int i2)
{
return i1 + i2;
}
}
}
复制代码
服务注册
:然后我们在入口文件中进行服务注册,如下所示:
builder.Services.AddScoped<Calculator>(); // 注册Calculator服务
复制代码
依靠注入
:然后我们在控制器文件中通过构造函数进行服务注入:
using Microsoft.AspNetCore.Mvc;
using netCoreWebApi.WebCore;
namespace netCoreWebApi.Controllers
{
[ApiController]
[Route("api/[controller]/[action]")]
[ApiExplorerSettings(GroupName = nameof(ApiVersionInfo.V1))]
public class FirstController : ControllerBase
{
private readonly Calculator calculator;
public FirstController(Calculator calculator)
{
this.calculator = calculator;
}
[HttpGet]
public int Add1()
{
return calculator.Add(1, 2);
}
}
}
复制代码
答应项目得到的结果如下所示,果然是3:
低使用频率服务:一些耗时的依靠注入可能会影响其他接口的调用,这里我们须要使用该注入方式进行解决,一般的接口创建不须要使用该服务,只有调用频率不高且资源的创建比力斲丧资源的服务才会使用
创建服务
:这里我们直接可以创建一个比力泯灭资源的扫描文件服务函数,如下所示:
namespace netCoreWebApi
{
public class SearchService
{
private string[] files;
public SearchService()
{
this.files = Directory.GetFiles("d:/","*.exe", SearchOption.AllDirectories);
}
public int Count
{
get
{
return this.files.Length;
}
}
}
}
复制代码
服务注册
:然后我们在入口文件中进行服务注册,如下所示:
builder.Services.AddScoped<SearchService>(); // 注册SearchService服务
复制代码
依靠注入
:然后我们在控制器文件中通过构造函数进行服务注入,把Action用到的服务通过Action的参数注入,在这个参数上标注[FromServices],和Action的其他参数不辩论,只有Action方法才气使用[FromServices],普通的类默认不支持,如下所示:
[HttpGet]
public int Test1([FromServices]SearchService searchService) // 只有请求这个方法时才会注入SearchService
{
return searchService.Count;
}
复制代码
如下当哀求泯灭较多资源的时间,哀求时间才会过长,哀求其他不泯灭资源的接口,正常哀求:
分层服务注册
从上面的依靠服务注册使用我们可以相识到,当我们想进行依靠注入的使用,都须要在入口文件进行服务的注册,但是项目一旦巨大起来或者说服务一旦多起来,多人协作开发的时间再要求所有的服务都必须注册在入口文件中就会导致一些问题的辩论,如下所示就是典范的例子:
这里我们须要对服务注册进行解耦操作,即进行分层处置处罚。在分层项目中,让各个项目负责各自的服务注册,这里我们须要先安装一下下面这个依靠包:
然后这里我们创建多个类库,模仿多个服务的使用,然后将这些服务引用到项目上:
然后在每个项目中创建一个或多个实现IModuleInitializer接口的类,然后将服务注册的函数写道该接口类当中,如下所示:
然后我们通过反射原理,将服务注册的函数来映射到入口函数当中,具体代码如下所示:
然后我们再次运行项目,发现我们的服务还是成功被运行起来了,如下所示:
缓存方法使用
缓存
:是系统优化中简单又有效的工具,投入小收效大,数据库中的索引等简单有效的优化功能本质上都是缓存,其将常常访问的数据存储在一个快速访问的存储地区(如内存)中,从而减少对数据库或其他慢速存储系统的重复访问。缓存可以大概明显进步应用步伐的性能,尤其是在须要频仍读取大量数据时。
客户端响应缓存
:RFC7324是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头,服务器如果返回cache-control: max-age-60,则表现服务器指示欣赏器端可以缓存这个响应内容60秒
这里我们只须要给进行缓存控制的控制器的操作方法添加ResponseCache这个Attribute,.net core会自动添加cache-control报文头,如下所示我们设置了一个获取当前时间的接口,正常环境下每次哀求接口都是最新的时间,这里添加了缓存20秒导致了哀求在20秒之内的数据都是不变的:
服务端响应缓存
:服务端缓存整个HTTP响应,而不是仅仅缓存其中的数据或部分内容。这样,服务器可以直接返回已经缓存的响应,而不须要重新处置处罚哀求和生成新的响应。服务端响应缓存可以明显进步性能,特别是在处置处罚重复的哀求时。
如果.net core中安装了响应缓存中间件,那么.net core不仅会继承根据[ResponseCache]设置来生成cache-control响应报文头来设置客户端缓存,而且服务器端也会按照[ResponseCache]的设置来对响应进行服务器端缓存,使用方法如下所示,在入口文件处在app.MapControllers()之前添加app.UseResponseCaching(),请确保如果你的项目如果存在app.UseCors()的话,该函数的调用也要写在app.UseResponseCaching()之前,如下所示:
注意,如果你勾选了欣赏器当中的禁用缓存的按钮,不仅是客户端,服务器端在哀求的时间由于带上了no-cache,服务器端也会禁用掉所有的缓存:
固然服务器缓存还是很鸡肋的,它无法解决恶意哀求带给服务器的压力,服务器响应缓存还有很大的限制,包括但不限于:响应状态码为200的GET或者HEAD响应才气被缓存;报文头中不能含有Authorization、Set-Cookie等,为相识决这些问题我们还须要接纳内存或者分布式进行缓存。
内存缓存使用
内存缓存
:是指将数据存储在盘算机的内存中以便快速访问和进步系统性能的一种技术,通常内存缓存用于存储那些频仍访问且盘算或获取成本较高的数据,目的是减少从磁盘或其他慢速存储装备中读取数据的次数,从而加快应用步伐的响应速度。
内存缓存的数据生存在当前运行的网站步伐的内存中,是和进程相干的。因为在Web服务器中多个不同的网站是运行在不同的进程中的,因此不同的网站的内存缓存是不会相互干扰的,而且网站重启之后内存缓存中的所有数据也就都被清空了。内存缓存的使用方法如下所示:
注册内存缓存服务:这里我们须要先在入口文件进行内存缓存服务的注册,如下所示:
builder.Services.AddMemoryCache(); // 添加内存缓存服务
复制代码
这里我们先创建一个MyDbContext来模仿一下数据库当中的数据,并设置一个函数返回数据:
namespace webapi_study
{
public class MyDbContext
{
public static Task<Book?> GetByIdAsync(long id)
{
var result = GetById(id);
return Task.FromResult(result);
}
public static Book? GetById(long id)
{
switch (id)
{
case 0:
return new Book(0, "C#", "张三");
case 1:
return new Book(1, "Java", "李四");
case 2:
return new Book(2, "Python", "王五");
default:
return null;
}
}
}
}
复制代码
接下来我们在控制器的接口中注册一下缓存服务,通过GetOrCreateAsync函数拿到缓存当中的数据,如果缓存当中没有数据的话我们就正常哀求接口拿到数据即可,如下所示:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace webapi_study.Controllers
{
[ApiController]
[Route("api/[controller]/[action]")]
public class TestController : ControllerBase
{
private readonly IMemoryCache cache; // 注入缓存服务
private readonly ILogger<TestController> logger; // 注入日志服务
public TestController(IMemoryCache cache, ILogger<TestController> logger)
{
this.cache = cache;
this.logger = logger;
}
[HttpGet]
public async Task<ActionResult<Book?>> GetBookById(long id)
{
// 1) 从缓存中获取数据 2)从数据库中获取数据 3)返回给调用者并将数据存入缓存
logger.LogInformation($"开始执行GetBookById: {id}");
Book? b = await cache.GetOrCreateAsync("book" + id, async (e) =>
{
logger.LogInformation($"缓存中没有找到,到数据库中查一查,id={id}");
return await MyDbContext.GetByIdAsync(id);
});
logger.LogInformation($"GetOrCreateAsync结果是:{id}");
if (b == null)
{
return NotFound($"Book with id {id} not found");
}
else
{
return b;
}
}
}
}
复制代码
最终出现的结果如下所示,我们哀求两次接口,第一次哀求数据库中的数据因为没有缓存数据,所有是哀求的接口,第二次是由于缓存中已经存在数据了,我们就直接拿到缓存当中的数据即可:
缓存过期清理
上面我们简单的先容了一下内存缓存的简单使用,但是上面的例子中缓存是不会过期的,除非重启服务器进行重置操作,但是重置服务器的代价太大了,这里我们须要对在数据改变的时间缓存的处置处罚,如下所示:
手动清理缓存
:在数据改变的时间调用Remove或者Set来删除或修改缓存(长处:及时)
设置过期时间
:只要过期时间比力短,缓存数据不一致的清空也不会连续太少时间,可以通过两种过期时间策略进行:绝对过期时间;滑动过期时间
绝对过期时间:顾名思义就是我设置了一个过期时间,凌驾这个时间缓存自动被清除,如下所示:
滑动过期时间:顾名思义就是只要在缓存没过期的时间哀求一次,缓存就会自动续命一段时间:
两种过期时间混用:使用滑动过期时间策略,如果一个缓存项一直被频仍访问,那么这个缓存项就会一直被续期而不会过期,可以对一个缓存项同时设定滑动过期时间和绝对过期时间,而且把绝对过期时间设定比滑动过期时间长,这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但是一旦凌驾了绝对过期时间,缓存项就会被删除,如下所示:
总结
:无论使用哪种过期时间策略,步伐中都会存在缓存不一致的清空,部分系统(博客系统等)无所谓,部分系统不能忍受(比如金融),可以通过其他机制获取数据源改变的消息,再通过代码调用IMemoryCache的Set方法更新缓存。
缓存存在问题
在内存缓存中,缓存穿透和缓存雪崩是两种常见且须要特别注意的问题,下面扼要讨论这两个问题及其解决方法:
缓存穿透
:是指查询的数据在缓存中不存在,而且每次查询都直接访问数据库。通常缓存穿透发生在以下几种环境:
1)查询的哀求数据根本不在数据库中(例如,恶意哀求或数据不存在)。
2)数据被误删除或没有被正确存入缓存。
造成影响
:
1)每次哀求都访问数据库,导致数据库负载加重,降低系统性能。
2)缓存无法有效进步访问速度,因为每次都须要从数据库中读取数据。
解决方案如下:
缓存空结果:对于一些常见的不存在数据(例如查询某个ID的数据返回为空),可以将“空”数据也缓存起来。设置一个较短的过期时间防止数据库不断查询雷同的无效数据:
缓存雪崩
:是指缓存中的大量数据在同一时间过期或失效,导致大量哀求同时访问数据库,造成数据库压力剧增,乃至瓦解。常见的触发场景是:
1)大量缓存失效:如果缓存的失效时间设置雷同或接近,那么这些缓存项会在同一时间失效,导致大量哀求同时查询数据库。
2)数据库访问压力骤增:所有缓存失效后,系统会将大量的哀求直接发送到数据库,从而加重数据库负载。
造成影响
:
1)短时间内大量哀求集中访问数据库,轻易造成数据库瓦解或性能严峻下降。
2)数据库的负载激增,可能导致响应延迟和系统整体性能下降。
解决方案如下:
在基础过期时间之上再加一个随机的过期时间:
分布式的缓存
分布式缓存是一种将缓存数据分布在多个节点上的技术,目的是进步系统的可扩展性、可用性和性能。在大型系统中,单一的缓存节点每每无法满足高并发、高可用的需求,分布式缓存应运而生。
分布式内存缓存:如果集群节点的数量非常多的话,这样的重复查询也同样可能会把数据库压垮
分布式缓存服务器: 分布式缓存是指将缓存数据分布到多个不同的服务器节点上,这些节点共同协作提供缓存服务。用户的哀求通过负载平衡的方式访问不同的缓存节点。常见的分布式缓存技术有:
1)Redis:最流行的分布式缓存系统之一,支持内存存储和丰富的数据布局。
2)Memcached:另一个常见的分布式缓存,适合简单的键值对缓存场景。
3)Alibaba Tair:阿里巴巴自研的分布式缓存系统,主要服务于大规模的互联网应用。
.net core中提供了同一的分布式缓存服务器的操作接口IDistributedCache,用法和内存缓存类似,分布式缓存和内存缓存的区别在于:缓存值的范例为byte[],须要我们进行范例转换,也提供了一些安装string范例存取缓存值的扩展方法,如下所示:
方法分析Task<byte[]>GetAsync(string key)查询缓存键key对应的缓存值,返回值是byte[]范例,如果对应的缓存不存在,则返回null。Task RefreshAsync(string key)刷新缓存键key对应的缓存项,会对设置了滑动过期时间的缓存项续期。Task RemoveAsync(string key)删除缓存键key对应的缓存项Task SetAsync(string key, byte[] value,DistributedCacheEntryOptions options)设置缓存键key对应的缓存项:value属性为byte范例的缓存值,注意value不能是null值Task<string> GetStringAsync(string key)按照string范例查询缓存键key对应的缓存值,返回值是string范例,如果对应的缓存不存在则返回null。Task SetStringAsync(string key. string value,DistributedCacheEntryOptions options)设置缓存键key对应的缓存项,value属性为string范例的缓存值,注意value不能是null值。 对于用什么做缓存服务器,用SQL Server做缓存其性能并不好;Memcached是缓存专用,性能非常高但是集群、高可用等方面比力弱,而且有”缓存键的最大长度为250字节“等限制,可以安装EnyimMemcachedCore这个第三方NuGet包;Redis不范围于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的高可用、集群等方面非常强大,适合在数据量大、高可用性等场所使用,可以按照如下插件进行使用:
然后我们在入口文件进行服务注册:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379"; // 配置连接字符串
options.InstanceName = "SampleInstance"; // 配置实例名称,避免缓存冲突
});
复制代码
然后我们在控制器当中构造分布式缓存的服务:
然后通过GetStringAsync函数构造当前的id,来判定当前是否存在缓存
[HttpGet]
public async Task<ActionResult<Book?>> GetBookById1(long id)
{
Book? book;
string? s = await disCache.GetStringAsync("book" + id);
if (s == null)
{
book = await MyDbContext.GetByIdAsync(id);
await disCache.SetStringAsync("book" + id, JsonSerializer.Serialize(book));
}
else
{
book = JsonSerializer.Deserialize<Book?>(s);
}
if (book == null)
{
return NotFound($"Book with id {id} not found");
}
else
{
return book;
}
}
复制代码
通过redis服务器可以看到我们的缓存信息:
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4