...

C# 10的新特性

2022-02-15

前言


们很高兴地宣布 C# 10 作为 .NET 6 和 Visual Studio 2022的一部分已经发布了。在这篇文章中,我们将介绍 C# 10 的许多新功能,这些功能使你的代码更漂亮、更具表现力、更快。阅读 Visual Studio 2022 公告和.NET 6 公告以了解更多信息,包括如何安装。


Visual Studio 2022 公告:https://aka.ms/vs2022gablog


.NET 6:https://aka.ms/dotnet6-GA


全局和隐式 usings


using 指令简化了您使用命名空间的方式。C# 10 包括一个新的全局 using 指令和隐式 usings,以减少您需要在每个文件顶部指定的 usings 数量。


全局 using 指令


如果关键字 global 出现在 using 指令之前,则 using 适用于整个项目:

global using System;

你可以在全局 using 指令中使用 using 的任何功能。例如,添加静态导入类型并使该类型的成员和嵌套类型在整个项目中可用。如果您在using 指令中使用别名,该别名也会影响您的整个项目:

global using static System.Console;
global using Env = System.Environment;

您可以将全局使用放在任何 .cs 文件中,包括 Program.cs 或专门命名的文件,如 globalusings.cs。全局 usings 的范围是当前编译,一般对应当前项目。

有关详细信息,请参阅全局 using 指令。

  • 全局 using 指令https://docs.microsoft.com/dotnet/csharp/languagereference/keywords/using-directive#global-modifier

隐式 usings

隐式 usings 功能会自动为您正在构建的项目类型添加通用的全局 using 指令。要启用隐式 usings,请在 .csproj 文件中设置 ImplicitUsings 属性:

<PropertyGroup>
    <!-- Other properties like OutputType and TargetFramework -->
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

在新的 .NET 6 模板中启用了隐式 usings 。在此博客文章中阅读有关 .NET 6 模板更改的更多信息。

一些特定全局 using 指令集取决于您正在构建的应用程序的类型。例如,控制台应用程序或类库的隐式 usings 不同于 ASP.NET 应用程序的隐式 usings。

有关详细信息,请参阅此隐式usings文章。

  • 博客文章

    https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#net-sdk-c-project-templates-modernized

  • 隐式usings

    https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview#implicit-using-directives

Combining using 功能

文件顶部的传统 using 指令、全局 using 指令和隐式 using 可以很好地协同工作。隐式 using 允许您在项目文件中包含适合您正在构建的项目类型的 .NET 命名空间。全局 using 指令允许您包含其他命名空间,以使它们在整个项目中可用。代码文件顶部的 using 指令允许您包含项目中仅少数文件使用的命名空间。

无论它们是如何定义的,额外的 using 指令都会增加名称解析中出现歧义的可能性。如果遇到这种情况,请考虑添加别名或减少要导入的命名空间的数量。例如,您可以将全局 using 指令替换为文件子集顶部的显式 using 指令。

如果您需要删除通过隐式 usings 包含的命名空间,您可以在项目文件中指定它们:

<ItemGroup>
  <Using Remove="System.Threading.Tasks" />
</ItemGroup>

您还可以添加命名空间,就像它们是全局 using 指令一样,您可以将 Using 项添加到项目文件中,例如:

<ItemGroup>
  <Using Include="System.IO.Pipes" />
</ItemGroup>

文件范围的命名空间

许多文件包含单个命名空间的代码。从 C# 10 开始,您可以将命名空间作为语句包含在内,后跟分号且不带花括号:

namespace MyCompany.MyNamespace;

class MyClass // Note: no indentation
{ ... } 

他简化了代码并删除了嵌套级别。只允许一个文件范围的命名空间声明,并且它必须在声明任何类型之前出现。

有关文件范围命名空间的更多信息,请参阅命名空间关键字文章。

  • 命名空间关键字文章https://docs.microsoft.com/dotnet/csharp/languagereference/keywords/namespace

对 lambda 表达式和方法组的改进

我们对 lambda 的语法和类型进行了多项改进。我们预计这些将广泛有用,并且驱动方案之一是使 ASP.NET Minimal API 更加简单。

  • lambda 的语法

    https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-10#lambda-expression-improvements

  • ASP.NET Minimal APIhttps://devblogs.microsoft.com/dotnet/announcing-asp-net-core-in-net-6/

lambda 的自然类型

Lambda 表达式现在有时具有“自然”类型。这意味着编译器通常可以推断出 lambda 表达式的类型。

到目前为止,必须将 lambda 表达式转换为委托或表达式类型。在大多数情况下,您会在 BCL 中使用重载的 Func<...> 或 Action<...> 委托类型之一:

Func<stringint> parse = (string s) => int.Parse(s);

但是,从 C# 10 开始,如果 lambda 没有这样的“目标类型”,我们将尝试为您计算一个:

var parse = (string s) => int.Parse(s);

你可以在你最喜欢的编辑器中将鼠标悬停在 var parse 上,然后查看类型仍然是 Func<string, int>。一般来说,编译器将使用可用的 Func 或 Action 委托(如果存在合适的委托)。否则,它将合成一个委托类型(例如,当您有 ref 参数或有大量参数时)。

并非所有 lambda 表达式都有自然类型——有些只是没有足够的类型信息。 例如,放弃参数类型将使编译器无法决定使用哪种委托类型:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

lambda 的自然类型意味着它们可以分配给较弱的类型,例如 object 或 Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

当涉及到表达式树时,我们结合了“目标”和“自然”类型。如果目标类型是LambdaExpression 或非泛型 Expression(所有表达式树的基类型)并且 lambda 具有自然委托类型 D,我们将改为生成 Expression<D>:

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

方法组的自然类型

法组(即没有参数列表的方法名称)现在有时也具有自然类型。您始终能够将方法组转换为兼容的委托类型:

Func<intread = Console.Read;
Action<string> write = Console.Write;

现在,如果方法组只有一个重载,它将具有自然类型:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

lambda 的返回类型

在前面的示例中,lambda 表达式的返回类型是显而易见的,并被推断出来的。情况并非总是如此:

var choose = (bool b) => b ? 1 : "two"// ERROR: Can't infer return type

在 C# 10 中,您可以在 lambda 表达式上指定显式返回类型,就像在方法或本地函数上一样。返回类型在参数之前。当你指定一个显式的返回类型时,参数必须用括号括起来,这样编译器或其他开发人员不会太混淆:

var choose = object (bool b) => b ? 1 : "two"// Func<bool, object>

lambda 上的属性

从 C# 10 开始,您可以将属性放在 lambda 表达式上,就像对方法和本地函数一样。当有属性时,lambda 的参数列表必须用括号括起来:

Func<string, int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";

就像本地函数一样,如果属性在 AttributeTargets.Method 上有效,则可以将属性应用于 lambda。

Lambda 的调用方式与方法和本地函数不同,因此在调用 lambda 时属性没有任何影响。但是,lambdas 上的属性对于代码分析仍然有用,并且可以通过反射发现它们。

structs 的改进

C# 10 为 structs 引入了功能,可在 structs (结构)和类之间提供更好的奇偶性。这些新功能包括无参数构造函数、字段初始值设定项、记录结构和 with 表达式。

01 无参数结构构造函数和字段初始值设定项

在 C# 10 之前,每个结构都有一个隐式的公共无参数构造函数,该构造函数将结构的字段设置为默认值。在结构上创建无参数构造函数是错误的。

从 C# 10 开始,您可以包含自己的无参数结构构造函数。如果您不提供,则将提供隐式无参数构造函数以将所有字段设置为默认值。您在结构中创建的无参数构造函数必须是公共的并且不能是部分的:

public struct Address
{
    public Address()
    {
        City = "<unknown>";
    }
    public string City { get; init; }
}

您可以如上所述在无参数构造函数中初始化字段,也可以通过字段或属性初始化程序初始化它们:

public struct Address
{
    public string City { getinit; } = "<unknown>";
}

通过默认创建或作为数组分配的一部分创建的结构会忽略显式无参数构造函数,并始终将结构成员设置为其默认值。有关结构中无参数构造函数的更多信息,请参阅结构类型。

02 Record structs

从 C# 10 开始,现在可以使用 record struct 定义 record。这些类似于 C# 9 中引入的record 类:

public record struct Person
{
    public string FirstName { getinit; }
    public string LastName { getinit; }
}

您可以继续使用 record 定义记录类,也可以使用 record 类来清楚地说明。

结构已经具有值相等——当你比较它们时,它是按值。记录结构添加 IEquatable<T> 支持和 == 运算符。记录结构提供 IEquatable<T> 的自定义实现以避免反射的性能问题,并且它们包括记录功能,如 ToString() 覆盖。

记录结构可以是位置的,主构造函数隐式声明公共成员:

public record struct Person(string FirstName, string LastName);

主构造函数的参数成为记录结构的公共自动实现属性。与 record 类不同,隐式创建的属性是读/写的。这使得将元组转换为命名类型变得更加容易。将返回类型从 (string FirstName, string LastName) 之类的元组更改为 Person 的命名类型可以清理您的代码并保证成员名称一致。声明位置记录结构很容易并保持可变语义。

如果您声明一个与主要构造函数参数同名的属性或字段,则不会合成任何自动属性并使用您的。

要创建不可变的记录结构,请将 readonly 添加到结构(就像您可以添加到任何结构一样)或将 readonly 应用于单个属性。对象初始化器是可以设置只读属性的构造阶段的一部分。这只是使用不可变记录结构的一种方法:

var person = new Person { FirstName = "Mads", LastName = "Torgersen"};

public readonly record struct Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

在本文中了解有关记录结构的更多信息。

  • 记录结构

https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record

03 Record类中 ToString () 上的密封修饰符

记录类也得到了改进。从 C# 10 开始,ToString() 方法可以包含 seal 修饰符,这会阻止编译器为任何派生记录合成 ToString 实现。

在本文中的记录中了解有关 ToString () 的更多信息。

  • 有关 ToString () 的更多信息

    https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display

04 结构和匿名类型的表达式

C# 10 支持所有结构的 with 表达式,包括记录结构,以及匿名类型:

var person2 = person with { LastName = "Kristensen" };

这将返回一个具有新值的新实例。您可以更新任意数量的值。您未设置的值将保留与初始实例相同的值。
在本文中了解有关 with 的更多信息

  • 了解有关 with 的更多信息

ttps://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display


内插字符串改进

当我们在 C# 中添加内插字符串时,我们总觉得在性能和表现力方面,使用该语法可以做更多事情。

01 内插字符串处理程序

今天,编译器将内插字符串转换为对 string.Format 的调用。这会导致很多分配——参数的装箱、参数数组的分配,当然还有结果字符串本身。此外,它在实际插值的含义上没有任何回旋余地。

在 C# 10 中,我们添加了一个库模式,允许 API “接管”对内插字符串参数表达式的处理。例如,考虑 StringBuilder.Append:

var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");

到目前为止,这将使用新分配和计算的字符串调用 Append(string? value) 重载,将其附加到 StringBuilder 的一个块中。但是,Append 现在有一个新的重载 Append(refStringBuilder.AppendInterpolatedStringHandler handler),当使用内插字符串作为参数时,它优先于字符串重载。

通常,当您看到 SomethingInterpolatedStringHandler 形式的参数类型时,API 作者在幕后做了一些工作,以更恰当地处理插值字符串以满足其目的。在我们的 Append 示例中,字符串 “Hello”、args[0] 和“,how are you?” 将单独附加到 StringBuilder 中,这样效率更高且结果相同。

有时您只想在特定条件下完成构建字符串的工作。一个例子是 Debug.Assert:

Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");

在大多数情况下,条件为真,第二个参数未使用。但是,每次调用都会计算所有参数,从而不必要地减慢执行速度。Debug.Assert 现在有一个带有自定义插值字符串构建器的重载,它确保第二个参数甚至不被评估,除非条件为假。

最后,这是一个在给定调用中实际更改字符串插值行为的示例:String.Create() 允许您指定 IFormatProvider 用于格式化插值字符串参数本身的洞中的表达式:

String.Create(CultureInfo.InvariantCulture, $"The result is {result}");

你可以在本文和有关创建自定义处理程序的本教程中了解有关内插字符串处理程序的更多信息。

  • 创建自定义处理程序
    https://docs.microsoft.com/dotnet/csharp/languagereference/tokens/interpolated#compilation-of-interpolated-strings

  • 内插字符串处理程序的更多信息

https://docs.microsoft.com/dotnet/csharp/whats-new/tutorials/interpolated-string-handler

02 常量内插字符串

如果内插字符串的所有洞都是常量字符串,那么生成的字符串现在也是常量。这使您可以在更多地方使用字符串插值语法,例如属性:

[Obsolete($"Call {nameof(Discard)} instead")]

请注意,必须用常量字符串填充洞。其他类型,如数字或日期值,不能使用,因为它们对文化敏感,并且不能在编译时计算。

其他改进

C# 10 对整个语言进行了许多较小的改进。其中一些只是使 C# 以您期望的方式工作。

在解构中混合声明和变量

在 C# 10 之前,解构要求所有变量都是新的,或者所有变量都必须事先声明。在 C# 10 中,您可以混合:

int x2;
int y2;
(x2, y2) = (01);       // Works in C# 9
(var x, var y) = (01); // Works in C# 9
(x2, var y3) = (01);   // Works in C# 10 onwards 

在有关解构的文章中了解更多信息。

改进的明确分配

如果您使用尚未明确分配的值,C# 会产生错误。C# 10 可以更好地理解您的代码并且产生更少的虚假错误。这些相同的改进还意味着您将看到更少的针对空引用的虚假错误和警告。

在 C# 10 中的新增功能文章中了解有关 C# 确定赋值的更多信息。

  • C# 10 中的新增功能文章https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-10#improved-definite-assignment

扩展的属性模式

C# 10 添加了扩展属性模式,以便更轻松地访问模式中的嵌套属性值。例如,如果我们在上面的 Person 记录中添加一个地址,我们可以通过以下两种方式进行模式匹配:

object obj = new Person
{
    FirstName = "Kathleen",
    LastName = "Dollard",
    Address = new Address { City = "Seattle" }
};

if (obj is Person { Address: { City: "Seattle" } })
    Console.WriteLine("Seattle");

if (obj is Person { Address.City: "Seattle" }) // Extended property pattern
    Console.WriteLine("Seattle");

扩展属性模式简化了代码并使其更易于阅读,尤其是在匹配多个属性时。

在模式匹配文章中了解有关扩展属性模式的更多信息。

  • 模式匹配文章

    https://docs.microsoft.com/dotnet/csharp/languagereference/operators/patterns#property-pattern

调用者表达式属性

CallerArgumentExpressionAttribute 提供有关方法调用上下文的信息。与其他 CompilerServices 属性一样,此属性应用于可选参数。在这种情况下,一个字符串:

void CheckExpression(bool condition, 
    [CallerArgumentExpression("condition"
)] string? message
 = null )
{
    Console.WriteLine($"Condition: {message}");
}

传递给 CallerArgumentExpression 的参数名称是不同参数的名称。作为参数传递给该参数的表达式将包含在字符串中。例如,

var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);

// Output:
// Condition: true
// Condition: b
// Condition: a > 5

ArgumentNullException.ThrowIfNull() 是如何使用此属性的一个很好的示例。它通过默认提供的值来避免必须传入参数名称:

void MyMethod(object value)
{
    ArgumentNullException.ThrowIfNull(value);
}


