2024年7月

前言

本文通过 WPF Gallery 这个项目学习依赖注入的相关概念与如何在WPF中进行依赖注入。

什么是依赖注入

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现控制反转(Inversion of Control,简称IoC)原则。依赖注入的主要目的是将对象的创建和对象之间的依赖关系的管理从对象内部转移到外部容器或框架中,从而提高代码的可维护性、可测试性和灵活性。

依赖注入的核心概念

  1. 依赖
    :一个对象需要另一个对象来完成其工作,那么前者就依赖于后者。例如,一个OrderService类可能依赖于一个ProductRepository类来获取产品信息。
  2. 注入
    :将依赖的对象传递给需要它的对象,而不是让需要它的对象自己去创建依赖的对象。注入可以通过构造函数、属性或方法参数来实现。
  3. 容器
    :一个管理对象创建和依赖关系的框架或库。容器负责实例化对象,解析依赖关系,并将依赖的对象注入到需要它们的对象中。

依赖注入的类型

构造函数注入
:依赖的对象通过类的构造函数传递。

public class OrderService
{
    private readonly IProductRepository _productRepository;

    public OrderService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
}

属性注入
:依赖的对象通过类的公共属性传递。

public class OrderService
{
    public IProductRepository ProductRepository { get; set; }
}

方法注入
:依赖的对象通过类的方法参数传递。

public class OrderService
{
    public void ProcessOrder(IProductRepository productRepository)
    {
        // 使用 productRepository 处理订单
    }
}

为什么要进行依赖注入

依赖注入(Dependency Injection,简称DI)是一种设计模式,通过它可以将对象的创建和对象之间的依赖关系的管理从对象内部转移到外部容器或框架中。进行依赖注入有以下几个重要的原因和优点:

  1. 降低耦合度
    : 依赖注入通过将依赖关系的管理从对象内部转移到外部容器,使得对象不需要知道如何创建其依赖的对象,只需要知道依赖对象的接口。这样可以显著降低对象之间的耦合度,使得代码更加模块化和灵活。
  2. 提高可测试性
    : 依赖注入使得单元测试变得更加容易和高效。通过使用模拟对象(Mock Object)或存根(Stub)来替代实际的依赖对象,开发者可以在不依赖于实际实现的情况下进行单元测试。这有助于确保测试的独立性和可靠性。
  3. 提高可维护性
    : 由于依赖注入降低了对象之间的耦合度,代码变得更加模块化和清晰。这使得代码更容易理解和维护。当需要修改或替换某个依赖对象时,只需要修改配置或注册信息,而不需要修改使用该对象的代码。
  4. 提高灵活性
    : 依赖注入使得系统更加灵活,能够轻松地替换依赖的对象,从而实现不同的功能或行为。例如,可以通过配置文件或代码来切换不同的数据库访问层实现,而不需要修改业务逻辑代码。
  5. 促进关注点分离
    : 依赖注入有助于实现关注点分离(Separation of Concerns),使得每个对象只需要关注自己的职责,而不需要关心如何创建和获取其依赖的对象。这有助于提高代码的清晰度和可维护性。
  6. 支持设计模式和最佳实践
    : 依赖注入是许多设计模式和最佳实践的基础,如控制反转(Inversion of Control,简称IoC)、服务定位器模式(Service Locator Pattern)等。通过使用依赖注入,开发者可以更容易地实现这些模式和实践,从而提高代码的质量和可扩展性。

如何实现依赖注入

本文通过 WPF Gallery 项目学习在WPF中如何使用依赖注入,代码地址:

https://github.com/microsoft/WPF-Samples/blob/main/SampleApplications/WPFGallery

这个项目中实现依赖注入,使用到了这两个包:

image-20240711100435001

首先查看App.xaml.cs中的内容:

public partial class App : Application
{

