全栈开发:使用.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服务器可以看到我们的缓存信息:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11893.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python写一个爱心

项目代码&#xff1a; import random from math import sin, cos, pi, log from tkinter import *# 定义窗口的大小 CANVAS_WIDTH 640 CANVAS_HEIGHT 480 CANVAS_CENTER_X CANVAS_WIDTH / 2 CANVAS_CENTER_Y CANVAS_HEIGHT / 2 IMAGE_ENLARGE 11 # 定义爱心的颜色 HEART_…

leetcode——二叉树的最近公共祖先(java)

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&#xff08;一个节点也可以是它自己的…

Android学习制作app(ESP8266-01S连接-简单制作)

一、理论 部分理论见arduino学习-CSDN博客和Android Studio安装配置_android studio gradle 配置-CSDN博客 以下直接上代码和效果视频&#xff0c;esp01S的收发硬件代码目前没有分享&#xff0c;但是可以通过另一个手机网络调试助手进行模拟。也可以直接根据我的代码进行改动…

20250202在Ubuntu22.04下使用Guvcview录像的时候降噪

20250202在Ubuntu22.04下使用Guvcview录像的时候降噪 2025/2/2 21:25 声卡&#xff1a;笔记本电脑的摄像头自带的【USB接口的】麦克风。没有外接3.5mm接口的耳机。 缘起&#xff1a;在安装Ubuntu18.04/20.04系统的笔记本电脑中直接使用Guvcview录像的时候底噪很大&#xff01; …

MySQL子查询

一、子查询的概述 1、理解&#xff1a;可以理解为嵌套查询&#xff0c;查询的内部进行查询 2、称谓规范&#xff1a;外查询&#xff08;主查询&#xff09;、内查询&#xff08;子查询&#xff09;&#xff0c;这种称呼是相对的。 子查询&#xff08;内查询&#xff09;在主查…

MongoDb user自定义 role 添加 action(collStats, EstimateDocumentCount)

使用 mongosh cd mongsh_bin_path mongosh “mongodb://user:passip:port/db”这样就直接进入了对应的db 直接输入&#xff1a; 这样 role “read_only_role" 就获得了3个 action&#xff0c; 分别是 查询&#xff0c;列举集合&#xff0c;集合元数据查询 P.S: 如果没有 …

结构体DMA串口接收比特错位

发送&#xff1a; 显示&#xff1a; uint16_t接收时候会比特错位。

经典本地影音播放器MPC-BE.

经典本地影音播放器MPC-BE 链接&#xff1a;https://pan.xunlei.com/s/VOIAZbbIuBM1haFdMYCubsU-A1?pwd4iz3# MPC-BE&#xff08;Media Player Classic Black Edition&#xff09;是来自 MPC-HC&#xff08;Media Player Classic Home Cinema&#xff09;的俄罗斯开发者重新…

python学opencv|读取图像(五十四)使用cv2.blur()函数实现图像像素均值处理

【1】引言 前序学习进程中&#xff0c;对图像的操作均基于各个像素点上的BGR值不同而展开。 对于彩色图像&#xff0c;每个像素点上的BGR值为三个整数&#xff0c;因为是三通道图像&#xff1b;对于灰度图像&#xff0c;各个像素上的BGR值是一个整数&#xff0c;因为这是单通…

【开源免费】基于Vue和SpringBoot的工作流程管理系统(附论文)

本文项目编号 T 193 &#xff0c;文末自助获取源码 \color{red}{T193&#xff0c;文末自助获取源码} T193&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

IntelliJ IDEA远程开发代理远程服务器端口(免费内网穿透)

IntelliJ IDEA远程开发代理远程服务器端口&#xff08;免费内网穿透&#xff09;&#xff08;JetBrains家的其他IDE应该也支持&#xff09; 之前看到宇宙第一IDE VS Code好像默认代理了远程的端口&#xff0c;但是一直没找到IDEA的同类功能&#xff0c;这次终于发现了 以Intell…

文字显示省略号

多行文本溢出显示省略号

STM32_SD卡的SDIO通信_DMA读写

本篇&#xff0c;将使用CubeMXKeil&#xff0c;创建一个SD卡的DMA读写工程。 目录 一、简述 二、CubeMX 配置 SDIO DMA 三、Keil 编辑代码 四、实验效果 实现效果&#xff0c;如下图&#xff1a; 一、简述 上篇已简单介绍了SD、SDIO&#xff0c;本篇不再啰嗦&#xff0c;…

智能小区物业管理系统推动数字化转型与提升用户居住体验

内容概要 在当今快速发展的社会中&#xff0c;智能小区物业管理系统的出现正在改变传统的物业管理方式。这种系统不仅仅是一种工具&#xff0c;更是一种推动数字化转型的重要力量。它通过高效的技术手段&#xff0c;将物业管理与用户居住体验紧密结合&#xff0c;无疑为社区带…

基于STM32景区环境监测系统的设计与实现(论文+源码)

1系统方案设计 根据系统功能的设计要求&#xff0c;展开基于STM32景区环境监测系统设计。如图2.1所示为系统总体设计框图。系统以STM32单片机作为系统主控模块&#xff0c;通过DHT11传感器、MQ传感器、声音传感器实时监测景区环境中的温湿度、空气质量以及噪音数据。系统监测环…

八. Spring Boot2 整合连接 Redis(超详细剖析)

八. Spring Boot2 整合连接 Redis(超详细剖析) 文章目录 八. Spring Boot2 整合连接 Redis(超详细剖析)2. 注意事项和细节3. 最后&#xff1a; 在 springboot 中 , 整合 redis 可以通过 RedisTemplate 完成对 redis 的操作, 包括设置数据/获取数据 比如添加和读取数据 具体整…

【Unity3D】Tilemap俯视角像素游戏案例

目录 一、导入Tilemap 二、导入像素风素材 三、使用Tilemap制作地图 3.1 制作Tile Palette素材库 3.2 制作地图 四、实现A*寻路 五、待完善 一、导入Tilemap Unity 2019.4.0f1 已内置Tilemap 需导入2D Sprite、2D Tilemap Editor、以及一个我没法正常搜出的2D Tilemap…

企微SCRM驱动企业私域流量营销与客户关系管理的智慧革新

内容概要 在当今竞争激烈的商业环境中&#xff0c;企微SCRM逐渐成为企业实现私域流量营销和优化客户关系管理的重要工具。它的出现不仅提升了企业的工作效率&#xff0c;也改变了传统的营销方式。那么&#xff0c;究竟什么是企微SCRM呢&#xff1f;简单来说&#xff0c;它是将…

数据库、数据仓库、数据湖有什么不同

数据库、数据仓库和数据湖是三种不同的数据存储和管理技术&#xff0c;它们在用途、设计目标、数据处理方式以及适用场景上存在显著差异。以下将从多个角度详细说明它们之间的区别&#xff1a; 1. 数据结构与存储方式 数据库&#xff1a; 数据库主要用于存储结构化的数据&…

前端力扣刷题 | 6:hot100之 矩阵

73. 矩阵置零 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 法一&#xff1a; var setZeroes function(matrix) {let setX new Set(); // 用于存储需要置零的行索引let setY new Set(); //…