[Abp vNext 源码分析] - 19. 多租户

一、简介

ABP vNext 原生支持多租户体系,可以让开发人员快速地基于框架开发 SaaS 系统。ABP vNext 实现多租户的思路也非常简单,通过一个 TenantId 来分割各个租户的数据,并且在查询的时候使用统一的全局过滤器(类似于软删除)来筛选数据。

关于多租户体系的东西,基本定义与核心逻辑存放在 Volo.ABP.MultiTenancy 内部。针对 ASP.NET Core MVC 的集成则是由 Volo.ABP.AspNetCore.MultiTenancy 项目实现的,针对多租户的解析都在这个项目内部。租户数据的存储和管理都由 Volo.ABP.TenantManagement 模块提供,开发人员也可以直接使用该项目快速实现多租户功能。

二、源码分析

2.1 启动模块

AbpMultiTenancyModule 模块是启用整个多租户功能的核心模块,内部只进行了一个动作,就是从配置类当中读取多租户的基本信息,以 JSON Provider 为例,就需要在 appsettings.json 里面有 Tenants 节。

Copy"Tenants": [{"Id": "446a5211-3d72-4339-9adc-845151f8ada0","Name": "tenant1"},{"Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d","Name": "tenant2","ConnectionStrings": {"Default": "...write tenant2's db connection string here..."}}]

2.1.1 默认租户来源

这里的数据将会作为默认租户来源,也就是说在确认当前租户的时候,会从这里面的数据与要登录的租户进行比较,如果不存在则不允许进行操作。

Copypublic interface ITenantStore
{Task<TenantConfiguration> FindAsync(string name);Task<TenantConfiguration> FindAsync(Guid id);TenantConfiguration Find(string name);TenantConfiguration Find(Guid id);
}

默认的存储实现:

Copy[Dependency(TryRegister = true)]
public class DefaultTenantStore : ITenantStore, ITransientDependency
{// 直接从 Options 当中获取租户数据。private readonly AbpDefaultTenantStoreOptions _options;public DefaultTenantStore(IOptionsSnapshot<AbpDefaultTenantStoreOptions> options){_options = options.Value;}public Task<TenantConfiguration> FindAsync(string name){return Task.FromResult(Find(name));}public Task<TenantConfiguration> FindAsync(Guid id){return Task.FromResult(Find(id));}public TenantConfiguration Find(string name){return _options.Tenants?.FirstOrDefault(t => t.Name == name);}public TenantConfiguration Find(Guid id){return _options.Tenants?.FirstOrDefault(t => t.Id == id);}
}

除了从配置文件当中读取租户信息以外,开发人员也可以自己实现 ITenantStore 接口,比如说像 TenantManagement 一样,将租户信息存储到数据库当中。

2.1.2 基于数据库的租户存储

话接上文,我们说过在 Volo.ABP.TenantManagement 模块内部有提供另一种 ITenantStore 接口的实现,这个类型叫做 TenantStore,内部逻辑也很简单,就是从仓储当中查找租户数据。