    private static readonly IHost _host = Host.CreateDefaultBuilder()
        .ConfigureServices((context, services) =>
        {
            services.AddSingleton<INavigationService, NavigationService>();
            services.AddSingleton<MainWindow>();
            services.AddSingleton<MainWindowViewModel>();
            
            services.AddTransient<DashboardPage>();
            services.AddTransient<DashboardPageViewModel>();

            services.AddTransient<ButtonPage>();
            services.AddTransient<ButtonPageViewModel>();
            services.AddTransient<CheckBoxPage>();
            services.AddTransient<CheckBoxPageViewModel>();
            services.AddTransient<ComboBoxPage>();
            services.AddTransient<ComboBoxPageViewModel>();
            services.AddTransient<RadioButtonPage>();
            services.AddTransient<RadioButtonPageViewModel>();
            services.AddTransient<SliderPage>();
            services.AddTransient<SliderPageViewModel>();
            services.AddTransient<CalendarPage>();
            services.AddTransient<CalendarPageViewModel>();
            services.AddTransient<DatePickerPage>();
            services.AddTransient<DatePickerPageViewModel>();
            services.AddTransient<TabControlPage>();
            services.AddTransient<TabControlPageViewModel>();
            services.AddTransient<ProgressBarPage>();
            services.AddTransient<ProgressBarPageViewModel>();
            services.AddTransient<MenuPage>();
            services.AddTransient<MenuPageViewModel>();
            services.AddTransient<ToolTipPage>();
            services.AddTransient<ToolTipPageViewModel>();
            services.AddTransient<CanvasPage>();
            services.AddTransient<CanvasPageViewModel>();
            services.AddTransient<ExpanderPage>();
            services.AddTransient<ExpanderPageViewModel>();
            services.AddTransient<ImagePage>();
            services.AddTransient<ImagePageViewModel>();
            services.AddTransient<DataGridPage>();
            services.AddTransient<DataGridPageViewModel>();
            services.AddTransient<ListBoxPage>();
            services.AddTransient<ListBoxPageViewModel>();
            services.AddTransient<ListViewPage>();
            services.AddTransient<ListViewPageViewModel>();
            services.AddTransient<TreeViewPage>();
            services.AddTransient<TreeViewPageViewModel>();
            services.AddTransient<LabelPage>();
            services.AddTransient<LabelPageViewModel>();
            services.AddTransient<TextBoxPage>();
            services.AddTransient<TextBoxPageViewModel>();
            services.AddTransient<TextBlockPage>();
            services.AddTransient<TextBlockPageViewModel>();
            services.AddTransient<RichTextEditPage>();
            services.AddTransient<RichTextEditPageViewModel>();
            services.AddTransient<PasswordBoxPage>();
            services.AddTransient<PasswordBoxPageViewModel>();
            services.AddTransient<ColorsPage>();
            services.AddTransient<ColorsPageViewModel>();

            services.AddTransient<LayoutPage>();
            services.AddTransient<LayoutPageViewModel>();
            services.AddTransient<AllSamplesPage>();
            services.AddTransient<AllSamplesPageViewModel>();
            services.AddTransient<BasicInputPage>();
            services.AddTransient<BasicInputPageViewModel>();
            services.AddTransient<CollectionsPage>();
            services.AddTransient<CollectionsPageViewModel>();
            services.AddTransient<MediaPage>();
            services.AddTransient<MediaPageViewModel>();
            services.AddTransient<NavigationPage>();
            services.AddTransient<NavigationPageViewModel>();
            services.AddTransient<TextPage>();
            services.AddTransient<TextPageViewModel>();
            services.AddTransient<DateAndTimePage>();
            services.AddTransient<DateAndTimePageViewModel>();
            services.AddTransient<StatusAndInfoPage>();
            services.AddTransient<StatusAndInfoPageViewModel>();
            services.AddTransient<SamplesPage>();
            services.AddTransient<SamplesPageViewModel>();
            services.AddTransient<DesignGuidancePage>();
            services.AddTransient<DesignGuidancePageViewModel>();

            services.AddTransient<UserDashboardPage>();
            services.AddTransient<UserDashboardPageViewModel>();

            services.AddTransient<TypographyPage>();
            services.AddTransient<TypographyPageViewModel>();

            services.AddSingleton<IconsPage>();
            services.AddSingleton<IconsPageViewModel>();

            services.AddSingleton<SettingsPage>();
            services.AddSingleton<SettingsPageViewModel>();

            services.AddSingleton<AboutPage>();
            services.AddSingleton<AboutPageViewModel>();
        }).Build();


    [STAThread]
    public static void Main()
    {
        _host.Start();

        App app = new();
        app.InitializeComponent();
        app.MainWindow = _host.Services.GetRequiredService<MainWindow>();
        app.MainWindow.Visibility = Visibility.Visible;
        app.Run();
    }
}

image-20240711083011393

IHost是什么?

在C#中,
IHost
是一个接口,它是.NET 中用于构建和配置应用程序的
Host
的概念的抽象。
IHost
接口定义了启动、运行和管理应用程序所需的服务和组件的集合。它通常用于ASP.NET Core应用程序,但也适用于其他类型的.NET 应用程序,如控制台应用程序或WPF程序。

image-20240711082817268

IHost
接口由
HostBuilder
类实现,它提供了创建和配置
IHost
实例的方法。
HostBuilder
允许你添加各种服务,如日志记录、配置、依赖注入容器等,并配置应用程序的启动和停止行为。

image-20240711083048854

image-20240711083156306

提供了用于使用预配置默认值创建Microsoft.Extensions.Hosting.IHostBuilder实例的方便方法。

image-20240711083713204

返回一个IHostBuilder。

image-20240711084145756

image-20240711084211035

向容器中添加服务。此操作可以调用多次,其结果是累加的。

参数configureDelegate的含义是配置Microsoft.Extensions.DependencyInjection.IServiceCollection的委托,
该集合将用于构造System.IServiceProvider。

该委托需要两个参数类型分别为HostBuilderContext、IServiceCollection没有返回值。

image-20240711084853971

这里传入了一个满足该委托类型的Lambda表达式。

在C#中,
() => {}
是一种Lambda表达式的语法。Lambda表达式是一种轻量级的委托包装器,它可以让你定义一个匿名方法,并将其作为参数传递给支持委托或表达式树的方法。

