一、业务背景及痛点
目前主流互联网智能分析平台中,数据查询作为基础的设施服务支撑着基础数据及业务分析的功能展现。随着数据量的增长,数据存储方式多元化,相对静态数据可能存储到关系型数据库中,订单类动态数据可能存储到ElasticSearch、Greenplum数据库中,实时类数据可能存储到Druid、Redis中,每种数据库都有各自的优势及局限。
传统的数据服务实现面临很多需求、技术挑战,存在诸多痛点,数据查询需要从多个、多种数据库中读取数据进行逻辑处理,过程中存在必不可少的筛选、遍历、关联、合并、拼接、转换等处理,代码实现一般采用ORM组件把从数据库中查询的数据反射为强类型的对象列表,然后使用LINQ进行对象列表的查询处理,实现过程逻辑比较繁琐,并且类似的业务数据在各个产品应用中不可避免的存在重复冗余的处理代码,当业务需求发生变动后需要修改代码并重新部署文件,而且数据量比较大时可能存在性能上的问题,这样的软件产品可维护性、灵活性、通用性、拓展性、开发效率上比较差。
为解决以上痛点,提升开发效率、降低维护成本以及提高系统响应及时性,针对这类查询业务,特来电云平台数据分析BI组设计实现了通用数据查询服务平台,通过简单的查询配置即可从多种、多个数据库中拉取数据,并根据配置进行相关业务数据的筛选联查拼接等处理,开发、部署简单轻量,当业务发生变化时无需编码调整,可以快速应对需求的变更。平台实现使用C# DataTable的DataColumn.Expression表达式、DataSet的DataRelation提升了数据处理的灵活性及通用性,使用微软开源的.Net下的Javascript脚本引擎ClearScript降低查询条件转换、计算字段等业务的复杂度及耦合度。
二、平台架构整体实现
1. 查询请求 请求信息中要有该查询的内码或编码,同时需要有本次查询的参数信息,前端传入单层结构的json对象,后台可转为Dictionary,这样参数信息的数据结构拓展性比较好些。 2. 配置读取、校验 查询的配置包括查询信息、数据源信息、参数转换信息、结果字段信息、参数转换函数信息,配置信息设计维护时存储到Sql Server,运行时从Redis或进程内存中读取,采用定时、实时更新机制。 查询配置的数据结构如下: 3. 参数转换 查询的参数最终需要附加到查询SQL或结果上,报表分析、Grafana等查询分析界面传入的参数格式各异,例如开始时间,查询的参数值可能是yyyyMMdd格式,如果从ES查询的话需要转为yyyy-MM-dd并附加+08:00,从Sql Server不需要转换,等等情况,参数的转换需要有非常灵活的机制,强识别并不明智,使用JavaScript引擎ClearScript非常完美的达到目的。 4. 数据组合 数据的读取原则上采用执行SQL语句方式,SQL字符串语句方式比较灵活通用,转换后的参数、分页、排序最终转换为SQL片段附加到要执行的SQL语句上,根据数据源类型采用工厂模式实现参数转换、数据读取,目前可以从Sql Server、ES、Druid数据库中拉取数据,后继可再添加其它数据库类型的支持类。 5. 字段计算 处理过程主要是如何把查询字段附加到主数据源表,字段可能是直接取值、Column表达式、JavaScirpt表达式,最后进行数据结构转换,并根据查询结果结构返回。 6. 转换处理 数据返回前要根据查询设置需要的数据结构进行转换,例如返回结果类似于强类型数据结构,不经转换DataTable无法满足要求,另外如果主数据源为ES,可能没有任何行的情况下返回的数据为空,不满足前台使用要求,等等情况下有必要进行一次DataTable转List>,然后再处理为查询配置要求的数据结构类型。三、关键技术亮点
1. 巧用JavaScript引擎动态语言特性,实现动态配置 软件实现平台化时需要整合同类逻辑处理为通用的实现过程,例如查询服务的参数处理,把入参处理为可执行的SQL片段,业务要求非常简单,但是逻辑处理情况非常多,整合过程中对业务的强识别并不明智,有时候强识别也不现实,该类逻辑处理一般都有相似的输入及输出,我们可以把业务处理过程写到一个字符串然后能动态执行后返回想要的结果,例如我们通常会用SQL来编写一些数据库对象如:存储过程、触发器之类的对象,这些对象通过脚本来描述然后被解析成一个个实实在在的可执行体,这些可执行体是通过一系列预处理、词法分析、语法分析等编译环节最终形成可执行体,当我们使用的时候能快速的进行调用,这样大大增加了我们的运行效率及软件实现的通用性及灵活性。 c#是静态语言,编译后才可以使用,所以首先想到的是动态编译生成dll文件后反射调用,但是不能每次调用都进行动态编译,需要进行规则判断,而且第一次调用编译时耗时较长,性能上也达不到要求,易用性、灵活性、可维护性上也差强人意,动态字符串的执行JavaScript有一个Eval函数可以实现,c#也可以动态执行脚本语言,这样我们可以编写易读性、灵活性强的脚本代码实现不可预测的逻辑处理,然后使用ClearScript JavaScript引擎执行脚本动态字符串达到我们的目的。 ClearScript引擎可以将脚本功能添加到 .NET 应用程序(.NET Framework 4 或更高版本),ClearScript 支持 VBScript、JavaScript 和 V8。V8 是由 Google 创建的开源JavaScript 引擎,并且与 Chrome 集成。V8 是高性能的 JavaScript 引擎,且非常适用于多线程和异步操作处理。 ClearScript将 JavaScript 或 VBScript 表达式传递到引擎,而且并不只是可以使用纯脚本对象,如数组、JSON 对象和基元类型,同时可以集成外部JavaScript 库和脚本托管的c#类库。 使用ClearScript时可以先从NuGet安装包查找添加引用,简单的调用示例如下:using Microsoft.ClearScript.JavaScript;object result =null;using (var engine = new JScriptEngine( )){ result = engine.Execute(@"var a = 3; var b = 5; function add(a, b) { return a + b; } return add(a, b);");}
性能测试如下:
ClearScript可以集成外部脚本托管的c#对象,测试代码如下:
using ( var engine = new JScriptEngine( ) ){ //添加主机的模式,以便js可以调用主机这边的c#Console函数 engine.AddHostType( "Console" ,typeof( Console ) ); engine.Execute("Console.WriteLine('{0} is an interesting number.', Math.PI)" ); //添加主机的模式,以便js可以调用主机这边的c#random函数 engine.AddHostObject( "random", new Random( ) ); engine.Execute("Console.WriteLine(random.NextDouble())" ); //添加主机的模式,以便js可以调用主机这边的c#System.Core类库 engine.AddHostObject( "lib" ,new HostTypeCollection( "mscorlib" , "System.Core" ) ); engine.Execute("Console.WriteLine(lib.System.DateTime.Now)" ); //创建C#时间对象 engine.Execute( @" birthday = newlib.System.DateTime(2007, 5, 22); Console.WriteLine(birthday.ToLongDateString()); " ); //创建Dictionary对象 engine.Execute( @" Dictionary =lib.System.Collections.Generic.Dictionary; dict = new Dictionary(lib.System.String,lib.System.Int32); dict.Add('foo', 123); " );}
可见无论灵活性、可用性、性能上ClearScript都非常理想,参数转换动态执行以及特殊字段处理我们可以引入到数据查询平台中使用。
2. 旧瓶装新酒,重用DataColumn.Expression
数据库存储设计一般只是对基础字段进行存储,对于动态计算后的字段需要业务代码逻辑实现,例如用户信息存储中只是存储用户的出生日期,并不会存储用户年龄,如果界面要显示用户的年龄,需要后台根据当前时间计算,当然关系型数据库可用直接查询,但是其它类型数据库未必支持,更为复杂的业务逻辑可能数据库SQL也不一定能编写实现,假设需要我们后台逻辑处理,一般的处理逻辑是执行SQL返回DataTable或ORM组件后的List<强类型>,然后DataTable添加需要的业务列,遍历每一行计算赋值,或者遍历List<强类型>计算赋值,这样实现后假设业务列的计算规则调整,需要我们重新编译代码,然后重新部署,灵活性、可维护性非常差,而且当数据量超出一定数值后性能上也比较差, CPU占用比较高。
ORM架构有其优点也存在缺点,数据库的数据到内存首先基本都是DataSet,数据处理前的数据类型我们建议还是使用DataTable,对于计算列的处理我们采用DataColumn.Expression方式,我们可以设置计算列的表达式实现复杂的业务逻辑处理,该方式可以整列计算或者关联计算,而非行遍历处理,灵活性、可维护性、性能上非常理想。
DataColumn.Expression表达式功能非常强大,除一般的计算表达式、一对多、多对一父子级关系外还支持聚合、函数等实用性非常强的功能。
计算表达式:
DataTable dtMain = new DataTable( );dtMain.Columns.Add( "price" ,typeof( double ) );dtMain.Rows.Add( "30");dtMain.Rows.Add( "90");dtMain.Columns.Add( "tax" ,typeof( double ) , "price * 0.0862" );dtMain.Columns.Add( "total" ,typeof( double ) , "price + tax" );
父/子关系引用
在表达式中,可以通过使用 Parent追加列名称来引用父表。例如,Parent.Price 引用名为 Price的父表的列。当子级具有多个父行时,请使用Parent (RelationName)。ColumnName. 例如,Parent (RelationName)。价格通过关系引用名为Price 的父表的列。通过使用 Child追加列名称,可以在表达式中引用子表中的列。但是,由于子关系可能返回多行,因此必须在聚合函数中包含对子列的引用。 例如,Sum(Child.Price) 将返回子表中名为 Price 的列的总和。
如果表具有多个子元素,则语法为:
Child(RelationName)
。
3. 内存关系型数据处理的利器:DataSet. Relations
有时候我们需要的结果需要汇总多个数据源并联查合并后才能得出,一般的处理方式还是遍历、筛选取值计算,这种处理方式不可取,一般从数据库中读取的第一数据类型是DataSet,然后我们取第一个表DataTable进行数据处理,对于DataSet我们很少利用它的特性,DataSet是从数据源检索的数据的内存中缓存,是ADO.NET 体系结构的主要组件。DataSet 包含 DataTable 对象的集合,这些对象可以与 DataRelation 对象相关联,也就是DataSet相当于是内存中的关系型数据库,我们可以利用这个特性,定义数据关联表达式,动态添加DataRelation,配合DataColumn.Expression实现非常通用、灵活高性能的数据关联、筛选补充计算逻辑操作。