ShardingCore实现Saas多租户

ShardingCore是一款efcore下高性能、轻量级针对分表分库读写分离的框架。正好多租户需要用到分库分表,因此选择了这个框架,其官方文档提供了详细的多租户教程,我实现的过程中绝大部分照搬了这个教程。最后架子搭起来,写个文章记录一下,也让自己理解的深入一些。

1. 多租户

多租户主要有三种实现的方式,多数据库,单数据库多架构(Schema),单数据库单架构。简单来说就是不同的租户的数据是存在不同的数据库,或者一个数据库的不同架构,或者仅仅以表中的某个字段来区分,由此可以很容易的理清三种方案在成本、编写逻辑、维护、数据备份与恢复等方面的优劣势。

因为ShardingCore并没有直接提供分架构的用法,我又想采取折中方案,即不同的租户的数据存储在相同数据库的不同的Schema中,所以我的设想是使用ShardingCore分库的方法来实现分架构,然后我失败了。这里需要简单介绍一下我的数据库,项目选用的是PostgreSQL,因为要存储地理数据,因此数据库加装了postgis扩展,项目中引用了Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite库。这时候问题来了,PostgreSQL数据库中postgis扩展只能属于一个架构,没法所有架构都用,而我每个租户有各自的地理数据,因此没法采用这种方案,最终选择了成本最高,隔离性和安全性最好的多数据库的方案。

2. 租户与用户

这部分是我与官方教程比较大的区别。我的想法是租户是租户,用户是用户,一个租户可以有很多个用户,租户的管理员只是租户用户中的一个,因此,租户存在主数据库中,用户存在租户数据库中。

主数据库的DbContext

public class DefaultDbContext: DbContext
{public DefaultDbContext(DbContextOptions<DefaultDbContext> options) : base(options){}protected override void OnConfiguring(DbContextOptionsBuilder builder){base.OnConfiguring(builder);}/// <summary>/// 租户表/// </summary>public DbSet<Tenant> Tenants { get; set; }protected override void OnModelCreating(ModelBuilder builder){base.OnModelCreating(builder);string assemblyName = AssemblyName.GetAssemblyName(Assembly.GetExecutingAssembly().Location).FullName;Assembly assembly = Assembly.Load(assemblyName);foreach (var type in assembly.GetTypes()){var baseclass = type.BaseType;if (baseclass == typeof(EntityBase)){//种子数据MethodInfo method = type.GetMethod("HasData");if (method != null){var data = (IEnumerable<object>)method.Invoke(null, null);builder.Entity(type.FullName).HasData(data);}else{builder.Entity(type.FullName);}}}}
}

租户数据库的DbContext