Lambda表达式提供了一种简洁的方式来定义方法,特别是在需要将方法作为参数传递给其他方法时,它们非常有用。

image-20240711085344696

在添加服务,这里出现了两种生命周期,除了AddSingleton、AddTransient外还有AddScoped。

这些方法定义了服务的生命周期,即服务实例在应用程序中的创建和管理方式。

AddSingleton

  • 生命周期
    :单例(Singleton)
  • 含义
    :在整个应用程序生命周期内,只创建一个服务实例。无论从容器中请求多少次,都会返回同一个实例。
  • 适用场景
    :适用于无状态服务,或者在整个应用程序中共享的资源,如配置、日志记录器等。

AddTransient

  • 生命周期
    :瞬时(Transient)
  • 含义
    :每次从容器中请求服务时,都会创建一个新的实例。
  • 适用场景
    :适用于有状态的服务,或者每次请求都需要一个新的实例的场景,如页面、视图模型等。

AddScoped

  • 生命周期
    :作用域(Scoped)
  • 含义
    :在每个作用域内,服务实例是唯一的。作用域通常与请求的生命周期相关联,例如在Web应用程序中,每个HTTP请求会创建一个新的作用域。
  • 适用场景
    :适用于需要在请求范围内共享实例的服务,如数据库上下文。

使用这些服务

在Main函数中:

image-20240711095100016

启动
_host
,通过
_host.Services.GetRequiredService<MainWindow>();
获取MainWindow实例。

以MainWindow类为例,查看MainWindow.xaml.cs中MainWindow的构造函数:

public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider, INavigationService navigationService)
{
    _serviceProvider = serviceProvider;
    ViewModel = viewModel;
    DataContext = this;
    InitializeComponent();

    Toggle_TitleButtonVisibility();

    _navigationService = navigationService;
    _navigationService.Navigating += OnNavigating;
    _navigationService.SetFrame(this.RootContentFrame);
    _navigationService.Navigate(typeof(DashboardPage));

    WindowChrome.SetWindowChrome(
        this,
        new WindowChrome
        {
            CaptionHeight = 50,
            CornerRadius = default,
            GlassFrameThickness = new Thickness(-1),
            ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
            UseAeroCaptionButtons = true
        }
    );

    this.StateChanged += MainWindow_StateChanged;
}

去掉与本主题无关的内容之后,如下所示:

public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider, INavigationService navigationService)
{
    _serviceProvider = serviceProvider;
    ViewModel = viewModel; 
    _navigationService = navigationService;  
}

有没有发现不用自己new这些对象了,这些对象的创建由依赖注入容器来管理,在需要这些对象的时候,像现在这样通过构造函数中注入即可。

如果没有用依赖注入,可能就是这样子的:

public MainWindow()
{
    _serviceProvider = new IServiceProvider();
    ViewModel = new MainWindowViewModel(); 
    _navigationService = new INavigationService();  
}

总结

本文先介绍依赖注入的概念,再解释为什么要进行依赖注入,最后通过 WPF Gallery 这个项目学习如何在WPF中使用依赖注入。

参考

1、[WPF-Samples/Sample Applications/WPFGallery at main · microsoft/WPF-Samples (github.com)](
https://github.com/microsoft/WPF-Samples/tree/main/Sample
Applications/WPFGallery)

作者:来自 vivo 互联网服务器团队-Leng Jianyu、Huang Haitao

HBase是一款开源高可靠性、扩展性、高性能和灵活性的分布式非关系型数据库,本文围绕数据库选型以及使用HBase的痛点展开,从四个方面对HBase的使用进行优化,取得了一些不错效果。

一、业务简介

统一内容平台主要承担vivo内容生态的内容审核、内容理解、内容智作和内容分发等核心功能,通过聚合全网内容,建设行业级的业务中台和内容生态,为上下游提供优质可靠的一站式服务,目前服务的业务方包括视频业务、泛信息流业务等。

图片

作为一个内容中台,每天要新增存储大量图文和视频内容来满足分发的需要。与此同时,对这些内容加工处理的数据也都需要存储,包括基础信息,分类标签信息,审核信息等等。而且不仅仅是需要存储的数据量级很大,对数据的读取和写入操作也非常频繁,当前对数据库的读写主要集中在两个方面:

  • 核心链路的内容处理包含了大量对内容特征信息的读取和写入操作;

  • 对外提供的查询服务会产生很多回源查询数据库的操作。

在经过多年累积后,当前存储的数据量越变越大,并且可以预见数据量级会不断膨胀下去,如何选择一种可靠的存储选型,来保证服务稳定性和扩展性成了当前项目架构的重中之重。

二、存在的问题