Copypublic class TenantStore : ITenantStore, ITransientDependency
{private readonly ITenantRepository _tenantRepository;private readonly IObjectMapper<AbpTenantManagementDomainModule> _objectMapper;private readonly ICurrentTenant _currentTenant;public TenantStore(ITenantRepository tenantRepository, IObjectMapper<AbpTenantManagementDomainModule> objectMapper,ICurrentTenant currentTenant){_tenantRepository = tenantRepository;_objectMapper = objectMapper;_currentTenant = currentTenant;}public async Task<TenantConfiguration> FindAsync(string name){// 变更当前租户为租主。using (_currentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!{// 通过仓储查询租户是否存在。var tenant = await _tenantRepository.FindByNameAsync(name);if (tenant == null){return null;}// 将查询到的信息转换为核心库定义的租户信息。return _objectMapper.Map<Tenant, TenantConfiguration>(tenant);}}// ... 其他的代码已经省略。
}

可以看到,最后也是返回的一个 TenantConfiguration 类型。关于这个类型,是 ABP 在多租户核心库定义的一个基本类型之一,主要是用于规定持久化一个租户信息需要包含的属性。

Copy[Serializable]
public class TenantConfiguration
{// 租户的 Guid。public Guid Id { get; set; }// 租户的名称。public string Name { get; set; }// 租户对应的数据库连接字符串。public ConnectionStrings ConnectionStrings { get; set; }public TenantConfiguration(){}public TenantConfiguration(Guid id, [NotNull] string name){Check.NotNull(name, nameof(name));Id = id;Name = name;ConnectionStrings = new ConnectionStrings();}
}

2.2 租户的解析

ABP vNext 如果要判断当前的租户是谁,则是通过 AbpTenantResolveOptions 提供的一组 ITenantResolveContributor 进行处理的。

Copypublic class AbpTenantResolveOptions
{// 会使用到的这组解析对象。[NotNull]public List<ITenantResolveContributor> TenantResolvers { get; }public AbpTenantResolveOptions(){TenantResolvers = new List<ITenantResolveContributor>{// 默认的解析对象,会通过 Token 内字段解析当前租户。new CurrentUserTenantResolveContributor()};}
}

这里的设计与权限一样,都是由一组 解析对象(解析器) 进行处理,在上层开放的入口只有一个 ITenantResolver ,内部通过 foreach 执行这组解析对象的 Resolve() 方法。

下面就是我们 ITenantResolver 的默认实现 TenantResolver,你可以在任何时候调用它。比如说你在想要获得当前租户 Id 的时候。不过一般不推荐这样做,因为 ABP 已经给我们提供了 MultiTenancyMiddleware 中间件。

也就是说,在每次请求的时候,都会将这个 Id 通过 ICurrentTenant.Change() 进行变更,那么在这个请求执行完成之前,通过 ICurrentTenant 取得的 Id 都会是解析器解析出来的 Id。

Copypublic class TenantResolver : ITenantResolver, ITransientDependency
{private readonly IServiceProvider _serviceProvider;private readonly AbpTenantResolveOptions _options;public TenantResolver(IOptions<AbpTenantResolveOptions> options, IServiceProvider serviceProvider){_serviceProvider = serviceProvider;_options = options.Value;}public TenantResolveResult ResolveTenantIdOrName(){var result = new TenantResolveResult();using (var serviceScope = _serviceProvider.CreateScope()){// 创建一个解析上下文,用于存储解析器的租户 Id 解析结果。var context = new TenantResolveContext(serviceScope.ServiceProvider);// 遍历执行解析器。foreach (var tenantResolver in _options.TenantResolvers){tenantResolver.Resolve(context);result.AppliedResolvers.Add(tenantResolver.Name);// 如果有某个解析器为上下文设置了值,则跳出。if (context.HasResolvedTenantOrHost()){result.TenantIdOrName = context.TenantIdOrName;break;}}}return result;}
}

2.2.1 默认的解析对象

如果不使用 Volo.Abp.AspNetCore.MultiTenancy 模块,ABP vNext 会调用 CurrentUserTenantResolveContributor 解析当前操作的租户。

Copypublic class CurrentUserTenantResolveContributor : TenantResolveContributorBase
{public const string ContributorName = "CurrentUser";public override string Name => ContributorName;public override void Resolve(ITenantResolveContext context){// 从 Token 当中获取当前登录用户的信息。var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();if (currentUser.IsAuthenticated != true){return;}// 设置解析上下文,确认当前的租户 Id。context.Handled = true;context.TenantIdOrName = currentUser.TenantId?.ToString();}
}

在这里可以看到,如果从 Token 当中解析到了租户 Id,会将这个 Id 传递给 解析上下文。这个上下文在最开始已经遇到过了,如果 ABP vNext 在解析的时候发现租户 Id 被确认了,就不会执行剩下的解析器。

2.2.2 ABP 提供的其他解析器

ABP 在 Volo.Abp.AspNetCore.MultiTenancy 模块当中还提供了其他几种解析器,他们的作用分别如下。

解析器类型作用优先级
QueryStringTenantResolveContributor通过 Query String 的 __tenant 参数确认租户。2
RouteTenantResolveContributor通过路由判断当前租户。3
HeaderTenantResolveContributor通过 Header 里面的 __tenant 确认租户。4
CookieTenantResolveContributor通过携带的 Cookie 确认租户。5
DomainTenantResolveContributor二级域名解析器,通过二级域名确定租户。第二

2.2.3 域名解析器

这里比较有意思的是 DomainTenantResolveContributor,开发人员可以通过 AbpTenantResolveOptions.AddDomainTenantResolver() 方法添加这个解析器。 域名解析器会通过解析二级域名来匹配对应的租户,例如我针对租户 A 分配了一个二级域名 http://a.system.com,那么这个 a 就会被作为租户名称解析出来,最后传递给 ITenantResolver 解析器作为结果。

注意:

在使用 Header 作为租户信息提供者的时候,开发人员使用的是 NGINX 作为反向代理服务器 时,需要在对应的 config 文件内部配置 underscores_in_headers on; 选项。否则 ABP 所需要的 __tenantId 将会被过滤掉,或者你可以指定一个没有下划线的 Key。

域名解析器的详细代码解释:

Copypublic class DomainTenantResolveContributor : HttpTenantResolveContributorBase
{public const string ContributorName = "Domain";public override string Name => ContributorName;private static readonly string[] ProtocolPrefixes = { "http://", "https://" };private readonly string _domainFormat;// 使用指定的格式来确定租户前缀,例如 “{0}.abp.io”。public DomainTenantResolveContributor(string domainFormat){_domainFormat = domainFormat.RemovePreFix(ProtocolPrefixes);}protected override string GetTenantIdOrNameFromHttpContextOrNull(ITenantResolveContext context, HttpContext httpContext){// 如果 Host 值为空,则不进行任何操作。if (httpContext.Request?.Host == null){return null;}// 解析具体的域名信息,并进行匹配。var hostName = httpContext.Request.Host.Host.RemovePreFix(ProtocolPrefixes);// 这里的 FormattedStringValueExtracter 类型是 ABP 自己实现的一个格式化解析器。var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true);context.Handled = true;if (!extractResult.IsMatch){return null;}return extractResult.Matches[0].Value;}
}

从上述代码可以知道,域名解析器是基于 HttpTenantResolveContributorBase 基类进行处理的,这个抽象基类会取得当前请求的一个 HttpContext,将这个传递与解析上下文一起传递给子类实现,由子类实现负责具体的解析逻辑。

Copypublic abstract class HttpTenantResolveContributorBase : TenantResolveContributorBase
{public override void Resolve(ITenantResolveContext context){// 获取当前请求的上下文。var httpContext = context.GetHttpContext();if (httpContext == null){return;}try{ResolveFromHttpContext(context, httpContext);}catch (Exception e){context.ServiceProvider.GetRequiredService<ILogger<HttpTenantResolveContributorBase>>().LogWarning(e.ToString());}}protected virtual void ResolveFromHttpContext(ITenantResolveContext context, HttpContext httpContext){// 调用抽象方法,获取具体的租户 Id 或名称。var tenantIdOrName = GetTenantIdOrNameFromHttpContextOrNull(context, httpContext);if (!tenantIdOrName.IsNullOrEmpty()){// 获得到租户标识之后,填充到解析上下文。context.TenantIdOrName = tenantIdOrName;}}protected abstract string GetTenantIdOrNameFromHttpContextOrNull([NotNull] ITenantResolveContext context, [NotNull] HttpContext httpContext);
}

2.3 租户信息的传递

租户解析器通过一系列的解析对象,获取到了租户或租户 Id 之后,会将这些数据给哪些对象呢?或者说,ABP 在什么地方调用了 租户解析器,答案就是 中间件。

在 Volo.ABP.AspNetCore.MultiTenancy 模块的内部,提供了一个 MultiTenancyMiddleware 中间件。

开发人员如果需要使用 ASP.NET Core 的多租户相关功能,也可以引入该模块。并且在模块的 OnApplicationInitialization() 方法当中,使用 IApplicationBuilder.UseMultiTenancy() 进行启用。

这里在启用的时候,需要注意中间件的顺序和位置,不要放到最末尾进行处理。

Copypublic class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{private readonly ITenantResolver _tenantResolver;private readonly ITenantStore _tenantStore;private readonly ICurrentTenant _currentTenant;private readonly ITenantResolveResultAccessor _tenantResolveResultAccessor;public MultiTenancyMiddleware(ITenantResolver tenantResolver, ITenantStore tenantStore, ICurrentTenant currentTenant, ITenantResolveResultAccessor tenantResolveResultAccessor){_tenantResolver = tenantResolver;_tenantStore = tenantStore;_currentTenant = currentTenant;_tenantResolveResultAccessor = tenantResolveResultAccessor;}public async Task InvokeAsync(HttpContext context, RequestDelegate next){// 通过租户解析器,获取当前请求的租户信息。var resolveResult = _tenantResolver.ResolveTenantIdOrName();_tenantResolveResultAccessor.Result = resolveResult;TenantConfiguration tenant = null;// 如果当前请求是属于租户请求。if (resolveResult.TenantIdOrName != null){// 查询指定的租户 Id 或名称是否存在,不存在则抛出异常。tenant = await FindTenantAsync(resolveResult.TenantIdOrName);if (tenant == null){//TODO: A better exception?throw new AbpException("There is no tenant with given tenant id or name: " + resolveResult.TenantIdOrName);}}// 在接下来的请求当中,将会通过 ICurrentTenant.Change() 方法变更当前租户,直到// 请求结束。using (_currentTenant.Change(tenant?.Id, tenant?.Name)){await next(context);}}private async Task<TenantConfiguration> FindTenantAsync(string tenantIdOrName){// 如果可以格式化为 Guid ,则说明是租户 Id。if (Guid.TryParse(tenantIdOrName, out var parsedTenantId)){return await _tenantStore.FindAsync(parsedTenantId);}else{return await _tenantStore.FindAsync(tenantIdOrName);}}
}

在取得了租户的标识(Id 或名称)之后,将会通过 ICurrentTenant.Change() 方法变更当前租户的信息,变更了当租户信息以后,在程序的其他任何地方使用 ICurrentTenant.Id 取得的数据都是租户解析器解析出来的数据。

下面就是这个当前租户的具体实现,可以看到这里采用了一个 经典手法-嵌套。这个手法在工作单元和数据过滤器有见到过,结合 DisposeAction() 在 using 语句块结束的时候把当前的租户 Id 值设置为父级 Id。即在同一个语句当中,可以通过嵌套 using 语句块来处理不同的租户。

Copyusing(_currentTenant.Change("A"))
{Logger.LogInformation(_currentTenant.Id);using(_currentTenant.Change("B")){Logger.LogInformation(_currentTenant.Id);}
}

具体的实现代码,这里的 ICurrentTenantAccessor 内部实现就是一个 AsyncLocal<BasicTenantInfo> ,用于在一个异步请求内部进行数据传递。

Copypublic class CurrentTenant : ICurrentTenant, ITransientDependency
{public virtual bool IsAvailable => Id.HasValue;public virtual Guid? Id => _currentTenantAccessor.Current?.TenantId;public string Name => _currentTenantAccessor.Current?.Name;private readonly ICurrentTenantAccessor _currentTenantAccessor;public CurrentTenant(ICurrentTenantAccessor currentTenantAccessor){_currentTenantAccessor = currentTenantAccessor;}public IDisposable Change(Guid? id, string name = null){return SetCurrent(id, name);}private IDisposable SetCurrent(Guid? tenantId, string name = null){var parentScope = _currentTenantAccessor.Current;_currentTenantAccessor.Current = new BasicTenantInfo(tenantId, name);return new DisposeAction(() =>{_currentTenantAccessor.Current = parentScope;});}
}

这里的 BasicTenantInfo 与 TenantConfiguraton 不同,前者仅用于在程序当中传递用户的基本信息,而后者是用于定于持久化的标准模型。

2.4 租户的使用

2.4.1 数据库过滤

租户的核心作用就是隔离不同客户的数据,关于过滤的基本逻辑则是存放在 AbpDbContext<TDbContext> 的。从下面的代码可以看到,在使用的时候会从注入一个 ICurrentTenant 接口,这个接口可以获得从租户解析器里面取得的租户 Id 信息。并且还有一个 IsMultiTenantFilterEnabled() 方法来判定当前 是否应用租户过滤器。

Copypublic abstract class AbpDbContext<TDbContext> : DbContext, IEfCoreDbContext, ITransientDependencywhere TDbContext : DbContext
{protected virtual Guid? CurrentTenantId => CurrentTenant?.Id;protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;// ... 其他的代码。public ICurrentTenant CurrentTenant { get; set; }// ... 其他的代码。protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() where TEntity : class{// 定义一个 Lambda 表达式。Expression<Func<TEntity, bool>> expression = null;// 如果聚合根/实体实现了软删除接口,则构建一个软删除过滤器。if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))){expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted");}// 如果聚合根/实体实现了多租户接口,则构建一个多租户过滤器。if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))){// 筛选 TenantId 为 CurrentTenantId 的数据。Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId;expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);}return expression;}// ... 其他的代码。
}

2.4.2 种子数据构建

在 Volo.ABP.TenantManagement 模块当中,如果用户创建了一个租户,ABP 不只是在租户表插入一条新数据而已。它还会设置种子数据的 构造上下文,并且执行所有的 种子数据构建者(IDataSeedContributor)。

Copy[Authorize(TenantManagementPermissions.Tenants.Create)]
public virtual async Task<TenantDto> CreateAsync(TenantCreateDto input)
{var tenant = await TenantManager.CreateAsync(input.Name);await TenantRepository.InsertAsync(tenant);using (CurrentTenant.Change(tenant.Id, tenant.Name)){//TODO: Handle database creation?//TODO: Set admin email & password..?await DataSeeder.SeedAsync(tenant.Id);}return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}

这些构建者当中,就包括租户的超级管理员(admin)和角色构建,以及针对超级管理员角色进行权限赋值操作。

这里需要注意第二点,如果开发人员没有指定超级管理员用户和密码,那么还是会使用默认密码为租户生成超级管理员,具体原因看如下代码。

Copypublic class IdentityDataSeedContributor : IDataSeedContributor, ITransientDependency
{private readonly IIdentityDataSeeder _identityDataSeeder;public IdentityDataSeedContributor(IIdentityDataSeeder identityDataSeeder){_identityDataSeeder = identityDataSeeder;}public Task SeedAsync(DataSeedContext context){return _identityDataSeeder.SeedAsync(context["AdminEmail"] as string ?? "admin@abp.io",context["AdminPassword"] as string ?? "1q2w3E*",context.TenantId);}
}

所以开发人员要实现为不同租户 生成随机密码,那么就不能够使用 TenantManagement 提供的创建方法,而是需要自己编写一个应用服务进行处理。

2.4.3 权限的控制

如果开发人员使用了 ABP 提供的 Volo.Abp.PermissionManagement 模块,就会看到在它的种子数据构造者当中会对权限进行判定。因为有一些 超级权限 是租主才能够授予的,例如租户的增加、删除、修改等,这些超级权限在定义的时候就需要说明是否是数据租主独有的。

关于这点,可以参考租户管理模块在权限定义时,传递的 MultiTenancySides.Host 参数。

Copypublic class AbpTenantManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{public override void Define(IPermissionDefinitionContext context){var tenantManagementGroup = context.AddGroup(TenantManagementPermissions.GroupName, L("Permission:TenantManagement"));var tenantsPermission = tenantManagementGroup.AddPermission(TenantManagementPermissions.Tenants.Default, L("Permission:TenantManagement"), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Create, L("Permission:Create"), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Update, L("Permission:Edit"), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Delete, L("Permission:Delete"), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageFeatures, L("Permission:ManageFeatures"), multiTenancySide: MultiTenancySides.Host);tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageConnectionStrings, L("Permission:ManageConnectionStrings"), multiTenancySide: MultiTenancySides.Host);}private static LocalizableString L(string name){return LocalizableString.Create<AbpTenantManagementResource>(name);}
}

下面是权限种子数据构造者的代码:

Copypublic class PermissionDataSeedContributor : IDataSeedContributor, ITransientDependency
{protected ICurrentTenant CurrentTenant { get; }protected IPermissionDefinitionManager PermissionDefinitionManager { get; }protected IPermissionDataSeeder PermissionDataSeeder { get; }public PermissionDataSeedContributor(IPermissionDefinitionManager permissionDefinitionManager,IPermissionDataSeeder permissionDataSeeder,ICurrentTenant currentTenant){PermissionDefinitionManager = permissionDefinitionManager;PermissionDataSeeder = permissionDataSeeder;CurrentTenant = currentTenant;}public virtual Task SeedAsync(DataSeedContext context){// 通过 GetMultiTenancySide() 方法判断当前执行// 种子构造者的租户情况,是租主还是租户。var multiTenancySide = CurrentTenant.GetMultiTenancySide();// 根据条件筛选权限。var permissionNames = PermissionDefinitionManager.GetPermissions().Where(p => p.MultiTenancySide.HasFlag(multiTenancySide)).Select(p => p.Name).ToArray();// 将权限授予具体租户的角色。return PermissionDataSeeder.SeedAsync(RolePermissionValueProvider.ProviderName,"admin",permissionNames,context.TenantId);}
}

而 ABP 在判断当前是租主还是租户的方法也很简单,如果当前租户 Id 为 NULL 则说明是租主,如果不为空则说明是具体租户。

Copypublic static MultiTenancySides GetMultiTenancySide(this ICurrentTenant currentTenant)
{return currentTenant.Id.HasValue? MultiTenancySides.Tenant: MultiTenancySides.Host;
}

2.4.4 租户的独立设置

关于这块的内容,可以参考之前的 这篇文章 ,ABP 也为我们提供了各个租户独立的自定义参数在,这块功能是由 TenantSettingManagementProvider 实现的,只需要在设置参数值的时候提供租户的 ProviderName 即可。

例如:

CopysettingManager.SetAsync("WeChatIsOpen", "true", TenantSettingValueProvider.ProviderName, tenantId.ToString(), false);

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

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

相关文章

八数码问题I-bfs和map标记

解题思路&#xff1a; bfs&#xff0c;将棋盘转化成一个整数表示其状态&#xff0c;比如我们到到达的状态是815736402&#xff0c;而样例给的输入状态是264137058&#xff0c;因为这些整数过大&#xff0c;标记数组开不下&#xff0c;所以可以用map来代替数组&#xff0c;写得时…

一个迷你ASP.NET Core框架的实现(下)

【框架内幕】| 作者 / Edison Zhou这是恰童鞋骚年的第196篇原创文章上一篇我们了解了AspNetCore.Mini这个项目的背景及项目结构和流程&#xff0c;这一篇我们继续解析几个核心对象。本文整理自A大&#xff08;蒋金楠&#xff09;的主题分享&#xff0c;点击本文底部“阅读原文”…

根据后序和中序求二叉树的先序

代码如下&#xff1a; #include <iostream> using namespace std; const int N 1010; int in_order[N], post_order[N], lch[N], rch[N];int build(int inL, int inR, int postL, int postR) {if (inL > inR)return 0;int root post_order[postR];int k inL;while…

30多岁程序员老W,无奈选择转行!问题出在哪?

作者&#xff1a;邹溪源&#xff0c;长沙资深互联网从业者&#xff0c;架构师社区特邀嘉宾&#xff01;一有一天&#xff0c;一位同事跟我说&#xff1a;老w已经改行做美缝去了&#xff0c;你怎么看&#xff1f;我想了想&#xff0c;说&#xff1a;他大概终于做出了眼下最符合他…

一维前缀和

代码如下&#xff1a; #include <iostream> using namespace std; const int N 1010; int a[N], s[N];int main() {int n;cin >> n;for (int i 1; i < n; i) {cin >> a[i];//原数组s[i] s[i - 1] a[i];}for (int i 1; i < n; i)cout << s[…

HttpClientFactory的套路,你知多少?

背景ASP.NET Core 在 2.1 之后推出了具有弹性 HTTP 请求能力的 HttpClient 工厂类 HttpClientFactory。替换的初衷还是简单摆一下&#xff1a;① using(var client new HttpClient()) 调用的 Dispose() 方法并不会立即释放底层 Socket 连接&#xff0c;新建 Socket 需要时间&a…

走迷宫-双向bfs解法

双向bfs适用于知道起点和终点的状态下使用&#xff0c;从起点和终点两个方向开始进行搜索&#xff0c;可以非常大的提高单个bfs的搜索效率同样&#xff0c;实现也是通过队列的方式&#xff0c;可以设置两个队列&#xff0c;一个队列保存从起点开始搜索的状态&#xff0c;另一个…

.NET Core开发实战(第16课:选项数据热更新:让服务感知配置的变化)--学习笔记...

16 | 选项数据热更新&#xff1a;让服务感知配置的变化选项框架还有两个关键类型&#xff1a;1、IOptionsMonitor2、IOptionsSnapshot场景&#xff1a;1、范围作用域类型使用 IOptinsSnapshot2、单例服务使用 IOptionsMonitor通过代码更新选项&#xff1a;IPostConfigureOption…

华为任职资格_看了华为的任职资格体系,你就明白员工为啥这么拼?

提到华为的18万奋斗者&#xff0c;职场上没人不竖起大拇指。而优秀人才的背后&#xff0c;就像任正非曾说过的那样&#xff0c;华为的成功&#xff0c;很大意义上讲就是人力资源的成功。华为的人力资源管理最有代表性的&#xff0c;除了狼性文化和薪酬绩效外&#xff0c;就是任…

如何使用有序GUID提升数据库读写性能

源宝导读&#xff1a;数据库设计时&#xff0c;经常会使用GUID作为表的主键&#xff0c;但由于GUID的随机性会导致数据库在读写数据时效率严重下降&#xff0c;影响应用程序整体性能。本文将深入探讨如何通过使用有序GUID提升数据读写的性能。一、背景常见的数据库设计是使用连…

《商业洞察力30讲》学习笔记(上)

【洞察力】| 作者 / Edison Zhou这是恰童鞋骚年的第197篇原创文章学习洞察力&#xff0c;也是新时代IT人员的一门进阶必修课...1学习背景2019年下半年至今&#xff0c;在领导的推荐下学习了刘润老师的《商业洞察力30讲》&#xff0c;刷新了我对于事物的认知&#xff0c;也为我提…

[蓝桥杯][历届试题]九宫重排-双向bfs和map标记

题目描述 如下面第一个图的九宫格中&#xff0c;放着 1~8 的数字卡片&#xff0c;还有一个格子空着。与空格子相邻的格子中的卡片可以移动到空格中。经过若干次移动&#xff0c;可以形成第二个图所示的局面。 我们把第一个图的局面记为&#xff1a;12345678. 把第二个图的局面记…

3月数据库排行:前10整体下行,出新技术了?

DB-Engines 数据库流行度排行榜 3 月更新已发布&#xff0c;排名前二十如下&#xff1a;排名方面没有任何变动&#xff08;仅针对前十&#xff09;&#xff0c;相信很长一段时间内也都不会变动&#xff0c;毕竟巨头的位置不是一时半刻就能动摇的。不过这个月的排行榜还是有值得…

每日一题——LeetCode160.相交链表

个人主页&#xff1a;白日依山璟 专栏&#xff1a;Java|数据结构与算法|每日一题 文章目录 1. 题目描述示例1&#xff1a;示例2&#xff1a;提示&#xff1a; 2. 思路3. 代码 1. 题目描述 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的…

.NET Core的HttpClient连接池管理

译者荐语&#xff1a;使用.NET Core的HttpClient连接池管理有哪些注意事项&#xff1f;本文给出了非常中肯的建议。原文来自互联网&#xff0c;由长沙DotNET技术社区编译。如译文侵犯您的署名权或版权&#xff0c;请联系小编&#xff0c;小编将在24小时内删除。本文来源[1]史蒂…

[蓝桥杯][2014年第五届真题]兰顿蚂蚁-模拟

题目描述 兰顿蚂蚁&#xff0c;是于1986年&#xff0c;由克里斯兰顿提出来的&#xff0c;属于细胞自动机的一种。 平面上的正方形格子被填上黑色或白色。在其中一格正方形内有一只“蚂蚁”。 蚂蚁的头部朝向为&#xff1a;上下左右其中一方。 蚂蚁的移动规则十分简单&#…

.NET Core开发实战(第17课:为选项数据添加验证:避免错误配置的应用接收用户流量)--学习笔记...

17 | 为选项数据添加验证&#xff1a;避免错误配置的应用接收用户流量三种验证方法1、直接注册验证函数2、实现 IValidateOptions3、使用 Microsoft.Extensions.Options.DataAnnotations延用上一节代码需要添加验证的时候不能用 Configure&#xff0c;而用 AddOptions 方法//se…

分支程序与循环程序设计-汇编实验二

DATA SEGMENT ;定义数据段BUF DB -1, 20, 3, 30, -5, 15, 100, -54, 0, 4, 78, 99DB -12, 32, 3, 23, -7, 24, 60,-51 ;定义比较大小的数据DATA ENDS ;数据段结束ESEG SEGMENT ;定义附加段RES1 DB 0;定义结果存放区RES2 DB 0RES3 DB 0 ESEG ENDS ;附加段结束CODE SEGMENT ;定义…

[蓝桥杯][2013年第四届真题]剪格子-dfs

题目描述 历届试题 剪格子 时间限制&#xff1a;1.0s 内存限制&#xff1a;256.0MB 问题描述 如下图所示&#xff0c;3 x 3 的格子中填写了一些整数。 我们沿着图中的星号线剪开&#xff0c;得到两个部分&#xff0c;每个部分的数字和都是60。 本题的要求就是请你编程判定&a…

使用有序GUID:提升其在各数据库中作为主键时的性能

原文出处&#xff1a;https://www.codeproject.com/articles/388157/guids-as-fast-primary-keys-under-multiple-database &#xff0c;避免今后忘记了再去阅读原英文。【】是感觉理解有问题的地方正确的使用有序GUID在大部分数据库中可以获得和 整型作为主键 时相媲美的性能。…