public class TenantDbContext: AbstractShardingDbContext
{public TenantDbContext(DbContextOptions<TenantDbContext> options) : base(options){}/// <summary>/// 用户表/// </summary>public DbSet<User> Users { get; set; }protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);string assemblyName = AssemblyName.GetAssemblyName(Assembly.GetExecutingAssembly().Location).FullName;Assembly assembly = Assembly.Load(assemblyName);foreach (var type in assembly.GetTypes()){var baseclass = type.BaseType;if (baseclass == typeof(TenantEntityBase)){//种子数据MethodInfo method = type.GetMethod("HasData");if (method != null){var data = (IEnumerable<object>)method.Invoke(null, null);modelBuilder.Entity(type.FullName).HasData(data);}else{modelBuilder.Entity(type.FullName);}}}}

这里TenantDbContext继承自AbstractShardingDbContext,是ShardingCore分库的配置。

我的想法是系统的超级管理员也只是系统中一个特别的租户而已,也就是这个租户可以管理其他租户,类似于二房东,也住这栋楼里。所以超级管理员登录,跟一个普通租户的普通用户登录应该没有什么区别,因此我额外加了一个常量,来记录这个二房东的ID。

/// <summary>
/// 租户相关的常量
/// </summary>
public static class TenantConstant
{/// <summary>/// 系统租户ID/// </summary>public const long SYSTEM_TENANT_ID = 1;
}

3. 租户与DbContext

每个租户是不同的数据库,有不同的连接字符串,因此不能如下直接注入,得对每个租户分别注入TenantDbContext。

builder.Services.AddDbContext<DefaultDbContext>(options => {options.UseNpgsql(builder.Configuration.GetConnectionString("NpgContext")); 
});

因为使用了ShardingCore,他允许通过IShardingRuntimeContext来进行注入。首先构造一个租户与注入参数的中间类:

/// <summary>
/// 租户分库配置
/// </summary>
public class ShardingTenantOptions
{/// <summary>/// 默认数据源名称/// </summary>public string DefaultDataSourceName { get; set; }/// <summary>/// 默认数据库地址/// </summary>public string DefaultConnectionString { get; set; }/// <summary>/// 分片迁移的命名空间关键字/// </summary>public string MigrationNamespace { get; set; }
}

创建一个IShardingRuntimeContext构建器

public class ShardingBuilder : IShardingBuilder
{public static readonly ILoggerFactory efLogger = LoggerFactory.Create(builder =>{builder.AddFilter((category, level) =>category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole();});private readonly IServiceProvider _serviceProvider;private readonly IConfiguration _configuration;public ShardingBuilder(IServiceProvider serviceProvider, IConfiguration configuration){_serviceProvider = serviceProvider;_configuration = configuration;}public IShardingRuntimeContext Build(ShardingTenantOptions tenantOptions){var shardingRuntimeBuilder = new ShardingRuntimeBuilder<TenantDbContext>().UseRouteConfig(o =>{//只有超级管理员租户才需要用到ShardingCore的分库特性,其他租户只需要用到自己的数据库if (tenantOptions.DefaultDataSourceName == $"tenant_{TenantConstant.SYSTEM_TENANT_ID}"){o.AddTenantDataSourceRoute();}}).UseConfig(o =>{o.ThrowIfQueryRouteNotMatch = false;o.UseShardingQuery((conStr, builder) =>{builder.UseNpgsql(conStr, b => b.UseNetTopologySuite(geographyAsDefault: true)).UseMigrationNamespace(new NpqsqlMigrationNamespace(tenantOptions.MigrationNamespace));builder.UseLoggerFactory(efLogger).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking).ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();});o.UseShardingTransaction((connection, builder) =>{builder.UseNpgsql(connection, b => b.UseNetTopologySuite(geographyAsDefault: true));builder.UseLoggerFactory(efLogger).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);});o.AddDefaultDataSource(tenantOptions.DefaultDataSourceName, tenantOptions.DefaultConnectionString);超级管理员租户需要用到ShardingCore的分库特性,需要添加额外数据库if (tenantOptions.DefaultDataSourceName == $"tenant_{TenantConstant.SYSTEM_TENANT_ID}"){using (var scope = _serviceProvider.CreateScope()){var defaultDbContext = scope.ServiceProvider.GetRequiredService<DefaultDbContext>();var tenantIds = defaultDbContext.Tenants.Where(t => t.Id != TenantConstant.SYSTEM_TENANT_ID).Select(t => t.Id).ToList();var connectionStr = _configuration["ConnectionStrings:NpgContext"];var baseDbName = _configuration["Tenant:BaseDb"];var dsource = new Dictionary<string, string>();foreach (var tid in tenantIds){var dbName = _configuration["Tenant:TenantDbTemp"].Replace("{tenant}", $"Tenant{tid}");dsource.Add($"tenant_{tid}", connectionStr.Replace(baseDbName, dbName));}o.AddExtraDataSource(sp =>{return dsource;});}}//注意这个迁移必须要十分重要o.UseShardingMigrationConfigure(b =>{b.ReplaceService<IMigrationsSqlGenerator, ShardingNpsqlMigrationsSqlGenerator>();});}).AddServiceConfigure(s =>{//IShardingRuntimeContext内部的依赖注入s.AddSingleton(tenantOptions);});shardingRuntimeBuilder.ReplaceService<ITableEnsureManager, PostgreSqlTableEnsureManager>(ServiceLifetime.Singleton);return shardingRuntimeBuilder.Build(_serviceProvider);}
}

代码中有大量关于迁移的内容以及超级管理员租户专用的分库配置,下面详细说明。用起来通常是通过租户信息实例化一个ShardingTenantOptions,用来Build一个IShardingRuntimeContext,通过租户管理器与租户一一对应存下来。

var shardingRuntimeContext = _shardingBuilder.Build(new ShardingTenantOptions() {DefaultDataSourceName = $"tenant_{tenant.Id}",DefaultConnectionString = connectionStr,MigrationNamespace = $"T{tenant.Id}",
});_tenantManager.AddTenantSharding(tenant.Id, shardingRuntimeContext);

最后通过IShardingRuntimeContext来一一注入TenantDbContext

builder.Services.AddDbContext<TenantDbContext>((sp, b) =>
{var tenantManager = sp.GetRequiredService<ITenantManager>();var currentTenantContext = tenantManager.GetCurrentTenantContext();//如果有上下文那么创建租户dbcontextif (currentTenantContext != null){var shardingRuntimeContext = currentTenantContext.GetShardingRuntimeContext();b.UseDefaultSharding<TenantDbContext>(shardingRuntimeContext);}
});

4. 租户管理器

上面提到了租户管理器将租户和租户的DbContext(IShardingRuntimeContext)管理起来,这是租户管理器的核心功能,为了实现更灵活的管理和解耦,对管理做了如下设计:

  1. 租户管理器管理租户域
public interface ITenantManager
{/// <summary>/// 获取所有的租户/// </summary>/// <returns></returns>List<long> GetAll();/// <summary>/// 获取当前租户/// </summary>/// <returns></returns>TenantContext GetCurrentTenantContext();/// <summary>/// 添加租户信息/// </summary>/// <param name="tenantId"></param>/// <param name="shardingRuntimeContext"></param>/// <returns></returns>bool AddTenantSharding(long tenantId, IShardingRuntimeContext shardingRuntimeContext);/// <summary>/// 创建租户环境/// </summary>/// <param name="tenantId"></param>/// <returns></returns>TenantScope CreateScope(long tenantId);
}public class TenantManager:ITenantManager
{private readonly ITenantContextAccessor _tenantContextAccessor;private readonly ConcurrentDictionary<string, IShardingRuntimeContext> _cache = new();public TenantManager(ITenantContextAccessor tenantContextAccessor){_tenantContextAccessor = tenantContextAccessor;}public List<long> GetAll(){return _cache.Keys.Select(k=>Convert.ToInt64(k.Replace("tenant_",""))).ToList();}public TenantContext GetCurrentTenantContext(){return _tenantContextAccessor.TenantContext;}public bool AddTenantSharding(long tenantId, IShardingRuntimeContext shardingRuntimeContext){return _cache.TryAdd($"tenant_{tenantId}", shardingRuntimeContext);}public TenantScope CreateScope(long tenantId){if (!_cache.TryGetValue($"tenant_{tenantId}", out var shardingRuntimeContext)){throw new InvalidOperationException("未找到对应租户的配置");}_tenantContextAccessor.TenantContext = new TenantContext(shardingRuntimeContext, tenantId);return new TenantScope(_tenantContextAccessor);}
}
  1. 租户域中通过租户上下文定位器管理租户上下文
public class TenantScope: IDisposable
{public TenantScope(ITenantContextAccessor tenantContextAccessor){TenantContextAccessor = tenantContextAccessor;}public ITenantContextAccessor TenantContextAccessor { get; }public void Dispose(){TenantContextAccessor.TenantContext = null;}
}public interface ITenantContextAccessor
{TenantContext? TenantContext { get; set; }
}public class TenantContextAccessor : ITenantContextAccessor
{private static readonly AsyncLocal<TenantContext?> _tenantContext = new AsyncLocal<TenantContext?>();public TenantContext? TenantContext{get => _tenantContext.Value;set => _tenantContext.Value = value;}
}
  1. 租户上下文中存储ShardingRuntimeContext
public class TenantContext
{private readonly IShardingRuntimeContext _shardingRuntimeContext;private readonly long _tenantId;public TenantContext(IShardingRuntimeContext shardingRuntimeContext, long tenantId){_shardingRuntimeContext = shardingRuntimeContext;_tenantId = tenantId;}public IShardingRuntimeContext GetShardingRuntimeContext(){return _shardingRuntimeContext;}public long GetTenantId() { return _tenantId;}
}

并实现以下业务:

  1. 系统初始化时在管理器中加入所有可用租户上下文
  2. 添加租户时在管理器中添加新的租户上下文
  3. 某个租户登录后请求系统时通过租户管理去创建域确保该租户所有的请求都在该域下进行,访问其能访问的数据库
  4. 部分需要全库的操作比如登陆验证,租户管理器可以手动创建能访问全部数据库管理员租户域
  5. 删除或者禁用租户时在管理器中删除

5. Code-First 迁移

原生EntityFramework Core框架不支持一个DBContext来迁移多个数据库,但ShardingCore的作者大佬做了这样的方案,并分享了出来,我基本是照搬他的博客的。唯一的不同是大佬的数据库是固定的几个,我这边做了个根据租户ID来确定数据库名和连接字符串的简单方案。

var dbName = configuration["Tenant:TenantDbTemp"].Replace("{tenant}", $"Tenant{tenant.Id}");
var option = new ShardingTenantOptions()
{DefaultDataSourceName = $"tenant_{tenant.Id}",DefaultConnectionString = connectionStr.Replace(baseDbName,dbName),MigrationNamespace = $"T{tenant.Id}",
};
var shardingRuntimeContext = shardingBuilder.Build(option);
tenantManager.AddTenantSharding(tenant.Id, shardingRuntimeContext);//创建数据库
string sql = $"SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_database u where u.datname='{dbName}')";
bool exist = Convert.ToBoolean(defaultDbContext.Database.ExecuteScalar(sql));if (!exist)
{string tsql = $"CREATE DATABASE \"{dbName}\" WITH OWNER = postgres TEMPLATE = postgis ENCODING = 'UTF8' LOCALE_PROVIDER = 'libc' CONNECTION LIMIT = -1 IS_TEMPLATE = False;";defaultDbContext.Database.ExecuteSqlRaw(tsql);
}using (tenantManager.CreateScope(tenantId))
using (var scope = serviceProvider.CreateScope())
{var tenantDbContext = scope.ServiceProvider.GetService<TenantDbContext>();if (tenantDbContext.Database.GetPendingMigrations().Any()){tenantDbContext.Database.Migrate();}
}

这里有几个问题需要说明以下。第一是DbContext.Database.Migrate()或者DbContext.Database.EnsureCreated()是能够自动创建数据库的,但是我需要存储空间数据,因此需要确保TEMPLATE = postgis,所以选择了手动创建。第二,DbContext.Database.Migrate()能够生效的前提是先手动Add-Migration,因此需要定期维护,比如提前知道大概有一百个租户,就得先提前加一百个迁移配置。

Add-Migration v0.0.1 -Context TenantDbContext -OutputDir Migrations\T1 -Args "--tenant 1"

与之匹配的,也需要在代码里将传入的参数与数据库和迁移配置代码对应起来。

var tenant = builder.Configuration.GetValue("Tenant", "1");
builder.Services.AddDbContext<TenantDbContext>((sp, b) =>
{var tenantManager = sp.GetRequiredService<ITenantManager>();var currentTenantContext = tenantManager.GetCurrentTenantContext();//如果有上下文那么创建租户dbcontextif (currentTenantContext != null){var shardingRuntimeContext = currentTenantContext.GetShardingRuntimeContext();b.UseDefaultSharding<TenantDbContext>(shardingRuntimeContext);}//用来Add-Migration的代码if (args != null && args.Length > 0 && !string.IsNullOrEmpty(args[0])){var connectionStr = builder.Configuration.GetConnectionString("NpgContext");var baseDbName = builder.Configuration["Tenant:BaseDb"];var dbName = builder.Configuration["Tenant:TenantDbTemp"].Replace("{tenant}", $"Tenant{tenant}");b.UseNpgsql(connectionStr.Replace(baseDbName, dbName), b => b.UseNetTopologySuite(geographyAsDefault: true)).UseMigrationNamespace(new NpqsqlMigrationNamespace($"T{tenant}")).ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();}
});

6. 联库操作

上面说到,根据上述配置,普通租户只能访问他自己的数据库,但管理员或者登录接口需要能够访问所有的数据库,因此在某些情况下需要联库操作。好在联库操作是ShardingCore的天赋申通,只需要添加数据源(见本文第三部分)并配置路由就行。

public class VirtualDataSourceRoute<TEntity>: AbstractShardingOperatorVirtualDataSourceRoute<TEntity,string> where TEntity : TenantEntityBase
{private readonly DefaultDbContext _defaultDbContext;private readonly List<string> _dataSources = new List<string>(){};public override List<string> GetAllDataSourceNames(){return _dataSources;}public override bool AddDataSourceName(string dataSourceName){if (_dataSources.Any(o => o == dataSourceName))return false;_dataSources.Add(dataSourceName);return true;}//我们设置区域就是数据库public override string ShardingKeyToDataSourceName(object shardingKey){return $"tenant_{shardingKey}";}public override Func<string, bool> GetRouteToFilter(string shardingKey, ShardingOperatorEnum shardingOperator){var t = ShardingKeyToDataSourceName(shardingKey);switch (shardingOperator){case ShardingOperatorEnum.Equal: return tail => tail == t;default:{return tail => true;}}}public override void Configure(EntityMetadataDataSourceBuilder<TEntity> builder){builder.ShardingProperty(o => o.TenantId);}
}

另外每次系统启动时,以及新加入租户时,都需要给系统租户的IShardingRuntimeContext添加额外的数据源

//系统启动时
using (tenantManager.CreateScope(TenantConstant.SYSTEM_TENANT_ID))
{using (var scope = serviceProvider.CreateScope()){var shardingRuntimeContext = tenantManager.GetCurrentTenantContext().GetShardingRuntimeContext();foreach (var tenantId in tenantIds){if (tenantId != TenantConstant.SYSTEM_TENANT_ID){shardingRuntimeContext.GetDataSourceRouteManager().AddDataSourceName($"tenant_{tenantId}");}}}
}//新增租户时
using (_tenantManager.CreateScope(TenantConstant.SYSTEM_TENANT_ID))
{using (var scope = _serviceProvider.CreateScope()){var sysShardingRuntimeContext = _tenantManager.GetCurrentTenantContext().GetShardingRuntimeContext();shardingRuntimeContext.GetDataSourceRouteManager().AddDataSourceName($"tenant_{tenant.Id}");}
}

7. 登录与验证

这部分就简单了,主要是生成JWT令牌组装Claims时将租户ID组装进去以及创建一个中间件,验证令牌以及租户ID后创建租户域。

/// <summary>
///组装Claims
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public static List<Claim> AddClaims(TokenModel user)
{var claims = new List<Claim>(){new(ClaimTypes.PrimarySid, user.UserId.ToString()),new(ClaimTypes.NameIdentifier, user.Phone.ToString()),new(ClaimTypes.Name, user.UserName),new(ClaimTypes.PrimaryGroupSid, user.TenantId.ToString()),new(ClaimTypes.GroupSid, user.DepartmentId.ToString()),new(ClaimTypes.UserData, JsonConvert.SerializeObject(user))};return claims;
}public class TenantSelectMiddleware
{private readonly RequestDelegate _next;private readonly ITenantManager _tenantManager;public TenantSelectMiddleware(RequestDelegate next, ITenantManager tenantManager){_next = next;_tenantManager = tenantManager;}/// <summary>/// 1.中间件的方法必须叫Invoke,且为public,非static。/// 2.Invoke方法第一个参数必须是HttpContext类型。/// 3.Invoke方法必须返回Task。/// 4.Invoke方法可以有多个参数,除HttpContext外其它参数会尝试从依赖注入容器中获取。/// 5.Invoke方法不能有重载。/// </summary>/// Author : Napoleon/// Created : 2020/1/30 21:30public async Task Invoke(HttpContext context){if (context.Request.Path.ToString().StartsWith("/api/tenant", StringComparison.CurrentCultureIgnoreCase)){if (!context.User.Identity.IsAuthenticated){await _next(context);return;}var tenantIdStr = context.User.Claims.FirstOrDefault((o) => o.Type == ClaimTypes.PrimaryGroupSid)?.Value;if (string.IsNullOrWhiteSpace(tenantIdStr)){context.Response.StatusCode = 403;await context.Response.WriteAsync("租户不正确");return;}var tenantId = Convert.ToInt64(tenantIdStr);using (_tenantManager.CreateScope(tenantId)){await _next(context);}}else{await _next(context);}}
}

8. 总结

感谢ShardingCore,感谢薛家明大佬,让我在拒绝Furion后可以这么方便快捷的实现多租户。

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

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

相关文章

Spring Boot入门(21):使用Spring Boot和Log4j2进行高效日志管理:配置详解

Spring Boot 整合 Log4j2 前言 Log4j2是Apache软件基金会下的一个日志框架&#xff0c;它是Log4j的升级版。与Log4j相比&#xff0c;它在性能和功能上有着极大的提升。Spring Boot本身已经默认集成了Logback作为日志框架&#xff0c;但如果需要使用Log4j2来替代Logback&#…

智慧校园:大数据助力校情分析

随着信息技术的快速发展&#xff0c;数据信息资源以井喷的姿态涌现。数据信息的大量涌现给人们带来丰富的数据信息资源&#xff0c;但面对海量的信息资源时&#xff0c;加大了人们对有效信息资源获取的难度&#xff0c;数据挖掘技术正是这一背景下的产物&#xff0c;基于数据挖…

小扎宣布开放 Meta Horizo​​n OS

日前&#xff0c;Meta以“混合现实的新时代”为题的博文宣布向第三方制造商开放Meta Horizon OS&#xff0c;包括华硕、联想和微软Xbox等等&#xff1a; Meta正在朝着为元宇宙建立一个更开放的计算平台的愿景迈出下一步。Meta正在向第三方硬件制造商开放赋能Meta Quest设备的操…

医院信创FTP要进行替代,有什么值得信赖的方案?

信创产业&#xff0c;即信息技术应用创新产业。其发展核心在于通过行业 应用拉动构建国产化信息技术软硬件底层架构体系和全生命周期生态体系&#xff0c;解决核心技术关键环节“卡脖子”的问题&#xff0c;为中国未来发展奠定坚实的数字基础。 2018 年 以来&#xff0c;受“华…

基于Springboot的网课管理系统

基于SpringbootVue的网课管理系统的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 课程表 论坛交流 学校公告 后端 学生管理 教师管理 班级管理 课程分类管理…

挑战一周完成Vue3实战项目硅谷甄选Day1:项目初始化、项目配置、项目集成

一、项目初始化 node v16.4.0以上&#xff08;查看node版本 : node -v&#xff09; pnpm 8.0.0&#xff08;npm i -g pnpm8.0.0&#xff09; 在想创建的位置新建文件夹自己命名 在此文件夹下cmd:pnpm create vite 选择如下配置 Project name&#xff08;项目名称&#xff0…

ROS1 驱动USB摄像头 2024年亲测

安装 查看官网文档A ROS Driver for V4L USB Cameras 里面提供了github链接&#xff0c;链接如下&#xff0c;这里要选择develop分支 将这个文件包放到你的工作空间的src目录下&#xff0c;然后回到工作空间编译catkin_make 此时报错no package libv4l2 found 参考stack ov…

【解决NodeJS项目无法在IDEA中调试的问题】使用JetBrains IDEA 2023 调试nodejs项目

项目采用Ant Design Pro React&#xff0c;使用前后端分离开发方式&#xff0c;后端可以很容易的打断点调试&#xff0c;但是前端通过网页进行调试&#xff0c;在IDEA中加了调试断点&#xff0c;但是没有什么用处。 解决方案如下&#xff1a; 点击新建运行配置 新建JavaScrip…

2024 年选择安全运营中心 (SOC) 工具指南

安全运营中心 (SOC) 是对抗网络威胁的前线。他们使用各种安全控制措施来监控、检测和快速响应任何网络威胁。这些控制措施对于确保信息系统全天候安全至关重要。 大型组织中的现代 SOC 与各种安全供应商合作&#xff0c;处理 75 到 100 种不同的工具。让我们探讨一下您可能遇到…

飞凌技术帖 | RK3568开发板的OTA升级教程

说起OTA我们应该都不陌生&#xff0c;它是一种可以为设备无损失升级系统的方式&#xff0c;能将新功能远程部署到产品上。我们不仅可以通过网络下载OTA升级包&#xff0c;也可以通过下载OTA升级包到SD卡或U盘后再对设备升级。 本文将通过飞凌嵌入式OK3568-C开发板来为大家介绍…

如何在Windows中使用NVM,如何在项目中使用NVM(nvm使用详细,如何使用nvm,使用nvm安装和切换各个nodejs版本)

简介&#xff1a;NVM全称Node Version Manager&#xff0c;是一个用于管理 Node.js 版本的工具&#xff0c;它允许你在同一台计算机上安装和切换多个 Node.js 版本。这对于我们来说特别有用&#xff0c;因为不同的项目可能需要不同版本的 Node.js 来运行。这里来记录一下 NPM &…

爬虫中怎么判断一个网页是否包含ajax请求

1、前言 在用爬虫抓取数据的时候&#xff0c;如果一个网页包含ajax请求&#xff0c;由于数据时动态加载的&#xff0c;直接根据网址是不能获取到想要的数据。因此&#xff0c;在爬虫需要首先判断一个网页是否包含ajax请求数据。 2、ajax请求 2.1 什么是ajax请求 AJAX Asynch…

新版ONENET(2024/4/24)通过view3.0可视化保姆级教程(一学就会)附效果图

⏩ 大家好哇&#xff01;我是小光&#xff0c;想要成为系统架构师的嵌入式爱好者。 ⏩上一篇是STM32通过ESP8266连接最新版的ONENET&#xff0c;成功将数据上传之后&#xff0c;本篇文章使用ONENET的view3.0可视化对数据进行可视化做一个详细教程。 ⏩感谢你的阅读&#xff0c;…

Java-AQS的原理

文章目录 基本概述1. 设计思想2. 基本实现 一些关键词语以及常用术语&#xff0c;主要如下&#xff1a; 信号量(Semaphore): 是在多线程环境下使用的一种设施&#xff0c;是可以用来保证两个或多个关键代码段不被并发调用&#xff0c;也是作系统用来解决并发中的互斥和同步问题…

SQLAlchemy的使用

SQLAlchemy中filter函数的使用 https://blog.csdn.net/m0_67093160/article/details/133318889

在浏览器输入网址,Enter之后发生了什么?

在浏览器输入网址&#xff0c;Enter之后发生了什么&#xff1f; 很多八股文会给出&#xff1a; 1. DNS Resolution2. Establishing a Connection3. Sending an Http Request4. Receiving the HTTP Response5. Rendering the Web Page 但今天我斗胆插入第0.9步URL Parsing&#…

适用于手机蓝牙的热敏晶体FA1612AS

EPSON推出的一款1612小尺寸无源热敏晶体:FA1612AS。FA1612AS的额定频率为38.4Mhz的晶体单元&#xff0c;采用无铅材料&#xff0c;符合ROHS标准&#xff0c;内置热敏电阻&#xff0c;可用于移动电话&#xff0c;蓝牙等。热敏晶体FA1612AS的产品特性:额定频率:38.4MHZ外部尺寸规…

JS----前端将列表数据转树型数据

前端将列表数据转树型数据 场景&#xff1a;后端返回列表数据&#xff0c;由前端根据业务需求完成树型数据转换&#xff0c; 常用于侧边导航菜单&#xff0c;下拉树型数据项等 export function listToTree(data: []) {var map: any {},tree: any []data.forEach((item: any…

每天五分钟计算机视觉:基于YOLO算法精确分类定位图片中的对象

滑动窗口的卷积的问题 滑动窗口的卷积实现效率很高,但是它依然不能够输出最精准的边界框,比如下面所示: 我们可以看到蓝色框不论在什么位置都不能很好的确定车的位置,有一个算法是YOLO 算法它能够帮助我们解决这个问题。 YOLO 算法 比如我们的输入图像是100*100,我们会…

【OceanBase诊断调优 】—— 如何快速定位SQL问题

作者简介&#xff1a; 花名&#xff1a;洪波&#xff0c;OceanBase 数据库解决方案架构师&#xff0c;目前负责 OceanBase 数据库在各大型互联网公司及企事业单位的落地与技术指导&#xff0c;曾就职于互联网大厂和金融科技公司&#xff0c;主导过多项数据库升级、迁移、国产化…