在选用HBase之前,内容平台核心数据的存储以Mongodb为主要存储选型,但是在日常的使用中,发现存储侧存在如下一些
痛点问题

  • 核心数据量大,大表有20TB 以上,总存储 60TB 以上,Mongodb 的存储架构,无法满足良好扩展性要求;

  • 访问查询流量大,需要承载智慧push、泛信息流、视频推荐侧的大回源查询流量,保持查询接口高性能;

  • 为了维护Mongodb的稳定性,需要定期切换 Mongodb 数据库主从节点,重做实例,需要运维长期投入,维护成本高。

所以我们迫切需要寻找一个更适合当前场景的数据库来满足业务请求量和存储大小日益增长的需求,并且要求具备高性能、高稳定、可扩展、低维护成本的特性。

三、存储选型

经过一些调研后发现HBase的一些特性能很好地满足当前场景的要求。

(1)高性能

HBase采用的是Key/Value的列式存储方式(对比Mongodb是行式数据库),同一个列族的数据存放在一个文件中,随着文件的增长会进行分裂,分散到其他机器上,所以即使随着数据量增大,也不会导致读写性能的下降。HBase具备毫秒级的读写性能,如果写入数据量大,还可以使用bulkload导入数据的方式进行高效入库。

(2)高扩展性、高容错性

HBase的存储是基于Hadoop的,Hadoop实现了一个分布式文件系统(HDFS),HDFS的副本机制使得其具有高容错性的特点,并且HDFS的Federation机制使得其具有高扩展性。基于Hadoop意味着HBase与生俱来的超强扩展性和高容错性。

(3)强一致性

HBase的数据是强一致性的,从CAP理论来看,HBase是属于CP的。CAP 定理表明,在存在网络分区的情况下,一致性和可用性必须二选一。HBase在写入数据时,先把操作的记录写入到预写日志中(Write-ahead log,WAL),然后再被加载到Memstore的。就算某个节点机器宕掉了,由于WAL的数据是存储在HDFS上的,所以数据并不会丢失,后续可以通过读取预写日志恢复内容。

(4)列值支持多版本

HBase的多版本特性可以针对某个列族控制列值的版本数,默认是1,即每个key保存一个版本,同一个rowkey的情况下,后面的列值会覆盖前面的列值。可以动态修改列族的版本数,每个版本使用时间戳进行标记,默认是写入时间作为该版本的时间戳,也可以在写入时指定时间戳。

综合以上特性,HBase是非常适合当前项目对数据库选型的要求。

四、HBase 优化实践

随着HBase在整个项目中逐步扩大使用,也发现了一些使用规范问题以及一些查询的性能问题。比如查询毛刺比较多、夜间Compact期间耗时比较高、流量高峰期的时候少量请求会有延迟。针对这些问题,我们从下面四个方面,对HBase的使用进行了优化。

4.1 集群升级

刚开始使用HBase的时候,我们使用的HBase集群版本是1.2版本的,此版本存在诸多弊端,如:RIT(Region-In-Transition)问题频发、请求延时突刺、建删表速度慢、meta 表稳定性差、节点故障恢复速度慢等问题。我们在使用过程发现的主要问题是响应时间突刺问题,该问题会导致我们实时查询接口在回源时超时较多,导致接口的响应时间有突刺被下游业务方熔断,影响业务查询。与HBase团队讨论与评估后,决定将业务使用的集群升级到HBase 2.4.8 版本。该版本在公司较多的业务场景中已经得到验证,可以解决大部分1.2.0版本存在的痛点问题,可以大幅提升读写性能,有效降低读毛刺,单机处理性能的提升可减少20%左右机器成本。

下面是集群升级后的读写平均耗时对比图。可以看到在升级之前,平均响应时间经常会有一些突刺,最高能达到超过10s,升级后几乎不存在这么高的平均响应时间突刺,能保持在10ms以下,偶尔较高也是几十毫秒级别。

图片

升级前

图片

升级后

4.2 连接池使用和连接预热

HBase Connection 创建对象并不是简单对应一个socket连接,需要与Zookeeper以及HMaster、RegionServer都建立连接,所以该过程是一个非常耗资源的过程,一般只创建一个 Connection 实例,其它地方共享该实例。在Connection初始化之后,用connection下的getTable方法实现对表格的连接。为了减少与表格连接带来的网络开销,我们建立了对不同表格的连接池来管理客户端和服务端的连接。大致流程图如下图所示。

通过建立连接池,带来了以下三点优势:

(1)对表和表之间进行了连接资源隔离,避免互相影响;

(2)对连接实现了复用,减少了创建连接的网络开销;

(3)防止突增的流量带来的影响,实现平滑处理流量。

此外,图中可以看到,在程序启动阶段,可以实现对HBase表连接的预热,提前建立对表格的连接,可以有效避免在程序启动阶段由于大量建立连接导致读写的响应时间变长,影响整体性能。

连接池通过使用Apache Commons Pool提供的GenericObjectPool通用对象池来实现,GenericObjectPool包含丰富的配置选项,能够定期回收空闲对象,并且支持对象验证,具有强大的线程安全性和可扩展性。然后将不同表格的连接池对象放到本地缓存LoadingCache中,LoadingCache底层通过LRU算法实现对最久远且没有使用的数据的淘汰,保证没有使用的表格连接能及时释放。通过使用第三方的对象池和本地缓存,建立了对HBase表格的连接池,并且实现了预加载,减少了一些读写HBase的开销,降低了读写耗时,对于刚启动服务时的读写突刺带来了一些改善。