来源:DotNet
相关内容[ { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 58, "groupNames": [], "tagNames": [ "软件架构", "架构模式" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "软件架构设计分层模型和构图思考", "subTitle": "", "seoTitle": null, "imageUrl": "@upload/images/2022/3/4e3acb84467c5f1c.png", "videoUrl": "", "fileUrl": "", "keywords": null, "description": null, "body": "

架构思维概述


对于架构思维本身仍然是类似系统思维,结构化思维,编程思维等诸多思维模式的一个合集。由于架构的核心作用是在业务现实世界和抽象的IT实现之间建立起一道桥梁,因此架构思维最核心的就是要理解到业务驱动技术,技术为最终的业务服务。要真正通过架构设计来完成业务和技术,需求和实现,软件和硬件,静态和动态,成本和收益等多方面的平衡。

\"图片\"

架构设计中有两个重点,一个是分解,一个是集成。

分解是最基础的,架构的重点就是要对复杂问题进行分而治之,同时保证分解后的各个部分还能够高内聚,松耦合,最终又集成为一个完整的整体。分解核心是定义问题,因此架构首先仍然需要理解清楚需求。

集成是配合分解完成的动作,最终分解完成的各个组件或子系统,通过合适的接口设计,最终还能够集成为一个完整的整体,分解仅仅是加速开发和降低问题复杂度,如果分解后的内容无法集成在一起,那么分解就没有任何意义。

分解+集成可以理解为架构最核心的思考方式和方法。

在分解完成后,一个大的系统已经拆分为了诸多的小模块,或者一个小模块实现本身又分为了多个步骤阶段。那么零散的节点必须向上汇集和归纳,形成一个完整的架构。

而这个架构的形成要给关键就是要又分层思维。架构分层是谈架构绝对绕不开的一个点,通过架构分层可以更好地全面理解业务系统或功能实现。




云平台三层架构:资源-平台-应用


在规划大架构的时候,常会参考云计算的标准三层架构,即IaaS层,PaaS层,SaaS层。对于IaaS层重点是IT基础设施和虚拟化;PaaS层重点是构建平台层服务能力;而对于SaaS层则是具体的应用。

对于资源层从物理资源,再到虚拟化逻辑资源,从虚拟机到现在更加轻量的容器资源。而对于平台层原来只谈技术平台,但是当前又进一步拆分出业务平台,也可以理解成当前说得比较多的中台层。

同时在平台层和应用层之间增加了服务层,实现资源和服务的解耦。

\"图片\"

如果涉及到物联网类应用,一般还会在底层增加网络层和感知层,比如一个智慧城市标准平台和应用的架构图类似如下:

\"图片\"

在平台+应用构建模式下,一般在平台和应用之间还会有一个单独的服务层来实现接口服务对外的能力开放。资源+服务+应用也是我们常说的SOA分层架构模式,因此对于服务层也可以单独拆分出来作为一个小分层。

\"图片\"

问题1:数据库和数据层

在构建一个完整的总体架构的时候,实际上没有数据层这个概念,数据层是在表达单个应用系统的分层架构实现的时候才会出现的内容。

在总架构图里面把类似结构化数据库,非结构化数据等全部列出单独一层这个也不对,这个应该是在技术架构里面体现。

还有一种是单独分出一个数据层,将大的公共基础数据列出,比如上面谈的智慧城市架构图。如果这些基础数据存在共性能力朝上提供,那么可以归纳到PaaS平台层,在PaaS平台层单独分出一个数据平台域来进行体现。

问题2:服务层和服务

在构建整体架构的时候可以单独出一个能力开放平台或服务层,但是不用体现具体有哪些业务服务能力。因为单独出业务服务能力本质已经属于应用层内容,即应用又细化拆分为了业务中台和前台应用,中间衔接的服务。我们可以参考网上的另外一个构图,如下:

\"图片\"

这个构图既不像云平台中的分层架构,也不像应用功能实现中的分层架构。实际可以看到如果体现单独的支撑层,支撑层已经类似现在经常说到的业务中台和能力提供。

那么整个架构应该为 技术平台+中台+应用 方式来进行构图。




SOA分层:组件-服务-流程


对于SOA架构分层,重点要体现的就是服务,对于组件本身是属于逻辑资源层的概念,而对于服务则是资源对外暴露的能力抽象。

\"图片\"

SOA架构分层重点就是要体现出独立的服务层,注意不是画服务总线,这里可以单独画出具体提供哪些业务服务能力,技术服务能力。在采用SOA架构进行开发的时候,整体业务系统拆分为4个组件,10类服务域,5类流程,那么在构建的时候重点就是将上述组件,服务域和流程类体现出来。对于参考SOA架构来进行的构图,参考如下:

\"图片\"

这里的数据层最好改为标准的组件层,更加贴近SOA架构模型。在图中的服务层已经可以看到一个个独立的API服务接口。如果服务接口数据大,一般只会划分到服务域,比如用户中心服务,采购类服务等。在这种方式下构图参考如下:

\"图片\"

在上图中结合了云和SOA两种架构融合在一起,对于上图中的服务层实际可以理解为组件资源层和服务接口层的融合。更好的构图方式应该是拆分为标准的中台资源层-服务层-应用层。




云和SOA架构融合


\"图片\"

注意对于云分层架构重点强调的是基础设施,平台和应用三层架构。而对于SOA架构强调的是资源,服务和应用三层。而对于对于传统的应用系统的构建一般又包括了IT基础设施,技术平台,数据库,中间件和应用。再到应用系统本身的分层架构可能又是标准的三层架构模式等。

这些架构分层方法都帮助我们进一步融合分层架构模式。

架构分层有很多方法,包括基础设施层,平台层,组件层,支撑层,服务层,应用层,数据层,展现层等。多种分发导致分层模型反而出现歧义和模糊。

在这里我们从技术架构和应用架构两个层面来谈,技术架构沿用云计算的三层模型;而对于应用架构则采用eTOM模型标准的资源,服务,应用三层模型。那么两种分层架构模型的融合则是一个完整的云和SOA融合的分层架构模型。

即云计算的三层中,每一个层次本身又可以进一步拆分为资源,服务和应用三层。

拿IaaS层来说,最底层的物理资源虚拟机等是属于资源层内容,通过IaaS层资源能力提供API接口作为技术服务进行能力开放,即是服务层;最终基于资源能力,构建了一个公有云的面向公众的运营服务平台,本身又属于应用层的内容。而对于SaaS层,则底层的业务组件是资源,抽象的API接口是服务层,最终的前端业务或流程是应用功能实现。




应用架构分层


回到单个应用的架构分层,谈得最多的就是常说的三层架构模式。在软件架构中,经典三层架构自顶向下由用户界面层(User Interface Layer)、业务逻辑层(Business Logic Layer)与数据访问层(Data Access Layer)组成。
在整个实现过程中,可能还会增加独立的Facade层,或独立的API接口服务提供层,统一的DTO数据传输对象层等,但是这些都不影响整体的三层逻辑结构。

\"图片\"

三层架构本身也和一个业务功能实现的完整对应,在数据访问层处理数据获取和持久化操作,在业务逻辑层对业务规则进行处理,在界面展现层进行相应的前端展现和用户交互。而谈到领域建模的时候,又引入了领域模型 中的分层架构,如下:

\"图片\"

领域驱动设计在经典三层架构的基础上做了进一步改良,在用户界面层与业务逻辑层之间引入了新的一层,即应用层(Application Layer)。同时,一些层次的命名也发生了变化。将业务逻辑层更名为领域层自然是题中应有之义,而将数据访问层更名为基础设施层(Infrastructure Layer),则突破了之前数据库管理系统的限制,扩大了这个负责封装技术复杂度的基础层次的内涵。

当然,也有融合了领域模型和传统三架构思路后的技术架构如下:

\"图片\"

领域层和业务逻辑层

在领域建模的一个核心是领域模型,领域模型不再是一个个独立的数据库表或数据对象,而是一个业务对象或领域对象。因此领域层是面向领域对象而设计实现,而业务规则能力本身也是属于领域对象对外提供的能力接口。即业务规则本身也是领域对象暴露的能力。

传统业务逻辑层实现往往是一个数据对象对应一个DAO,一个Service和一个Interface。而领域模型下DAO可以是分开的,但是Service逻辑层往往则更多应该按领域模型思路对DAO层的能力进行组装和聚合。

独立应用层拆分

\"图片\"

在我原来理解里面,领域层提供领域模型和领域服务能力接口,而应用层更多的是对领域层多个领域对象模型提供的服务能力进一步进行组装和编排,然后再暴露给前端应用。

谈到应用层的概念,实际上可以理解为前端应用中存在的共性能力的进一步下沉。即应用本身只是用户业务功能实现的承载,但是这个功能的实现可以通过多种前端展现形式,比如传统的CS桌面应用,BS应用,或手机端APP。

在电商里面,一个商品订购就是一个独立的应用,用户可以在APP完成,也可以在BS端完成,但是不论在哪里完成最终应用层提供的能力都应该一样。比如完成一个商品订购需要同时和底层的订单,库存,支付多个服务进行交付和协同。那么这个逻辑显然不适合同时在BS端应用和APP端应用中进行重复编写和开发。那么这个内容就应该在应用层实现。

如果回到微服务和中台架构下,这个应用层拆分更加必要,即通过应用层来下沉共性的服务组合和组装逻辑,这个逻辑和协同不应该属于任何一个前端应用。

界面层还是接口层

在开发一个聚合能力的中台微服务模块的时候,可以看到这个微服务模块本身并没有界面展现层,那么该微服务的最上层仅仅是提供API接口的接口服务层。

该API接口服务能力既可以提供给APP前端,也可以提供给BS端使用。




软件技术架构分层


软件技术架构构图,分层仍然可以沿用软件三层分层模型,重点是说明清楚各层用到的关键技术组件或技术服务能力。比如软件开发三层模型的技术架构分层如下:

\"图片\"

如果本身就是一个技术平台,类似大数据平台,那么我们在整体构图的时候仍然需要考虑先进行分层,再详细说明每层里面的技术内容。

比如对应一个大数据平台,包括了大数据采集,大数据存储,大数据处理,大数据分析和应用,那么这个就是关键的分层,可以基于这个分层再来考虑各层采用的关键技术。

\"图片\"

对于技术栈构图基本也可以参考技术架构构图模式进行。

\"图片\"

技术架构重点需要回答的就是你在进行软件架构设计过程中,究竟会用到哪些关键技术,哪些开源产品或工具等。可以细化到具体的技术产品,也可以仅细化到产品类型。

比如消息中间件,你可以细化到采用RabbitMQ,也可以在技术架构中只体现采用消息中间件。

技术架构和软件功能分层架构唯一相同的就是分层,技术架构在各个分层里面都没有具体的业务功能点和实现内容,仅仅是关键技术点说明。




单个应用功能架构


注意应用功能架构完全是重点描述应用系统具备哪些功能,一个功能究竟是采用什么三层技术架构实现并不用关心。因此功能架构不应该体现数据层,逻辑层,技术点这些内容。

那么对于一个应用系统的功能如何分层?

我们可以参考业务分层分类,将业务分为基础支撑层,执行层,决策管理层。这样基本的分层模式就出来了,基于该方式可以完成一个功能架构构图。

\"图片\"

对于单个应用来说一般不会自身有云平台,PaaS平台这类概念。但是单个应用构建一定存在共性技术支撑平台能力,比如有自己的流程管理,各自共性技术功能组件等。因此单应用构建还可以采用基础技术支撑层+应用层+门户层的方式进行构图。

在应用层再按具体的业务域或业务阶段进行进一步细分。

\"图片\"




架构图的分层构图逻辑


\"图片\"

在前面基本给出了不同类型的架构图的核心分层逻辑,可以看到在画架构图的时候尽量不要混合使用不同场景下的构图方式,否则就导致整体架构图混乱。

在画整体架构的时候一般需要重点参考云三层架构,SOA三层架构的构图模式进行构图。而在细化到某一个应用系统的时候,仍然还需要分清是构建技术架构图还是功能架构图,两者本身的分层逻辑也存在很大的差别而不能混用。

架构图的构图逻辑

要完成一个完整的架构图构图,可以先拆分为两边+中间。两边一般是放具体的标准,规范等,比如安全管理,质量管理,技术标准规范,开发运维规范等。

中间即是重点需要考虑进行分层构建的地方。

在前面也谈到了中间部分重点参考云计算和SOA的架构分层逻辑。一般来说核心的还是资源层,平台层,应用层,门户层。而对于应用层本身又可以考虑业务域进一步拆分,或者根据价值链或业务生命周期拆分为多个阶段域再展开描述。

在云和SOA下,更加强调平台+应用构建模式

而两者之间一般是服务层,通过SOA平台或API能力开放平台来统一接入和发布服务,以形成一个完整的资源+服务+应用的松耦合架构。同时一个完整的架构本身就 是多视角的,如下:

\"图片\"

功能架构往往可以给具体用户和业务人员看,而对于技术架构往往更多是内部团队开发人员研讨使用。而设计到资源和平台的架构图往往又是运维工程人员进行部署架构搭建的重要参考。因此不同维度的架构分层属性本身不能随意融合使用,而导致架构图混乱。


", "summary": "对于架构思维本身仍然是类似系统思维,结构化思维,编程思维等诸多思维模式的一个合集。", "author": "", "source": " 架构师优雅之道", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-03-21 09:42:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 721, "guid": "b5814c31-d543-44fd-9bdf-459443e6817e", "createdDate": "2022-03-21 09:43:29", "lastModifiedDate": "2022-03-21 09:43:29" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 57, "groupNames": [], "tagNames": [ "Redis" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "为什么 Redis 集群要使用反向代理? 看这篇就明白了!", "subTitle": "", "seoTitle": null, "imageUrl": "@upload/images/2022/3/faed80a0815ffbdb.png", "videoUrl": "", "fileUrl": "", "keywords": null, "description": null, "body": "

如果没有反向代理,一台Redis可能需要跟很多个客户端连接:

\"图片\"

看着是不是很慌?看没关系,主要是连接需要消耗线程资源,没有代理的话,Redis要将很大一部分的资源用在与客户端建立连接上,redis的高可用和可扩展无论是自带的Redis Sentinel还是Redis Cluster都要求客户端进行额外的支持,而目前基本上没有合适的客户端能够做这些事情,客户端来做这些事情也并不合适,它会让维护变得特别困难。

因此在客户端和redis服务端之间加一层代理成了一种理想的方案,代理屏蔽后端Redis实现细节向客户端提供redis服务,可以完美的解决Redis的高可用和扩展性问题,同时代理的引入也使得Redis维护变得更加简单。

于是乎,有了代理:

如何使用代理?

很简单,将请求连接到调度代理器上,由Proxy负责将请求转发到后面的Redis服务实例,图示:

\"图片\"

又有了新的问题,Proxy挂了可咋整?

所以Proxy又需要做集群,甚至前面可以加一层负载均衡,负载均衡嘛,单机也存在单点故障等问题,一个Director肯定不行,搞不好又挂了,所以整一个主备,备机通过KeepAlived来检测主LVS健康状况,出了问题顶上去。

\"图片\"

Redis代理插件

Redis代理插件有很多,这儿简单介绍几款

predixy高性能全特征redis代理,支持Redis Sentinel和Redis Cluster
twemproxy快速、轻量级memcached和redis代理
codisredis集群代理解决方案
redis-cerberusRedis Cluster代理

代理详细功能对比

特性predixytwemproxycodisredis-cerberus
高可用Redis Sentinel或Redis Cluster一致性哈希Redis SentinelRedis Cluster
可扩展Key哈希分布或Redis ClusterKey哈希分布Key哈希分布Redis Cluster
开发语言C++CGOC++
多线程
事务Redis Sentinel模式单Redis组下支持不支持不支持不支持
BLPOP/BRPOP/BLPOPRPUSH支持不支持不支持支持
Pub/Sub支持不支持不支持支持
Script支持load不支持不支持不支持
Scan支持不支持不支持不支持
Select DB支持不支持支持Redis Cluster只有一个DB
Auth支持定义多个密码,给予不同读写及管理权限和Key访问空间不支持同redis不支持
读从节点支持,可定义丰富规则读指定的从节点不支持支持,简单规则支持,简单规则
多机房支持支持,可定义丰富规则调度流量不支持有限支持有限支持
统计信息丰富丰富丰富简单

简单来说,predixy既支持Redis Sentinel也支持Redis Cluster

  • 后端为Redis Sentinel监控的一组Redis,功能完全等同于原始Redis
  • 后端为Redis Sentinel监控的多组Redis,则有部分功能受限
  • 后端为Redis Cluster,功能完全等同于Redis Cluster


", "summary": "如果没有反向代理,一台Redis可能需要跟很多个客户端连接", "author": "等不到的口琴", "source": " 架构师优雅之道", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-03-21 09:39:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 720, "guid": "88fb7262-24eb-4929-b84f-710bf07febae", "createdDate": "2022-03-21 09:40:41", "lastModifiedDate": "2022-03-21 09:40:41" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 56, "groupNames": [], "tagNames": [ "SQL" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "SQL 常用脚本大全", "subTitle": "", "seoTitle": null, "imageUrl": "", "videoUrl": "", "fileUrl": "", "keywords": null, "description": null, "body": "

1、行转列的用法PIVOT


CREATE table test
(id int,name nvarchar(20),quarter int,number int)
insert into test values(1,N'苹果',1,1000)
insert into test values(1,N'苹果',2,2000)
insert into test values(1,N'苹果',3,4000)
insert into test values(1,N'苹果',4,5000)
insert into test values(2,N'梨子',1,3000)
insert into test values(2,N'梨子',2,3500)
insert into test values(2,N'梨子',3,4200)
insert into test values(2,N'梨子',4,5500)
select * from test


结果:

\"图片\"


select ID,NAME,
[1as '一季度',
[2as '二季度',
[3as '三季度',
[4as '四季度'
from
test
pivot
(
sum(number)
for quarter in
([1],[2],[3],[4])
)
as pvt


结果:

\"图片\"


2、列转行的用法UNPIOVT


create table test2
(id int,name varchar(20), Q1 int, Q2 int, Q3 int, Q4 int)
insert into test2 values(1,'苹果',1000,2000,4000,5000)
insert into test2 values(2,'梨子',3000,3500,4200,5500)
select * from test2


结果:

\"图片\"


--列转行
select id,name,quarter,number
from
test2
unpivot
(
number
for quarter in
([Q1],[Q2],[Q3],[Q4])
)
as unpvt


结果:

\"图片\"


3、字符串替换SUBSTRING/REPLACE


SELECT REPLACE('abcdefg',SUBSTRING('abcdefg',2,4),'**')


结果:

\"图片\"


SELECT REPLACE('13512345678',SUBSTRING('13512345678',4,11),'********')


结果:

\"图片\"


SELECT REPLACE('12345678@qq.com','1234567','******')


结果:

\"图片\"


4、查询一个表内相同纪录 HAVING


如果一个ID可以区分的话,可以这么写


SELECT * FROM HR.Employees


结果:


\"图片\"


select * from HR.Employees
where title in (
select title from HR.Employees
group by title
having count(1)>1)


结果:


\"图片\"


对比一下发现,ID为1,2的被过滤掉了,因为他们只有一条记录


如果几个ID才能区分的话,可以这么写


select * from HR.Employees
where title+titleofcourtesy in
(select title+titleofcourtesy
from HR.Employees
group by title,titleofcourtesy
having count(1)>1)


结果:


\"图片\"


title在和titleofcourtesy进行拼接后符合条件的就只有ID为6,7,8,9的了


5、把多行SQL数据变成一条多列数据,即新增列


SELECT 
 id,
 name,
 SUM(CASE WHEN quarter=1 THEN number ELSE 0 END'一季度',
 SUM(CASE WHEN quarter=2 THEN number ELSE 0 END'二季度',
 SUM(CASE WHEN quarter=3 THEN number ELSE 0 END'三季度',
 SUM(CASE WHEN quarter=4 THEN number ELSE 0 END'四季度'
FROM test
GROUP BY id,name


结果:


\"图片\"


我们将原来的4列增加到了6列。细心的朋友可能发现了这个结果和上面的行转列怎么一模一样?其实上面的行转列是省略写法,这种是比较通用的写法。 


6、表复制


语法1:Insert INTO table(field1,field2,...) values(value1,value2,...)

语法2:Insert into Table2(field1,field2,...) select value1,value2,... from 

Table1

(要求目标表Table2必须存在,由于目标表Table2已经存在,所以我们除了插入源表Table1的字段外,还可以插入常量。)

语法3:SELECT vale1, value2 into Table2 from Table1

(要求目标表Table2不存在,因为在插入时会自动创建表Table2,并将Table1中指定字段数据复制到Table2中。)

语法4:使用导入导出功能进行全表复制。如果是使用【编写查询以指定要传输的数据】,那么在大数据表的复制就会有问题?因为复制到一定程度就不再动了,内存爆了?它也没有写入到表中。而使用上面3种语法直接执行是会马上刷新到数据库表中的,你刷新一下mdf文件就知道了。


7、利用带关联子查询Update语句更新数据


--方法1:
Update Table1
set c = (select c from Table2 where a = Table1.a)
where c is null 

--方法2:
update  A
set  newqiantity=B.qiantity
from  A,B
where  A.bnum=B.bnum

--方法3:
update
(select A.bnum ,A.newqiantity,B.qiantity from A
left join B on A.bnum=B.bnum) AS C
set C.newqiantity = C.qiantity
where C.bnum ='001'


8、连接远程服务器


--方法1:
select *  from openrowset(
'SQLOLEDB',
'server=192.168.0.1;uid=sa;pwd=password',
'SELECT * FROM dbo.test')

--方法2:
select *  from openrowset(
'SQLOLEDB',
'192.168.0.1';
'sa';
'password',
'SELECT * FROM dbo.test')


当然也可以参考以前的示例,建立DBLINK进行远程连接


9、Date 和 Time 样式 CONVERT


CONVERT() 函数是把日期转换为新数据类型的通用函数。

CONVERT() 函数可以用不同的格式显示日期/时间数据。


语法

CONVERT(data_type(length),data_to_be_converted,style)

data_type(length) 规定目标数据类型(带有可选的长度)。data_to_be_converted 含有需要转换的值。style 规定日期/时间的输出格式。


可以使用的 style 值:


Style IDStyle 格式
100 或者 0mon dd yyyy hh:miAM (或者 PM)
101mm/dd/yy
102yy.mm.dd
103dd/mm/yy
104dd.mm.yy
105dd-mm-yy
106dd mon yy
107Mon dd, yy
108hh:mm:ss
109 或者 9mon dd yyyy hh:mi:ss:mmmAM(或者 PM)
110mm-dd-yy
111yy/mm/dd
112yymmdd
113 或者 13dd mon yyyy hh:mm:ss:mmm(24h)
114hh:mi:ss:mmm(24h)
120 或者 20yyyy-mm-dd hh:mi:ss(24h)
121 或者 21yyyy-mm-dd hh:mi:ss.mmm(24h)
126yyyy-mm-ddThh:mm:ss.mmm(没有空格)
130dd mon yyyy hh:mi:ss:mmmAM
131dd/mm/yy hh:mi:ss:mmmAM

SELECT CONVERT(varchar(100), GETDATE(), 0)
--结果:
12  7 2020  9:33PM
SELECT CONVERT(varchar(100), GETDATE(), 1)
--结果:
12/07/20
SELECT CONVERT(varchar(100), GETDATE(), 2)
--结果:
20.12.07
SELECT CONVERT(varchar(100), GETDATE(), 3)
--结果:
07/12/20
SELECT CONVERT(varchar(100), GETDATE(), 4)
--结果:
07.12.20
SELECT CONVERT(varchar(100), GETDATE(), 5)
--结果:
07-12-20
SELECT CONVERT(varchar(100), GETDATE(), 6)
--结果:
07 12 20
SELECT CONVERT(varchar(100), GETDATE(), 7)
--结果:
12 0720
SELECT CONVERT(varchar(100), GETDATE(), 8)
--结果:
21:33:18
SELECT CONVERT(varchar(100), GETDATE(), 9)
--结果:
12  7 2020  9:33:18:780PM
SELECT CONVERT(varchar(100), GETDATE(), 10)
--结果:
12-07-20
SELECT CONVERT(varchar(100), GETDATE(), 11)
--结果:
20/
12/07
SELECT CONVERT(varchar(100), GETDATE(), 12)
--结果:
201207
SELECT CONVERT(varchar(100), GETDATE(), 13)
--结果:
07 12 2020 21:33:18:780
SELECT CONVERT(varchar(100), GETDATE(), 14)
--结果:
21:33:18:780
SELECT CONVERT(varchar(100), GETDATE(), 20)
--结果:
2020-12-07 21:33:18
SELECT CONVERT(varchar(100), GETDATE(), 21)
--结果:
2020-12-07 21:33:18.780
SELECT CONVERT(varchar(100), GETDATE(), 22)
--结果:
12/07/20  9:33:18 PM
SELECT CONVERT(varchar(100), GETDATE(), 23)
--结果:
2020-12-07
SELECT CONVERT(varchar(100), GETDATE(), 24)
--结果:
21:33:18
SELECT CONVERT(varchar(100), GETDATE(), 25)
--结果:
2020-12-07 21:33:18.780
SELECT CONVERT(varchar(100), GETDATE(), 100)
--结果:
12  7 2020  9:33PM
SELECT CONVERT(varchar(100), GETDATE(), 101)
--结果:
12/07/2020
SELECT CONVERT(varchar(100), GETDATE(), 102)
--结果:
2020.12.07
SELECT CONVERT(varchar(100), GETDATE(), 103)
--结果:
07/12/2020
SELECT CONVERT(varchar(100), GETDATE(), 104)
--结果:
07.12.2020
SELECT CONVERT(varchar(100), GETDATE(), 105)
--结果:
07-12-2020
SELECT CONVERT(varchar(100), GETDATE(), 106)
--结果:
07 12 2020
SELECT CONVERT(varchar(100), GETDATE(), 107)
--结果:
12 072020
SELECT CONVERT(varchar(100), GETDATE(), 108)
--结果:
21:33:18
SELECT CONVERT(varchar(100), GETDATE(), 109)
--结果:
12  7 2020  9:33:18:780PM
SELECT CONVERT(varchar(100), GETDATE(), 110)
--结果:
12-07-2020
SELECT CONVERT(varchar(100), GETDATE(), 111)
--结果:
2020/12/07
SELECT CONVERT(varchar(100), GETDATE(), 112)
--结果:
20201207
SELECT CONVERT(varchar(100), GETDATE(), 113)
--结果:
07 12 2020 21:33:18:780
SELECT CONVERT(varchar(100), GETDATE(), 114)
--结果:
21:33:18:780
SELECT CONVERT(varchar(100), GETDATE(), 120)
--结果:
2020-12-07 21:33:18
SELECT CONVERT(varchar(100), GETDATE(), 121)
--结果:
2020-12-07 21:33:18.780



10、SQL中的相除


方法一


--SQL中的相除
SELECT 
CASE WHEN ISNULL(A-B,0)=0 THEN ''
ELSE CAST(CONVERT(DECIMAL(18,2),A*100.0/(A-B)) AS VARCHAR(10))+'%'  
END AS '百分数'  --FROM 表


这里我们先要判断被除数是否为0,如果为0给出一个想输出的结果,这里我们返回空白(是字符类型,不是NULL),在不为0的时候就给出具体的计算公式,然后转换成字符类型再和“%”进行拼接。例如:


SELECT 
CASE WHEN ISNULL(5-2,0)=0 THEN ''
ELSE CAST(CONVERT(DECIMAL(18,2),5*100.0/(5-2)) AS VARCHAR(10))+'%'  
END AS '百分数'  --FROM 表


返回的结果:

\"图片\"


方法二


SELECT 
(CONVERT(VARCHAR(20),ROUND(41*100.0/88,3))+'%'AS '百分比' 
--FROM A


执行结果:

\"图片\"




11、四舍五入ROUND函数


ROUND ( numeric_expression , length [ ,function ] )
function 必须为 tinyint、smallint  或 int。
如果省略 function 或其值为 0(默认值),则将舍入 numeric_expression。
如果指定了0以外的值,则将截断 numeric_expression。


SELECT ROUND(150.456482);
--保留小数点后两位,需要四舍五入
--结果:
150.46000

SELECT ROUND(150.4564820);
--保留小数点后两位,0为默认值,表示进行四舍五入
--结果:
150.46000

SELECT ROUND(150.4564821);
--保留小数点后两位,不需要四舍五入,这里除0以外都是有同样的效果,
--与Oracle的TRUNC函数效果相同

--结果:
150.45000

SELECT ROUND(150.4564822);
--保留小数点后两位,不需要四舍五入,这里除0以外都是有同样的效果,
--与Oracle的TRUNC函数效果相同

--结果:
150.45000


12、对字段出现NULL值的处理


方法一


--CASE
SELECT 
CASE WHEN  '字段名' IS NULL THEN 'NULL' 
ELSE CONVERT(VARCHAR(20),'字段名1'END 
AS 'NewName'
--结果:
字段名1

SELECT CASE WHEN NULL IS NULL THEN 'N' 
ELSE CONVERT(VARCHAR(20),NULLEND 
AS 'NewName'
--结果:
N


方法二


--SQL Server 2005:COALESCE
SELECT COALESCE('字符串类型字段','N'AS 'NewName'
--结果:
字符串类型字段

SELECT COALESCE(CONVERT(VARCHAR(20),'非字符串类型字段'),'N'AS 'NewName'
--结果:
非字符串类型字段

SELECT COALESCE(CONVERT(VARCHAR(20),NULL),'N'AS 'NewName'
--结果:
N

--COALESCE,返回其参数中的第一个非空表达式
SELECT COALESCE(NULL,NULL,1,2,NULL)
--结果:
1

SELECT COALESCE(NULL,11,12,13,NULL)
--结果:
11

SELECT COALESCE(111,112,113,114,NULL)
--结果:
111


13、COUNT的几种情况


--以下三种方法均可统计出表的记录数
--第一种
select count(*) from tablename

--第二种
select count(IDfrom tablename

--第三种,1换成其它值也是可以的
select count(1from tablename


14、UNION ALL多表插入


把两个相同结构的表union后插入到一个新表中,
当然两个以上的相同结构的表也是可以的,
这里的相同是指两个或多个表的列数和每个对应列的类型相同,
列名称可以不同


select *
into table_new
from table_1
union all
select * from table_2


15、查看数据库缓存的SQL


use master
declare @dbid int
Select @dbid = dbid from sysdatabases
where name = 'SQL_ROAD'--修改成数据库的名称

select
dbid,UseCounts ,RefCounts,CacheObjtype,ObjType,
DB_Name(dbid) as DatabaseName,SQL
from syscacheobjects
where dbid=@dbid
order by dbid,useCounts desc,objtype


我们可以看到数据库中当前正在运行的SQL有哪些


16、删除计划缓存


--删除整个数据库的计划缓存
DBCC FREEPROCCACHE

--删除某个数据库的计划缓存
USE master
DECLARE @dbid INT
SELECT @dbid=dbid FROM sysdatabases WHERE NAME = 'SQL_ROAD'
DBCC FLUSHPROCINDB (@dbid)

17、SQL换行

SQL的换行
制表符 CHAR(9)
换行符 CHAR(10)
回车 CHAR(13)

PRINT 'SQL'+CHAR(13)+'ROAD'
PRINT 'SQL'+CHAR(10)+'ROAD'
PRINT 'SQL'+CHAR(9)+'ROAD'

执行结果:

\"图片\"


如果将查询结果以文本格式显示,而不是网格格式显示,SELECT语句也适用,我们先将查询结果改成以文本格式显示


\"图片\"


--以文本格式显示结果
SELECT 'SQL'CHAR(10)+'ROAD'
SELECT 'SQL'CHAR(13)+'ROAD'
SELECT 'SQL' + CHAR(10) + CHAR(13) + 'ROAD'


结果如下:

\"图片\"



18、TRUNCATE 与 DELETE


TRUNCATE 是SQL中的一个删除数据表内容的语句,用法是:

TRUNCATE TABLE [Table Name] 速度快,而且效率高,因为: 
TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行。但 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。 


DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。

 
TRUNCATE TABLE 删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。


如果想保留标识计数值,请改用 DELETE。


如果要删除表定义及其数据,请使用 DROP TABLE 语句。

 
对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句。由于 TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器。TRUNCATE TABLE 不能用于参与了索引视图的表。 


19、常用系统检测脚本


--查看内存状态
dbcc memorystatus

--查看哪个引起的阻塞,blk
EXEC sp_who active

--查看锁住了那个资源id,objid
EXEC sp_lock


还有如何查看查询分析器的SPID,可以在查询分析器的状态栏看到,比如sa(57),这就表示当前查询分析器SPID为57,这样在使用profile的时候就可以指定当前窗体进行监控。状态栏在查询窗口的右下角。


\"图片\"


20、获取脚本的执行时间


declare @timediff datetime
select @timediff=getdate()
select * from Suppliers
print '耗时:'convert(varchar(10),datediff(ms,@timediff,getdate()))


结果如下:

\"图片\"

在状态栏是不会精确到毫秒的,只能精确到秒


\"图片\"


这个脚本可以更加有效的查看SQL代码的执行效率。


", "summary": "SQL 常用脚本大全", "author": "", "source": "数仓宝贝库", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-03-21 09:35:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 719, "guid": "ee710412-ecea-4f72-b7a4-72e6bb346e82", "createdDate": "2022-03-21 09:37:10", "lastModifiedDate": "2022-03-21 09:37:10" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 3, "lastEditAdminId": 3, "userId": 0, "taxis": 55, "groupNames": [], "tagNames": [], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "C# 10的新特性", "subTitle": "", "seoTitle": null, "imageUrl": "https://www.xuey.net/upload/images/2022/2/39048d8cca039453.png", "videoUrl": "", "fileUrl": "", "keywords": null, "description": null, "body": "

前言


们很高兴地宣布 C# 10 作为 .NET 6 和 Visual Studio 2022的一部分已经发布了。在这篇文章中,我们将介绍 C# 10 的许多新功能,这些功能使你的代码更漂亮、更具表现力、更快。阅读 Visual Studio 2022 公告和.NET 6 公告以了解更多信息,包括如何安装。


Visual Studio 2022 公告:https://aka.ms/vs2022gablog


.NET 6:https://aka.ms/dotnet6-GA


全局和隐式 usings


using 指令简化了您使用命名空间的方式。C# 10 包括一个新的全局 using 指令和隐式 usings,以减少您需要在每个文件顶部指定的 usings 数量。


全局 using 指令


如果关键字 global 出现在 using 指令之前,则 using 适用于整个项目:

global using System;

你可以在全局 using 指令中使用 using 的任何功能。例如,添加静态导入类型并使该类型的成员和嵌套类型在整个项目中可用。如果您在using 指令中使用别名,该别名也会影响您的整个项目:

global using static System.Console;
global using Env = System.Environment;

您可以将全局使用放在任何 .cs 文件中,包括 Program.cs 或专门命名的文件,如 globalusings.cs。全局 usings 的范围是当前编译,一般对应当前项目。

有关详细信息,请参阅全局 using 指令。

  • 全局 using 指令https://docs.microsoft.com/dotnet/csharp/languagereference/keywords/using-directive#global-modifier

隐式 usings

隐式 usings 功能会自动为您正在构建的项目类型添加通用的全局 using 指令。要启用隐式 usings,请在 .csproj 文件中设置 ImplicitUsings 属性:

<PropertyGroup>
    <!-- Other properties like OutputType and TargetFramework -->
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

在新的 .NET 6 模板中启用了隐式 usings 。在此博客文章中阅读有关 .NET 6 模板更改的更多信息。

一些特定全局 using 指令集取决于您正在构建的应用程序的类型。例如,控制台应用程序或类库的隐式 usings 不同于 ASP.NET 应用程序的隐式 usings。

有关详细信息,请参阅此隐式usings文章。

  • 博客文章

    https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#net-sdk-c-project-templates-modernized

  • 隐式usings

    https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview#implicit-using-directives

Combining using 功能

文件顶部的传统 using 指令、全局 using 指令和隐式 using 可以很好地协同工作。隐式 using 允许您在项目文件中包含适合您正在构建的项目类型的 .NET 命名空间。全局 using 指令允许您包含其他命名空间,以使它们在整个项目中可用。代码文件顶部的 using 指令允许您包含项目中仅少数文件使用的命名空间。

无论它们是如何定义的,额外的 using 指令都会增加名称解析中出现歧义的可能性。如果遇到这种情况,请考虑添加别名或减少要导入的命名空间的数量。例如,您可以将全局 using 指令替换为文件子集顶部的显式 using 指令。

如果您需要删除通过隐式 usings 包含的命名空间,您可以在项目文件中指定它们:

<ItemGroup>
  <Using Remove="System.Threading.Tasks" />
</ItemGroup>

您还可以添加命名空间,就像它们是全局 using 指令一样,您可以将 Using 项添加到项目文件中,例如:

<ItemGroup>
  <Using Include="System.IO.Pipes" />
</ItemGroup>

文件范围的命名空间

许多文件包含单个命名空间的代码。从 C# 10 开始,您可以将命名空间作为语句包含在内,后跟分号且不带花括号:

namespace MyCompany.MyNamespace;

class MyClass // Note: no indentation
{ ... } 

他简化了代码并删除了嵌套级别。只允许一个文件范围的命名空间声明,并且它必须在声明任何类型之前出现。

有关文件范围命名空间的更多信息,请参阅命名空间关键字文章。

  • 命名空间关键字文章https://docs.microsoft.com/dotnet/csharp/languagereference/keywords/namespace

对 lambda 表达式和方法组的改进

我们对 lambda 的语法和类型进行了多项改进。我们预计这些将广泛有用,并且驱动方案之一是使 ASP.NET Minimal API 更加简单。

  • lambda 的语法

    https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-10#lambda-expression-improvements

  • ASP.NET Minimal APIhttps://devblogs.microsoft.com/dotnet/announcing-asp-net-core-in-net-6/

lambda 的自然类型

Lambda 表达式现在有时具有“自然”类型。这意味着编译器通常可以推断出 lambda 表达式的类型。

到目前为止,必须将 lambda 表达式转换为委托或表达式类型。在大多数情况下,您会在 BCL 中使用重载的 Func<...> 或 Action<...> 委托类型之一:

Func<stringint> parse = (string s) => int.Parse(s);

但是,从 C# 10 开始,如果 lambda 没有这样的“目标类型”,我们将尝试为您计算一个:

var parse = (string s) => int.Parse(s);

你可以在你最喜欢的编辑器中将鼠标悬停在 var parse 上,然后查看类型仍然是 Func<string, int>。一般来说,编译器将使用可用的 Func 或 Action 委托(如果存在合适的委托)。否则,它将合成一个委托类型(例如,当您有 ref 参数或有大量参数时)。

并非所有 lambda 表达式都有自然类型——有些只是没有足够的类型信息。 例如,放弃参数类型将使编译器无法决定使用哪种委托类型:

var parse = s => int.Parse(s); // ERROR: Not enough type info in the lambda

lambda 的自然类型意味着它们可以分配给较弱的类型,例如 object 或 Delegate:

object parse = (string s) => int.Parse(s);   // Func<string, int>
Delegate parse = (string s) => int.Parse(s); // Func<string, int>

当涉及到表达式树时,我们结合了“目标”和“自然”类型。如果目标类型是LambdaExpression 或非泛型 Expression(所有表达式树的基类型)并且 lambda 具有自然委托类型 D,我们将改为生成 Expression<D>:

LambdaExpression parseExpr = (string s) => int.Parse(s); // Expression<Func<string, int>>
Expression parseExpr = (string s) => int.Parse(s);       // Expression<Func<string, int>>

方法组的自然类型

法组(即没有参数列表的方法名称)现在有时也具有自然类型。您始终能够将方法组转换为兼容的委托类型:

Func<intread = Console.Read;
Action<string> write = Console.Write;

现在,如果方法组只有一个重载,它将具有自然类型:

var read = Console.Read; // Just one overload; Func<int> inferred
var write = Console.Write; // ERROR: Multiple overloads, can't choose

lambda 的返回类型

在前面的示例中,lambda 表达式的返回类型是显而易见的,并被推断出来的。情况并非总是如此:

var choose = (bool b) => b ? 1 : "two"// ERROR: Can't infer return type

在 C# 10 中,您可以在 lambda 表达式上指定显式返回类型,就像在方法或本地函数上一样。返回类型在参数之前。当你指定一个显式的返回类型时,参数必须用括号括起来,这样编译器或其他开发人员不会太混淆:

var choose = object (bool b) => b ? 1 : "two"// Func<bool, object>

lambda 上的属性

从 C# 10 开始,您可以将属性放在 lambda 表达式上,就像对方法和本地函数一样。当有属性时,lambda 的参数列表必须用括号括起来:

Func<string, int> parse = [Example(1)] (s) => int.Parse(s);
var choose = [Example(2)][Example(3)] object (bool b) => b ? 1 : "two";

就像本地函数一样,如果属性在 AttributeTargets.Method 上有效,则可以将属性应用于 lambda。

Lambda 的调用方式与方法和本地函数不同,因此在调用 lambda 时属性没有任何影响。但是,lambdas 上的属性对于代码分析仍然有用,并且可以通过反射发现它们。

structs 的改进

C# 10 为 structs 引入了功能,可在 structs (结构)和类之间提供更好的奇偶性。这些新功能包括无参数构造函数、字段初始值设定项、记录结构和 with 表达式。

01 无参数结构构造函数和字段初始值设定项

在 C# 10 之前,每个结构都有一个隐式的公共无参数构造函数,该构造函数将结构的字段设置为默认值。在结构上创建无参数构造函数是错误的。

从 C# 10 开始,您可以包含自己的无参数结构构造函数。如果您不提供,则将提供隐式无参数构造函数以将所有字段设置为默认值。您在结构中创建的无参数构造函数必须是公共的并且不能是部分的:

public struct Address
{
    public Address()
    {
        City = "<unknown>";
    }
    public string City { get; init; }
}

您可以如上所述在无参数构造函数中初始化字段,也可以通过字段或属性初始化程序初始化它们:

public struct Address
{
    public string City { getinit; } = "<unknown>";
}

通过默认创建或作为数组分配的一部分创建的结构会忽略显式无参数构造函数,并始终将结构成员设置为其默认值。有关结构中无参数构造函数的更多信息,请参阅结构类型。

02 Record structs

从 C# 10 开始,现在可以使用 record struct 定义 record。这些类似于 C# 9 中引入的record 类:

public record struct Person
{
    public string FirstName { getinit; }
    public string LastName { getinit; }
}

您可以继续使用 record 定义记录类,也可以使用 record 类来清楚地说明。

结构已经具有值相等——当你比较它们时,它是按值。记录结构添加 IEquatable<T> 支持和 == 运算符。记录结构提供 IEquatable<T> 的自定义实现以避免反射的性能问题,并且它们包括记录功能,如 ToString() 覆盖。

记录结构可以是位置的,主构造函数隐式声明公共成员:

public record struct Person(string FirstName, string LastName);

主构造函数的参数成为记录结构的公共自动实现属性。与 record 类不同,隐式创建的属性是读/写的。这使得将元组转换为命名类型变得更加容易。将返回类型从 (string FirstName, string LastName) 之类的元组更改为 Person 的命名类型可以清理您的代码并保证成员名称一致。声明位置记录结构很容易并保持可变语义。

如果您声明一个与主要构造函数参数同名的属性或字段,则不会合成任何自动属性并使用您的。

要创建不可变的记录结构,请将 readonly 添加到结构(就像您可以添加到任何结构一样)或将 readonly 应用于单个属性。对象初始化器是可以设置只读属性的构造阶段的一部分。这只是使用不可变记录结构的一种方法:

var person = new Person { FirstName = "Mads", LastName = "Torgersen"};

public readonly record struct Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

在本文中了解有关记录结构的更多信息。

  • 记录结构

https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record

03 Record类中 ToString () 上的密封修饰符

记录类也得到了改进。从 C# 10 开始,ToString() 方法可以包含 seal 修饰符,这会阻止编译器为任何派生记录合成 ToString 实现。

在本文中的记录中了解有关 ToString () 的更多信息。

  • 有关 ToString () 的更多信息

    https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display

04 结构和匿名类型的表达式

C# 10 支持所有结构的 with 表达式,包括记录结构,以及匿名类型:

var person2 = person with { LastName = "Kristensen" };

这将返回一个具有新值的新实例。您可以更新任意数量的值。您未设置的值将保留与初始实例相同的值。
在本文中了解有关 with 的更多信息

  • 了解有关 with 的更多信息

ttps://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display


内插字符串改进

当我们在 C# 中添加内插字符串时,我们总觉得在性能和表现力方面,使用该语法可以做更多事情。

01 内插字符串处理程序

今天,编译器将内插字符串转换为对 string.Format 的调用。这会导致很多分配——参数的装箱、参数数组的分配,当然还有结果字符串本身。此外,它在实际插值的含义上没有任何回旋余地。

在 C# 10 中,我们添加了一个库模式,允许 API “接管”对内插字符串参数表达式的处理。例如,考虑 StringBuilder.Append:

var sb = new StringBuilder();
sb.Append($"Hello {args[0]}, how are you?");

到目前为止,这将使用新分配和计算的字符串调用 Append(string? value) 重载,将其附加到 StringBuilder 的一个块中。但是,Append 现在有一个新的重载 Append(refStringBuilder.AppendInterpolatedStringHandler handler),当使用内插字符串作为参数时,它优先于字符串重载。

通常,当您看到 SomethingInterpolatedStringHandler 形式的参数类型时,API 作者在幕后做了一些工作,以更恰当地处理插值字符串以满足其目的。在我们的 Append 示例中,字符串 “Hello”、args[0] 和“,how are you?” 将单独附加到 StringBuilder 中,这样效率更高且结果相同。

有时您只想在特定条件下完成构建字符串的工作。一个例子是 Debug.Assert:

Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");

在大多数情况下,条件为真,第二个参数未使用。但是,每次调用都会计算所有参数,从而不必要地减慢执行速度。Debug.Assert 现在有一个带有自定义插值字符串构建器的重载,它确保第二个参数甚至不被评估,除非条件为假。

最后,这是一个在给定调用中实际更改字符串插值行为的示例:String.Create() 允许您指定 IFormatProvider 用于格式化插值字符串参数本身的洞中的表达式:

String.Create(CultureInfo.InvariantCulture, $"The result is {result}");

你可以在本文和有关创建自定义处理程序的本教程中了解有关内插字符串处理程序的更多信息。

  • 创建自定义处理程序
    https://docs.microsoft.com/dotnet/csharp/languagereference/tokens/interpolated#compilation-of-interpolated-strings

  • 内插字符串处理程序的更多信息

https://docs.microsoft.com/dotnet/csharp/whats-new/tutorials/interpolated-string-handler

02 常量内插字符串

如果内插字符串的所有洞都是常量字符串,那么生成的字符串现在也是常量。这使您可以在更多地方使用字符串插值语法,例如属性:

[Obsolete($"Call {nameof(Discard)} instead")]

请注意,必须用常量字符串填充洞。其他类型,如数字或日期值,不能使用,因为它们对文化敏感,并且不能在编译时计算。

其他改进

C# 10 对整个语言进行了许多较小的改进。其中一些只是使 C# 以您期望的方式工作。

在解构中混合声明和变量

在 C# 10 之前,解构要求所有变量都是新的,或者所有变量都必须事先声明。在 C# 10 中,您可以混合:

int x2;
int y2;
(x2, y2) = (01);       // Works in C# 9
(var x, var y) = (01); // Works in C# 9
(x2, var y3) = (01);   // Works in C# 10 onwards 

在有关解构的文章中了解更多信息。

改进的明确分配

如果您使用尚未明确分配的值,C# 会产生错误。C# 10 可以更好地理解您的代码并且产生更少的虚假错误。这些相同的改进还意味着您将看到更少的针对空引用的虚假错误和警告。

在 C# 10 中的新增功能文章中了解有关 C# 确定赋值的更多信息。

  • C# 10 中的新增功能文章https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-10#improved-definite-assignment

扩展的属性模式

C# 10 添加了扩展属性模式,以便更轻松地访问模式中的嵌套属性值。例如,如果我们在上面的 Person 记录中添加一个地址,我们可以通过以下两种方式进行模式匹配:

object obj = new Person
{
    FirstName = "Kathleen",
    LastName = "Dollard",
    Address = new Address { City = "Seattle" }
};

if (obj is Person { Address: { City: "Seattle" } })
    Console.WriteLine("Seattle");

if (obj is Person { Address.City: "Seattle" }) // Extended property pattern
    Console.WriteLine("Seattle");

扩展属性模式简化了代码并使其更易于阅读,尤其是在匹配多个属性时。

在模式匹配文章中了解有关扩展属性模式的更多信息。

  • 模式匹配文章

    https://docs.microsoft.com/dotnet/csharp/languagereference/operators/patterns#property-pattern

调用者表达式属性

CallerArgumentExpressionAttribute 提供有关方法调用上下文的信息。与其他 CompilerServices 属性一样,此属性应用于可选参数。在这种情况下,一个字符串:

void CheckExpression(bool condition, 
    [CallerArgumentExpression("condition"
)] string? message
 = null )
{
    Console.WriteLine($"Condition: {message}");
}

传递给 CallerArgumentExpression 的参数名称是不同参数的名称。作为参数传递给该参数的表达式将包含在字符串中。例如,

var a = 6;
var b = true;
CheckExpression(true);
CheckExpression(b);
CheckExpression(a > 5);

// Output:
// Condition: true
// Condition: b
// Condition: a > 5

ArgumentNullException.ThrowIfNull() 是如何使用此属性的一个很好的示例。它通过默认提供的值来避免必须传入参数名称:

void MyMethod(object value)
{
    ArgumentNullException.ThrowIfNull(value);
}


", "summary": "我们很高兴地宣布 C# 10 作为 .NET 6 和 Visual Studio 2022的一部分已经发布了。在这篇文章中,我们将介绍 C# 10 的许多新功能,这些功能使你的代码更漂亮、更具表现力、更快。阅读 Visual Studio 2022 公告和.NET 6 公告以了解更多信息,包括如何安装。", "author": "", "source": "DotNet", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-02-15 09:21:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 718, "guid": "bd2aceeb-7bbc-433e-a602-6fe8cedba66b", "createdDate": "2022-02-15 09:28:19", "lastModifiedDate": "2022-02-15 09:28:19" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 54, "groupNames": [], "tagNames": [ "优化", ".NET" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "优化.NET 应用程序 CPU 和内存的11 个实践", "subTitle": "", "seoTitle": null, "imageUrl": "@upload/images/2022/2/3af29f50ca81e193.png", "videoUrl": "", "fileUrl": "", "keywords": "优化,.NET", "description": "凡事都有其限度,对吧?汽车只能开这么快,进程只能使用这么多内存,程序员只能喝这么多咖啡。我们的生产力受到资源的限制,我们有能力更好或更差地利用它们。", "body": "

前言


凡事都有其限度,对吧?汽车只能开这么快,进程只能使用这么多内存,程序员只能喝这么多咖啡。我们的生产力受到资源的限制,我们有能力更好或更差地利用它们。尽可能接近其极限使用我们的每一种资源是我们的目标,我们希望使用我们的 CPU 和内存的每一点,否则我们会为昂贵的机器多付钱。然而,若是我们使用了过多的资源,我们就有可能导致性能问题、服务不可用问题和程序宕机底崩溃问题。软件开发看似简单,但一旦遇到性能问题,就会变得非常棘手,这就是我们今天要讨论的内容。


定义最佳基准


让我们尝试描述我们的最佳应用程序行为。假设我们有许多服务器机器需要处理高吞吐量的请求。为简单起见,让我们暂时忘记高峰时间或周末。我们的服务器负载在一天中的所有时间都或多或少相同。我们为这些服务器机器支付了很多钱,我们希望从它们那里获得尽可能多的价值,这意味着处理尽可能多的请求。按照我们对简单性的承诺,我们还假设服务器仅使用内存和 CPU 来处理所述请求,并且没有其他瓶颈,例如慢速网络或锁争用。


在所描述的场景中,我们的最佳行为是在任何给定时间使用尽可能多的 CPU 和内存,对吗?这样,我们可以用更少的机器来处理相同数量的请求。但是你可能不想利用这些资源中的 99.9%,因为负载的轻微增加可能会导致性能问题、服务器崩溃、数据丢失和其他令人头疼的问题。所以我们应该选择一个有足够缓冲问题的数值。平均 85% 或 90% 的 CPU 和内存利用率听起来是正确的。


我们应该首先优化什么?


我们的应用程序不是为平等利用 CPU 和内存而构建的。或者到它托管的机器的确切限制。因此,你首先应该查看的是你的服务器是CPU-bound还是Memory-bound当服务器受 CPU 限制时,这意味着服务器可以处理的吞吐量受到其 CPU 的限制。换句话说,如果你尝试处理更多请求,CPU 将在其他资源(如内存)达到其限制之前达到 100%。同样的逻辑也适用于Memory-bound服务器。


服务器的吞吐量将受到它可以分配的内存的限制,当尝试处理更多负载时,在其他资源(如 CPU)达到其限制之前,该内存将达到 100%。还有其他资源可以限制服务器,例如I/O,在这种情况下,吞吐量会受到磁盘或网络的读取或写入限制。但是我们将在这篇文章中忽略这一点,乐观地假设我们的 I/O 是快速且无限的。一旦你知道是什么限制了你的服务器的性能,你就会知道首先要尝试和优化什么。


如果你的服务器受 CPU 限制,那么优化内存使用没有意义,因为它不会提高处理的吞吐量。事实上,它可能会损害吞吐量,因为你可能会因为更多的 CPU 利用率而提高内存使用率。对于内存受限的服务器也是如此,在这种情况下,你应该在查看 CPU 之前优化内存使用。


测量 .NET 服务器中的 CPU 和内存消耗


CPU 和内存的实际测量最简单的是使用Performance Counters完成。CPU 使用率的指标是Process | % 处理器时间内存有几个指标,但我建议查看Process | 私有字节你可能还对**.NET CLR 内存感兴趣 | # 代表托管内存的所有堆中的字节**(CLR 占用的部分,而不是所有内存,即托管 + 本机内存)。要查看性能计数器,你可以在 Windows 计算机上使用Process Explorer或 PerfMon,或者在 .NET Core 服务器上使用dotnet-counters 。如果你的应用程序部署在云中,你可以使用像Application Insights(Azure Monitor的一部分)这样的 APM 工具来显示这些信息。或者,你可以在代码中获取性能计数器值并每 10 秒左右记录一次,使用Azure 数据资源管理器之类的工具在图表中显示数据。

\"图片\"

提示:检查机器级指标和进程级指标。你可能会发现其他进程正在限制你的性能。

一旦确定了哪些资源限制了你的 .NET 服务器,就该优化该资源消耗了。如果你受 CPU 限制,让我们减少 CPU 使用率。如果你受内存限制,让我们减少内存使用量。至少如果你在云中运行,一种简单的方法是更改机器规格。如果你受内存限制,请增加内存。如果你受 CPU 限制,请增加内核数量或获得更快的 CPU。这将提高成本,但在此之前,你可以检查一些容易实现的目标,以优化 CPU 或内存消耗。在更改机器规格之前尝试进行这些优化,因为优化后一切都会改变。你可能会优化 CPU 使用率并变得受内存限制。然后优化内存使用并再次成为 CPU 密集型。因此,如果你想避免不得不不断更改机器资源以适应最新的优化,最好把它留到最后。所以让我们谈谈一些内存优化。

优化内存使用

有很多方法可以优化 .NET 中的内存使用。深入讨论它们需要一整本书,而且已经有好几本了。但我会尽量给你一些方向和想法。

1、了解什么占用了你的内存

尝试优化内存时,你应该做的第一件事是了解全局。什么占用了大部分内存?有哪些数据类型?它们分配在哪里?它们会在记忆中停留多久?有几种工具可以获取此信息:•捕获转储文件并使用内存分析器或WinDbg打开它。•使用新的GC 转储(.NET Core 3.1+) 并使用 Visual Studio 进行调查。•捕获堆快照并使用内存分析器、PerfView或Visual Studio 诊断工具对其进行探索。此分析将显示哪些对象占用了你的大部分内存。如果你发现它被采取了MyProgram.CustomerData那就更好了。但通常,最大的对象类型是stringbyte[]byte[][]。由于应用程序中的几乎所有内容都可以使用这些类型,因此你需要找到引用它们的人。为此,查看所占用的包容性内存(又名保留内存)很重要。这个指标不仅包括对象本身占用的内存,还包括它引用的对象占用的内存。例如,你可能会发现它MyProgram.Inventory.Item本身并不占用太多内存,但它引用了一个byte[]它保存内存中的图像并占用高达 70% 的内存。上面描述的所有工具都可以显示包含最多字节的对象和到 GC 根的引用路径(也就是到根的最短路径)。

2、了解谁把内存放在了哪里

找出谁引用了最大的内存块很棒,但这可能还不够。有时你需要知道这些内存是如何分配的。你可能从引用路径中知道,一些占用大部分内存的对象位于缓存中,但谁将它们放在那里?来自单个时间点的内存快照无法提供该答案。为此,你需要分配堆栈跟踪。分析器使你能够记录你的应用程序并在每次分配时保存调用堆栈。例如,你可能会发现创建有问题MyProgram.Inventory.Item对象的流程将它们分配到调用堆栈App.OnShowHistoryClicked | App.SeeItemHistory | App.GetItemFromDatabase中。要获得分配堆栈,你可以:•使用商业内存分析器来显示分配。

\"图片\"

•使用 PerfView 的 GC Heap [] Stacks 之一

\"图片\"

分配让你全面了解占用大部分内存的内容以及它是如何产生的。一旦你知道了这一点,你就可以开始切割最大的块并优化它们以减少内存使用。

3、检查内存泄漏

在 .NET 中导致内存泄漏非常容易。有了足够多的泄漏,内存消耗会随着时间的推移而增加,你会遇到各种各样的问题。内存瓶颈就是其中之一,但由于 GC 压力,你最终也会遇到 CPU 问题。当你不再需要对象但由于某种原因它们仍然被引用并且垃圾收集器永远不会释放它们时,就会发生内存泄漏。发生这种情况的原因有很多。要了解你是否有严重的内存泄漏,请查看一段时间内的内存消耗图表(进程 | 私有字节计数器)。如果内存一直在增加,而没有偏离某个水平,则可能存在内存泄漏。

\"图片\"

使用内存分析器调试泄漏相当简单。

4、切换到 GC 工作站模式

.NET 中有几种垃圾收集器模式。主要的两种模式是Workstation GCServer GC。Workstation GC 针对更短的 GC 暂停和更快的交互性进行了优化,非常适合桌面应用程序。服务器 GC 具有更长的 GC 暂停时间,并且针对更高的吞吐量进行了优化。

在 Server GC 模式下,应用程序可以在垃圾回收之间处理更多数据。服务器 GC 为每个 CPU 核心创建不同的托管堆。这意味着不同的 X 代内存空间需要更长的时间才能填满,因此内存消耗会更高。你基本上是在用内存换取吞吐量。从 GC 服务器模式(.NET 服务器的默认模式)更改为 GC 工作站模式将减少内存使用量。这在请求负载不重的小型应用程序中可能是合理的。也许在与主应用程序一起运行的 IIS 主机中的辅助进程中。Sergey Tepliakov对此有一篇很棒的文章。

5、检查你的缓存

在第 1 步之后,你应该能够看到哪些对象占用了你的内存,但我想特别强调缓存。每当涉及到高内存消耗时,根据我的经验,它总是最终成为内存泄漏或缓存。缓存似乎是许多问题的神奇解决方案。当你可以将结果保存在内存中并重新使用它时,为什么要执行两次?但是缓存是有代价的。一个简单的实现会将对象永远保存在内存中。你应该按时间限制或以其他方式使缓存无效。缓存还会将临时对象留在内存中相对较长的时间,这会导致更多的 Gen 1 和 Gen 2 收集,进而导致GC 压力。以下是一些优化内存缓存的想法:

•使用.NET 中的现有缓存实现可以轻松创建失效策略。

•考虑为某些事情选择不缓存。你可能会用 CPU 或 IO 换取内存,但是当你受到内存限制时,你应该这样做。

•考虑使用内存不足缓存。这可能是将数据保存在文件或本地数据库中。或者使用像Redis这样的分布式缓存解决方案。

6、定期调用GC.Collect()

这条建议是违反直觉的,因为最好的做法是永远不要调用GC.Collect(). 垃圾收集器很聪明,它应该自己知道何时触发收集。但问题是垃圾收集器只考虑自己的进程。如果它没有足够的内存,它会小心触发收集并腾出空间。但如果它确实有足够的内存,GC 会非常乐意忍受过多的内存消耗。

因此,GC 的自私本性可能是生活在同一台机器上的**其他进程的问题,可能托管在同一个 IIS 上。

这种多余的内存可能会导致其他进程更快地达到它们的极限,或者导致它们各自的垃圾收集器更加努力地工作,因为它们可能错误地认为它们即将耗尽内存。你可能会认为,如果其他进程的 GC 会达到认为我们内存不足并因此更加努力地工作的程度,那么我们自己的进程也会这样认为并触发垃圾收集来解决问题。但我们不能做出这样的假设。一方面,这些进程可能运行不同的 GC 实现版本(因为不同的 CLR 版本)。此外,你有不同的应用程序行为可以使 GC 以不同的方式工作。例如,一个进程可能会以更高的速率分配内存,因此 GC 将更快地开始“强调”可用内存。底线是软件很困难,当你在一台机器上有多个进程时,就像 IIS 一样,你需要考虑到这一点,并可能采取一些不寻常的步骤。

优化 CPU 使用率

硬币的另一面是 CPU 使用率。一旦你发现 CPU 是应用程序吞吐量的瓶颈,就需要做很多事情。

1、分析你的应用程序

优化 CPU 的第一步是了解它。究竟是什么原因造成的?哪些方法负责?哪些请求是最大的 CPU 消耗者,哪些是流量?这一切都可以通过分析应用程序来解决。分析允许你记录执行范围并显示所有被调用的方法以及它们在记录期间使用了多少 CPU。分析器通常允许将这些结果视为普通列表、调用树甚至火焰图。这是 PerfView 中的简单列表视图:

\"图片\"

这是相同场景的火焰图:

\"图片\"

你可以通过以下方式分析你的应用:

•如果场景在本地重现,请使用性能分析器,如PerfView、dotTrace、ANTS perf profiler,或在你的开发计算机上使用 Visual Studio 。

•在生产环境中,最简单的分析方法是使用应用程序性能监控 (APM) 工具,例如Azure Application Insights profiler或RayGun。

•你可以通过将代理复制到生产机器并记录快照来分析没有 APM 的生产环境。使用 PerfView,你应该复制整个程序。它结构紧凑,无需安装。使用 dotTrace,你可以复制允许在生产中记录快照的轻量级代理。

•在 .NET Core 3.0+ 应用程序中,你可以安装 .NET Core 3.0 SDK 并使用 dotnet-trace 命令行工具记录快照,然后使用 PerfView 将其复制到开发机器并进行分析。

2、检查垃圾收集器的使用情况

我想说优化 .NET CPU 使用最重要的一点是正确的内存管理。在这方面要问的重要问题是:“垃圾收集浪费了多少 CPU?”。GC 的工作方式是在收集期间,你的执行线程被冻结。这意味着垃圾收集直接影响性能。因此,如果你受 CPU 限制,我建议你检查的第一件事是性能计数器。NET CLR 内存 | % GC 时间。我不能给你一个指示问题的神奇数字,但根据经验,当这个值超过 20% 时,你可能会遇到问题。如果超过 40%,那么你肯定有问题。如此高的百分比表明 GC 压力,并且有办法处理它。

3、使用数组和对象池来重用内存

阵列的分配和不可避免的解除分配可能非常昂贵。高频率执行这些分配会造成 GC 压力并消耗大量 CPU 时间。解决这个问题的一个好方法是使用内置的ArrayPoolObjectPool (仅限 .NET Core)。这个想法很简单。为数组或对象分配一个共享缓冲区,然后在不分配和取消分配新内存的情况下重复使用。这是一个简单的使用示例ArrayPool

public void Foo()
{
    var pool = ArrayPool<int>.Shared;
    int[] array = pool.Rent(ArraySize);// do stuf
    pool.Return(array);
}

4、切换到 GC 服务器模式

我们已经讨论过转移到GC 工作站模式以节省内存。但如果你受 CPU 限制,请考虑切换到服务器模式以节省 CPU。权衡是服务器模式以更多内存为代价允许更高的吞吐量。

因此,如果你保持相同的吞吐量,你最终将节省 CPU 时间,否则垃圾收集会花费这些时间。默认情况下,.NET 服务器很可能具有 GC 服务器模式,因此可能不需要此更改。但是可能有人之前将其更改为工作站模式,在这种情况下,你应该小心将其更改回来,因为他们可能有充分的理由。

更改时,请务必监控内存消耗和 GC 中的 % Time。你可能想查看第 2 代回收率,但如果这个数字很高,它将反映在更高的 GC 时间百分比中。

5、检查其他进

当试图将你的服务器发挥到最佳极限时,你可能想要彻底了解它,这意味着不要放弃存在于你的进程之外的问题。很有可能其他进程不时消耗一堆CPU,并导致一段时间的性能下降。这些可能是你在 IIS 上部署的其他应用程序、定期 Web 作业、由操作系统触发的东西、防病毒程序或其他一千种东西。对此进行分析的一种方法是使用 PerfView 记录整个系统中的 ETW 事件。PerfView 从所有进程中捕获 CPU 堆栈。你可以以很小的性能开销运行它很长时间。你可以在达到某个 CPU 峰值时自动停止收集并进行挖掘。你可能会对结果感到惊讶。

总结

在我看来,从自上而下的层面处理大规模的性能问题是令人着迷的。你可能有一个团队花费数月时间优化一段代码,相比之下,资源分配的简单更改将产生更大的影响。而且,如果你的业务足够大,那么这个微小的变化就会转化为一大笔钱。你记得在你的合同中要求一个佣金条款吗?无论如何,我希望这篇文章对你有用。


", "summary": "凡事都有其限度,对吧?汽车只能开这么快,进程只能使用这么多内存,程序员只能喝这么多咖啡。我们的生产力受到资源的限制,我们有能力更好或更差地利用它们。", "author": "", "source": "DotNet", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-02-12 16:35:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 711, "guid": "6651e281-4803-458c-8df4-9e6deacf1a82", "createdDate": "2022-02-12 16:40:17", "lastModifiedDate": "2022-02-12 16:40:17" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 53, "groupNames": [], "tagNames": [ "C#", "可变参数" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "浅谈 C# 可变参数 params", "subTitle": "", "seoTitle": null, "imageUrl": "", "videoUrl": "", "fileUrl": "", "keywords": "C#,params", "description": "在群里看到群友写了一个基础框架,其中涉及到关于同一个词语可以添加多个近义词的一个场景。当时群友的设计是类似字典的设计,直接添加k-v的操作,本人看到后思考了一下觉得使用c#中的params可以更优雅的实现一个key同时添加一个集合的操作,看起来会更优雅一点,这期间还有群友说道params和数组有啥区别的问题。", "body": "

前言


群里看到群友写了一个基础框架,其中涉及到关于同一个词语可以添加多个近义词的一个场景。当时群友的设计是类似字典的设计,直接添加k-v的操作,本人看到后思考了一下觉得使用c#中的params可以更优雅的实现一个key同时添加一个集合的操作,看起来会更优雅一点,这期间还有群友说道params和数组有啥区别的问题。本篇文章就来大致的说一下。


示例


params是c#的一个关键字,用用汉语来说的话叫可变参数,这里的可变,不是说的类型可变,而是指的个数可变,这是c#的一个基础关键字,相信大家都有一定的了解,今天咱们就来进一步看一下c#的可变参数params。首先来看一下简单的自定义使用,随便定义一个方法

static void ParamtesDemo(string className, params string[] names)
{
    Console.WriteLine($"{className}的学生有:{string.Join(",", names)}");
}

定义可变参数类型的时候需要有几个注意点•params修饰在参数的前面且参数类型得是一维数组类型

params修饰的参数默认是可以不传递的

params参数不能用ref或out修饰且不能手动给默认值调用的时候更简单了,如下所示

ParamtesDemo("小四班""jordan""kobe""james""curry");
// 如果不传递值也不会报错
// ParamtesDemo("小四班");

由上面的示例可知,使用可变参数最大的优势就是你可以传递一个不确定个数的集合类型并且不用声明单独的类型去包装,这种场景特别适合传递参数不确定的场景,比如我们经常使用到的string.Format就是使用的可变参数类型。

探究本质

通过上面我们了解到的params的遍历性,当集合参数个数不确定的时候是使用可变参数的最佳场景,看着很神奇很便捷,本质到底是什么呢?之前楼主也没有在意这个问题,直到前几天怀揣着好奇的心情看了一下。废话不多说,我们直接借助ILSpy工具看一下反编译之后的源码

[CompilerGenerated]
internal class Program
{
    private static void <Main>$(string[] args)
    {
        //声明了一个数组
        ParamtesDemo("小四班"new string[4] { "jordan""kobe""james""curry" });
        Console.ReadKey();

        //已经没有params关键字了,就是一个数组
        static void ParamtesDemo(string className, string[] names)
        {
            Console.WriteLine(className + "的学生有:" + string.Join(",", names));
        }
    }
}

通过ILSpy反编译的源码我们可以看到params是一个语法糖,其实就是增加了编程效率,本质在编译的时候会被具体的声明的数组类型替代,不参与到运行时。这个时候如果你怀疑反编译的代码有问题,可以直接通过ILSpy看生成的IL代码,由于IL代码比较长,首先看一下Main方法

// Methods
.method private hidebysig static 
        void '<Main>$' (
            string[] args
        ) cil managed 
{
    // Method begins at RVA 0x2092
    // Header size: 1
    // Code size: 57 (0x39)
    .maxstack 8
    .entrypoint

    // ParamtesDemo("小四班", new string[4] { "jordan""kobe""james""curry" });
    IL_0000: ldstr "小四班"
    IL_0005: ldc.i4.4
        //通过newarr可知确实是声明了一个数组类型
    IL_0006: newarr [System.Runtime]System.String
    IL_000b: dup
    IL_000c: ldc.i4.0
    IL_000d: ldstr "jordan"
    IL_0012: stelem.ref
    IL_0013: dup
    IL_0014: ldc.i4.1
    IL_0015: ldstr "kobe"
    IL_001a: stelem.ref
    IL_001b: dup
    IL_001c: ldc.i4.2
    IL_001d: ldstr "james"
    IL_0022: stelem.ref
    IL_0023: dup
    IL_0024: ldc.i4.3
    IL_0025: ldstr "curry"
    IL_002a: stelem.ref
    // 这个地方调用了ParamtesDemo,第二个参数确实是一个数组类型
    IL_002b: call void Program::'<<Main>$>g__ParamtesDemo|0_0'(string, string[])
    // Console.ReadKey();
    IL_0030: nop
    IL_0031: call valuetype [System.Console]System.ConsoleKeyInfo [System.Console]System.Console::ReadKey()
    IL_0036: pop
    // }
    IL_0037: nop
    IL_0038: ret
} // end of method Program::'<Main>$' 

通过上面的IL代码可以看到确实是一个语法糖,编译完之后一切尘归尘土归土还是一个数组类型,类型是和params修饰的那个数组类型是一致的。接下来我们再来看一下ParamtesDemo这个方法的IL代码是啥样的

//names也是一个数组
.method assembly hidebysig static 
    void '<<Main>$>g__ParamtesDemo|0_0' (
        string className,
        string[] names
    ) cil managed 
{
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
    )
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x20d5
    // Header size: 1
    // Code size: 30 (0x1e)
    .maxstack 8

    // {
    IL_0000: nop
    // Console.WriteLine(className + "的学生有:" + string.Join(",", names));
    IL_0001: ldarg.0
    IL_0002: ldstr "的学生有:"
    IL_0007: ldstr ","
    IL_000c: ldarg.1
    IL_000d: call string [System.Runtime]System.String::Join(stringstring[])
    IL_0012: call string [System.Runtime]System.String::Concat(stringstringstring)
    IL_0017: call void [System.Console]System.Console::WriteLine(string)
    // }
    IL_001c: nop
    IL_001d: ret
// end of method Program::'<<Main>$>g__ParamtesDemo|0_0'

一切了然,本质就是那个数组。我们上面还提到了params修饰的参数默认不传递的话也不会报错,这究竟是为什么呢,我们就用IL代码来看一下究竟进行了何等操作吧

// Methods
.method private hidebysig static 
    void '<Main>$' (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2092
    // Header size: 1
    // Code size: 24 (0x18)
    .maxstack 8
    .entrypoint

    // ParamtesDemo("小四班", Array.Empty<string>());
    IL_0000: ldstr "小四班"
        // 本质是编译的时候帮我们声明了一个空数组Array::Empty<string>
    IL_0005: call !!0[] [System.Runtime]System.Array::Empty<string>()
    IL_000a: call void Program::'<<Main>$>g__ParamtesDemo|0_0'(stringstring[])
    // Console.ReadKey();
    IL_000f: nop
    IL_0010: call valuetype [System.Console]System.ConsoleKeyInfo [System.Console]System.Console::ReadKey()
    IL_0015: pop
    // }
    IL_0016: nop
    IL_0017: ret
// end of method Program::'<Main>$'

原来这得感谢编译器,如果默认不传递params修饰的参数的话,默认它会帮我们生成一个这个类型的空数组,这里需要注意的不是null,所以代码不会报错,只是没有数据。

扩展知识

我们上面提到了string.Format也是基于params实现的,毕竟Format具体的参数依赖于前面声明的字符串的占位符个数。在翻看相关代码的时候还发现了一个ParamsArray这个类,用来包装params可变参数,简单的来说就是便于快速操作params,这个我是在Format方法中发现的,源代码如下

public static string Format(string format, params object?[] args)
{
    if (args == null)
    {
        throw new ArgumentNullException((format == null) ? nameof(format) : nameof(args));
    }
    return FormatHelper(null, format, new ParamsArray(args));
}

params参数也可以为null值,默认不会报错,但是需要进行判断,否则程序处理null可能会报错。在这里我们可以看到把params参数传递给ParamsArray进行包装,我们可以看一下ParamsArray类本身的定义,这个类是一个struct类型的

internal readonly struct ParamsArray
{
    //定义是三个数组分别去承载当传递进来的params不同个数时的数据
    private static readonly object?[] s_oneArgArray = new object?[1];
    private static readonly object?[] s_twoArgArray = new object?[2];
    private static readonly object?[] s_threeArgArray = new object?[3];

    //定义三个值分别存储params的第0、1、2个参数的值
    private readonly object? _arg0;
    private readonly object? _arg1;
    private readonly object? _arg2;

    //承载最原始的params值
    private readonly object?[] _args;

    //params值为1个的时候
    public ParamsArray(object? arg0)
    {
        _arg0 = arg0;
        _arg1 = null;
        _arg2 = null;

        _args = s_oneArgArray;
    }

    //params值为2个的时候
    public ParamsArray(object? arg0, object? arg1)
    {
        _arg0 = arg0;
        _arg1 = arg1;
        _arg2 = null;

        _args = s_twoArgArray;
    }

    //params值为3个的时候
    public ParamsArray(object? arg0, object? arg1, object? arg2)
    {
        _arg0 = arg0;
        _arg1 = arg1;
        _arg2 = arg2;

        _args = s_threeArgArray;
    }

    //直接包装整个params的值
    public ParamsArray(object?[] args)
    {
        //直接取出来值缓存
        int len = args.Length;
        _arg0 = len > 0 ? args[0] : null;
        _arg1 = len > 1 ? args[1] : null;
        _arg2 = len > 2 ? args[2] : null;
        _args = args;
    }

    public int Length => _args.Length;

    public objectthis[int index] => index == 0 ? _arg0 : GetAtSlow(index);

    //判断是否从承载的缓存中取值
    private object? GetAtSlow(int index)
    {
        if (index == 1)
            return _arg1;
        if (index == 2)
            return _arg2;
        return _args[index];
    }
}

ParamsArray是一个值类型,目的就是为了把params参数的值给包装起来提供读相关的操作。根据二八法则来看,params大部分场景的参数个数或者高频访问可能是存在于数组的前几位元素上,所以使用ParamsArray针对热点元素提供了快速访问的方式,略微有一点像Java中的IntegerCache的设计。这个结构体是internal类型的,默认程序集之外是没办法访问的,我当时看到的时候比较好奇,就多看了一眼,感觉设计思路还是考虑的比较周到的。

总结

本文主要简单的聊一下c#可变参数params的本质,了解到了其实就是一个语法糖,编译完成之后本质还是一个数组。它的好处就是当我们不确定集合个数的时候,可以灵活的使用params进行参数传递,不用自行定义一个集合类型。然后微软针对params在内部实现了一个ParamsArray结构体进行对params包装,提升params类型的访问。

新年伊始,聊一点个人针对学习的看法。学习最理想的结果就是把接触到的知识进行一定的抽象,转换为概念或者一种思维方式,然后细化这种思维,让它成为细颗粒度的知识点,然后我们通过不断的接触不断的积累,后者不同领域的接触等,不断吸收壮大这个思维库。然后当看到一个新的问题的时候,或者需要思考的时候,能达到快速的多角度的整合这些思维碎片,得到一个更好的思路或解决问题的办法,这也许是一种更行之有效的状态。类比到我们架构设计上来说,以前的思维方式是一种类似单体应用的方式,灵活性差扩展性更差,后来微服务概念大行其道,更多独立的服务相互协调工作,形成一种更强大的聚合力。


", "summary": "在群里看到群友写了一个基础框架,其中涉及到关于同一个词语可以添加多个近义词的一个场景。当时群友的设计是类似字典的设计,直接添加k-v的操作,本人看到后思考了一下觉得使用c#中的params可以更优雅的实现一个key同时添加一个集合的操作,看起来会更优雅一点,这期间还有群友说道params和数组有啥区别的问题。", "author": "", "source": "DotNet", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-02-12 16:33:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 710, "guid": "52e806b0-f023-466b-92e8-725c13736176", "createdDate": "2022-02-12 16:34:58", "lastModifiedDate": "2022-02-12 16:34:58" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 52, "groupNames": [], "tagNames": [ "SQL", "索引" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 1, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "SQL 索引您了解多少", "subTitle": "", "seoTitle": null, "imageUrl": "", "videoUrl": "", "fileUrl": "", "keywords": "SQL,索引", "description": "深入浅出理解索引结构", "body": "

)深入浅出理解索引结构

实际上,您可以把索引理解为一种特殊的目录。微软的SQL SERVER提供了两种索引:聚集索引(clustered index,也称聚类索引、簇集索引)和非聚集索引(nonclustered index,也称非聚类索引、非簇集索引)。下面,我们举例来说明一下聚集索引和非聚集索引的区别:

其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自然地翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字;同样的,如果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是说,字典的正文部分本身就是一个目录,您不需要再去查其他目录来找到您需要找的内容。我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。

如果您认识某个字,您可以快速地从自动中查到这个字。但您也可能会遇到您不认识的字,不知道它的发音,这时候,您就不能按照刚才的方法找到您要查的字,而需要去根据“偏旁部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张”字,我们可以看到在查部首之后的检字表中“张”的页码是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页面是390页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰、张、弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所需要的页码。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。

通过以上例子,我们可以理解到什么是“聚集索引”和“非聚集索引”。进一步引申一下,我们可以很容易的理解:每个表只能有一个聚集索引,因为目录只能按照一种方法进行排序。

二、何时使用聚集索引或非聚集索引

下面的表总结了何时使用聚集索引或非聚集索引(很重要):

动作描述

使用聚集索引

使用非聚集索引

列经常被分组排序

返回某范围内的数据

不应

一个或极少不同值

不应

不应

小数目的不同值

不应

大数目的不同值

不应

频繁更新的列

不应

外键列

主键列

频繁修改索引列

不应

事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。如:返回某范围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列,这时您查询2004年1月1日至2004年10月1日之间的全部数据时,这个速度就将是很快的,因为您的这本字典正文是按日期进行排序的,聚类索引只需要找到要检索的所有数据中的开头和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码,然后再根据页码查到具体内容。

三、结合实际,谈索引使用的误区

理论的目的是应用。虽然我们刚才列出了何时应使用聚集索引或非聚集索引,但在实践中以上规则却很容易被忽视或不能根据实际情况进行综合分析。下面我们将根据在实践中遇到的实际问题来谈一下索引使用的误区,以便于大家掌握索引建立的方法。

1、主键就是聚集索引

这种想法笔者认为是极端错误的,是对聚集索引的一种浪费。虽然SQL SERVER默认是在主键上建立聚集索引的。

通常,我们会在每个表中都建立一个ID列,以区分每条数据,并且这个ID列是自动增大的,步长一般为1。我们的这个办公自动化的实例中的列Gid就是如此。此时,如果我们将这个列设为主键,SQL SERVER会将此列默认为聚集索引。这样做有好处,就是可以让您的数据在数据库中按照ID进行物理排序,但笔者认为这样做意义不大。

显而易见,聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这使得聚集索引变得更加珍贵。

从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够根据查询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为ID号是自动生成的,我们并不知道每条记录的ID号,所以我们很难在实践中用ID号来进行查询。这就使让ID号这个主键作为聚集索引成为一种资源浪费。其次,让每个ID号都不同的字段作为聚集索引也不符合“大数目的不同值情况下不应建立聚合索引”规则;当然,这种情况只是针对用户经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。

在办公自动化系统中,无论是系统首页显示的需要用户签收的文件、会议还是用户进行文件查询等任何情况下进行数据查询都离不开字段的是“日期”还有用户本身的“用户名”。

通常,办公自动化的首页会显示每个用户尚未签收的文件或会议。虽然我们的where语句可以仅仅限制当前用户尚未签收的情况,但如果您的系统已建立了很长时间,并且数据量很大,那么,每次每个用户打开首页的时候都进行一次全表扫描,这样做意义是不大的,绝大多数的用户1个月前的文件都已经浏览过了,这样做只能徒增数据库的开销而已。事实上,我们完全可以让用户打开系统首页时,数据库仅仅查询这个用户近3个月来未阅览的文件,通过“日期”这个字段来限制表扫描,提高查询速度。如果您的办公自动化系统已经建立的2年,那么您的首页显示速度理论上将是原来速度8倍,甚至更快。

在这里之所以提到“理论上”三字,是因为如果您的聚集索引还是盲目地建在ID这个主键上时,您的查询速度是没有这么高的,即使您在“日期”这个字段上建立的索引(非聚合索引)。下面我们就来看一下在1000万条数据量的情况下各种查询的速度表现(3个月内的数据为25万条):

(1)仅在主键上建立聚集索引,并且不划分时间段:

Select gid,fariqi,neibuyonghu,title from tgongwen

用时:128470毫秒(即:128秒)

(2)在主键上建立聚集索引,在fariq上建立非聚集索引:

select gid,fariqi,neibuyonghu,title from Tgongwen\nwhere fariqi> dateadd(day,-90,getdate())

用时:53763毫秒(54秒)

(3)将聚合索引建立在日期列(fariqi)上:

select gid,fariqi,neibuyonghu,title from Tgongwen\nwhere fariqi> dateadd(day,-90,getdate())

用时:2423毫秒(2秒)

虽然每条语句提取出来的都是25万条数据,各种情况的差异却是巨大的,特别是将聚集索引建立在日期列时的差异。事实上,如果您的数据库真的有1000万容量的话,把主键建立在ID列上,就像以上的第1、2种情况,在网页上的表现就是超时,根本就无法显示。这也是我摒弃ID列作为聚集索引的一个最重要的因素。得出以上速度的方法是:在各个select语句前加:

declare @d datetime\nset @d=getdate()

并在select语句后加:

select [语句执行花费时间(毫秒)]=datediff(ms,@d,getdate())

2、只要建立索引就能显著提高查询速度

事实上,我们可以发现上面的例子中,第2、3条语句完全相同,且建立索引的字段也相同;不同的仅是前者在fariqi字段上建立的是非聚合索引,后者在此字段上建立的是聚合索引,但查询速度却有着天壤之别。所以,并非是在任何字段上简单地建立索引就能提高查询速度。

从建表的语句中,我们可以看到这个有着1000万数据的表中fariqi字段有5003个不同记录。在此字段上建立聚合索引是再合适不过了。在现实中,我们每天都会发几个文件,这几个文件的发文日期就相同,这完全符合建立聚集索引要求的:“既不能绝大多数都相同,又不能只有极少数相同”的规则。由此看来,我们建立“适当”的聚合索引对于我们提高查询速度是非常重要的。

3、把所有需要提高查询速度的字段都加进聚集索引,以提高查询速度

上面已经谈到:在进行数据查询时都离不开字段的是“日期”还有用户本身的“用户名”。既然这两个字段都是如此的重要,我们可以把他们合并起来,建立一个复合索引(compound index)。

很多人认为只要把任何字段加进聚集索引,就能提高查询速度,也有人感到迷惑:如果把复合的聚集索引字段分开查询,那么查询速度会减慢吗?带着这个问题,我们来看一下以下的查询速度(结果集都是25万条数据):(日期列fariqi首先排在复合聚集索引的起始列,用户名neibuyonghu排在后列):

select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>''2004-5-5''

查询速度:2513毫秒

select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>'2004-5-5' and neibuyonghu='办公室'

查询速度:2516毫秒

select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu='办公室'

查询速度:60280毫秒

从以上试验中,我们可以看到如果仅用聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(在查询结果集数目一样的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的话,这个索引是不起任何作用的。当然,语句1、2的查询速度一样是因为查询的条目数一样,如果复合索引的所有列都用上,而且查询结果少的话,这样就会形成“索引覆盖”,因而性能可以达到最优。同时,请记住:无论您是否经常使用聚合索引的其他列,但其前导列一定要是使用最频繁的列。

四、其他书上没有的索引使用经验总结

1、用聚合索引比用不是聚合索引的主键速度快

下面是实例语句:(都是提取25万条数据)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16'

使用时间:3326毫秒

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000

使用时间:4470毫秒

这里,用聚合索引比用不是聚合索引的主键速度快了近1/4。

2、用聚合索引比用一般的主键作order by时速度快,特别是在小数据量情况下

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi

用时:12936

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid

用时:18843

这里,用聚合索引比用一般的主键作order by时,速度快了3/10。事实上,如果数据量很小的话,用聚集索引作为排序列要比使用非聚集索引速度快得明显的多;而数据量如果很大的话,如10万以上,则二者的速度差别不明显。

3、使用聚合索引内的时间段,搜索时间会按数据占整个数据表的百分比成比例减少,而无论聚合索引使用了多少个:

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1'

用时:6343毫秒(提取100万条)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-6-6'

用时:3170毫秒(提取50万条)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16'

用时:3326毫秒(和上句的结果一模一样。如果采集的数量一样,那么用大于号和等于号是一样的)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' and fariqi<'2004-6-6'

用时:3280毫秒

4、日期列不会因为有分秒的输入而减慢查询速度

下面的例子中,共有100万条数据,2004年1月1日以后的数据有50万条,但只有两个不同的日期,日期精确到日;之前有数据50万条,有5000个不同的日期,日期精确到秒。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-1' order by fariqi

用时:6390毫秒

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi<'2004-1-1' order by fariqi

用时:6453毫秒

五、其他注意事项

“水可载舟,亦可覆舟”,索引也一样。索引有助于提高检索性能,但过多或不当的索引也会导致系统低效。因为用户在表中每加进一个索引,数据库就要做更多的工作。过多的索引甚至会导致索引碎片。

所以说,我们要建立一个“适当”的索引体系,特别是对聚合索引的创建,更应精益求精,以使您的数据库能得到高性能的发挥。

当然,在实践中,作为一个尽职的数据库管理员,您还要多测试一些方案,找出哪种方案效率最高、最为有效。

(二)改善SQL语句

很多人不知道SQL语句在SQL SERVER中是如何执行的,他们担心自己所写的SQL语句会被SQL SERVER误解。比如:

select * from table1 where name='zhangsan' and tID > 10000

和执行

select * from table1 where tID > 10000 and name='zhangsan'

一些人不知道以上两条语句的执行效率是否一样,因为如果简单的从语句先后上看,这两个语句的确是不一样,如果tID是一个聚合索引,那么后一句仅仅从表的10000条以后的记录中查找就行了;而前一句则要先从全表中查找看有几个name='zhangsan'的,而后再根据限制条件条件tID>10000来提出查询结果。

事实上,这样的担心是不必要的。SQL SERVER中有一个“查询分析优化器”,它可以计算出where子句中的搜索条件并确定哪个索引能缩小表扫描的搜索空间,也就是说,它能实现自动优化。

虽然查询优化器可以根据where子句自动的进行查询优化,但大家仍然有必要了解一下“查询优化器”的工作原理,如非这样,有时查询优化器就会不按照您的本意进行快速查询。

在查询分析阶段,查询优化器查看查询的每个阶段并决定限制需要扫描的数据量是否有用。如果一个阶段可以被用作一个扫描参数(SARG),那么就称之为可优化的,并且可以利用索引快速获得所需数据。

SARG的定义:用于限制搜索的一个操作,因为它通常是指一个特定的匹配,一个值得范围内的匹配或者两个以上条件的AND连接。形式如下:

列名 操作符 <常数>或<常数> 操作符列名

列名可以出现在操作符的一边,而常数或变量出现在操作符的另一边。如:

Name=’张三’

价格>5000

5000<价格

Name=’张三’ and 价格>5000

如果一个表达式不能满足SARG的形式,那它就无法限制搜索的范围了,也就是SQL SERVER必须对每一行都判断它是否满足WHERE子句中的所有条件。所以一个索引对于不满足SARG形式的表达式来说是无用的。

介绍完SARG后,我们来总结一下使用SARG以及在实践中遇到的和某些资料上结论不同的经验:

1、Like语句是否属于SARG取决于所使用的通配符的类型

如:name like ‘张%’ ,这就属于SARG

而:name like ‘%张’ ,就不属于SARG。

原因是通配符%在字符串的开通使得索引无法使用。

2、or 会引起全表扫描

Name=’张三’ and 价格>5000 符号SARG,而:Name=’张三’ or 价格>5000 则不符合SARG。使用or会引起全表扫描。

3、非操作符、函数引起的不满足SARG形式的语句

不满足SARG形式的语句最典型的情况就是包括非操作符的语句,如:NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等,另外还有函数。下面就是几个不满足SARG形式的例子:

ABS(价格)<5000

Name like ‘%三’

有些表达式,如:

WHERE 价格*2>5000

SQL SERVER也会认为是SARG,SQL SERVER会将此式转化为:

WHERE 价格>2500/2

但我们不推荐这样使用,因为有时SQL SERVER不能保证这种转化与原始表达式是完全等价的。

4、IN 的作用相当与OR

语句:

Select * from table1 where tid in (2,3)和Select * from table1 where tid=2 or tid=3

是一样的,都会引起全表扫描,如果tid上有索引,其索引也会失效。

5、尽量少用NOT

6、exists 和 in 的执行效率是一样的

很多资料上都显示说,exists要比in的执行效率要高,同时应尽可能的用not exists来代替not in。但事实上,我试验了一下,发现二者无论是前面带不带not,二者之间的执行效率都是一样的。因为涉及子查询,我们试验这次用SQL SERVER自带的pubs数据库。运行前我们可以把SQL SERVER的statistics I/O状态打开:

select title,price from titles where title_id in (select title_id from sales where qty>30)

该句的执行结果为:

表 ''sales''。扫描计数 18,逻辑读 56 次,物理读 0 次,预读 0 次。

表 ''titles''。扫描计数 1,逻辑读 2 次,物理读 0 次,预读 0 次。

select title,price from titles where exists (select * from sales where sales.title_id=titles.title_id and qty>30)

第二句的执行结果为:

表 ''sales''。扫描计数 18,逻辑读 56 次,物理读 0 次,预读 0 次。

表 ''titles''。扫描计数 1,逻辑读 2 次,物理读 0 次,预读 0 次。

我们从此可以看到用exists和用in的执行效率是一样的。

7、用函数charindex()和前面加通配符%的LIKE执行效率一样

前面,我们谈到,如果在LIKE前面加上通配符%,那么将会引起全表扫描,所以其执行效率是低下的。但有的资料介绍说,用函数charindex()来代替LIKE速度会有大的提升,经我试验,发现这种说明也是错误的:

select gid,title,fariqi,reader from tgongwen where charindex('刑侦支队',reader)>0 and fariqi>'2004-5-5'

用时:7秒,另外:扫描计数 4,逻辑读 7155 次,物理读 0 次,预读 0 次。

select gid,title,fariqi,reader from tgongwen where reader like '%' + '刑侦支队' + '%' and fariqi>'2004-5-5'

用时:7秒,另外:扫描计数 4,逻辑读 7155 次,物理读 0 次,预读 0 次。

8、union并不绝对比or的执行效率高

我们前面已经谈到了在where子句中使用or会引起全表扫描,一般的,我所见过的资料都是推荐这里用union来代替or。事实证明,这种说法对于大部分都是适用的。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' or gid>9990000

用时:68秒。扫描计数 1,逻辑读 404008 次,物理读 283 次,预读 392163 次。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=''2004-9-16''\nunion\nselect gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid>9990000

用时:9秒。扫描计数 8,逻辑读 67489 次,物理读 216 次,预读 7499 次。

看来,用union在通常情况下比用or的效率要高的多。

但经过试验,笔者发现如果or两边的查询列是一样的话,那么用union则反倒和用or的执行速度差很多,虽然这里union扫描的是索引,而or扫描的是全表。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16' or fariqi='2004-2-5'

用时:6423毫秒。扫描计数 2,逻辑读 14726 次,物理读 1 次,预读 7176 次。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-16'\nunion\nselect gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-2-5'

用时:11640毫秒。扫描计数 8,逻辑读 14806 次,物理读 108 次,预读 1144 次。

9、字段提取要按照“需多少、提多少”的原则,避免“select *”

我们来做一个试验:

select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc

用时:4673毫秒

select top 10000 gid,fariqi,title from tgongwen order by gid desc

用时:1376毫秒

select top 10000 gid,fariqi from tgongwen order by gid desc

用时:80毫秒

由此看来,我们每少提取一个字段,数据的提取速度就会有相应的提升。提升的速度还要看您舍弃的字段的大小来判断。

10、count(*)不比count(字段)慢

某些资料上说:用*会统计所有列,显然要比一个世界的列名效率低。这种说法其实是没有根据的。我们来看:

select count(*) from Tgongwen

用时:1500毫秒

select count(gid) from Tgongwen

用时:1483毫秒

select count(fariqi) from Tgongwen

用时:3140毫秒

select count(title) from Tgongwen

用时:52050毫秒

从以上可以看出,如果用count(*)和用count(主键)的速度是相当的,而count(*)却比其他任何除主键以外的字段汇总速度要快,而且字段越长,汇总的速度就越慢。我想,如果用count(*), SQL SERVER可能会自动查找最小字段来汇总的。当然,如果您直接写count(主键)将会来的更直接些。

11、order by按聚集索引列排序效率最高

我们来看:(gid是主键,fariqi是聚合索引列):

select top 10000 gid,fariqi,reader,title from tgongwen

用时:196 毫秒。 扫描计数 1,逻辑读 289 次,物理读 1 次,预读 1527 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by gid asc

用时:4720毫秒。 扫描计数 1,逻辑读 41956 次,物理读 0 次,预读 1287 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc

用时:4736毫秒。 扫描计数 1,逻辑读 55350 次,物理读 10 次,预读 775 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi asc

用时:173毫秒。 扫描计数 1,逻辑读 290 次,物理读 0 次,预读 0 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi desc

用时:156毫秒。 扫描计数 1,逻辑读 289 次,物理读 0 次,预读 0 次。

从以上我们可以看出,不排序的速度以及逻辑读次数都是和“order by 聚集索引列” 的速度是相当的,但这些都比“order by 非聚集索引列”的查询速度是快得多的。

同时,按照某个字段进行排序的时候,无论是正序还是倒序,速度是基本相当的。

12、高效的TOP

事实上,在查询和提取超大容量的数据集时,影响数据库响应时间的最大因素不是数据查找,而是物理的I/0操作。如:

select top 10 * from (\nselect top 10000 gid,fariqi,title from tgongwen\nwhere neibuyonghu='办公室'\norder by gid desc) as a\norder by gid asc

这条语句,从理论上讲,整条语句的执行时间应该比子句的执行时间长,但事实相反。因为,子句执行后返回的是10000条记录,而整条语句仅返回10条语句,所以影响数据库响应时间最大的因素是物理I/O操作。而限制物理I/O操作此处的最有效方法之一就是使用TOP关键词了。TOP关键词是SQL SERVER中经过系统优化过的一个用来提取前几条或前几个百分比数据的词。经笔者在实践中的应用,发现TOP确实很好用,效率也很高。但这个词在另外一个大型数据库ORACLE中却没有,这不能说不是一个遗憾,虽然在ORACLE中可以用其他方法(如:rownumber)来解决。在以后的关于“实现千万级数据的分页显示存储过程”的讨论中,我们就将用到TOP这个关键词。

到此为止,我们上面讨论了如何实现从大容量的数据库中快速地查询出您所需要的数据方法。当然,我们介绍的这些方法都是“软”方法,在实践中,我们还要考虑各种“硬”因素,如:网络性能、服务器的性能、操作系统的性能,甚至网卡、交换机等。

)实现小数据量和海量数据的通用分页显示存储过程

建立一个 Web 应用,分页浏览功能必不可少。这个问题是数据库处理中十分常见的问题。经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO自带的分页功能(利用游标)来实现分页。但这种分页方法仅适用于较小数据量的情形,因为游标本身有缺点:游标是存放在内存中,很费内存。游标一建立,就将相关的记录锁住,直到取消游标。游标提供了对特定集合中逐行扫描的手段,一般使用游标来逐行遍历数据,根据取出数据条件的不同进行不同的操作。而对于多表和大表中定义的游标(大的数据集合)循环很容易使程序进入一个漫长的等待甚至死机。

更重要的是,对于非常大的数据模型而言,分页检索时,如果按照传统的每次都加载整个数据源的方法是非常浪费资源的。现在流行的分页方法一般是检索页面大小的块区的数据,而非检索所有的数据,然后单步执行当前行。

最早较好地实现这种根据页面大小和页码来提取数据的方法大概就是“俄罗斯存储过程”。这个存储过程用了游标,由于游标的局限性,所以这个方法并没有得到大家的普遍认可。

后来,网上有人改造了此存储过程,下面的存储过程就是结合我们的办公自动化实例写的分页存储过程:

CREATE procedure pagination1\n\n(@pagesize int, --页面大小,如每页存储20条记录\n\n@pageindex int --当前页码\n\n)\n\nas\n\n\n\nset nocount on\n\n\nbegin\n\ndeclare @indextable table(id int identity(1,1),nid int) --定义表变量\n\ndeclare @PageLowerBound int --定义此页的底码\n\ndeclare @PageUpperBound int --定义此页的顶码\n\nset @PageLowerBound=(@pageindex-1)*@pagesize\n\nset @PageUpperBound=@PageLowerBound+@pagesize\n\nset rowcount @PageUpperBound\n\ninsert into @indextable(nid) select gid from TGongwen\n\n      where fariqi >dateadd(day,-365,getdate()) order by fariqi desc\n\nselect O.gid,O.mid,O.title,O.fadanwei,O.fariqi from TGongwen O,@indextable t\n\nwhere O.gid=t.nid and t.id>@PageLowerBound\n\nand t.id<=@PageUpperBound order by t.id\n\nend\n\n\nset nocount off

以上存储过程运用了SQL SERVER的最新技术――表变量。应该说这个存储过程也是一个非常优秀的分页存储过程。当然,在这个过程中,您也可以把其中的表变量写成临时表:CREATE TABLE #Temp。但很明显,在SQL SERVER中,用临时表是没有用表变量快的。所以笔者刚开始使用这个存储过程时,感觉非常的不错,速度也比原来的ADO的好。但后来,我又发现了比此方法更好的方法。

笔者曾在网上看到了一篇小短文《从数据表中取出第n条到第m条的记录的方法》,全文如下:

--从publish 表中取出第 n 条到第 m 条的记录:\n\nSELECT TOP m-n+1 *\n\nFROM publish\n\nWHERE (id NOT IN\n\n    (SELECT TOP n-1 id\n\n     FROM publish))\n\n\n\n--id 为publish 表的关键字

我当时看到这篇文章的时候,真的是精神为之一振,觉得思路非常得好。等到后来,我在作办公自动化系统(ASP.NET+ C#+SQL SERVER)的时候,忽然想起了这篇文章,我想如果把这个语句改造一下,这就可能是一个非常好的分页存储过程。于是我就满网上找这篇文章,没想到,文章还没找到,却找到了一篇根据此语句写的一个分页存储过程,这个存储过程也是目前较为流行的一种分页存储过程,我很后悔没有争先把这段文字改造成存储过程:

CREATE PROCEDURE pagination2\n\n(\n\n@SQL nVARCHAR(4000), --不带排序语句的SQL语句\n\n@Page int, --页码\n\n@RecsPerPage int, --每页容纳的记录数\n\n@ID VARCHAR(255), --需要排序的不重复的ID号\n\n@Sort VARCHAR(255) --排序字段及规则\n\n)\n\nAS\n\n \n\nDECLARE @Str nVARCHAR(4000)\n\n \n\nSET @Str=''SELECT TOP ''+CAST(@RecsPerPage AS VARCHAR(20))+'' * FROM\n\n(''+@SQL+'') T WHERE T.''+@ID+''NOT IN (SELECT TOP''+CAST((@RecsPerPage*(@Page-1))\n\nAS VARCHAR(20))+'' ''+@ID+'' FROM (''+@SQL+'') T9 ORDER BY''+@Sort+'') ORDER BY ''+@Sort\n \nPRINT @Str\n\n \n\nEXEC sp_ExecuteSql @Str\n\nGO\n\n--其实,以上语句可以简化为:\n\nSELECT TOP 页大小 *\n\nFROM Table1 WHERE (ID NOT IN (SELECT TOP 页大小*页数 id FROM 表 ORDER BY id))\n\nORDER BY ID\n\n--但这个存储过程有一个致命的缺点,就是它含有NOT IN字样。虽然我可以把它改造为:\n\nSELECT TOP 页大小 *\n\nFROM Table1 WHERE not exists\n\n(select * from (select top (页大小*页数) * from table1 order by id) b where b.id=a.id )\n\norder by id\n\n--目前流行的一种分页存储过程

即,用not exists来代替not in,但我们前面已经谈过了,二者的执行效率实际上是没有区别的。既便如此,用TOP 结合NOT IN的这个方法还是比用游标要来得快一些。

虽然用not exists并不能挽救上个存储过程的效率,但使用SQL SERVER中的TOP关键字却是一个非常明智的选择。因为分页优化的最终目的就是避免产生过大的记录集,而我们在前面也已经提到了TOP的优势,通过TOP 即可实现对数据量的控制。

在分页算法中,影响我们查询速度的关键因素有两点:TOP和NOT IN。TOP可以提高我们的查询速度,而NOT IN会减慢我们的查询速度,所以要提高我们整个分页算法的速度,就要彻底改造NOT IN,同其他方法来替代它。

我们知道,几乎任何字段,我们都可以通过max(字段)或min(字段)来提取某个字段中的最大或最小值,所以如果这个字段不重复,那么就可以利用这些不重复的字段的max或min作为分水岭,使其成为分页算法中分开每页的参照物。在这里,我们可以用操作符“>”或“<”号来完成这个使命,使查询语句符合SARG形式。如:

Select top 10 * from table1 where id>200\n\n--于是就有了如下分页方案:\n\nselect top 页大小 *\n\nfrom table1\n\nwhere id>\n\n(select max (id) from\n\n(select top ((页码-1)*页大小) id from table1 order by id) as T\n\n)\n\norder by id

在选择即不重复值,又容易分辨大小的列时,我们通常会选择主键。下表列出了笔者用有着1000万数据的办公自动化系统中的表,在以GID(GID是主键,但并不是聚集索引。)为排序列、提取gid,fariqi,title字段,分别以第1、10、100、500、1000、1万、10万、25万、50万页为例,测试以上三种分页方案的执行速度:(单位:毫秒)

页码

方案1

方案2

方案3

1

60

30

76

10

46

16

63

100

1076

720

130

500

540

12943

83

1000

17110

470

250

10000

24796

4500

140

100000

38326

42283

1553

250000

28140

128720

2330

500000

121686

127846

7168

从上表中,我们可以看出,三种存储过程在执行100页以下的分页命令时,都是可以信任的,速度都很好。但第一种方案在执行分页1000页以上后,速度就降了下来。第二种方案大约是在执行分页1万页以上后速度开始降了下来。而第三种方案却始终没有大的降势,后劲仍然很足。

在确定了第三种分页方案后,我们可以据此写一个存储过程。大家知道SQL SERVER的存储过程是事先编译好的SQL语句,它的执行效率要比通过WEB页面传来的SQL语句的执行效率要高。下面的存储过程不仅含有分页方案,还会根据页面传来的参数来确定是否进行数据总数统计。

--获取指定页的数据:\n\nCREATE PROCEDURE pagination3\n\n@tblName varchar(255), -- 表名\n\n@strGetFields varchar(1000) = ''*'', -- 需要返回的列\n\n@fldName varchar(255)='''', -- 排序的字段名\n\n@PageSize int = 10, -- 页尺寸\n\n@PageIndex int = 1, -- 页码\n\n@doCount bit = 0, -- 返回记录总数, 非 0 值则返回\n\n@OrderType bit = 0, -- 设置排序类型, 非 0 值则降序\n\n@strWhere varchar(1500) = '''' -- 查询条件 (注意: 不要加 where)\n\nAS\n\n \n\ndeclare @strSQL varchar(5000) -- 主语句\n\ndeclare @strTmp varchar(110) -- 临时变量\n\ndeclare @strOrder varchar(400) -- 排序类型\n\n \n\nif @doCount != 0\n\nbegin\n\nif @strWhere !=''''\n\nset @strSQL = "select count(*) as Total from [" + @tblName + "] where "+@strWhere\n\nelse\n\nset @strSQL = "select count(*) as Total from [" + @tblName + "]"\n\nend\n\n--以上代码的意思是如果@doCount传递过来的不是0,就执行总数统计。以下的所有代码都是@doCount为0的情况:\n\nelse\n\nbegin\n\nif @OrderType != 0\n\nbegin\n\nset @strTmp = "<(select min"\n\nset @strOrder = " order by [" + @fldName +"] desc"\n\n--如果@OrderType不是0,就执行降序,这句很重要!\n\nend\n\nelse\n\nbegin\n\nset @strTmp = ">(select max"\n\nset @strOrder = " order by [" + @fldName +"] asc"\n\nend\n\n \n\nif @PageIndex = 1\n\nbegin\n\nif @strWhere != ''''\n\n \n\nset @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "\n\n        from [" + @tblName + "] where " + @strWhere + " " + @strOrder\n\nelse\n\n \n\nset @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "\n\n        from ["+ @tblName + "] "+ @strOrder\n\n--如果是第一页就执行以上代码,这样会加快执行速度\n\nend\n\nelse\n\nbegin\n\n--以下代码赋予了@strSQL以真正执行的SQL代码 \n\nset @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from ["\n\n+ @tblName + "] where [" + @fldName + "]" + @strTmp + "(["+ @fldName + "])\n\n      from (select top " + str((@PageIndex-1)*@PageSize) + " ["+ @fldName + "]\n\n      from [" + @tblName + "]" + @strOrder + ") as tblTmp)"+ @strOrder\n\n \n\nif @strWhere != ''''\n\nset @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ " from ["\n\n+ @tblName + "] where [" + @fldName + "]" + @strTmp + "(["\n\n+ @fldName + "]) from (select top " + str((@PageIndex-1)*@PageSize) +" ["\n\n+ @fldName + "] from [" + @tblName + "] where " + @strWhere + " "\n\n+ @strOrder + ") as tblTmp) and " + @strWhere + " " + @strOrder\n\nend\n\n \n\nend\n\n \n\nexec (@strSQL)\n\n \n\nGO

上面的这个存储过程是一个通用的存储过程,其注释已写在其中了。在大数据量的情况下,特别是在查询最后几页的时候,查询时间一般不会超过9秒;而用其他存储过程,在实践中就会导致超时,所以这个存储过程非常适用于大容量数据库的查询。笔者希望能够通过对以上存储过程的解析,能给大家带来一定的启示,并给工作带来一定的效率提升,同时希望同行提出更优秀的实时数据分页算法。

)聚集索引的重要性和如何选择聚集索引

在上一节的标题中,笔者写的是:实现小数据量和海量数据的通用分页显示存储过程。这是因为在将本存储过程应用于“办公自动化”系统的实践中时,笔者发现这第三种存储过程在小数据量的情况下,有如下现象:

1、分页速度一般维持在1秒和3秒之间。

2、在查询最后一页时,速度一般为5秒至8秒,哪怕分页总数只有3页或30万页。

虽然在超大容量情况下,这个分页的实现过程是很快的,但在分前几页时,这个1-3秒的速度比起第一种甚至没有经过优化的分页方法速度还要慢,借用户的话说就是“还没有ACCESS数据库速度快”,这个认识足以导致用户放弃使用您开发的系统。

笔者就此分析了一下,原来产生这种现象的症结是如此的简单,但又如此的重要:排序的字段不是聚集索引!

本篇文章的题目是:“查询优化及分页算法方案”。笔者只所以把“查询优化”和“分页算法”这两个联系不是很大的论题放在一起,就是因为二者都需要一个非常重要的东西――聚集索引。

在前面的讨论中我们已经提到了,聚集索引有两个最大的优势:

1、以最快的速度缩小查询范围。

2、以最快的速度进行字段排序。

1条多用在查询优化时,而第2条多用在进行分页时的数据排序。

而聚集索引在每个表内又只能建立一个,这使得聚集索引显得更加的重要。聚集索引的挑选可以说是实现“查询优化”和“高效分页”的最关键因素。

但要既使聚集索引列既符合查询列的需要,又符合排序列的需要,这通常是一个矛盾。笔者前面“索引”的讨论中,将fariqi,即用户发文日期作为了聚集索引的起始列,日期的精确度为“日”。这种作法的优点,前面已经提到了,在进行划时间段的快速查询中,比用ID主键列有很大的优势。

但在分页时,由于这个聚集索引列存在着重复记录,所以无法使用max或min来最为分页的参照物,进而无法实现更为高效的排序。而如果将ID主键列作为聚集索引,那么聚集索引除了用以排序之外,没有任何用处,实际上是浪费了聚集索引这个宝贵的资源。

为解决这个矛盾,笔者后来又添加了一个日期列,其默认值为getdate()。用户在写入记录时,这个列自动写入当时的时间,时间精确到毫秒。即使这样,为了避免可能性很小的重合,还要在此列上创建UNIQUE约束。将此日期列作为聚集索引列。

有了这个时间型聚集索引列之后,用户就既可以用这个列查找用户在插入数据时的某个时间段的查询,又可以作为唯一列来实现max或min,成为分页算法的参照物。

经过这样的优化,笔者发现,无论是大数据量的情况下还是小数据量的情况下,分页速度一般都是几十毫秒,甚至0毫秒。而用日期段缩小范围的查询速度比原来也没有任何迟钝。聚集索引是如此的重要和珍贵,所以笔者总结了一下,一定要将聚集索引建立在:

1、您最频繁使用的、用以缩小查询范围的字段上;

2、您最频繁使用的、需要排序的字段上。

结束语

本篇文章汇集了笔者近段在使用数据库方面的心得,是在做“办公自动化”系统时实践经验的积累。希望这篇文章不仅能够给大家的工作带来一定的帮助,也希望能让大家能够体会到分析问题的方法;最重要的是,希望这篇文章能够抛砖引玉,掀起大家的学习和讨论的兴趣,以共同促进,共同为公安科技强警事业和金盾工程做出自己最大的努力。

最后需要说明的是,在试验中,我发现用户在进行大数据量查询的时候,对数据库速度影响最大的不是内存大小,而是CPU。在我的P4 2.4机器上试验的时候,查看“资源管理器”,CPU经常出现持续到100%的现象,而内存用量却并没有改变或者说没有大的改变。即使在我们的HP ML 350 G3服务器上试验时,CPU峰值也能达到90%,一般持续在70%左右。

本文的试验数据都是来自我们的HP ML 350服务器。服务器配置:双Inter Xeon 超线程 CPU 2.4G,内存1G,操作系统Windows Server 2003 Enterprise Edition,数据库SQL Server 2000 SP3

(完)

有索引情况下,insert速度一定有影响,不过:
1. 你不大可能一该不停地进行insert, SQL Server能把你传来的命令缓存起来,依次执行,不会漏掉任何一个insert。
2. 你也可以建立一个相同结构但不做索引的表,insert数据先插入到这个表里,当这个表中行数达到一定行数再用insert table1 select * from table2这样的命令整批插入到有索引的那个表里。

 

注:文章来源与网络,仅供读者参考!

人是有思想的,这是人与动物本质的区别。人的社会属性要求我们在操守的规范下实现自我价值,越有这越给予。因此,我们要实现自己的社会价值 。这些都离不开坚定的信仰,有无信仰是一个在精神层面状态好坏的体现,不能觉得一切都无所谓。生活是一面镜子,自己是什么样子很快现行。 用知识武装自己,用信仰升华自己,用爱好装点自己,用个性标识自己。 我就是我,不一样的烟火;我就是我,不一样的水果;我就是我,不一样的花朵;我就是我,不一样的自我。 生活寄语:越努力,越幸运。 做最好的自己!


", "summary": "深入浅出理解索引结构,何时使用聚集索引或非聚集索引,结合实际,谈索引使用的误区", "author": "", "source": "cnblogs", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-02-09 12:25:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 698, "guid": "49f06a66-2358-4f7a-a342-1e60f3c905a7", "createdDate": "2022-02-09 12:29:00", "lastModifiedDate": "2022-02-09 14:03:02" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 51, "groupNames": [], "tagNames": [ "代码重用", "程序员" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "代码重用是什么,怎样更能使程序员受益?", "subTitle": "", "seoTitle": null, "imageUrl": "@upload/images/2022/2/3d9b6e017b493953.png", "videoUrl": "", "fileUrl": "", "keywords": "代码重用", "description": "现代应用程序要成功,准确和速度是两个必要优势。全球消费者想要的产品要体现它的价值,企业为了保持竞争力,创新势在必行。", "body": "
\"图片\"

作者丨Code Reuse
译者丨朱钢
审校丨孙淑娟、梁策

现代应用程序要成功,准确和速度是两个必要优势。全球消费者想要的产品要体现它的价值,企业为了保持竞争力,创新势在必行。

对于软件开发人员而言,代码重用有助于简化和加快软件生产,并解决与业务相关的技术挑战。要有效地重用代码,就必须对它非常了解。

在这篇文章中,我将向你展示有关代码重用的全部。从它的定义、好处,到什么时候最好不用以及不用的原因,为你提供优化工作流程所需的所有信息。

什么是代码重用,何时重用?

代码重用也称为软件重用。顾名思义,它是对现有(已经写好的)代码进行重用,这些代码来自外部资源或过往项目,并用之开发新软件。

程序员这样做是为了实现相同或相似的功能。然而,只有高质量的代码(无错或不复杂)才能被重用。这很容易理解,如果代码有缺陷,也就不可靠不安全。

开发快速、可靠和安全的软件始终需要大量编程技能和知识。因此,开发人员需要先分析应用程序的条件和要求,然后再进行代码重用。总的来看,代码重用的有利条件包括:

  • 代码转移到不同的硬件。

  • 代码没有影响应用程序长期安全性的缺陷。

  • 代码可以轻松在新应用程序中扩展和接受。

鉴于以上条件,现在让我们看看代码重用的好处。

代码重用的好处

重用代码优点很多,并和速度以及质量有关。具体来看:

1、大幅缩短整体开发时间

企业旨在快速开发软件,因为这可以缩短上市时间,让业务受益。因此,如果希望在市场上获得“早鸟优势”并对同行保持领先,快速开发应用程序对于企业来说至关重要。

通过代码重用,程序员在开发新应用程序时不必从头开始。由于他们可以在不同应用程序中使用相同代码来实现类似功能,这大大减少了整体应用程序的开发时间。

此外,Python Package Index 和 GitHub 等工具可帮助开发人员查找新软件可用的现有代码。由于基础编码额外节省了时间,开发人员可以有时间编写新的、特有的代码,为产品增加更多的价值。

2、降低成本,改进产品

除了减少整体应用开发时间外,代码重用还有助于保障应用开发预算。由于开发人员可以使用已有代码,企业不再需要使用额外资源,从而控制应用程序开发成本。

此外,如果需要,人们可以将重复编写相同代码节省的时间投入到编写特有代码中,以提高产品质量。对高质量可重用的代码的唯一要求就是安全可靠。

3、增强用户体验

在提升用户体验和提高用户保留率方面,代码重用可以发挥重要作用。由于重用的代码安全可靠,因此将为应用程序的功能增加更多价值。此外,由于代码少了,出错的几率也会降低。

此外,使用代码更少,应用程序就可能更简单易懂。用户体验因此提升,会带来更多的客户参与和二次访问,因此又有助于实现商业软件目的。

4、避免代码臃肿

高效、系统化的代码重用有助于避免代码臃肿问题,即计算机指令和源代码过多的情况。任何代码过长且浪费大量资源的情况都被视为臃肿。

应用程序开发过程中,有效利用资源是重中之重。因此,其关键是将代码作为单个组件在所有系统之间系统地共享,以避免代码中出现不需要的功能。

何时避免代码重用?

代码重用并不适合所有项目。因此开发人员有必要先评估重用条件,然后再进行代码重用。

一般来说,如果开发人员使用的代码只是部分,那么最好完全避免代码重用,因为在这种情况下,它可能会导致质量问题,从而带来反效果:浪费时间并造成产品缺陷。

在这种情况下,最好先确认应用的基本功能,即了解代码的功能,然后再创建新代码来执行所需行为。

代码重用的缺点

除了高大上的优势外,代码重用也有一些缺点。企业也应该明白,代码重用的弊端往往要看当前情况。例如,第三方库可能较弱,但与自定义库相比,它减少了编程时间。

以下是代码重用的一些缺点:

1、性能较差

框架或库的性能取决于两个重要因素:

  • 编程语言

  • 平台

因此,在某些情况下,框架的工作速度可能比预期要慢,这可能会妨碍应用的整体性能。所以在这种情况下,建议构建一个专门的解决方案,而并非一个公共库。

此外,如果你在本地系统中工作,在整个系统上访问 API 有时可能慢于解决问题。除了 API 会减慢系统速度之外,模块化系统也容易产生瓶颈。

2、无法控制第三方解决方案

长远来看,缺乏对第三方解决方案的控制,可能会产生负面影响。这可能产生如下技术问题:没有进行足够的安全测试。添加所需功能增强时出现问题。

除了技术问题外,还可能存在责任和许可问题。重用代码在安全上不如新编写的符合安全标准的代码。

代码重用的挑战

代码重用所涉及的挑战既是操作性的,也是技术性的。我们将在下面详细介绍这两种挑战。

1、操作性挑战

在开发可重用代码时,项目经理需要在项目本身开始时添加额外资源。因此项目经理必须同时考虑长期和短期计划。

为了获得短期利益,项目经理必须快速设计出满足客户期望的软件;为了长期利益,他们必须检阅适当的文档、设计和代码质量。

此外,对于有计划的代码重用,开发人员需要额外时间来编写代码文档。为此,他们需要对代码彻底测试。由于需要额外的时间,因此如果开发人员的时间要求紧张,他们可能很难在最后期限前完成。

2、技术性挑战

开发人员需要确保代码的效率和可靠性才能重用它,可以通过提高内存、利用率和响应时间以及监控处理器来提高代码效率。除了代码高效之外,它还需要可维护。确保其合规性是检查代码是否可维护的一种简单而有效的方法。

代码重用最佳实践

代码重用有很多好处。但是,如果开发人员不实施最佳实践,他们就无法最大化收益。为此,我列出了一些代码重用的最佳实践。

1、监视代码重用中的外部组件

对于程序员来说,管理代码重用中的补丁总是很重要的。监控外部组件意味着只要检测到漏洞,开发人员就会收到通知。此外,监控还允许开发人员在任何恶意软件袭击之前修复问题。

因此,请确保你有一个文档化的程序来说明如何及时实施补丁。

2、仅从可信来源重用代码

可重用代码必须始终来自具有大量活动用户的真实库。如果使用该库,开发人员就可以更好地处理暴露出来的漏洞。

另一方面,如果你使用不可靠的源代码,它可能会损害产品和企业声誉。

3、培训开发人员

开发过程中的重大变化可能来自思维方式的转变。有必要培训开发人员有关安全的重要性。因此,凭借适当的知识和理解,他们在选择可重用代码时将做出更好的决策。

此外,代码安全方面的基础培训将使开发人员能够创建更安全的代码。

4、适当的文档

软件供应链文档是一项非常重要的实践,这有助于最大限度地发挥代码重用的好处。此外,因为涉及许多第三方组件,适当的文档对于大型企业来说至关重要,否则,很有可能会忘记代码在哪里被重用了。

5、重用代码协助优化

正如我于文中所示,重用代码可以减少开发时间,优化流程,确保你拥有强大的产品,甚至保持在预算限制范围内。

当然,并非所有情况都支持代码重用。正如我们所看到的,有时你就是需要简单创建些代码段用于抓取。此外,代码重用也有一些缺点,对你可能适用,也可能不适用。

虽然最终决定取决于你和你的个人需求,但依照我分享的最佳实践可以帮助你了解何时重用代码以及如何重用代码,从而提高工作效率。


", "summary": "现代应用程序要成功,准确和速度是两个必要优势。全球消费者想要的产品要体现它的价值,企业为了保持竞争力,创新势在必行。", "author": "Code Reuse", "source": " 51CTO", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-02-08 09:21:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 681, "guid": "0ca3eb55-ac92-4b9e-b35d-aea12f0bd93f", "createdDate": "2022-02-08 09:22:37", "lastModifiedDate": "2022-02-08 09:22:37" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 50, "groupNames": [], "tagNames": [ "C#" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "C# 客户端程序调用外部程序的3种实现方法", "subTitle": "", "seoTitle": null, "imageUrl": "", "videoUrl": "", "fileUrl": "", "keywords": "C#", "description": "C# 客户端程序调用外部程序的3种实现方法", "body": "

前言

文章主要给大家介绍关于 C# 客户端程序调用外部程序的 3 种实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值。

简介

大家都知道,当我们用 C# 来开发客户端程序的时候,总会不可避免的需要调用外部程序或者访问网站,本文介绍了三种调用外部应用的方法,供参考,下面话不多说了,来一起看看详细的介绍吧。

实现

第一种利用 shell32.dll

实现 ShellExecute 方法,该方法可同时打开本地程序、文件夹或者访问网站,只要直接输入路径字符串即可, 如 C:\\Users\\Desktop\\xx.exe 或者 https://cn.bing.com/,可以根据返回值判断是否调用成功 (成功0x00000002a , 失败0x00000002)

Window wnd = Window.GetWindow(this); //获取当前窗口
var wih = new WindowInteropHelper(wnd); //该类支持获取hWnd
IntPtr hWnd = wih.Handle;    //获取窗口句柄
var result = ShellExecute(hWnd, "open""需要打开的路径如C:\\Users\\Desktop\\xx.exe"nullnull, (int)ShowWindowCommands.SW_SHOW);
[DllImport("shell32.dll")]
public static extern IntPtr ShellExecute(IntPtr hwnd, //窗口句柄
 string lpOperation, //指定要进行的操作
 string lpFile,  //要执行的程序、要浏览的文件夹或者网址
 string lpParameters, //若lpFile参数是一个可执行程序,则此参数指定命令行参数
 string lpDirectory, //指定默认目录
 int nShowCmd   //若lpFile参数是一个可执行程序,则此参数指定程序窗口的初始显示方式(参考如下枚举)
 )
;
public enum ShowWindowCommands : int
{
 SW_HIDE = 0,
 SW_SHOWNORMAL = 1,
 SW_NORMAL = 1,
 SW_SHOWMINIMIZED = 2,
 SW_SHOWMAXIMIZED = 3,
 SW_MAXIMIZE = 3,
 SW_SHOWNOACTIVATE = 4,
 SW_SHOW = 5,  //显示一个窗口,同时令其进入活动状态
 SW_MINIMIZE = 6,
 SW_SHOWMINNOACTIVE = 7,
 SW_SHOWNA = 8,
 SW_RESTORE = 9,
 SW_SHOWDEFAULT = 10,
 SW_MAX = 10
}

第二种是利用 kernel32.dll

实现 WinExec 方法,该方法仅能打开本地程序,可以根据返回值判断是否调用成功(<32表示出现错误)

var result = WinExec(pathStr, (int)ShowWindowCommands.SW_SHOW);
[DllImport("kernel32.dll")]
public static extern int WinExec(string programPath, int operType);

第三种方法是利用 Process 类

Process 类具体应用可以看类的定义,这里只实现它打开文件和访问网站的用法,调用失败会抛出异常

/// <devdoc>
/// <para>
///  Provides access to local and remote
///  processes. Enables you to start and stop system processes.
/// </para>
/// </devdoc>

具体实现为

//调用程序  
Process process = new Process();
try
 {
  process.StartInfo.UseShellExecute = false;
  process.StartInfo.FileName = pathStr;
  process.StartInfo.CreateNoWindow = true;
  process.Start();
 }
  catch (Exception ex)
 {
  MessageBox.Show(ex.Message);
 }
//访问网站
try
{
  Process.Start("iexplore.exe", pathStr);
}
  catch (Exception ex)
{
  MessageBox.Show(ex.Message);
}

- EOF -


", "summary": "文章主要给大家介绍关于 C# 客户端程序调用外部程序的 3 种实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值。", "author": "", "source": "DotNet", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-01-30 12:29:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 671, "guid": "49b1ef2d-80c2-459a-8246-d56d5e08dadf", "createdDate": "2022-01-30 12:30:40", "lastModifiedDate": "2022-01-30 12:30:40" }, { "imageUrlCount": 0, "videoUrlCount": 0, "fileUrlCount": 0, "channelId": 105, "siteId": 1, "adminId": 2, "lastEditAdminId": 2, "userId": 0, "taxis": 49, "groupNames": [], "tagNames": [ "前端", "性能优化" ], "sourceId": 0, "referenceId": 0, "templateId": 0, "checked": true, "checkedLevel": 0, "hits": 0, "hitsByDay": 0, "hitsByWeek": 0, "hitsByMonth": 0, "lastHitsDate": null, "downloads": 0, "title": "Web前端性能优化深度解读", "subTitle": "", "seoTitle": null, "imageUrl": "@upload/images/2022/1/1dfddb910089530a.png", "videoUrl": "", "fileUrl": "", "keywords": "前端", "description": "Web前端性能优化深度解读", "body": "

导读: 用户体验是web产品非常重要的部分,核心是让用户使用舒服,帮助用户流畅地得到所求,用户体验的优劣甚至会影响到用户的留存。体验差的网站各有各的不同,但是体验好的网站往往都有一些共性,这些优秀的特征凝结了设计师、研发工程师和产品经理的大量智慧。

  • 访问交互速度迅速

  • 动画效果顺滑流畅

  • 有用户操作的反馈

  • 简单的操作步骤

  • 整站体验一致性

  • 主体内容在最显眼的位置

  • 无障碍访问,不同的人群均可使用

在这些优秀体验的特性中,最容易让人产生共鸣的往往是网站的性能问题,比如网站的访问交互速度。如何发现性能问题?性能如何优化(性能优化的常规方法和框架方法)?如何衡量收益?本文根据多年在性能优化方面的实践,着重分享一下首屏性能优化的一些经验。

01 性能采集

工欲利器事,必先利其器。我们所说的性能采集并不是性能分析Devtools,而是指在产品真实用户访问的大数据中进行抽样,对于抽样用户进行性能数据采集,得到真实用户环境下产品性能数据。各浏览器厂商都已认识到性能对于web开发的重要性,为了解决当前性能测试的困难,W3C推出了一套性能API标准,目的是简化开发者对网站性能进行精确分析与控制的过程,方便开发者采取手段提高web性能。整套标准包含了10余种API,在下图中可以看到它们当前在规范流程中的进展。

图:性能API标准(摘录51CTO图片)

这套标准中提供了导航定时(Navigation Timing)、资源定时(Resource Timing)、用户定时User Timing和性能时间线(Performance Timeline)规范可以帮助开发人员精确地测量文档的导航时间,在页面上获取资源的情况,以及开发人员脚本执行情况。

在这套API中,页面加载Navigation Timing和页面资源加载Resource Timing这两个API可以帮助我们获取页面的Domready时间、onload时间、白屏时间以及单个页面资源在从发送请求到获取到response各阶段例如带宽、延迟或主页的整体页面加载时间的性能参数,这些都是基于真实用户数据(RUM)。

图:Navigation Timing关系图(摘录W3C)

在获取用户访问Timing数据的前提下,我们可以结合具体业务场景定义访问性能的核心指标,例如白屏时间、首屏时间FSP、用户可交互时间TTI、页面onload时间等作为核心优化指标,其中首屏时间和用户可交互时间需要单独埋点自定义。

还可以通过获取DNS查询耗时、TCP链接耗时、request请求耗时、解析dom树耗时、白屏时间、domready时间、onload时间等做性能分析,后续根据症状对这些细致阶段做性能优化,这些参数是通过上面的performance.timing各个属性的差值组成的。

通过使用API对各个阶段性能指标进行采集,等待到所有数据都获取完成之后,通过网络请求将数据发送到服务器用作后续数据分析使用。

02 性能优化

快速加载、及时响应用户反馈、提供流畅的动画、以及拥有类似原生APP一般沉浸的用户体验是web应用在性能优化上的目标,这主要关系到加载性能和渲染性能两个方面,本章节介绍一些常规优化方法和框架级优化方案。

2.1 加载性能优化

Web 页面通常由 HTML、CSS、JavaScript 和其他多媒体资源组成,充斥着各种同步资源和异步资源。页面加载时,必须从服务器获取这些资源。

2.1.1 减小资源体积

  • 压缩文本内容

  • 优化JavaScript第三方库引入

压缩虽然简单,但十分有效,这也是最广泛的优化资源体积的操作。许多工具可以帮助我们完成HTML、CSS、JavaScript、图片等压缩。例如,TerserPlugin可以用于压缩 JavaScript,PostCSS可以对 CSS 进行压缩,以及完成前缀自动补全工作。除了压缩单个文件外,在服务器上配置 Gzip 也十分重要。Gzip 对文本资源的压缩效果非常明显,通常可以将体积再压缩至原本的 30% 左右,但 Gzip 对已经单独压缩的图像等非文本资源来说,效果并不好。

如果我们只需要使用工具库中少数几个简单函数,可以考虑使用原生 JavaScript 代替。不计后果地引入第三方库,会迅速增大 JavaScript 资源的体积。

2.1.2 对资源进行缓存

缓存在优化页面加载性能的工作中有举足轻重的作用,缓存无处不在,包括浏览器端、网络代理、服务端缓存,往往能大幅加快响应速度。

图:web全链路缓存

  • HTTP 缓存

  • Local Storage

  • Cache Storage

  • IndexedDB

  • CDN

现代浏览器都实现了 HTTP 缓存机制。浏览器在初次获取资源后,会根据 HTTP 响应头部的Cache-Control和ETag字段,来决定该资源的强缓存策略或者协商缓存策略。

Local Storage主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小。

Cache Storage它用来存储 Response 对象的,也就是说用来对 HTTP响应做缓存的,通常在PWA技术中使用。

IndexedDB是一种在浏览器中持久存储数据的方法,允许我们不考虑网络可用性,创建具有丰富查询能力的可离线web应用程序。

内容缓存在CDN网络节点,位于用户接入点,是面向最终用户的内容提供设备,可缓存静态Web内容和流媒体内容,实现内容的边缘传播和存储,以便用户的就近访问。

2.1.3 调整资源优先级

通过调整资源加载优先级,保证主体内容能够较快的被加载完成,通过预加载、懒加载等多种方式,调整资源加载的行为,优化网页加载性能。

  • 预加载

  • 预连接与 DNS 预解析

  • 预取

  • 懒加载

  • Service Worker

通过来提前声明当前页面所需的资源,以便浏览器能预加载这些资源。通过media属性进行媒体查询,根据响应式的情况选择性地预加载资源。

预连接会提前完成 DNS 解析、TCP 握手和 TLS 协商的工作,但并不会提前加载资源。也可以考虑使用,提前与资源建立 socket 连接。

浏览器会在空闲时,使用最低优先级下载预取的资源。预取通过声明,通常用于点击“下一页”的页面动作之前提前加载用户接下来可能需要的html资源。

按需加载和延时加载都属于懒加载的范畴,例如对图像资源采用“懒加载”策略,即仅加载当前在视口内的图像,对于视口外未加载的图像,在其即将滚动进入视口时才开始加载。

利用Service Worker 线程脱离在主线程之外来进行 Web 资源和请求的持久离线缓存。

2.1.4 合理拆分代码

浏览器支持并行加载资源,合理拆分资源也是一种有效的优化方法。为了更好的效果,我们往往不需要在首屏一次性加载所有 JavaScript 代码,合理的拆分代码、区分开发和生产环境使用少量主要代码,将当前暂时不需要的代码拆分出去可以有效加快首屏展现的速度。通过webpack区分开发环境和生产环境差异化配置打包资源可以有效优化代码,Tree shaking使得模块间依赖可以通过静态分析来更好地优化剪枝(仅ES modules支持)。webpack-bundle-analyzer 是一个关于 webpack 构建产物的可视化插件,可以清晰地看到构建产物的体积,帮助分析后续的优化方向。

2.1.5 HTTP/2

HTTP/2带给WEB带来了很大的性能提升,同时多路复用、头部压缩、Server Push等特点,使得可以在一个连接上同时打开多个流双向传输数据,服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。

图:http1 vs http2

2.2 渲染性能优化

浏览器在渲染页面前,首先会将 HTML 文本内容解析为 DOM,将 CSS 解析为 CSSOM。DOM 和 CSSOM 都是树状数据结构,两者相互独立,但又有相似之处。接着,浏览器会将 DOM 和 CSSOM 树合并成渲染树。从 DOM 树的根节点开始遍历,并在 CSSOM 树中查找节点对应的样式规则,合并成渲染树中的节点。在遍历的过程中,不可见的节点将会被忽略。渲染树随后会被用于布局,就是计算渲染树节点在浏览器视口中确切的位置和大小。浏览器进行一次布局的性能开销较大,我们需要小心地避免频繁触发页面重新布局。得到渲染树节点的几何布局信息后,浏览器就可以将节点绘制到屏幕上了,包括绘制文本、颜色、边框和阴影等。

绘制的过程,首先会根据布局和视觉相关的样式信息生成一系列绘制操作,随后执行栅格化(栅格化是将向量图形格式表示的图像转换成位图以用于显示器或者打印机输出的过程),将待绘制项转换为位图存储在 GPU 中,最终通过图形库将像素绘制在屏幕上。

图:浏览器渲染过程

页面不是一次性被绘制出来的。实际上,页面被分成了多个图层进行绘制,这些图层会在另一个单独的线程里绘制到屏幕上,这个过程被称作合成。合成线程可以对图层进行剪切、变换等处理,因此可以用于响应用户基本的滚动、缩放等操作,又不会受到主线程阻塞的影响。

2.2.1 关键渲染路径

由于渲染都是在主进程中执行的,所以合理的利用主进程渲染非常重要。首屏渲染所必须的关键资源,共同组成了关键渲染路径,减少非关键渲染路径的资源消耗可以有效提升渲染速度。

  • 延迟非关键 CSS 加载

  • async 和 defer

Web 应用中往往会有一些首屏渲染时用不到的 CSS,如弹框的样式等。通过引用的 CSS 都会在加载时阻塞页面渲染。为了使这些非关键 CSS 不阻塞页面渲染,可以通过拆分资源的方式并延迟非关键资源加载。

由于渲染都是在主进程中执行的,所以合理的利用主进程渲染非常重要。首屏渲染所必须的关键资源,共同组成了关键渲染路径,减少非关键渲染路径的资源消耗可以有效提升渲染速度。

2.2.2 非阻塞 JavaScript

用户对于不流畅的滚动或动画十分敏感,一般要求页面帧率应达到每秒 60 帧。由于 JavaScript 一般是单线程执行的,长时间执行的任务会阻塞浏览器的主线程,使页面失去响应,出现卡顿和假死的现象。

  • 页面滚动

  • requestAnimationFrame 任务在浏览器渲染下一帧之前执行

  • requestIdleCallback 将任务安排在浏览器空闲时执行

  • Web Workers

当我们监听 touchstart、touchmove 等事件时,由于合成线程并不知道我们是否会通过 event.preventDefault() 来阻止默认的滚动行为,从而在每次事件触发时,都会等待事件处理函数执行完毕后再进行页面滚动。这通常会导致较明显的延迟,影响页面滚动的流畅性。通过在addEventListener()时声明{passive: true},来表明事件处理函数不会阻止页面滚动,使得用户的操作更快得到响应。

我们可以将一些耗性能的逻辑放在 worker 线程中进行处理,这样主线程就能继续响应用户操作和渲染页面了。

2.2.3 降低渲染树计算复杂性

结构越复杂的页面往往性能越差,动画多的页面出现卡顿的几率也越大。

  • 减少查找与元素匹配成本

  • 减少布局次数

  • 优化绘制与合成

渲染树由 DOM 和 CSSOM 树合并而成,对于每个 DOM 元素,需要查找与元素匹配的样式规则。CSS Modules 是一种较为主流的 CSS-in-JS 解决方案,利用 webpack 等构建工具,可以对类选择器生成自定义格式的唯一类名,同样能减少浏览器匹配 CSS 选择器的开销。

浏览器进行一次布局的开销很大,所以我们需要尽可能避免直接修改这些属性,尤其是不应将布局属性用于动画效果,否则会出现明显的掉帧现象。

修改绝大多数样式属性都会导致页面重绘,这很难避免。仅有的例外是transform和opacity,这是由于它们可以仅由合成器操作图层来实现。transform和opacity非常适合用于实现动画效果,但我们仍需要通过will-change为它们创建独立的图层,避免影响其他图层的绘制。

2.3 框架优化方法

CSR、SSR、NSR、ESR、hybrid离线包、Big pipe、app cache等,都是不错的方法。

2.3.1 CSR(Client Side Render)

浏览器渲染顾名思义就是所有的页面渲染、逻辑处理、页面路由、接口请求均是在浏览器中发生,也就是从服务端请求一个简单HTML文件然后通过执行JavaScript在HTML上进行内容的添加。其实,现代主流的前端框架均是这种渲染方式,这种渲染方式的好处在于实现了前后端架构分离,利于前后端职责分离,并且能够首次渲染迅速有效减少白屏时间。同时,CSR可以通过在打包编译阶段进行预渲染或者骨架屏生成,可以进一步提升首次渲染的用户体验。

图:CSR

2.3.2 SSR(Server Side Render)

服务端渲染则是在服务端完成页面的渲染,在服务端完成页面模板、数据填充、页面渲染,然后将完整的HTML内容返回给到浏览器。由于所有的渲染工作都在服务端完成,因此网站的首屏时间和TTI都会表现比较好。

图:SSR

但是,渲染需要在服务端完成,并不能很好进行前后端职责分离,而且白屏时间也会比较长,同时,对于服务端的负载要求也会比较高。

2.3.3 NSR(Native Side Render)

GMTC2019 全球大前端技术上 UC 团队提到了 0.3 秒的 “闪开” 方案。这种方案适用于混合开发,NSR本质是分布式SSR,通过加载离线页面模板,Ajax预加载页面数据,Native渲染生成Html数据并且缓存在客户端,将服务器的渲染工作放在了一个个独立的移动设备中,实现了页面的预加载,同时又不会增加额外的服务器压力。核心思路是借助浏览器启用一个 JS-Runtime,提前将下载好的 html 模板及预取的 feed 流数据进行渲染,然后将 html 设置到内存级别的 MemoryCache 中,从而达到点开即看的效果。

图:NSR

2.3.4 ESR(Edge Side Render)

边缘渲染的核心思想是,借助边缘计算的能力,将静态内容与动态内容以流式的方式,先后返回给用户。CDN 节点相比于Server距离用户更近,有着更短的网络延时。在 CDN 节点上将可缓存的页面静态部分先快速返回给用户,同时在 CDN 节点上发起动态部分内容请求,并将动态内容在静态部分的响应流后继续返回给用户。

图:ESR

03、收益衡量

速度是应用性能最直接体现。做性能收益衡量也需要多维度全方位的进行分析与对比。通过等量实验组和对照组在核心指标方面大量真实数据的分位值对比,可以得到性能方面的收益,也可以关联到用户PV、UV以及收入等方面是数据收益。

监控网站真实用户可感知的白屏、首屏、可交互等用户体验指标,从服务器端响应时间、网络延时、DOM解析等细致指标的变化也可以做日常性能优化。

  • 统计核心指标不同分位数的占比数据。

  • 统计不同版本浏览器和设备类型的核心指标数据,基于多平台浏览器性能分析。

  • 统计不同区域(包括国家、省份、城市)、不同运营商以及接入方式(包括2G/3G/4G/WiFi)下的各关键网络性能指标。

图:性能平台

业内不错的性能监控平台包括ONEAPM、听云、性能魔方等,各个大公司和云平台也都提供不错的相关监控服务。

04 总结

你做事的时候不只是靠经验教训的历史积累,还有一套系统的流程或者模板。做性能优化是一件需要具有闭环思维的事情,特别是这种端到端的优化要注意事前规划、事中执行和事后总结三个阶段,而且还要结合不同的业务场景进行优化,有时候还要与客户端相协同,并不是生拉硬套就可以完成的事情。

甚至很多大厂的业务前端还要一边解决历史包袱,一边进行优化,小心前行!随着优化后业务仍然在不断的迭代和发展,如何巩固性能优化结果也是一件任重道远持续投入的事情,掌握性能优化基本原理结合具有优秀性能结构设计或许是一种智慧的方法。


", "summary": "导读: 用户体验是web产品非常重要的部分,核心是让用户使用舒服,帮助用户流畅地得到所求,用户体验的优劣甚至会影响到用户的留存。体验差的网站各有各的不同,但是体验好的网站往往都有一些共性,这些优秀的特征凝结了设计师、研发工程师和产品经理的大量智慧。", "author": "", "source": "51CTO", "top": false, "recommend": false, "hot": false, "color": false, "linkUrl": null, "addDate": "2022-01-29 09:38:00", "price": 0.0, "oldPrice": 0.0, "stockQuantity": 0, "priceUnit": null, "isMainContent": true, "allowAddSubContent": false, "relatedContentId": 0, "subContentNum": 0, "mainContent": null, "subContents": null, "attributeIds": null, "attributeValueIds": null, "id": 662, "guid": "33c7b802-e439-4ef2-bf03-74c0024cc1e6", "createdDate": "2022-01-29 09:39:58", "lastModifiedDate": "2022-01-29 09:39:58" } ]