利用 Microsoft StreamInsight 控制较大数据流

Microsoft StreamInsight 概述 

  Microsoft StreamInsight 为开发和部署复杂事件处理 (CEP) 应用程序提供一个功能强大的平台。CEP 是一种高吞吐、低滞后处理事件流的技术。典型的事件流源包括来自生产应用程序、财贸应用程序、Web 分析或运营分析的数据。StreamInsight 流处理体系结构和熟悉的基于 .NET 的开发平台使开发人员可以快速实现强大而高效的事件处理应用程序。

  StreamInsight 具有下列主要优点:

  • 高度优化的性能和数据吞吐量

    StreamInsight 实现了轻量级流式体系结构,可支持对高速数据高度并行执行连续查询。使用内存中缓存和增量结果计算,可带来高数据吞吐、低滞后时间的卓越性能。因为在关键处理路径中无需进行数据加载或存储操作,所以可实现低滞后时间。借助 StreamInsight,所有处理都由传入事件自动触发。特别地,应用程序无须为事件轮询付出任何开销。此平台提供了无序事件处理功能。此外,可以访问静态引用或历史数据,这些内容包括在低滞后时间分析中。

  • .NET 开发环境

    开发人员可以使用 Microsoft 的 .NET 语言(如 Visual C#)来编写 CEP 应用程序,从而利用高级语言平台 LINQ(Language Integrated Query,语言集成查询)作为嵌入式查询语言。如果庞大的开发人员社区都已经熟悉了这些技术,则此功能将降低从应用程序开发到生产整个过程中所需的开发成本和时间。在当前版本中,StreamInsight 只支持 C# 作为宿主语言。

    使用 LINQ,熟悉 SQL 的开发人员可以很快以声明方式编写查询,将来自多个流的数据处理并关联到有意义的结果中。StreamInsight 服务器中的优化器和计划程序相继确保优化查询性能。

  • 灵活的开发功能

    StreamInsight 支持三种部署方案:

    • 作为托管(嵌入式)DLL 完全集成到应用程序中。

    • 作为一台拥有多个应用程序和共享该服务器的用户的独立服务器。在其独立配置中,StreamInsight 服务器在一个包装(如可执行文件)中运行,也可以将服务器打包为一项 Windows 服务。

    • 托管或独立 StreamInsight 服务器可作为服务器场的一部分。

  • 可管理性

    • StreamInsight 服务器中内置了监控和可管理性功能,支持 CEP 应用程序低总拥有成本 (TCO)。StreamInsight 服务器中提供的管理界面和诊断视图允许管理员监控和管理 CEP 应用程序。可管理性框架也允许 ISV 和系统集成商在生产以及其他扩展安装中远程监控和支持 StreamInsight 部署的系统。

    • StreamInsight 提供独立的事件流调试程序,可用来分析、诊断和排查 StreamInsight 中使用的查询。

利用 Microsoft StreamInsight 控制较大数据流

  生产线的产量下降后,将容易出现用户媒体流跳过这些流程,或者您的一个产品成为了“必需产品”的情况。真正的窍门是在这些情况发生时进行识别,或根据以往趋势对其做出预测。

  成功预测这些情况需要使用近乎实时的方法。在对相关数据进行提取、转换并加载到 SQL Server Analysis Services (SSAS) 等传统商业智能 (BI) 解决方案中时,情况早已发生改变。同样,一些系统依靠请求-响应模式来从事务性数据存储(如 SQL Server Reporting Services 或 SSRS、报告)中请求已更新的数据,像这样的系统总是在接近请求-轮询间隔结束时运行陈旧数据。轮询间隔通常是固定的,因此即使突然发生有趣的活动,消耗系统也不会知道,直到进入下一个间隔。相反,消耗系统应该在满足趣味条件时连续收到通知。

  在检测新兴趋势时,时间间隔至关重要 - 在过去的五分钟内,一个特定项目发生了 100 次购买,显而易见,这比过去五个月间的持续购买更能指示新兴趋势。SSAS 和 SSRS 等传统系统需要开发人员通过事务性存储中多维数据集或时间戳列中的单独维度来自行跟踪数据的及时性。理论上,用于识别新兴情况的工具可能具有时间内置的概念,并能提供使用该工具所需的丰富 API。

  最后,对未来的准确指示来源于对过去的分析。实际上,这就是传统 BI 的所有功能 - 对大量的历史数据进行汇总和分析,从而识别趋势。遗憾的是,与更多的事务性系统相比,在使用这些系统时需要不同的工具和查询语言。成功识别新兴情况需要实现过去数据和当前数据的无缝关联。只有当对这两种数据使用相同的工具和查询语言时,才可能实现这种紧密集成。

  对于生产线监视等特定情况,可通过存在的针对性极强的自定义工具来执行这些功能,但是这些工具通常比较昂贵且用途并不广泛。

  为了防止生产线产量下降或确保您的产品定价合适,关键在于具有足够的响应能力,能够根据情况的更改而进行识别与调整。若要轻松快速地识别这些情况,历史查询和实时查询应使用相同的开发人员友好的工具集和查询语言,系统应该以近乎实时的方式来处理大量的数据(大约为每秒成百上千个事件),同时引擎应该足够灵活,能够处理跨越多个问题域的情况。

  幸运的是,存在这样的工具。它称为 Microsoft StreamInsight。

StreamInsight 体系结构概述

  StreamInsight 是一种复杂事件处理引擎,它每秒能够处理成百上千的事件,且延迟极低。它可以由任何进程(如 Windows 服务)托管,也可以直接嵌入任何应用程序。StreamInsight 具有简单的适配器模型,用于输入和输出数据,并且实时数据和历史数据的查询像任何其他来自任何 Microsoft .NET Framework 语言的程序集一样使用获取的相同 LINQ 语法。其作为 SQL Server 2008 R2 的一部分授予许可。

  StreamInsight 的高级体系结构非常简单:通过输入适配器从各种源收集事件。这些事件均通过查询进行分析和转换,并且查询结果通过输出适配器分发给其他系统和人。  图 1 显示了这一简单结构。

图 1 Microsoft StreamInsight 高级体系结构

  就像面向服务的体系结构关注消息,而数据库系统关注行一样,StreamInsight 等复杂事件处理系统按照事件进行组织。事件是简单的数据段以及与该数据相关的时间 - 与一天中特定时间的传感器读数或股票行情价格相似。事件所携带的数据称为它的负载。

  StreamInsight 支持三种类型的事件。点事件是即时且不持续的事件。间隔事件是其负载与特定时间段相关的事件。边缘事件与间隔事件相似,但当边缘事件到达时,其持续时间未知。而系统设置了开始时间,且事件实际上具有无限持续时间,直到另一个边缘事件到达才会为这一事件设置结束时间。例如,速度计读数可能为点事件,因为它不断更改,但是超市的牛奶价格可能为边缘事件,因为其关联时间较长。当牛奶的零售价格更改时(比如,由于分销商定价发生更改),新价格的持续时间未知,因此,与间隔事件相比,边缘事件要更为合适。稍后,当分销商再次更新其定价时,新的边缘事件将覆盖先前定价更改的持续时间,而另一个边缘事件将设置新的价格以便继续。

  StreamInsight 中的输入适配器和输出适配器是适配器设计模式的抽象示例。StreamInsight 引擎在其自有的事件表示上运行,但是这些事件的实际来源可能有较大差异,范围从专有接口到硬件传感器到由企业的应用程序生成的状态消息。输入适配器将源事件转换为引擎能够理解的事件流。

  来自 StreamInsight 查询的结果表示特定商业知识,且能够高度专业化。将这些结果路由至最合适的地点,这点至关重要。输出适配器可用于将事件的内部表示转换为打印到控制台的文本、通过 Windows Communication Foundation (WCF) 发送到另一个系统以供处理的消息,甚至 Windows Presentation Foundation 应用程序中图表上的点。有关使用文本文件、WCF 和 SQL 等的示例适配器可从 streaminsight.codeplex.com 获得。

StreamInsight Queries by Example

  乍一看,StreamInsight 查询似乎与从数据库中查询行相似,但是两者之间存在重大差异。查询数据库时,系统会构造并执行查询,同时返回结果。如果基础数据发生更改,输出并不会因为已运行查询而受影响。数据库查询结果表示某一时刻的快照,可以通过请求-响应模式使用。

  StreamInsight 查询为现有查询。随着新输入事件的到达,查询不断响应,并且根据需要创建新的输出事件。

  本文中的查询示例来自可供下载的示例解决方案。这些示例开始较简单,但随着查询语言新功能的引入,功能变得更加强大。所有查询都使用同一负载类。以下是一个简单类的定义,该类具有 Region 属性和 Value 属性:

1 public class EventPayload
2 {
3   public string Region { get; set; }
4   public double Value { get; set; }
5   public override string ToString()
6   {
7     return string.Format("{0}\t{1:F4}", Region, Value);
8   }
9 }

  示例应用程序中的查询使用一台输入适配器和一台输出适配器来进行,输入适配器可随机生成数据,输出适配器只需将各事件写入控制台。为清晰起见,对示例应用程序中的适配器进行了简化。

  若要运行每个查询,请在示例解决方案中取消注释 Program.cs 文件中的行,该示例解决方案可将查询分配给称为“template”的本地变量。

  以下是一个基本查询,它通过 Value 属性来筛选事件:

1 var filtered =
2   from i in inputStream
3   where i.Value > 0.5
4   select i;

  具有使用 LINQ 经验的任何开发人员应该非常熟悉此查询。因为 StreamInsight 使用 LINQ 作为它的查询语言,因此此查询与 LINQ to SQL 查询类似,访问数据库或对 IList 进行内存中筛选。当事件从输入适配器到达时,其负载将受到检查,并且如果 Value 属性的值大于 0.5,事件将被传递到输出适配器,并在此将其打印到控制台。

  应用程序运行时,可以看到事件不断到达输出中。这实际上是一个推模型。当事件到达时,StreamInsight 会计算来自输入的新输出事件,这与数据库等拉模型不同,在拉模型中,应用程序必须定期轮询数据源,以查看新数据是否已经到达。这能与 Microsoft .NET Framework 4 中可用的 IObservable 支持完美结合,我们将在后续章节中对此进行介绍。

  使用推模型代替轮询来处理连续数据是个非常好的主意,但是 StreamInsight 的真正功能体现在查询时间相关的属性上。当事件通过输入适配器到达时,它们获得了一个时间戳。该时间戳可能来自数据源本身(假设事件表示历史数据,且带有用于存储时间的显示列),或者可以设置为事件到达的时间。实际上,时间是 StreamInsight 查询语言中的第一个类。

  查询通常与标准数据库查询类似,标准数据库查询在尾部粘贴有时间限制符,如“每五秒”或“五秒的时间跨度上每三秒”。例如,以下是一个简单查询,它每五秒查询一次 Value 属性的平均值:

1 var aggregated =
2   from i in inputStream.TumblingWindow(TimeSpan.FromSeconds(5),
3   HoppingWindowOutputPolicy.ClipToWindowEnd)
4   select new { Avg = i.Avg(p => p.Value)};

  

数据窗口

  因为时间概念是复杂事件处理系统的基础必需概念,因此应以简单的方式来使用系统中查询逻辑的时间组件,这点非常重要。StreamInsight 使用窗口概念来表示按时间分组。之前的查询使用翻转窗口。应用程序运行时,查询将每五秒生成单个输出事件(窗口的大小)。输出事件表示前五秒的平均值。像 LINQ to SQL 或 LINQ to Object 一样,聚合方法(如 Sum 和 Average)能够将按时间分组的事件汇总为单个值,或可以使用 Select 将输出投影成不同格式。

  翻转窗口只是另一种窗口类型的特例:跳跃窗口。跳跃窗口也有大小,但是它们也具有不等于其窗口大小的跳跃大小。这表示跳跃窗口可以互相重叠。

  例如,窗口大小为五秒、跳跃大小为三秒的跳跃窗口将每三秒生成输出(跳跃大小),提供前五秒的平均值(窗口大小)。它一次向前跳跃三秒,且持续五秒。图 2 显示分组为翻转窗口和跳跃窗口的事件流。

图 2 翻转窗口和跳跃窗口

  请注意,翻转窗口并不重叠,但是对于跳跃窗口,如果跳跃大小小于窗口大小,则可以重叠。如果窗口重叠,事件将可能在多个窗口中结束,如同时存在于窗口 1 和窗口 2 中的第三个事件。边缘事件(具有持续时间)也可能在窗口边缘重叠,并在多个窗口中结束,如翻转窗口中的倒数第二个事件。

  另一种常见窗口类型为计数窗口。计数窗口包含特定数量的事件,而不是某一时间点或时间段内的事件。要查询最后三个到达的事件的平均数,可能需要使用计数窗口。计数窗口当前的一个限制是不支持 Sum 和 Average 等内置聚合方法。您必须创建用户定义的聚合。下文会对这一简单流程进行介绍。

  最后一种窗口类型为快照窗口。在边缘事件的环境下,快照窗口最容易理解。每次事件的开始或结束即表示当前窗口的完成和新窗口的开始。图 3 显示如何将边缘事件分组为快照窗口。请注意每个事件边界触发窗口边界的方式。E1 开始,w1 也开始。当 E2 开始时,w1 完成,而 w2 开始。下个边缘是 E1 结束,使得 w2 完成,而 w3 开始。结果为三个窗口:包含 E1 的 w1,包含 E1 和 E2 的 w2 以及包含 E3 的 w3。事件分组为窗口后,它们会受到拉伸,从而使事件的开始与结束时间与窗口的相同。

图 3 快照窗口

更多复杂查询

  在提供可用窗口与基本查询方法(如地点、分组依据和排序依据)的情况下,可以进行多种查询。以下是一个查询,其将输入事件按地区分组,然后使用跳跃窗口来输出最后一分钟各个 Region 的负载 Value 的总和:

  

 1 var payloadByRegion = 
2    from i in inputStream
3    group i by i.Region into byRegion
4 from c in byRegion.HoppingWindow(
5   TimeSpan.FromMinutes(1),
6   TimeSpan.FromSeconds(2),
7    HoppingWindowOutputPolicy.ClipToWindowEnd)
8    select new {
9   Region = byRegion.Key,
10   Sum = c.Sum(p => p.Value)};

  这些窗口使用两秒的跳跃大小,因此引擎每两秒发送输出事件。

  因为查询运算符是在 IQueryable 接口中定义的,因此可以撰写查询。以下代码使用上一个查询,其按地区查找总和,并计算总和最高的地区。快照窗口允许事件流按总和分类,因此可以使用 Take 方法获取总和最高的地区:

1 var highestRegion = 
2 // Uses groupBy query
3   (from i in payloadByRegion.SnapshotWindow(
4   SnapshotWindowOutputPolicy.Clip)
5    from sumByRegion in i
6   orderby sumByRegion.Sum descending
7   select sumByRegion).Take(1);

  

  一般情况是有关快速移动事件(如传感器中的读数)到慢速移动或静态参考数据(如传感器的固定位置)流的查询。查询使用联接来实现此目的。

  StreamInsight 联接语法与任何其他 LINQ 联接相同,但有一点需要注意:当事件的持续时间重叠时,它们才会联接在一起。如果传感器 1 在时间 t1 报告了一个值,但是有关传感器 1 位置的参考数据仅对时间 t2 到 t3 有效,那么联接将不匹配。持续时间的联接条件并没有明确写入查询定义中;这是 StreamInsight 引擎的基本属性。使用静态数据时,通常情况下,输入适配器实际上将数据处理为带有无限持续时间的边缘事件。这样将能成功完成到快速移动事件流的所有联接。

  通过联接来关联多个事件流是一个非常强大的概念。装配线、石油生产设施或高容量网站通常不会因为隔离的事件而发生故障。一个用于触发温度警报的设备部件通常不会导致生产线瘫痪;生产线瘫痪可能由于多个原因造成,如温度在某一持续时间段内过高,同时某一工具使用过多,而操作员正在换班。

  如果没有联接,隔离事件将不会有这么多的商业价值。通过对历史数据使用联接和 StreamInsight 查询,用户可以将隔离流与非常具体的监控条件相关联,然后进行实时监控。现有查询能够查找可能导致故障的情况,并自动生成可路由至系统的输出事件,该系统知道如何使过热的设备部件脱机,而不是等到该部件造成整条生产线停产。

  在零售情况中,有关某段时间按项目划分的销售量的事件可以输入到定价系统和客户订单历史记录中,从而确保每个项目具有最佳的定价,或决定在用户结账前向其推荐的项目。由于查询易于创建、修改和撰写,因此您可以从简单的情况开始,并随时间的流逝进行优化,从而增加业务价值。

用户定义的聚合

  StreamInsight 附带最常见的聚合函数,包括 Count、Sum 和 Average。当这些函数不够时(或您需要在前文提到的计数窗口进行聚合),StreamInsight 支持用户定义的聚合函数。

  要创建用户定义的聚合,其流程包括两个步骤:编写实际聚合方法,然后通过扩展方法将该方法公布到 LINQ。

  进行第一步时,如果聚合与时间无关,则从 CepAggregate<TInput, TOutput> 继承,如果聚合与时间有关,则从 CepTimeSensitiveAggregate<TInput,TOutput> 继承。这些抽象类具有单独的实现方法,称为 GenerateOutput。 图 4 显示 EveryOtherSum 聚合的实现,这种聚合将每个其他事件加起来。

图 4 EveryOtherSum 聚合

 1 public class EveryOtherSum :
2   CepAggregate<double, double> {
3     public override double GenerateOutput(
4       IEnumerable<double> payloads) {
5         var sum = default(double);
6         var include = true;
7         foreach (var d in payloads) {
8           if (include) sum += d;
9           include = !include;
10         }
11         return sum;
12       }
13    }

  

  进行第二步时,需要在 CepWindow<TPayload> 上创建扩展方法,以便可以在查询中使用您的聚合。CepUserDefinedAggregateAttribute 适用于扩展方法,以便通知 StreamInsight 在哪里可以找到聚合的实现(在这种情况下,类是在第一步中创建的)。在可下载的示例应用程序中,本流程两个步骤的代码均可在 EveryOtherSum.cs 文件中找到。

更多适配器信息

  查询表示对适配器提供的数据进行操作的业务逻辑。示例应用程序使用一台简单输入适配器和一台输出适配器来进行,输入适配器可生成随机数据,输出适配器可将数据写入控制台。它们均遵循相似的模式,CodePlex 网站上提供的适配器也遵循这一模式。

  StreamInsight 使用 Factory 模式来创建适配器。给定配置类后,工厂可创建相应适配器的实例。在示例应用程序中,输入适配器和输出适配器的配置类都非常简单。输出适配器配置具有保存格式字符串的单个字段,可在编写输出时使用。输入适配器配置具有填写生成随机事件之间睡眠时间的字段,也具有另一个称为 CtiFrequency 的字段。

CtiFrequency 中的 Cti 代表当前时间增量。StreamInsight 使用 Cti 事件来帮助确保事件以正确的顺序传递。默认情况下,StreamInsight 支持不按顺序到达的事件。当通过查询传递事件时,引擎将自动对事件进行相应的排序。然而,这一重新排序具有一定的限制。

  假设事件真的能够以任意顺序到达。那么怎么能够确定最早的事件已经到达,并因此通过查询来推送?这不可能,因为下一个事件的时间可能比您收到最早事件的时间更早。StreamInsight 使用 Cti 事件来通知引擎比已接收事件更早的事件将不会到达。Cti 事件实际上提示引擎去处理已经到达的事件,随后忽略或调整任何带有早于当前时间的时间戳的事件。

  示例输入适配器生成排序事件流,因此它在每个生成的事件后自动插入一个 Cti 事件,以便保持流程的进行。如果您已编写输入适配器,而您的程序没有产生输出,则请确保您的适配器插入了 Cti,因为如果没有 Cti,引擎将一直等下去。

  StreamInsight 附带了适配器的各种基本类:特型、泛型、点型、间隔型和边缘型。特型适配器总是产生带有常见负载类型的事件 - 在示例案例中,为 RandomPayload 类。泛型适配器适用于可产生多种事件类型的事件源,或不能提前得知行布局和内容的事物,如 CSV 文件。

  示例输入适配器具有常见负载类型,可生成点事件,因此其继承自 TypedPointInputAdapter<RandomPayload>。基本类具有两个必须实现的抽象方法:Start 和 Resume。在示例中,Start 方法使得计时器在配置指定的间隔内触发。计时器的 Elapsed 事件运行 ProduceEvent 方法,该方法完成适配器的主要工作。此方法的主体遵循通用模式。

  首先,适配器检查引擎自上次运行后是否已停止而现在仍在运行。然后,调用基本类中的一种方法来创建点事件的实例,其负载已设置且事件已排列在流中。在示例中,SetRandomEventPayload 方法可代替任何真实适配器逻辑 - 例如,读取文件、与传感器对话或查询数据库。

  输入适配器工厂也非常简单。它实现了接口 ITypedInputAdapterFactory<RandomPayloadConfig>,因为它是特性适配器的工厂。本工厂的唯一特点在于它也实现了 ITypedDeclareAdvanceTimeProperties<RandomPayloadConfig> 接口。此接口允许工厂处理前文所述的 Cti 插入操作。

  示例应用程序的输出适配器遵循的模式与输入适配器基本相同。包括配置类、工厂与输出适配器本身。适配器类与输入适配器十分相似。主要区别是适配器从队列中移除事件,而不是对其进行排队。因为 Cti 事件与其他事件相似,它们也到达输出适配器,并很容易被忽略。

可观察量

  虽然适配器模型十分简单,但还可以使用以下一种更简单的方式来将事件输入和输出引擎。如果应用程序使用的是 StreamInsight 的内嵌部署模型,则您可以使用 IEnumerable 和 IObservable 作为引擎的输入和输出。给定一个 IEnumerable 或 IObservable,您可以通过调用所提供的扩展方法(如 ToStream、ToPointStream、ToIntervalStream 或 ToEdgeStream)之一创建输入流。这将创建一个看上去与输入适配器创建的事件流极为相似的事件流。

  同样,给定一个查询,扩展方法(如 ToObservable/Enumerable、ToPointObservable/Enumerable、ToIntervalObservable/Enumerable 或 ToEdgeObservableEnumerable)会分别将查询输出路由至 IObservable 或 IEnumerable。这些模式特别适用于重播保存在数据库中的历史数据。

  使用 Entity Framework 或 LINQ to SQL 创建数据库查询。使用 ToStream 扩展方法将数据库结果转换为事件流,并定义关于该事件流的 StreamInsight 查询。最后,使用 ToEnumerable 将 StreamInsight 结果路由至方便您 foreach 并打印的位置。

部署模型和其他工具

  若要使用 Observable 和 Enumerable 支持,必须在您的应用程序中嵌入 StreamInsight。但是 StreamInsight 不支持独立模型。在安装时,系统会询问您是否创建 Windows 服务以托管默认实例。该服务可随后托管 StreamInsight,允许多个应用程序连接到相同的实例并共享适配器和查询。

  通过共享服务器而非嵌入的服务器来进行的通信会使用 Server 类上的一种不同的静态方法。不调用具有实例名称的 Create,而是调用 Connect,其带有指向共享实例的 EndpointAddress。此部署策略更适用于企业情况,在此情况下,多个应用程序可能需要使用共享的查询或适配器。

  在两种情况下,有时需要弄清楚为什么 StreamInsight 生成的输出不是应该生成的输出。该产品附带名为 Event Flow Debugger 的工具,以用于此用途。本文不介绍该工具的使用方法,但总而言之,该工具允许您连接到实例并通过查询跟踪输入和输出事件。

灵活、反应迅速的工具

  灵活的部署选项、熟悉的编程模型和可轻松创建的适配器使得 StreamInsight 成为各种情况下的好选择。从查询并在一秒内关联数以千计的传感器输入的集中式实例到在单个应用程序中监控当前事件和历史事件的嵌入式实例,StreamInsight 均采用开发人员友好的框架(如 LINQ)来实现高度自定义的解决方案。

  易于创建的适配器以及用于在事件流与 IEnumerable 和 IObservable 之间进行转换的内置支持使得它能够快速找到解决方案并运行,从而增加封装了特定商业知识的查询的创建和完善工作。在完善过程中,这些查询提供越来越多的值,使得应用程序和组织能够在发生有趣情况时进行识别并做出反应,而不错过处理的机会。

下载代码示例

 

P.S. 大家可以从SQL SERVER官网获取StreamInsight的相关资料和开发环境,链接:http://msdn.microsoft.com/en-us/sqlserver/ee476990.aspx。对于StreamInsight的最新发布或者技术问题大家可以看一下Rob Pierry的Blog。园子里面也有几个不错的有关StreamInsight的手札,大家可以找找看

  唉,这新技术发布的速度,怎么追都追不上啊:-(

转载于:https://www.cnblogs.com/CSNotBack/archive/2011/08/26/2154789.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值