图片

4.3 按列读取

HBase建表的时候是不需要确定列的,因为列是可变的,它非常灵活,唯一需要确定的就是列族。一张表的很多属性比如过期时间、数据块缓存以及是否压缩等都是定义在列族上,而不是定义在表上或者列上,这一点做法跟以往的数据库有很大的区别。同一个表里的不同列族可以有完全不同的属性配置,但是同一个列族内的所有列都会有相同的属性。一个没有列族的表是没有意义的,因为列必须依赖列族而存在,所以在HBase中一个列的名称前面总是带着它所属的列族。列族存在使得HBase会把相同列族的列尽量放在同一台机器上,不同列族的列分布在不同的机器上。

一般情况下,从客户端发起请求读取数据,到数据返回大致有如下几步:

  1. 客户端从ZooKeeper中获取meta表所在regionServer节点信息。

  2. 客户端访问meta表所在的regionServer节点,获取region所在节点信息。

  3. 客户端访问具体region所在regionServer,找到对应的region。

  4. 首先从blockCache中读取数据,存在则返回,不存在则去memstore中读取数据,存在则返回,不存在去storeFile(HFile)中读取数据,存在会先将数据写入到blockCache中,然后返回数据,不存在则返回空。

简单的示意图如下所示:

图片

整个过程中如果读取字段过多,或者字段长度过大,那么返回所有列的数据会导致大量无效的数据传输,进而导致集群网络带宽等系统资源被大量占用,必然导致读取性能降低,所以需要减少一些不必要字段的查询。

Get类是HBase官方提供的查询类,在该类中主要有以下几个方法提供来实现减少字段读取:

  • addFamily
    :添加要取出的列族;

  • addColumn
    :添加要取出的列;

  • setTimeRange
    :设置要取出的版本范围;

  • setMaxVersions
    :设置取出版本数量。

当前项目中没有使用到HBase的版本范围和版本数量的特性,但是主要场景使用的表字段都比较多(如内容的基本属性能达到上百个字段),或者字段的大小都比较大(如内容解析的一些向量字段),原本在查询时,都是直接读取所有字段,导致很多字段其实不需要使用也被一直读取,浪费性能。通过改用按列读取的方式来实现不同场景下不同字段的查询,避免了超过一半无用字段的返回,平均响应时间也下降了一些。

4.4 compact优化

HBase是基于LSM树存储模型的分布式NoSQL数据库。LSM树相比于普遍使用在各种数据库的B+树来说,能够获得较高随机写性能的同时,也能保持可靠的随机读性能。在进行读请求的时候,LSM树要把多个子树(类似B+树结构)进行归并查询, 因此归并查询的子树数越少,查询的性能就越高。当MemStore超过阀值的时候,就要flush到HDFS上生成一个HFile。因此随着不断写入,HFile的数量将会越来越多,根据前面所述,HFile数量过多会降低读性能。为了避免对读性能的影响,可以对这些HFile进行compact操作,把多个HFile合并成一个HFile。compact操作需要对HBase的数据进行多次的重新读写,因此这个过程会产生大量的IO。可以看到compact操作的本质就是以IO操作换取后续的读性能的提高。

图片

HBase的compact是针对HRegion的HFile文件进行操作的。compact操作分为major和minor两种。major compaction会把所有的HFile都compact为一个HFile,并同时忽略标记为delete的KeyValue(被删除的KeyValue只有在compact过程中才真正被"删除"),可以想象major compaction会产生大量的IO操作,对HBase的读写性能产生影响。minor则只会选择数个HFile文件compact为一个HFile,minor的过程一般较快,而且IO相对较低。在业务高峰期间,都会禁止major操作,只在业务空闲的时段定时执行。

hbase.hstore.compaction.throughput.higher.bound是HBase中控制HFile文件合并(compaction)速度的参数之一。它指定了一个HFile文件每秒最大合并数据大小的上限,以字节为单位。如果一个HFile文件的大小超过了这个上限,HBase就会尝试将其分裂成较小的文件来加快合并速度。通过调整该参数,可以控制HBase在什么条件下开始尝试合并HFile文件。较小的值会导致更频繁的文件合并,也会降低HBase的性能。较大的值则可能导致HFile文件的大小增长过快,从而影响读取性能。

hbase.hstore.compaction.throughput.lower.bound也是HBase中控制HFile文件合并速度的参数之一。它指定了一个HFile文件每秒最小合并数据大小的下限,以字节为单位。当合并速度达到这个下限时,HBase会停止合并更小的HFile文件,而等待更多的数据到达之后再进行合并操作。与higher.bound参数相比,lower.bound参数更加影响文件合并频率和性能。过高的值会导致较少的文件合并和较大的HFile文件,这会影响读取性能和写入并发性。反之,过低的值会导致过于频繁的文件合并,从而占用过多的CPU和磁盘I/O资源,影响整个HBase集群的性能。针对Compact对业务耗时的影响,我们对Compact 操作进行了限流,并且通过多次测试调整Compact上文提到的两个限流的阈值,取得了非常好的效果。Compact期间的耗时下降了70%y以上。下图展示了采取限流前后的耗时对比。

图片

4.5 字段级版本管理

除了上述提到的优化点,我们也探索了一些HBase的其它特性,以备将来用来优化其他方面。上文提到,通过对HBase进行按列读取数据,可以减少get查询的时间,通常意义来讲,列(也就是每个字段)已经是每条数据的最基本单位了,但是HBase中的数据粒度比传统数据结构更细一级,同一个位置的数据还细分成多个版本,一个列上可以存储多个版本的值,多个版本的值被存储在多个单元格里面,多个版本之间用版本号( version)来区分。所以,唯一确定一条结果的表达式应该是行键:列族:列:版本号(rowkey:column family:column:version)。不过,版本号通常是可以省略的,如果写入时不写版本号,每个列或者单元格的值都被赋予一个时间戳,这个时间戳默认是由系统制定的,当然写入时也可以由用户显式指定具体的版本号。在查询时如果不指定版本号,HBase默认获取最后一个版本的数据返回给你。当然也可以指定版本号返回需要的其他版本的数据。简单的示意图如下所示:

图片

同时HBase为了避免数据存在过多的版本造成不必要的负担,HBase提供了两种数据版本的回收方式,一是按照数量维度,保存最后的n个版本,二是按照时间维度,保存最近一段时间的版本数据,比如保存一个月。通过多版本同时存储,对于一些有时序要求的场景非常友好,通过指定版本的时间戳,可以避免在已经更新了新数据的情况下,被旧数据覆盖。当前我们建表是都是只指定了一个版本,使用也都是用的以时间戳为版本号的默认版本,没有采取版本管理的措施,不同单元格可以记录多版本的特性可以考虑应用于字段更新时记录下多个版本的数据,在不影响读写效率的情况下,方便后续在没有相关日志的情况下,回溯最近几次更新的值,并且可以防止误操作或数据损坏,因为用户可以恢复到之前的版本数据。此外我们的系统中存在一些通过消息队列异步更新场景,此时可以使用消息体中的时间戳作为当前版本号,这样可以在多线程消费时,也能保证消费的时序性,因为低版本的版本号无法更新高版本的版本号。

五、总结

本文在对统一内容平台在数据库选型分析和优化的基础上,简要介绍了HBase在实际使用中的一些优化方案,经优化后,项目整体读取和写入性能都有比较明显的提升,较好的保障了统一内容平台业务的稳定性,并且大大降低了业务侧的运维成本。Hbase本身就具备强大的功能,在大数据领域有独有的优势,但是在不同的业务场景,对于HBase的要求也是不一样的,可以结合具体的实际情况,从使用的数据库版本、从HBase底层机制的调参、从客户端调用机制的优化等多方面挖掘,探索更适合业务的方式,希望本文中提到的一些优化方案能给读者带来一些启发。

image

在C#中,
委托
是一种引用类型的数据类型,允许我们封装方法的引用。通过使用委托,我们可以将方法作为参数传递给其他方法,或者将多个方法组合在一起,从而实现更灵活的编程模式。委托类似于函数指针,但提供了类型安全和垃圾回收等现代语言特性。

基本概念

定义委托

定义委托需要指定它所代表的方法的原型,包括返回类型和参数列表。例如:

public delegate int MyDelegate(int a, int b);

这定义了一个名为
MyDelegate
的委托,它代表一个接受两个整数参数并返回一个整数的方法。

创建委托实例

创建委托实例时,可以使用
new
关键字加上委托类型和要封装的方法名:

MyDelegate myDelegate = new MyDelegate(AddNumbers);

其中
AddNumbers
是一个符合
MyDelegate
定义的方法。

调用委托

一旦有了委托实例,就可以像调用方法一样调用它:

int result = myDelegate(5, 3);

多播委托

委托可以被设计为“多播”的,这意味着它们可以调用多个方法。当调用多播委托时,所有注册的方法都会按顺序执行:

MyDelegate del1 = new MyDelegate(AddNumbers);
MyDelegate del2 = new MyDelegate(MultiplyNumbers);
MyDelegate combinedDel = del1 + del2;
combinedDel(5, 3); // 先调用 AddNumbers, 然后调用 MultiplyNumbers

匿名方法和Lambda表达式

从C# 2.0开始,可以使用匿名方法来定义委托:

MyDelegate anonMethod = delegate(int x, int y) { return x + y; };

而在C# 3.0之后,Lambda表达式成为更简洁的选择:

MyDelegate lambdaExpr = (int x, int y) => x + y;

事件

在C#中,
事件
是一种特殊的委托类型,用于封装消息的发送机制,通常用于组件之间或对象之间的通信。事件是.NET框架中处理异步操作、UI交互和信号通知的核心机制。下面详细介绍事件的概念、定义和使用。

事件的基本概念

事件允许一个类(事件发布者)向另一个或多个类(事件订阅者)广播信息,而无需直接调用接收方的任何方法。这实现了发布者和订阅者之间的解耦,使得代码更加模块化和可维护。

定义事件

事件基于委托类型定义,通常使用
System.EventHandler
或自定义的委托类型。定义事件时,通常在类中声明一个委托类型的私有字段,并在此基础上声明一个公共的事件属性。例如:

public class Publisher
{
    // 自定义委托类型
    public delegate void MyEventHandler(object source, MyEventArgs args);

    // 定义事件
    private event MyEventHandler myEvent;

    // 使用事件属性暴露事件
    public event MyEventHandler MyEvent
    {
        add { myEvent += value; }
        remove { myEvent -= value; }
    }

    // 触发事件
    public void RaiseEvent()
    {
        if (myEvent != null)
        {
            myEvent(this, new MyEventArgs());
        }
    }
}

在这个例子中,
Publisher
类有一个名为
MyEvent
的事件,它基于
MyEventHandler
委托类型。事件触发时,
RaiseEvent
方法会被调用,它会检查是否有订阅者,并调用所有订阅者的相应方法。

订阅和取消订阅事件

事件可以通过事件处理程序来订阅。事件处理程序是一个与事件委托类型匹配的方法。订阅事件时,将事件处理程序添加到事件中;取消订阅则移除该处理程序。

class Subscriber
{
    public void OnMyEvent(object sender, MyEventArgs e)
    {
        Console.WriteLine("Event triggered.");
    }
}

// 在某个地方订阅事件
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.MyEvent += subscriber.OnMyEvent;

// 取消订阅事件
publisher.MyEvent -= subscriber.OnMyEvent;

使用事件参数

事件经常与事件参数一起使用,这些参数通常封装了事件发生时的附加信息。事件参数是通过继承
EventArgs
类来创建的。例如:

public class MyEventArgs : EventArgs
{
    public string Message { get; set; }
}

在触发事件时,可以创建事件参数的实例并传递给事件:

public void RaiseEvent()
{
    if (myEvent != null)
    {
        myEvent(this, new MyEventArgs { Message = "Hello, world!" });
    }
}

一、互联网项目架构-特点


互联网项目架构-特点

1.用户多:微信号称13亿用户;

2.流量大,并发高:百度统计,百度 一天承载超五十亿次搜索,天猫:双十一每秒4200万次请求;

3.海量数据:微信号称13亿用户,用户数据要存数据库;天猫,天猫的商品非常多;

4.易受攻击:项目是公网项目,容易受到不法分子攻击。

5.功能繁琐:如支付宝,就有很多功能。

6.变更快:互联网产品要快速响应需求;要快速完成需求,抢占市场;

二、传统项目和互联网项目


传统项目和互联网项目的不同:

传统项目:如 使用OA系统请假;HR:人力资源系统,CRM:客户关系管理系统;供内部员工使用。
互联网项目: 如京东,淘宝,网民使用。

传统项目和互联网项目的区别:

1.用户群体不一样;用户群体为企业员工和网民;

2.用户人数不同:

公司人数:大概 :5000 人或一万人;
网民: 2023年6月,中国网民规模达10.79亿人
所以 用户数量不一样;需要考虑并发;


3.用户忍耐力不一样;
企业用户忍耐力高;互联网项目不一样;要重视用户体验:

用户体验有:

美观(UI负责)、功能(产品经理负责)、速度、稳定性。

三、互联网项目架构-目标


大型互联网架构目标:
词语解释:架构:如架构师 ,做项目之前需要设计架构,按要求设计,以达到目标。架构师需要做技术选型,如用什么语言开发Java 还是python , 数据库用MySQL 还是Oracle,用不用Redis ,还是用memcache,用不用ElasticSearch。

衡量网站的性能指标:

1.响应时间:指执行一个请求从开始到最后收到响应数据所花费的总体时间。

2.并发数:指系统同时能处理的请求数量。

并发连接数: 指的是客户端向服务器发起请求,并建立了TCP连接。每秒钟服务器连接的总TCP数量

请求数:也称为QPS(Query Per Second)指每秒多少请求.

并发用户数:单位时间内有多少用户

3.吞吐量:指单位时间内系统能处理的请求数量。

- QPS: Query Per Second每秒查询数。
- PS: Transactions Per Second每秒事务数。


一个事务是指一 个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。


一个页面的一次访问,只会形成一 个TPS; 但-次页面请求,可能产生多次对服务器的请求,就会有多个QPS


QPS>=并发连接数>= TPS

大型互联网项目架构目标:

1.高性能:提供快速的访问体验。
2.高可用:网站服务- 可以正常访问
3.可伸缩:可伸缩:通过硬件增加/减少,提高/降低处理能力;

如订单服务项目,数据库写性能有瓶颈,加个服务器,部署订单项目,处理能力就增加了;
4.高可扩展: 系统间耦合低,方便的通过新增/移除方式,增加/减少新的功能/模块
5.安全性:提供网站安全访问和数据加密,安全存储等策略;
6.敏捷性:随需应变,快速响应.

前言

.NET 官方有一个用来管理国际化资源的扩展包
Microsoft.Extensions.Localization
,ASP.NET Core也用这个来实现国际化功能。但是这个包的翻译数据是使用resx资源文件来管理的,这就意味着无法动态管理。虽然官方有在文档中提供了一些第三方管理方案,但是都不太方便。其中一个是基于Json文件的,虽然可以动态管理,但是正确的Key值有时很难猜对,特别是对于嵌套类和泛型类之类名字比较特殊的。另外两个基于EF Core的一个只是个demo;另一个已多年未更新,且上下文生命周期和并发管理有缺陷(这个库还是我提交pr才支持的 .NET 5)。最近项目有用到国际化功能,只好重新写一个。

新书宣传

有关新书的更多介绍欢迎查看
《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!
image

相关旧文

Asp.Net Core 混合全球化与本地化支持

正文

这个扩展包代码不多也不算复杂,主要结构参考官方内置实现。对于 .NET 5以上支持上下文工厂的版本使用上下文工厂,而对于旧版本则创建内部作用域获取私有上下文,以此彻底避免并发问题。作用域和上下文都是需要查询时临时获取和使用,查询完数据立即销毁避免内存泄漏。如果使用池化上下文工厂性能会更好。

对代码感兴趣的朋友可以移步
Github
。这里直接介绍一下基本用法。

这个库分为三个包:抽象包定义了所需接口,实体模型包定义基本实体类型,功能包定义了服务接口的实现类和用于注册服务的扩展方法。方便为分离项目的解决方案按需引用,减少无关类型的污染。

以在ASP.NET Core中使用为例:

实体模型和上下文

public class YourLocalizationRecord : LocalizationRecord
{
    public int YourProperty { get; set; }
}

public class YourDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 使用默认类型。
        modelBuilder.UseLocalizationRecord();

        // 使用自定义类型,需要继承LocalizationRecord。
        modelBuilder.UseLocalizationRecord<YourLocalizationRecord>(b =>
        {
            b.Property(r => r.ResourceCulture).HasMaxLength(32);
            b.ToTable($"{nameof(YourLocalizationRecord)}s");
        });
    }
}

服务注册

// 对于 .NET 5 以上请使用上下文工厂。
services.AddDbContextFactoty<YourDbContext>(options => options.UseSqlite("Localization.db"));
// 或者池化工厂也是一样的,而且更好。
services.AddPooledDbContextFactoty<YourDbContext>(options => options.UseSqlite("Localization.db"));
// 注册一个自定义工厂服务模拟作用域上下文服务
services.AddScoped<YourDbContext>(sp => sp.GetRequiredService<IDbContextFactory<YourDbContext>>().CreateDbContext());

// 对于 .NETStandard 2.0 或 2.1 请使用上下文。
services.AddDbContext<YourDbContext>(options => options.UseSqlite("Localization.db"));

// 注册使用默认实体类型的服务。
services.AddEntityFrameworkCoreLocalization<YourDbContext>(options =>
{
    options.ResourcesPath = "Resources";
    // 是否自动创建缺失的资源记录
    options.CreateLocalizationResourcesIfNotExist = true;
});

//  注册使用自定义实体类型的服务。
services.AddEntityFrameworkCoreLocalization<YourDbContext, YourLocalizationRecord>(options =>
{
    options.ResourcesPath = "Resources";
    // 是否自动创建缺失的资源记录
    options.CreateLocalizationResourcesIfNotExist = true;
});

其他的和官方文档用法完全一致,如果需要清除缓存使资源能在下次读取时更新,可以使用服务
IDynamicResourceStringLocalizerFactory
。这个服务继承自内置服务,获取的
IStringLocalizerFactory
服务实际上也是
IDynamicResourceStringLocalizerFactory
的实现。

既然已经有上下文了,想怎么读写数据应该不必多言了吧。实体类的属性
LocalizedContent
就是翻译后的文本。如果使用自动创建记录,只需要查找所有这个属性为null的记录并翻译保存,最后清除缓存即可。

image

结语

为了实现对 .NETStantard 2.0 的兼容代码上使用了条件编译预处理实现一份代码一个项目同时编译到所有框架,最大程度共用代码简化代码管理。

附上国际化官方文档:
使 ASP.NET Core 应用内容可本地化

在本地化 ASP.NET Core 应用中为每个请求选择语言/区域性

许可证:MIT
代码仓库:
CoreDX.Extensions.Localization.EntityFrameworkCore - Github
Nuget:
CoreDX.Extensions.Localization.EntityFrameworkCore
Nuget:
CoreDX.Extensions.Localization.EntityFrameworkCore.Abstractions
Nuget:
CoreDX.Extensions.Localization.EntityFrameworkCore.Models

QQ群

读者交流QQ群:540719365
image

欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者。

本文地址:
https://www.cnblogs.com/coredx/p/18294729.html