Paper Reader:Visual Abstraction and Exploration of Multi-class Scatterplots

接着上一篇文章,探索密集地理数据可视化的优化。这次从论文上下手。以浙大博士陈海东写的《Visual Abstraction and Exploration of Multi-class Scatterplots》入手。这篇文章首先介绍了解决散点图覆盖问题的优化方法,并设计了一个系统原型。我从散点图特征的介绍写到论文笔记,并思考进一步研究的方向。大概有10篇左右的相关论文,论文名都给出,大家可以google到,如有不便,也可直接向我索要。当然论文以及文章的图片都是归作者所有,如若侵权请告之。最后的最后,本人行文潦草,如若发现错误也可联系我。


1. 散点图

散点图[2]是用来显示两个定量变量之间关系的图像,两个变量通过x轴和y轴的定位分布在一张图里面。散点图的数据集依据图像可能得出的模式有:两个变量的线性相关性、图像的斜率来表示两个变量的相对变化趋势、点分散的程度表示变量之间的关系。可以看看下图的几个例子,值得留意的是下图中第一行第三个散点图出现的离群值(outlier)。散点图被广泛用在可视化数据集的离群值、积累、局部趋势和关联关系。在多类散点图中,每个不同的类别通过不同的颜色或者形状呈现给分析人员。

当数据太多时,在散点图中就会出现密集区域的点重叠的问题,也就是在当前图像范围中,一个像素点包含多个数据点最终导致散点图无法提供给分析人员的洞察,这在上一篇文章中也说过了。当然,在多变量数据可视化的情形下,重叠问题还会导致不同的类别相互扰乱。针对这种问题,现有研究已经在使用的一些公共方法有等高线(contouring)、混色(color blending)、颜色交织(color weaving)。这三个方法的描述如下:

  • 等高线:Adrian Mayorga在13年发表的一篇论文,题目为《Splatterplots: Overcoming Overdraw in Scatter Plots》,中设计了一种新颖的散点图——splatterplot,他在文中指出等高线的引入能很好地勾勒出不同类别的形状,但等高线不能捕捉到离群值。这里我们再看看多变量重叠的地方(比如下图)出现图中混淆视觉的情况,这是因为出现了同心的等高线。我们需要避免这种情况。

  • 混色:Adrian使用混色来区分不同类别、变量的图像,更具体点的是使用色调来区别不同的类别,应对图像重叠的问题则通过亮度和饱和度来区分重叠程度。这样就规定了一个颜色混合的设置(如下图):从白色(密度为0)到单变量的密度颜色、再到多变量的颜色混合。

  • 颜色交织:H.Hagh-Shenas在03年发表的论文《Weaving versus Blending: a quantitative assessment of the information carrying capacities of two alternative methods for conveying multivariate data with color》中针对颜色的渲染方法进行了实验,并指出相比混色,颜色交织能更有效地传达多元数据的空间分布信息。可以看下图(取自另一篇论文《A new Weaving Technique for Handling Overlapping Regions》,Martin等人写),虽然交织的方法比混色能更加突出重叠区域,但是不是很清晰地表达复杂区域的分析特征。

作者指出这三种方法都存在陈述性误导。此外,对于不同类别之间渲染的顺序也会导致重叠区域的变化,不能保留完好的定量分析。

在进一步阐述中,作者接着又将常用的方法划分为3类,前面这3种方法都属于第一类。这三类方法分为视觉通道改变、密度估计、空间变形。

  • 视觉通道改变:视觉通道改变就是改变图上点的大小和透明度等属性,这种方法的问题在于使用了一个有限的方法(可视化属性的类型)来兼容可能数量极大的类别。比较简单的方法有动态调节点的大小(在密集区域使用小点,在稀疏区域使用大点来突出离群值的显示),比较复杂点的就是Martin采用的对齐交织技术(The Aligned Weaving Technique),这种对齐的方法将重叠区域的不同类型均匀地着色,使得颜色的区分更加明显。对齐策略又分为堆栈对齐、膜对齐,其中的区别如下图。

  • 密度估计:密度估计就是将点的数量压缩,这种方法一定程度上忽略了离群值。简单的方式就是将绘图空间划分为不同的区域,并计算不同区域内点的数量(Alpha blended scatterplots)。划分方法也有六边形格、精确的数学模型(continuous scatterplot)、核密度估计(kernel density estimation,简称KDE)。KDE使用来估计未知密度函数的工具,其详细公式定义可看wiki。这个名称中的内核(kernel)是在密度估计公式中\(\hat{f}{n}(x) = \frac{1}{n} \sum{i=1}^{n} K{h}(x-x{i}) = \frac{1}{nh} \sum{i=1}^{n} K(\frac{x-x{i}}{h}) \)的函数K,函数K为一个非负的函数,有多种类型。除了类型的选择会影响估计外,带宽h是也一个影响元素。Michael在12年发表的论文《Interactive Level-of-Detail Rendering of Large Graphs》中提到KDE这种密度估计的方法能避免节点的扭曲和数据的展示。在节点的密度估计公式上,将节点的位置和其对相邻节点的影响作为一个权值融入到计算中,详细的计算过程可看论文。最后在图像上得出的结果与热力图类似,可以从下图结果中看出,不同的缩放级别展示的散点也是不一样的,这样因为使用了四叉树来分层次存储节点。

    还有一些重叠轨迹的可视化方法。但这些方法都对离群值进行了忽略,David Feng在10年的论文《Matching Visual Saliency to Confidence in Plots of Uncertain Data》中使用mean emphasis(翻译过来叫什么?平均偏重)来弥补KDE密度估计方法中离群值的可视化,将离群值展示出来。mean emphasis。通过下面的对比图中,左边为标准的散点图,中间为使用mean emphasis的概率密度分布,右边为纯净的概率密度分布。可以看到左上角的离群值有非常大的偏差,因此在密度估计图里看不到,这也是密度图能发现过滤后的模式的原因。而mean emphasis将本来因为聚合关系被模糊为一个整体的几个亚类别分解了出来。

    上面提到的Splatterplot在限制屏幕复杂度的情况下,将离群点进行采样显示(详细过程可看splatterplots的论文4.5章)。混色和颜色交织的方法也只是在一定程度上实现多类的密度表示可视化。

  • 空间变形:空间变形就是将密集区域的点按照某种算法移动到稀疏区域,这种方法直接改变了点的分布,这种方法在需要定量分析的场景是不合适的。Daniel A. Keim在98年发表的《The Gridfit Algorithm: An Efficient and Effective Approach to Visualizing Large Amounts of Spatial Data》文中制定了一个解决大量数据的重叠的方法——Gridfit algorithm,其思想为层次化地分割数据空间。将密度区域划分为4个子区域,并以四叉树存储必要信息。以下图为例,36个像素区域中有14个点,首先均等地划分4个3X3的子区域。当某个区域的节点数超过了其区域的大小时,比如左上角的区域有10个节点超过了其子区域的大小9,我们就需要重新划分区域使得最后结果中,左上角的区域大小为15,里头有10个节点。划分好后,改变每个子区域内的节点使得相对均匀(也就是每个像素点上只有一个节点)。

    其他还有让使用者自己在图像的重叠和失真之间找到平衡的generalized scatterplot(《Generalized scatter plots》),通过椭圆的图像参数存储聚类数据的the ellipsoid pixel placement scheme(《Enhancing scatter plots using ellipsoid pixel placement and shading》),通过修改局部特征区域的坐标曲度的warping method(《ViSizer: A Visualization Resizing Framework》)。这三种方法的图像展示如下图,从左到右依次为generalized scatterplot(其将重叠区域在某个维度放大)、scatterplot using ellipsoid pixel placement and shading、scatterplot using warping method。

他们也参考了上一篇文章提到的芝加哥人种分布图,并指出该图使用的dot map采用了point sampling scheme来呈现多类的分布呈现方法,指出该方法的优点有二:其一,不同类型的颜色展示没有导致多种类型混淆的染色方法。其二,相关的特征比较都是局部的,没有使得整张图的混乱。最重要的是,这个方法提供了一种点取样的方法来还原的多变量的分布。当然其问题也在上一篇文章中提到过。基于这张图的灵感,论文作者提供了一种多类散点图取样方法——基于蓝噪声的多类散点图取样方法,并应用于一种层次结构上的取样过程,在系统级别上也改进了交互方式,尝试提供了辅助操作以提高散点图的可读性,比如刷选(brushing)、聚焦加上下文(focus+context)。作者通过这样的可视化抽象方法(visual abstraction scheme)使得采样点能描述多类节点的分布。

2. 点密度估计

密度估计的方法在上面已经详细介绍,作者使用的是KDE的密度估计方法,密度公式 \( \hat{f}{i}(x) = \sum{i=1}^{m} K{h}(x-x{i}^{j}) \)中的xji为第i类的数据点,采用的是高斯核密度估计。

3. 蓝噪声取样

取样是保留特征的前提下还能减少渲染点数量的一种方法。蓝噪声指具有最小的低频分量并且频谱中没有明显峰值出现的非周期性随机信号,在有限带宽内,其功率谱密度与频率成正比。在计算机图形学中,蓝噪声用于生成半色调抖动图像,产生适合印刷等再现方式的像素空间结构,这种结构更符合人类视觉特点。蓝噪声样本分布的随机性避免了图像失真问题,这经常出现在使用更结构化样本模式;样本分布的均匀性降低了单纯使用随机取样会产生的噪音数量。现有的蓝噪声取样方法大部分都是从其空间位置获取样本属性,其分布取决于从空间位置获取的样本属性。也就是说不能直接作用于非空域数据。

当扩展到多类取样时,当前的研究实现了对更加抽象的离散值的多类取样。然而这组特殊的方法也只是通过一个矩阵将单个类别以及总和相关的蓝噪声属性取样,这个矩阵记录的是不同类别之间间距的。陈家挺写的《Bilateral Blue Noise Sampling》在两个方面优化了蓝噪声多类取样:第一,能够处理更一般的非空间样本参数;第二,实现针对连续的蓝噪声属性取样。

回到这篇论文来,作者采用的是一个自适应的多类蓝噪声取样方法。在样本点的生成过程中,一个n×n(n为数据的类别)的对称矩阵Rn用来检查位置x是否有冲突发生。Rxi,j表示第i类和第j类之间在x点上的距离约束,由密度估计来提供具体的数值。从下图可以看到,x最终渲染成了Class1。

重取样是重构通过KDE生成的连续密度场,在论文中提高对于散点图的重取样可能会生成新的点。作者的方法中,重取样作用在一个离散的样本空间内,这个样本空间是基于所有的输入。在《 Multi-class blue noise sampling》文中,计算了每个类别都计算了填充率(fill rate)。在重取样时,总是从当前填充率最高的类别中选取样本点。如若其能通过上述的冲突检测。

当然取样导致的数据不一致还会出现在操作人员在进行缩放的时候,比如下图当中的边界值。

作者实现了层次抽样方法的原因,使得缩放时页面展示的结果能平滑的展现。层次抽样的处理方法是一个粗粒度到细粒度的取样预计算过程。具体的说,在粗粒度大的层次初始化一个参数,使得图上的某个小区域内所有的点能展示出来。这样层级的预计算取样点在下一轮取样中使用。使得最后记录的点以缩放的升序。通过这种方法在缩放时能平滑地展示取样的数据点。

4. 抽象可视化

通过上一步最后得到重取样的多类别点是不会在页面上产生覆盖问题的数据点。但是视觉混乱还是没有完全消除,在空间密度比较高的情况下还是会导致视觉混乱。作者通过两种方法来消除这种影响:一者是点的颜色,二者是点的形状。

  • 作者设计的方式是提供用户自己选择颜色,或者自动生成一套颜色方案。自定义颜色的考虑应该是考虑到不同使用者实际的颜色敏感度以及软硬件的具体表现,而自动生成的颜色。总之就是提高不同点之间的区别。
  • 作者采用了圆点型、椭圆型、点线型的形状。椭圆型是来自《Generating pointillism paintings based on Seurat’s color composition》中的点图,作者通过局部线性回归分析计算出椭圆的参数,并将主半径与副半径之比限制为1.618。点线型是来自《Flow-based Scatterplots for Sensitivity Analysis》中的敏感度分析,敏感度分析是分析通过细小的输入变化得到的结果,点上的线为其计算得来的导数。这两种形状的点产生的图如下图,感受一下。

5. 可视化系统应用

作者设计的可视化系统如上图。(a)区域为每个数据类别的提供了一维的直方图来过滤需要呈现的数据,并可以拖拽到主界面(b)区域。(b)区域提供还提供一下高亮、区域选择、刷、注释、快照这些常用的功能。(c)区域为工具栏,可以看到上文提高过得点的颜色自定义,点的形状选定。而(d)区就是配置面板,从KDE的参数到图像中点的大小都能配置。在局部探索方式上,还提供了通过等高线来分析数据分布的密度特征。

6. 个人总结

看这篇文章的目的考虑需要写的论文点。首先KDE和蓝噪声取样都是属于比较成熟的方法,在学术上有很多实施方案和改良,但是将两者结合使用还比较少见,所以作者将其结合起来。传统的处理高密度区域数据渲染方法(混色、颜色交织、等高线)也是在可视化早期就探索出了一些结合方法和变体。离群点的保留和抽象化数据(取样)之间的取舍也有人有所研究。这些内容在我上述提到的10来篇论文中都有所涉及。作者最后还提到一个改进的方向:数据类别的图像渲染顺序会影响到最终呈现的结果。但个人觉得这个问题的解决可能比较难,只能作开放性探索,目前能想到的方法是安装类别的重要程度渲染散点图等人为地设置渲染顺序。第二个是将取样方法在地图上应用,这个方面也是一个比较好的点,但是在前面的论文搜索发现单独的等高线优化散点图,我们只能往将等高线和取样方法结合方向上思考,但也需要注意作者在系统设计上提及过等高线,所以若需要往这个方向写需要加快脚步。


参考

  1. Visual Abstraction and Exploration of Multi-class Scatterplots@IEEE
  2. 散点图@stattrek
  3. 这篇论文的评述@浙大可视化小组

本文原地址

Dot Maps: 密集型地理数据可视化方案探索

最近师哥给的任务是地理数据可视化的研究,首先将现有情况说明清除。我们拿到的数据不是很多,但由于数据的特点,在可视化过程中可能会出现下图这种状况,地理标识数据相互覆盖。

这就是我们遇到的第一个问题:怎么减少这种地理数据的相互覆盖。按照老习惯首先在网上搜索是否存在比较成熟的方案,然后再区论文中搜索比较新颖的解决方案。本篇文章和下一篇文章都与这个主题有关。首先这篇文件描述一个点描法地图的可视化学习,下一篇将是论文报告的形式呈现可以借鉴的思路。


1. Dot Maps

这一部分内容是看宾夕法尼亚大学的一门地理可视化课程[1]的学习笔记。在Lession 5: Dot Maps就介绍了这种地理图表。

点描法(dot method)又称点数法、点值法、点子法或点法,是用代表一定数值的大小相等、形状相同的点,反映某地图要素的分布范围、数量特征和密度变化的方法。 采用点值法的最重要的是确定点权值,即每个点子所代表的对象数值。确定点权值的基本原则是:使密度小的地区能得到表示,而密度大的地区点子不产生连续、重叠现象。但有时因制图对象各区域分布的数量差异太大,采用一个点值无法兼顾两极值区域,这时只好采用两个不同大小的点子和两种权值加以表示。在编图时,根据点权值计算各区域的点子数目,采用定位法或根据制图现象分布规律把点子绘到地图上。

这种点描法地图最好的使用场景是呈现包含离散的、在地理上分布不均(最好是平滑的变化)的数据。这种地图上的点并不是为了呈现精确的数量统计,而是对于地图上某种现象的量级、密度及其地图上的变化趋势的描述。

这种图的关键因素有两点:点的大小和代表的数值含义。点在地图上出现太大会导致重叠(overlap),密度的变化也会比较难以察觉。点的取值也会影响对于空间模式(spatial patterns)的识别。这种关系的趋势是一般为:越小的点和越大的数值量会给地图观察者一种现象出现比较稀疏、特征不明显的感觉。这两个属性的选择也与我们探索的现象的空间分布以及丰富程度密切相关。

前面说道的点重叠的问题也是我们正在解决的问题,这就需要考虑在高密度地图中数据合并的问题,这里引入一种诺模图(nomograph)——the Mackay nomograph[2](见下图),这种图的作用就是通过选定维度某一个参数后,在诺模图经过尺规作图找到你需要的另一个参数。通过该工具可以指导我们大小和取值的选择。

注意这个合并地带(zone of coalescing dots),通过图上直径维度(图中上侧的维度)选择的一点找到单位范围上点的数量(图中下侧的维度)。比如下图箭头的52就是每平方厘米的数量。

再在地图上找到密度最高的区域的面积,比如这个北美小麦产量的地图中的一个最高密度区域面积就是1.2cm2。

将点数以及范围相乘可以得到该区域一个放置的点数: 52dots/cm2×1.2cm2=62dots。最后就是将该区域的总数量除以点数,得到每个点代表的数量: 62,000acres/62dots=1000acres/dot。

当然这种系统方法在实操上可能比较复杂,直接通过试错法慢慢调优也是一种比如容易实施的方式。

点描法地图通过不同颜色的点可以延伸到多变量的地理分布比较。比如下图的芝加哥区域的人种分布[3],该网站提供了收入的分布以及2010的人种数据。作者探索的是城市社区(图中黑线描绘的)的一种现象,就是这些社区以不同的种族或者收入划分居住人群(具体可搜索Chicago’s official “community areas”)。

我们可以看到在不同种族的交接处有不同类型混合的点出现,这是我们可以进一步研究学习的地方。

接着我们在说说其他的问题。这个问题就是地理信息不完整的问题,就是将一些GPS不全的数据怎么在地图上展示出来。有一种做法就是将地图以区域分块,将同一区域但没有精确地理位置现象的数据通过随机放到地图上。但是这还是会影响我们获得知识的过程和精确度,这种方法的优化只有将每个区域限制得更小来减少信息的误差。

还有一种方法就是通过辅助的数据提供我们感兴趣的数据的地理信息。比如下图的人口普查,就将一些公园、公路和其他不是住房区的地理位置限制,不让我们将数据投入这些位置。

其他一些调优的方法还有将图例的展示之类,我的重点还是在其提供的两种防止点相互覆盖的方法以及缺失地理数据的展示限制。


参考

  1. dot maps@psu
  2. the Mackay nomograph@MappingCenter
  3. Chicago @MappingCenter

本文的原地址()

Paper Reader:Knowledge Generation Model for Visual Analytics

这篇论文[1]先阐述了知识产生模型,然后在现有系统上的佐证其模型的适用性,最后是总结以及展望下一步工作,中间穿插着很多其他模型、工具、理论。我的阅读侧重的是模型的建立基础以及内容阐述,其现有系统的应用以及分析方面一览而过。


  1. 知识产生模型
    可视化分析依赖于人的感知、认知推理以及领域知识以及机器的计算、存储能力的有效组合,最近的研究成果表明在可视化分析中需要将“人在分析过程中作用”转移到“人既是分析过程”的思维,也就是提高人在分析流程中的参与程度。可视化分析过程分为:高级别的领域内专家分析、和低级并的工具交互行为。其他人员关于于人力认知相关的行的研究太多,本模型不会契合所有已知的理论模型,本模型在前人的基础上只进行了第一步:建立生成模型,往后各个分析部分是怎么作用于知识生成过程的,以及整合相关的工作待后人研究。

    接下来的第二部分为知识生成模型的各部分的解释,第三部分为与其他模型的关系,第四部分为模型的应用,第五部分为讨论总结。

    知识生成模型是基于Keim等人(一批欧洲研究可视化分析的专家)研究的,Keim等人总结2年的VisMaster CA项目经验成一小册子《Mastering the Information Age Solving Problems with Visual Analytics》[2],本书的内容从可视分析到数据管理、数据挖掘、时域空域数据的分析乃至基础架构等都有所涉及,考虑时间原因,我直接跳到总结章节。文末提出可视化分析的涉及认知相关领域的成果有利于应用系统的设计,而分析的好坏取决于工具的选择、使用以及测试方法。其团队后来所做论文《Visual analytics: Scope and challenges》[3]也提到可视化分析是从信息、科学可视化发展过来,当下可视化分析的范围如图。

    其中我觉得重要的一点是:为什么有些信息无法自动获取,这表明了可视分析中人参与的必要性。我们不能在没有考虑清除是否使用的情况下,就将一些统计学上、KDD中的自动化方法加入到分析中。而可视化就是一种将自动化方法在信息空间中获取失败时有效的探索以及交互方式。知识生成模型的参考对象就是Keim等人提出的可视化分析模型。如下图:S表示数据源(data sources)、V表示可视化(visualizations)、H表示假说(hypotheses)、I表示顿悟(Insight),箭头表示信息的转变。DW表示数据预处理,VS、VH表示可视化,HS、HV表示假说生成,U*表示不同可视分析过程。相比“可视化分析是通过交互式可视化界面加强分析推理的科学”的定义,更加严格的定义就是: F : S → I。

    Pohl等人在《The User Puzzle—Explaining the Interaction with Visual Anlytics Systems》[4]中强调人的推理是可视分析中的重要一环,并探讨了人类的推理过程,5个相关的理论有意识构建理论(sensemaking theory)、格式塔理论(gestalt theory)、分布认知(distributed cognition)、图理解理论(graph comprehension theories)、技能-规则-知识模型(skill-rule-knowledge models)。

    • 意识构建理论:在与可视化分析系统的交互过程中,是一种反复生成假设的推理过程。获取信息、消化并生成新的信息(意识生成的知识)的问题-解决/探索过程。但意识构建强调的是信息在人脑中的表示以及重组这类细节建模过程,其缺乏与感知(perception)、系统交互相关的推理过程。因此无法适用与可视化系统的设计。
    • 格式塔理论: 在可视化分析中应用格式塔心理的中心思想就是顿悟——是对可视化信息的突然重组。在人脑中对信息的复杂的重组过程中,格式塔理论强调问题解决的表示。在可视分析中,问题的解决通常不是直接的,需要交互的多样性来获取多种方法。这类交互能提供一种信息转换(往往也是突然性的),从而能提供一种获得顿悟的机会。所以,该理论应用到可视化强调的是对信息的重组能力。而顿悟在可视化中充当了评估可视化的角色,这种顿悟的特点是复杂、深度、高质量、意想不到但有密切相关。 但需要注意的是复杂的格式塔理论是一种普遍性的理论知识,不是特别容易应用到可视化中的交互当中,因为没有细节上支持的交互很难最终引导系统的提高。
    • 分布认知:分布认知认为知识是分布在可视化用户、机器内包含的人类知识。该理论的应用关键在于人机之间的知识分布以及交互。认知上的行为可以帮助获取问题相关的信息,而实践操作则能帮助分析人员接近目标。怎么使可视化系统的交互平稳有效是应用分布认知的难点。
    • 图理解理论:其理论来源于视觉空间推理,其包含两个类别:第一类提高了用户与图表交互的描述,第二类是解释人怎样理解图表。通过该理论可以帮助设计以及优化图表的表达能力。但对于问题解决这种高级抽象的处理过程,应用该理论会缺乏对问题解决的整合能力。
    • 技能-规则-知识模型:该理论可能可以提供一种处在意识构建以及图理解中间级别的理解模型理论。该理论着重于特定应用环境下,错误类目的形成。但是过于专注于错误的方法在可视化探索这种错误高频发生的过程中也是不可行的。我们只需要利用该理论将可视化探索能更好追踪就行了。

    这篇文章随后将这几种理论横向比较(如图),如何将这五种理论整合利用,可以参考其空白特性的互补。

    说了这么多理论基础,回到这篇文章。知识生成模型是基于可视分析过程,并描述了知识在这过程中的产生。也就是将上述两方面的内容以更好理解的方式整合在一起。知识获取模型在前 人的基础上,定义一些相关的元素提高这种分析的理解。作者将模型分为两部分:计算系统部分以及人类相关的认知部分。人类处理大量数据力不从心,但分析细节上的关联性方面比较厉害。知识获取包含一些思维过程(溯因推理、演绎推理、归纳推理)。其模型的整体构成如图:

    可以看到计算机与人之间连接的通过人的交互行为来实现。而对于人类认知的过程划分为3个循环,从探索环、证实环乃至知识产生环是一个越来越抽象、精炼的过程。接下来通过文章介绍每个具体的组成要素。

  2. 模型解析

    计算机这边的基础要素是数据,数据的获取、选取等处理决定了数据的分析质量。原始数据的处理决定着分析的真实性,而元数据提供给分析系统关于数据的数据,这包括数据的结构、特性描述以及数据的概括信息,这些信息通常帮助可视化工作实现数据的展示。由问题的特点决定数据的使用,但通常来说,这两类数据在可视化中不太区别对待,而是直接获取或者展示。在数据之上需要产生模型,模型可以是简单的统计分析描述,也可以通过KDD的一些复杂算法将数据中的模型以及一些复杂的模式找到,这里也包括机器学习方面知识(逻辑回归等)。这些模型在分析过程中提供给简单的计算任务或者顿悟。可视化结果的来源的就是上述的数据或者模型。可视化依赖于数据模型(聚类模型),对于模型的方法依据可视化的当前状态有所不同,比如不同尺寸的缩放级别(zoom level),呈现数据的不同属性。分析人员与系统最直接打交道的地方也在可视化结果这个层级。

    接下来的3个循环就更依赖与分析人员的行为,第一个比较初级层次就是探索环,其分为行为(action)以及发现(finding)。这一层级主要为交互操作产生的新模型或者可视化结果,行为依赖发现或者分析的任务目标。单个行为是能引起到系统原子级别响应的动作,这与用户的目的和可视化结果更相关。一个简单的寻求“how”的行为业余更高级别的“why” 相关。一般来说源于假说(针对问题的解释或者预测)的行为更加复杂,而源于发现的行为则较简单(上图中’Action‘的入度)。作者也将行为进行了分类(如图):预备、建模、使用模型、可视化映射、模型可视化映射、操作。

    发现则是系统操作得出的具体观察。发现能指导下一步操作或者顿悟的产生。发现也可以是一个新模式。这么说,模式除了机器自动识别,也可以通过人力发现,这不局限与数据、可视化结果,也可以仅仅知识分析人员感兴趣的地方。在知识产生模型中,发现是独立于问题域的,虽然解释发现需要分析人员依据问题域的环境。通过模型整体构成图可以看出,发现并并不一定需要指导顿悟的产生,也可以来触发简单的几个操作操作。探索环可以看作使用可视化分析系统来搜索有用的发现内容,探索环的这两个部分与整体紧密相关。分析人员如若理解并能在具体问题上解释发现就能获得顿悟(insights)。

    证实环指引探索环的结果来佐证假说或者形成新的假说。发现能验证一个已有的假说,但随之产生的顿悟可以与假说无关,而是解决不同的分析问题或者产生新的发现问题。假说在可视化分析中扮演来中心角色,验证假说的真实性是为了获取新知识(解决问题)。通过统计测试或者可视化可以验证具体假说,但假说一般是模糊不清的,比如有一个未知的元素影响问题域。这就需要更具体的假说来解决问题,而这一步只能回到原来的探索阶段。

    顿悟在前面有所涉及,其在本模型的定义为:相比认知科学立德顿悟更加高级,能通过已有知识生成新的知识。顿悟与发现的区别在于,顿悟还需要当前的问题域进行一番解释。作者认为顿悟并不直接是知识,需要有比较严格的证据来证实顿悟,顿悟这就成为来了假说。

    知识产生环在可视分析过程中产生新的知识给分析人员。无需过多的分析,其组成要素——知识就是来源于上一处理流程的顿悟。当顿悟结果验证并形成新的知识时,也会影响假说的状态。可视分析从数据和分析的问题开始,并存在已有的知识,这些知识也会影响分析的处理过程。

    人类的认知思维参与的3个环节填补了从数据到知识的处理流程,将数据以发现、顿悟以及知识的层次展现,也体现了可视化的目的。文章接下来介绍了相关的模型以及应用。

  3. 相关模型以及应用

    相关的模型如下图

    系统层级上,信息可视化管道(InfoVis Pipeline)包含了主要的数据类型:未加工的数据、数据表、可视化结构以及视图。而KDD使用数据挖掘的技术获取模式。而前面提到的Keim一批人所提出的可视化分析模型包含的两部分:可视化数据探索部分(从可视化结果到知识)以及自动化数据分析部分(从模型到知识)。总之,在系统这一层,数据转变成了可视化结果。

    在交互层级上,作者参考了Norman提出的7层行为,其中值得注意的是执行行为以及评估行为。执行隔阂(The Gulf of Execution)表示分析人员不知道怎么行动,而评估隔阂(The Guld of Evaluation)则表示分析人员无法评估行为的结果。这两类问题会在系统使用中发生,而目标(Goal)的引入则有效的评估行为以及结果,在作者提出的模型中,以假说作为目标,执行则是从假说到分析系统的过程,而评估则依据模型中分析人员七种的操作行为(也就是图中的右侧的七个箭头)。而行为的不同交互类型依据针对的方面、字段、领域不同分为:大多数操作针对一至两个字段,比如可视化、推理、数据处理等。而操作类型依据目的的部分也可分为Why、How、What,Why属于高级的操作目标,而how和what则是相对比较初级的操作目标。高级的操作来自于证实环的假说和顿悟,而低级的操作来自行为和发现。

    在认知推理层级上,人类可以观察可视化结果和模型的改变来进行知识的生成。作者提出的模型将意识构建循环化分为上述的3个循环。推理和决策都依赖与心理模型的构建。而根据Legrenzi指出,决策构建的关键组成为信息获取、形成假说、产生推断、衡量利弊以及应用决策准则。人类认知模型(The Human Cognition Model,HCM)也应用到了知识产生模型中,信息探索和知识构建是HCM的核心。信息探索在知识生成模型中以直接与数据打交道的方式实现,而相关模式的产生源自证实环的顿悟,而最后知识的产生则是知识产生模型的中心角色。总之,探索包含整个知识产生过程。

    作者描述模型在现有工具的体现主要目的有:第一,模型的交互可以得到检验;第二,可以检查是否一个系统支持3个循环;第三,可以使用该模型进行不同应用工具的横向比较。但由于我只接触过Tableau。我就只看工具的横向比较,具体可看下图。不同的颜色深浅表示相应理论的支持程度。由图可知,目前的成熟工具主要都是满足于可视化探索的需求,其知识产生方面比较薄弱。

  4. 总结、讨论及展望

    作者提出的整个知识产生模型将人机两个融合到了一个系统,不仅仅在全局上提供了完备的论述,在细节上也提出了几个新方面,主要是将人类的认知思维融入整个系统当中,体现了分析人员对于可视化分析的重要性。文章中的模型目前还只是阐述了一个独立的分析过程,当涉及协作分析的要素,知识的分享就需要在分布认知理论上考虑。而一个带有他人评论的可视化结果可能可以揭示数据之间有意思的方面,这就涉及到发现或者顿悟在同行之间的分享了。知识生成的自动化方面依据不同的层级有不同的发展潜力。在探索环中,系统应该能够提供机器计算的前后状态切换,使得分析人员在交互中学到更多。自动检查意想不到的结果是复杂的,因为这需要分析人员定义什么是他所期望的。而在交互中给分析人员提供下一步操作的建议则是比较有意义的事情,也就是以行为驱动的推荐系统。在证实环中,系统可以针对分析后的发现数据,自动地生成假说也是一个比较合理的改进,当然一般是针对数据之间简单的关键产生的假说。在知识产生环中,自动地从顿悟转换为知识或者从知识转换成假说是不可能实现的,因为这需要分析人员的参与。

    对于可视化分析系统未来的发展,作者认为数据趋于越来越复杂以及动态,人与模型之间的交互将会处在更加重要的地步。所以关键是可视化分析的交互方式,所以我们需要考虑交互的代价。而当今分析系统的单向操作应该也能改进成能返回先前的分析结果的功能。总之作者提出的模型将3个循环框架融入进模型,并阐述了人机组合作为可视分析的基础的可能性。


参考:

  1. Knowledge Generation Model for Visual Analytics
  2. Mastering the Information Age Solving Problems with Visual Analytics
  3. Visual analytics: Scope and challenges
  4. The User Puzzle—Explaining the Interaction with Visual Anlytics Systems
  5. 北大可视化小组关于本论文的报告

以及博客地址

MongoBD+Solr全文搜索的历程

当存储到数据库中的数据涉及到文本,针对文本的搜索需求就应运而生。MongoDB也支持文本的搜索,不过很可惜的是,MongoDB的$text不支持中文分词功能,在搜索中文文本时只会字符的匹配,这在使用上非常不方便。

最近做的一个项目中就有这个需求。师哥推荐solr+mmseq4j的组合实现中文搜索。最初的设想只是针对邮件系统的标题以及正文两个字段的提供搜索。在2次迭代后,增加了其他一些关键字段的索引,以提供搜索页面的二次过滤。本文首先简介Solr工具,在实机安装了Solr,接着实现针对中文文本字段的索引,以及一步步的增加索引字段以实现项目需求。技术上解决的问题主要有:Solr获取MongoDB增量数据,针对数组对象建立索引。


1. Solr安装

Solr是Apache开发的开源搜索服务器,基于HTTP和Apache的Lucene。也就是说,索引的生成、搜索都是通过向Solr服务器发出HTTP请求。

在 Solr 和 Lucene 中,使用一个或多个 Document 来构建索引。Document 包括一个或多个 Field。Field 包括名称、内容以及告诉 Solr 如何处理内容的元数据。例如,Field 可以包含字符串、数字、布尔值或者日期,也可以包含您想添加的任何类型。Field 可以使用大量的选项来描述,这些选项告诉 Solr 在索引和搜索期间如何处理内容。

在接下来的安装使用中继续说明不同Field的参数。这里在罗嗦一些基本概念。Solr的分析主要涉及到3个组件:分析器(Analyzers)、分词器(Tokenizers)和标记过滤器(Token Filters)。分析器用来预处理文本。分词器将文本分割成不同的token,这些token传递给标记过滤器来增删改token。这里还有提一下字符过滤器,它用来预处理字符。一般地,一个分析器由零个或多个字符过滤器,一个分词器以及零个到多个标记过滤器。这些都是概念都是为了衡量文本之间的相似性以便搜索文本内容。

例如,Solr 的 WhitespaceTokenizer 根据空白断词,而它的 StopFilter 从搜索结果中删除公共词。其他类型的分析包括词干提取、同义词扩展和大小写折叠。如果应用程序要求以某种特殊方式进行分析,则 Solr 所拥有的一个或多个断词工具和筛选器可以满足您的要求。

正如前面所说,Solr通过HTTP请求来进行索引操作,而Solr的索引操作分为如下4种:

  • add/update:添加文档或更新文档。直到提交后才能搜索到这些添加和更新。
  • commit:明确更新,使得上次提交以来所做的所有更改都可以搜索到。
  • optimize:重构文件以优化搜索性能。
  • delete:删除文档,可以通过 id 或查询来指定。

这里的文档就是需要搜索的对象文件。Solr的搜索命令只接受GET和POST方法,收到请求的Solr通过SolrRequestHandler来处理。

图来自IBM文档

上面简单介绍了Solr的特性,其内部技术核心还是比较复杂的,接下来就介绍安装配置Solr的方法。

2. 安装配置Solr

安装的Solr版本为4.10.3,在阿里云的Centos系统上。整个安装过程比较顺利,但在开始选择版本上做过多的犹豫。解压后进入solr-4.10.3/exmaple/multicore/目录,我们使用multicore的场景,这是solr.home就是multicore了。接着在mmseg4j-solr@github上下载mmes4j,版本选择匹配的2.2.0(里头3个文件分别为:mmseg4j-analysis-1.9.1.jar、mmseg4j-core-1.10.0.jar、mmseg4j-solr-2.2.0.jar)。将3个jar放入../multicore/lib中就安装完成了。

接下来配置Solr的模式(Schema),模式可以由类型、字段和其他声明组成。这里我们首先需要定义类型,然后依据类型定义字段。用编辑器打开./multicore/core0/config/schema.xml。可以看到Solr已经预定义了如下类型:

mmseg4j的中文类型的配置也挺简单,这里我们选择中文分词的’complex’模式,在schema.xml文件中配置一个fieldType节点,如下:

然后就可以在field节点中引用该filedType了,假设你有个字段叫my_content需要支持中文分词,只需要定义示例filed节点如下:

接下来我们依据项目的具体业务设置需要搜索的字段,其中MongoDB的主键id字段就是原始的id字段,我们需要修改其字段并为id,并且将uniqueKey也修改为<uniqueKey>id</uniqueKey>。记得我们需要搜索的中文字段为邮件的中文以及标题,所以这里把两个字段的字段设置为’textzh’类型。

最后一步就是运行Solr了。回到刚才的example目录,运行目录java -Dsolr.solr.home=multicore -jar start.jar

这是打开浏览器的8983端口,就能看到Solr的后台。慢着,我们的数据还没有,需要到MongoDB上获取。这里使用mongodb-labs的mongo-connector,按照官方说明安装即可。

3. mongo-connector的安装与使用

mongo-connector的工作原理就是获取MongoDB副本集的oplog(操作日志),日志信息记录了文档的CRUD中的时间、命名空间以及修改的文档数据等。具体可以看Oplog文件@官方WIKISystem Overview@官方WIKI。嗯,所以使用之前需要将副本集配置好。

这里就设置一个简单的主从副本,端口分别为27001、27002。配置先开启2个mongod程序,再进入主节点配置副本集。

这样就将MongoDB启动了,接下来安装好mongo-connector,并运行程序(可全局运行)。

这样就行了吗?这样会报错的,还必须在Solr的Schema中存储metadata的定义以及开启LukeRequestHandler开启。在schema.xml中添加如下片段。

 

以及同目录下的solrconfig.xml

通过netstat命令可以指定mongo-connector的端口以及监听端口。

命令output

这样mongo-connector就能监听MongoDB的数据变化并传递给Solr。整个搜索的基础架构如下。

搜索架构

4. checkout:搜索

这一部分通过solr的web后台,进行数据搜索的测试。首先在浏览器输入:http://serverIp:8983/solr/#/core0。注意我们Solr采取的是多核模式。

core0的首页面如下,展示的是总文档数以及系统参数。

首页面

字段分析器的测试如下。

分词

最后query页为搜索的功能测试,可以看到结果还是蛮理想的。

5. 数组的索引

第二次迭代主要是满足搜索的需求,比如检查邮件是否有附件,在通过imap获取邮件时,检查附件数组,并存储到邮件的一个字段记录是否有附件hasAttach。这个好处理,直接添加schema中的字段即可。

比较麻烦的是最后一个需求需要去索引数组中的元素。这里就要引入一个schema配置中的动态字段(dynamicField)、和复制字段(copyfield)。动态字段可以通过来匹配多个字段,这又要提到在mongo-connector会将数组扁平化。比如一个数组rules,形成rules.0.ruleId以及rules.1.ruleId这样的字段,我们可通过动态字段rules来匹配这些数组里的元素。但是我们无法在Solr中通过rules*来搜索字段,这时就要通过复制字段来搜索了。复制字段的两个属性source、dest如同一个管道一样将source数据映射到dest上,当搜索dest时,就会到source上去建立索引。那么在我们的第三次修改中将数组字段这样处理,具体的配置信息如下。

其实这种方式是十分丑陋的,copyField会将source字段中的所有元素转换为dest的同类型数据,比如我在rules数组下有一个int类型的变量来标记状态,但搜索时的结果就转换为string了。其次,资源的消耗也增加来。但对于目标搜索还是能够满足的,暂且就这样妥协的实现。

5. Node as Solr Client

最后再简单描述一下node端调用Solr的HTTP接口,这里使用的是solr-client库。

最后做出的搜索应用如下,还不错。

搜索


参考

  1. 《使用 Apache Solr 实现更加灵巧的搜索,第 1 部分: 基本特性和 Solr 模式》@IBM文档
  2. Analyzers, Tokenizers, and Token Filters@SolrWiki
  3. Solr与MongoDB集成@cnblogs
  4. 《论mongo-connector如何将MongoDB中的json数组和嵌套对象更新至Solr引擎》@csdn

以及博客地址

使用Async进行流程控制

Async[1]提供了一些使用的工具,比如对象操作以及流程控制。我使用它的原因主要是解决js中回调函数嵌套过多的问题,在一边使用一边学习中习得了更多的功能的使用。在初识Promise就已经提及Async回调函数的控制内部机制。本文主要总结在项目中用到Async控制流程的部分,其他API的使用可以查看官网。

1. waterfall 瀑布流

看到waterfall立马想到的是软工中的瀑布模型,其实意思也是字面意思。通过将任务分配到每个回调函数,可以充分利用到I/O的性能。但是当每一步的开始需要上一步返回的结果时,就进入到一个需要step by step按序执行任务的需求的,比如从数据库获取不同集合的文档数据,再处理成一个整体的数据存入数据库的另一个集合中。

这么说可能有点模糊,这里引入一个场景:在项目的邮件系统中,需要获取某个类别(category)的邮件列表,而前端向node端提供的查询条件为分页、状态过滤以及类别的id。在生成完查询语句后,我们需要将任务拆分为几步:第一步,通过Mongoose的.count()方法获取该类别下用户的未阅数量;第二步,按照前面的查询过滤条件向MongoDB请求获取一页的邮件列表。

Mongoose的方法遵循异步编程,也就是Async发挥用处的地方了。其waterfall的使用方法如下:waterfall(tasks, [callback]),tasks为函数数组,而callback参数可选,在每个子任务完成后执行,并传递最后一个task的回调参数。在tasks中每个task通过callback(null, param)来传递到下一个task,而第一个参数为捕获的错我,null表示没有错误,当然也来用传递标识量提前退出waterfall。以function(param1,…,callback){}分割每个task,一般出现回调函数时,就在回调函数中跳到下一个task。具体如下代码:

对于上述代码中不熟悉的Mongoose操作可以看Mongoose系列中了解详细内容。细想一下,waterfall就是将异步运行强扭成了同步运行,所以使用waterfall还是需要仔细划分业务逻辑,尽量减少watrefall的水量(water)。

2. parallel 并行

parallel(tasks, [callback]) 的参数和waterfall一样,不过tasks是并行执行的。在原始的流程中,通过条件判断来确定需要更新数据库中的Configs配置信息。后来发现只要当前端传递过来的参数中有更新数据的关键字段就表示需要修改某部分配置信息。那我们只需要并行的检测是否有一些字段就可以同时更新数据库的数据了。代码如下:这里精简了一些对async内容无意义的代码。

3. each 数组迭代与 forEachOf 对象迭代

正如中文表述的一样,两者在方法使用上时一致的,只是一个是对数组迭代,一个是对对象迭代。使用方法为: each(arr, iterator, [callback]) 对应对象的操作只需要修改方法名为 forEachOf(obj, iterator, [callback])。迭代器iterator针对每个元素进行操作,在操作之后。需要强调的是,迭代时并发处理每段函数,如果需要按序处理每个元素的话需要调用 eachSeries(arr, iterator, [callback])。

实际使用的场景是这样的,当修改category的status字段为1,将category的_id更新到每个用户的colMenu和ruleMenu数组。所以我们将迭代用户数组,先将用户数组的用户数组需要更新的字段获取,在async.each()中对每个用户的对应数组增加元素。具体如下代码:

4. whilst 有条件循环

async.whilst(test, fn, callback) 重复地执行fn,直到test返回的false进入回调函数callback。所以这里需要有一些变量来判定条件的是否。

项目中使用了solr对数据库中的数据建立索引。在邮件过滤的业务中,需要获取solr的返回,在下列代码,gotLoop变量在每个迭代中增加一次,表示从solr端获取的一次数据。而pageLoop是以500份分割索引数获得的总页数。这样每次迭代就只获取500份数据的_id。

注意代码中的25行,调用的回调函数 cb(null, mailIdsArr); 为waterfall的回调函数,下一个task为更新mailId指定的邮件相关字段。将两个结合方式可能会占用不少资源,但提供老清晰的逻辑,便于代码修改和审查。


总结

通过Async这几个比较使用的方法,解放了js中回调嵌套过多的问题。使用Async的重点是何时使用这个工具,比如提高I/O操作的效率以及减少重复的代码。

参考:
  1. Async的github页面

以及我的博客原址

MongoDB更新操作符的实践

和查询操作符类似,更新操作符处理最复杂的对象也是数组。我们按照上一篇文章的方法,先总结我所用到的操作符,再结合项目中遇到的实例组合使用操作符。


1. 字段更新操作符

上图描述十分清晰地阐明了使用场景。$inc不仅可以增加还可以减少字段的值。$setOnInsert则需要和{ upsert: true }一起使用,当需要插入新数据时,$setOnInsert就会实效;反之则不会触发插入新字段。

2. 数组更新操作符

$的作为为占位符,匹配查询条件的数组元素以提供更新数据的数组位置,由于其使用条件的限制,就不能和upsert参数一起使用,因为upsert插入新数据时会将$解释为字段名的一个元素。比如下面的代码就是将grades数组中的grade为80的元素std更新为6。

$addToSet更新不存在的数组元素,如果参数是传递一个数组,则将整个数组以一个更新上去,只有与$each一起使用才可以同时更新多个元素。比如下面的代码就是将”camera”, “electronics”, “accessories”更新到tags数组中。

接下来的5个就说数组更新操作了。

3. 修饰符

$slice将指定数量的数组元素保留到文档中,$position则是将数组元素插入数组的指定位置。其他不表。


1. 添加数组的元素案例

operations字段的格式为对象数组,而$push操作能将新添加的operation数据插入到数组最末端。Mongoose的update方法的第二个参数就是更新数据。

2. 添加对象数组的字段案例

与上篇文章的privateInfo字段格式一样(如下),我们需要依据应用的业务逻辑,添加数组中的_creator和userStatus字段。

我们就可以这样编写更新语句。

3. 移除数组元素案例

通过在数据库中记录特征字段来指定文档的临时状态,当需要取消这个状态或者条件时,就需要清楚记录。比如一个标记实效、收藏操作的一个取消操作。这里需要使用$pull操作符。 以取消精华操作举例,每个用户都可以将数据设置为自己的收藏或者取消。

4. 更新不存在的数据案例

当我们按照数据定期获取数据时,不可避免的出现重复数据的更新,而我们只需要没有提交到mongodb的数据,当$addToSet与$each一起使用时就能提供这个功能,在$each的参数设置为一个数组,里面有数据库已经存入的数据。比如当我们通过一个字段’contactAddress’与mails的mail.to.address字段匹配时就插入ContactLists数组表示对同一个联系人的归类的功能实现。

所以Mongoose的对象是ContactList,而使用findOneAndUpdate方法的意义在于当数据库没有查询的数据中时能插入当前数据。


总结

上述内容就是MongoDB更新操作符使用,update的操作符在使用上可能更常见,数组操作也是最复杂的。通过这篇文章和上一篇文章总结了MongoDB的一些比较常用的操作符,希望在以后的项目中更新提供姿势水平。

参考
  1. 更新数组元素
  2. 删除数组元素
  3. upsert数组元素

博客地址

MongoDB查询以及投影操作符的实践

当使用MongoDB提供复制操作时,需要简单的CURD操作基础上使用一些操作符(如同$eq这样的操作符),mongoDB官网文档[1]有其详细介绍。需要注意的是,Node端所使用的Mongoose工具,在API上与原生的驱动还是有细微的区别。,本文结合mean.js(MongoDB, Express, AngularJS, and Node.js)开发实例来学习查询操作符的使用,先介绍做项目涉及到的操作然后结合实践中的涉及的内容。


1. 比较操作符

比较查询操作符符则有两类,一类是大于($gt、$gte)、小于($lt、$lte)、等于($eq、$ne)这种2值比较,一类是是否与数组内元素匹配($in、$nin)。当数值型数据进行比较时,MongoDB会先转换再进行比较。

2. 逻辑操作符

逻辑查询操作符有4种,分别是:与($and)、或($or)、非($not)以及或非($nor)逻辑。$and、$or、$nor后接大括号来框住的所接受的条件。而$not的使用有点不同,需要在指定字段之后,可以匹配后接的子条件以及该字段不存在的情况。

3. 元素操作符

元素选择器中$exists就是来选择指定字段存在数值的文档,$type则是选择字段的数值类型为指定类型,BSON当中的数据类型都有相应的数字来表示,这种类型数字就是在这里使用了。

4. 数组操作符

这3种针对数据选择的操作器如字面意思理解就可以了。需要注意几点就是:$all相当于$and 的行为一样,也就是数组中的元素条件要同时满足所以条件,而$elemMatch则需要对象数组中满足$elemMatch条件的所有条件,并且只返回第一个匹配的数组元素,这里还可以联系依稀$in选择器,$in限定来选择条件为一组值来匹配数组,而$elemMatch匹配的是对象数组元素。

5. 投影操作符

投影(project)的功能可以与SQL中的select类比,满足此条件的文档只返回包含指定字段的结果集。位置$符来匹配查询条件的第一个文档,$一般伴随着其他条件来限定数组元素的获取。比如这个条件 { semester: 1, grades: { $gte: 85 } }, { "grades.$": 1 } 就是在满足前面两个条件下,只获取grades数组第一个匹配的元素。$elemMatch如上述的应用一样返回符合匹配的第一个元素。

$meta的意义比较有意思,它的格式定义为{ $meta: }, 里头的metaDataKeyword指定’textScore’为关键词。这里需要引入$text文本搜索(其格式为{ $text: { $search: , $language: } })来设置搜索文本的查询条件,引入来TextScore来评价文档与搜索关键词的关联性。当$meta指定’textScore’时,查询结果就是文本相关的分数。

$slcie就如同数组操作slice一样,只返回指定数量、范围的元素,如db.posts.find( {}, { comments: { $slice: [ -20, 10 ] } } ),就只返回从comments数组倒数20开始的前10个元素。

照本宣科就到这里,还有一些类别(Evaluation、Geospatial)的操作符没有提到,是因为这些类别暂时没有使用过。下面汇总项目中使用到的查询实例,以便项目接盘侠能轻松上手。


1. 针对数组查询案例

第一个场景是数据库集合中存储了数据处理的两个状态,一个是公共编辑的状态(‘commonStatus’),一个是用户编辑的私有状态(‘userStatus’)。业务上需要通过这两种状态的组合获取数据,比较麻烦的地方就是用户私有的信息需要涉及数组操作。具体数据结构如下:

私有状态的业务的查询是这样的,前端传递’userId’和’userStatus’给Node端,当’userStatus’为’unchecked’时在数据库中可能的情况有3种:第一种情况为’privateInfo’存在当前用户其userStatus为’unchecked’,第二种情况为privateInfo数组存在但没有当前用户数据,第三种情况为privateInfo数组为空的情况,也就是集合中文档的缺省值。查询语句的编写如下代码

这样就将’unchecked’状态囊括了,但其他私有条件还进行不同条件的查询,如下所示,前端传递个Node端私有状态的值提供查询。

2. 时间范围查询案例

接下来的案例是获取时间时大于指定时间的情况,直接使用$gte比较操作符就可以来,


总结

本文简单罗列来mongoDB的查询以及投影操作符的含义并解释来两个实例应用。其实还有一些需要实践经验才能知道的,比如参考后两个就是比较典型的例子,总体来说mongoDB操作符不难,多多实践就可以来。mongoDB下一部分为更新操作符涉及来数组的操作以及$符的占位作用等,这些都在暑假实践过,更加有意思。

参考

  1. 官网文档的操作符介绍
  2. Mongoose内嵌对象查询
  3. Mongose对象数组查询

以及其他

个人博客地址

初识Promise

对于mongoBD的异步控制,在项目实践中使用过async库来控制复杂的异步回调函数处理。很有幸参加i5ting老师的《Node.js最新技术栈之Promise篇》微课堂。老师主要从promise的起源、实现以及实践和展望几点简述使用Promise的心得。有所收获,现在总结如下,其中例子为老师提供。

roadmap


  1. Why首先回调和异步在nodejs中十分常见,因为nodejs的最大的优点:高并发,适合适合I/O密集型应用,就是通过异步处理实现。但回调函数的嵌套会导致意大利面条式的代码[1]。比如下面这种一个分为4步的任务:每步任务都需要上一步的返回才能继续执行。

    promise[2]就是来解决js中这种频繁回调导致的问题,promise是可以通过像jQuery的链式调用这样来控制回调函数,链式写法:$('#tab').eq($(this).index()).show().siblings().hide();。每个方法都返回this对象传递给下一个方法。比如下面这个例子:

    输出结果abc以及dba。这就是promise,而promise的意思是还未获得的值,只是作为一个占位符给下一个方法。正如promise是还未获得的值,deferred表示未尚未完成的任务步骤,而deferred可以被解决(resolved),也可以被拒绝(rejected)。

    回到上面的回调函数:step1().step2().step3().step4(),每个操作都是独立的,可组装。但如果需要上一步的输出为下一步的输入,比如linux的 pipe:ps -ef|grep node|awk ‘{print $2}’|xargs kill -9,或者需要捕获异常,控制流程呢?这就是promise规范的起因。

  2. What官方给出的promise/a+规范的定义为:Promise表示一个异步操作的最终结果。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。还是上面那个例子:异步操作的最终结果: return step1().step2().step3().step4()。如果在promise里,就是这样:

    定义中交互方式的几点都都对应了,step2传入then方法来获取step1的返回值,catch来捕获error,catch后的回调函数就是处理error。有一个reject,还对应一个resolve方法。

    • reject 是拒绝,跳转到catch error
    • resolve 是解决,下一步,即跳转到下一个promise操作

    比如下方这个例子,step1失败就不运行step2,调到下一个promise操作,执行step3。

    return step1().then(step2).then(step3).then(step4).catch(function(err){ // do something when err });

    再来看看promise的几个要点。

    • promise 是一个包含了兼容promise规范then方法的对象或函数。我们可以这样理解,每一个promise只要返回的可以then的都可以。就像上面举例返回的this一样,只要每一个都返回this,她就可以无限的链式下去。
    • thenable 是一个包含了then方法的对象或函数。
    • value 是任何Javascript值。 (包括 undefined, thenable, promise等).
    • exception 是由throw表达式抛出来的值。当流程出现异常的适合,把异常抛出来,由catch err处理。
    • reason 是一个用于描述Promise被拒绝原因的值。

    最后总结一下promise,1) 每个操作都返回一样的promise对象,保证链式操作;2) 每个链式都通过then方法;3) 每个操作内部允许犯错,出了错误,统一由catch error处理;4) 操作内部,也可以是一个操作链,通过reject或resolve再生成流程。还有一些复杂的规范细节[3]需要在实践中才知道意义。

    nodejs里的promise实现主要有一下几种,最近在使用async来控制回调流程。

    • bluebird 拥有不错的性能,后面继续讲
    • q Angularjs的$q对象是q的精简版
    • then teambition作品
    • when Promises/A+和when()的实现
    • async 最简单的)
    • eventproxy 朴灵作品,使用event来

    其他语言实现,详见https://promisesaplus.com/implementations

  3. how下面进入到如何实现promise的内容,一个很初步的原型如下代码:

    我们还可以看看q,angularjs的$q就是它的精简版,它把q的7个版本是如何实现的都详细记录4了。

    了解到基本的实现就需要考虑在项目中的实践了。i5ting的经验是公司项目使用bluebird,而小项目使用async。几种实现的benchmark。这样一看bludbird确实性能比较好。比如并行执行任务的比较如下:

    bluebird特性

    • 速度最快
    • api和文档完善,(对各个库支持都不错)
    • 支持generator等未来发展趋势
    • github活跃
    • 还有让人眼前一亮的功能点

    下面举例的是bluebird的promisify,promisify原理就是你给他传一个对象或者prototype,它去遍历,给他们加上async方法,此方法返回promise对象,你就可以像上面那样使用promise的特点了,但这样得谨防对象过大,导致内存问题。熟悉fs的API的都知道,fs有fs.readFile()fs.readFileSync()方法,但没有fs.readFileAsync()。实际上fs.readFileAsync()是bluebird加上去的,使用promisifyAll()来实现这个功能。

    按照MVC的流程,首先定义模型:

    然后就是业务逻辑,直接调用findAsync的:

    可以优化的地方在于,直接mongoose的static和method上扩展,不暴露太多细节。面对promise,保证每个操作都是函数,使得流程可以组装。比如下列这种情况,Team可以添加新用户:

    扩展一下数据库driver这边,ioredis也支持的很好,能将与redis的交互建立在promise的控制下,而mongoose支持promise也越发证明这个工具与原生driver的不同。

  4. es6的实现 generator/yieldgenerator是es6新添加的功能,generator(生成器函数):不会立即执行,需要再执行迭代操作(.next),yield抛出断点等待next的调用。使用这种方法的库有co,其使用例子如下:

    而yield的并行直接则是这样写:yield[fun1(), fun2()];

  5. es7的实现 async/await
    es7则通过async关键词来执行异步操作,使用await执行异步操作的例子如下,可以看到这与async库之间的区别: 

    最后再总结一次promise的要点:

    • 异步操作的最终结果,尽可能每一个异步操作都是独立操作单元
    • 与Promise最主要的交互方法是通过将函数传入它的then方法(thenable)
    • 捕获异常catch error
    • 根据reject和resolve重塑流程

    而generator是一种新的定义方式,定义操作单元,尤其在迭代器的情况,搭配yield来执行,可读性上差了很多,好处是真的解耦了。co是一个中间产品,可以说是给generator增加了promise实现,可读性和易用性是愿意好于generator + yield的。最后我们看看async,它实际上是通过async这个关键词,定义的函数就可以返回promise对象,可以说async就是能返回promise对象的generator,yield关键词以及被generator绑架了,那它就换个名字,叫await。

    其实从这段历史来看,反复就是promise上的折腾,只是加了generator这个别名,只是async是能返回promise的generator。

    这次学习大致就是这么多内容,其细节还需要在项目上实践,也确定了使用bb作为以后express的异步控制库。


参考:

  1. Spaghetti Code:意大利面条代码的定义
  2. Promise Github:源码
  3. Promise+官网:规范细节
  4. q的7个版本详细记录:设计思路
  5. Promise & Deferred objects in JavaScript Pt.1: Theory and Semantics.:promise和deferred这些概念的理解

以及其他

个人博客地址

MongoDB理论浅入浅出

本篇文章抛开以前使用MongoDB的开发细节,完完全全作为MongoDB的吹水文。前半部分主要简单讲述数据发展过程,后半部分主要讲述MongoDB这种文档型数据库的特征和功能。


数据库发展简史

在软件工程中,数据建模是运用规范的数据建模技术,建立信息系统的数据模型的过程。数据库把信息系统中大量的数据按一定的模型结构组织起来,提供存储、维护、检索数据的功能,使信息系统可以方便、及时、准确地从数据库中获得所需的信息。在计算机诞生初期,计算机的功能是进行科学计算,数据存储在穿孔卡片上,软件和硬件都不支持存储和管理大量的计算结果的。先是外存储的出现推动软件技术的发展,随之而来的操作系统提供来专门管理数据的文件系统,文件系统设计之初被构想成包罗万象的组织范式。数据库管理系统从60年代后期开始萌发,发展阶段大致分为navigational database、relational database(SQL)、post-SQL database3阶段。

  1. navigational database

    old database

    20世纪60年代,数据库管理系统刚开始出现,这个时候出现了两种数据库的结构范式:层次模型和网格模型,相对应的数据库管理系统为层次和网状数据库管理系统,他们统称为navigational database,比较著名的数据库管理系统有IBM的IMS、CODASYL。这一阶段的数据记录的寻找使用pointer、path来定位磁带上的位置。通过这种“当前记录的下一条记录”的方式,来查找数据可想而知是非常慢的,不过在当时已经是一次性能的提升了。

    Sample From Wiki

    比如上图这个例子,当我们需要从Node6读取到Node1的数据,则需要通过这两个节点的连接(Node6->Node4->Node5->Node1),这个过程就是导航(navigation)嘛。如同编程语言中的goto语句,这种原始的方式被批评为无组织的意大利面条。随着时间的检验和技术的进步,第一代数据库在80年代被设计得更为合理的关系性数据库取代了,但是这种思想依然保留着,比如DOM的层次结构,并启发了图数据库这种非关系性数据库的设计。

  2. relational database

    70年代,IBM的员工Edgar F. Codd发表了一篇名叫《A Relational Model of Data for Large Shared Data Banks》的论文,这篇论文提出了关系模型这一革命性的概念。关系模型是基于谓词逻辑和集合论的一种数据模型。这种模型以二维表作为数据的组织形式,也就是本科学习数据库的笛卡尔集。这种关系表以列为属性,每一列的元素为同一数据类型;以行为记录,每一行的元素不能完全相同。行列的顺序没有严格限定。

    关系模型应用在关系型数据库上并成为其结构范式。关系型数据库依据此范式将应用的数据存储在数据表面,数据操作则通过SQL(结构化查询语言)查询语句来操作。这种结构化查询语言核心是对表的引用,使用其作为数据的输入与管理。随着而来的索引和日志记录功能让关系型数据库满足了当时大量的计算应用的大量数据的存储和读写。

    Picture From Wiki

    关系模型由关系数据结构、关系操作集合、关系完整性约束三部分组成。数据结构遵守数据库规范化来减少数据更新的开销,也就是通过满足一级级的范式(normal form)来限定数据结构。现在的数据库设计最多满足第3NF,其原因为范式越高,虽然具有对数据关系更好的约束性,但也导致数据关系表增加而令数据库IO更易繁忙。关系操作集合包括查询操作和插入、删除、修改操作两大部分。关系完整性约束是为保证数据库中数据的正确性,包括数据库完整性、实体完整性 、引用完整性。最开始两大RDBS原型有Ingres和System R(又是IBM的)。承接Ingres原型的系统有MSSQL、Sybase等,而承接System R的系统有Oracle、DB2(又是IBM的)。

    RDBMS的这些复杂的约束能保证系统的可靠性(数据一致性),但缺乏可伸缩性(扩展)。数据模型的表达能力也比较差,修改成本因为结构本身的复杂增加了不少。当编程方法进入到下一个阶段(也就是下文的面向对象技术)时,关系数据库和编程语言不一致导致的问题就更加影响开发效率了,因为编程语言的存储结构是面向对象的,而关系性数据库却是关系的,使得从一个环境转换到另一个环境时需要多至30%的附加代码来进行转换。一些ORM框架能一定程度地简化这个过程,但面对查询需要高性能需求时,这些ORM从根本上来说还是很作急。

  3. post SQL database

    80年代,面向对象的方法和技术的出现,给面临新挑战的数据库技术带来了新的机遇和希望。数据库研究人员借鉴和吸收了面向对象的方法和技术,在RDBMS上进行不同层次的扩充,提出了面向对象的数据库管理系统(OODBMS)。向对象的数据库模型能有效地表达客观世界和有效地查询信息。OODBMS还解决了一个关系数据库运行中的典型问题:应用程序语言与数据库管理系统对数据类型支持的不一致问题,这一问题通常称之为阻抗不匹配问题。早期的产品有Gemstone,200后,出现了db4o的开源产品。但正如面向对象技术一样,需要的开发培训是比较长的。再加上本身技术、理论的不完善,关系数据库系统基本适合商业事务处理的前提下,对工程中的应用用面向对象数据库来补充不足的问题。

    2000年以后,出现了NoSQL数据库和NewSQL数据库。NoSQL数据库取的是Not Only SQL的意思,而NewSQL则是追求NoSQL适应网络高扩展需求的新一代RDBMS,这两种数据库都是为了解决关系型数据库的痛点。单说NoSQL,NoSQL这一术语在90年代末就提出来了,一位名叫Carlo Strozzi的人开发的数据库名称,这种数据库不使用SQL来存取数据而是直接用shell脚本存取文件。但真正意义上的NoSQL技术是在09年一场尾随BigTable和Dynamo产品的会议上提出的。发展至今,NoSQL数据库大约有150多种。依据结构描述的不同,大致有4大类。

    第一类是key-value型数据库。这类数据库的数据模型就是采用key-value这种形式的键值对,操作时以keys来读取values。这样简单的结构对于存读非常高效,用来当缓存处理大量数据的高访问负载不错,并容易扩展,不过数据的无结构会导致数据的完整性,通常只被当作字符串或二进制数据的临时数据存储,完整性和容错控制交给应用。Redis数据库就属于这种。

    第二类是column型数据库。这类数据库数据以列的方式存放在一起,数据表是列的集合。由于同列数据的类型是一样的,能很好地实现数据压缩。在查询速度有保证的情况下,还便于廉价的分布式扩展,这样也导致column型比较局限于分布式文件系统。对于事务操作的不支持加大来操作的繁杂。这类数据库比较典型的就是HBaese了。

    第三类是Document型数据库。它的主要特定就是将半结构化的数据存在文档中。这种结构支持灵活的查询语句,也方便水平扩展。这种数据库比如我最近在用的MongoDB特别适合Web应用。

    第四类是Graph型数据库。它的存储结构为图这种以节点为基础的数据结构。当应用的数据模型能映射到图这种结构时,图的操作能提高联合查找的速度。比如IngoGrid,在社交网络和推荐系统都用不错的应用。但这种结构不太好做分布式集群方案。

    这四类数据库之所以叫NoSQL主要是反规范化,也就是不依照RDBMS的范式做。这样虽然数据量大了,但是读取速度却因为数据存储的集中进而加快了,也降低了查询复杂度。通过上面的介绍,能看到所有的NoSQL数据库都提供灵活的Schema。

    还有一点值得提及的是关系型与NoSQL数据库对于事务处理的要求是不一样的。关系型数据库为保障事务的正确可靠,必须具备ACID要求,这这个特征分别是原子性、一致性、隔离性、持久性。这4个特性是Jim Grey上世纪90年代提出来的,经后人完善总结出特性的具体含义:

    • 原子性(Atomicity):一个事务的所有操作要么全部完成,要么全部不完成。以银行转账举例,从原账户扣除金额与向目标账户添加金额这两个操作就为一个事务。原子性需保障事务在执行过程中发生错误就需要回滚到事务未执行前的状态。
    • 一致性(Consistency):事务必须满足预设规则,数据库的完整性没有被破坏。再拿银行举例就是银行对于所有用户的金额有个总和完整性保护。当执行转账这一事务时,银行的总金额是不能被改变的,否则这一事务就破坏了一致性。
    • 隔离性(Isolation):当多个事务并发执行时,不同事务之间的相互影响应该受到控制。比如上例中若同时对同一账户进行操作,应该将账户的金额扣除操作串行化处理以免数据结果错误;而查询同一账户的事务则能同时进行。
    • 持久性(Durability):在事务完成以后,该事务对数据库所作的更改便持久地保存在数据库之中,并且是完全的。当系统故障时,也能通过备份和恢复来保障持久性。

    RDBMS的事务特性保障了数据的一致性,但对于细小的操作,一些开销只是为了满足这些约束条件,这在刚开始没成为很大的问题。当网络时代的数据爆发带来分布式系统和大数据,Eric Brewer和他提出来经过后人证明的CAP定理就应运而生,这套定理指出对于一个分布式计算系统来说,不可能同时满足以下三点:

    • 一致性(Consistency):等同于所有节点访问同一份最新的数据副本,也就是所有节点的数据同步要求。从任何一个节点读取的数据都是一样的,这是所谓的强一致性读。弱一致性则是的异步延时同步方案,以提高可用性。
    • 可用性(Availability):对数据更新具备高可用性
    • 容忍网络分区(Partition Tolerance):系统中的数据分布性的大小对系统的正确性,性能的影响。

    NoSQL一定程度就是基于这个理论提出来的,因为传统的SQL数据库(关系型数据库)都是都是具有ACID属性,对一致性(C)要求很高,因此降低了容忍网络分区(P),因此,为了提高系统性能和可扩展性,必须牺牲一致性(C),推翻关系型数据库中ACID这一套。

    NoSQL通过弱化了事务和多表关联的功能,牺牲一致性,增强分区的扩展性,也就是水平扩展。NoSQL在设计策略上就选择了A和P,牺牲了C。这一理论就是BASE(基本可用性、软状态与最终一致性)的含义——“NoSQL数据库设计可以通过牺牲一定的数据一致性和容错性来换取高性能”。

    • 基本可用性(Basically Available):这一条保证CAP中的可用性,不管何种请求,系统都会给出响应。哪怕给出的响应可能是错误代码。
    • 软状态(Soft state):server端会以client的名义维护状态,但是仅仅维持一小段时间,过了这段时间,server就会将这些状态信息丢弃掉。
    • 最终一致性(Eventual Consistency):最终整个系统的数据是一致的。

    ACID是关系型数据库强一致性的四个要求,而BASE是NoSQL数据库通常对可用性及一致性的弱要求原则。比如在Amazon购物,单个用户看到的库存数是不一样的,但最终买完后,系统的库存数是同步的。这种只需要整个系统经过一定时间后最终达到是一致性就是最终一致性。


MongoDB

MongoDB是一种文档型数据库,这里的文档其实就是一个数据记录,能够对包含的数据内容和类型进行自我描述。文档型数据能存储多样化的数据模型和复杂的数据建模,很方便地在快速迭代的Web开发上替换原有的关系型数据库。可以从下图看出文档型数据库与关系型数据库在结构上的对应关系。

MongoDB采用的文档格式是JSON这种流行的Web程序交互的数据对象,而在硬盘上以BSON格式存储二进制的JSON。其CURD操作在以前也有所提及(使用mongoose中间件调用MongoDB),这一部分功能的使用能有效降低开发难度,也展现出了MongoDB的灵活。而MongoDB还有扩展性相关的分片和可用性相关的复本备份。

  1. 高可用性——replica set

    可用性通过自动故障转移和数据冗余来支持,MongoDB通过replica set(复本集)来实现。replica set是一组存储相同数据的mongod进程。当一个节点的数据丢失时,数据可以从其他复本集恢复过来。当然replication技术还不止这些,当数据请求很大时,复本还能提高读数据库的能力。

    首先,Replica set的成员分为三种:Primary、Secondary和Arbiter。

    • Primary:一个复本集中有且仅有一个Primary,它主要接收写操作。
    • Secondary: 为Primary的复本,接收读操作。当Primary出现故障时能成为Primary。其又可细分为优先级为0的Secondary和隐藏的Secondary,优先级为0就表示其不能成为Primary,隐藏的复本则只是充当备份和记录,不被应用直接使用。
    • Arbiter: Arbiter不用来存储数据,而是通过参与到选举来决定哪个Secondary成为Primary。

    当有数据需要存入数据库时,MongoDB首先将写操作分配给Primary并将操作记录到Primary的oplog(operations log)集合。Secondary则将日志复制并执行操作,这过程以异步的方式进行来保证系统的响应速度。通过覆写 write concern 参数来强制写操作的处理对象个数。比如我们可以设置writeConcern: { w: 2 } 来决定设置写操作到达2个复本集完成才返回响应。

    当需要读取数据给上层时,默认地读取Primary的数据,这是因为Primary上的数据时最新的,而Secondaries则提供实时性不是太高的数据。我们也通过设置读偏好模式来决定数据读取到哪个复本。MongoDB的驱动总共支持五种偏好:只读取Primary、以Primary为主、只读取Secondary、以Secondary为主以及基于地理位置最近的方式来处理读操作。

    当Primary出现故障时,会通过一个选举机制来决定哪个Secondary提升为Primary。当新复本启动、Secondary失去Primary的Heartbests响应或者Primary关闭时触发选举事件。能参与投票的复本默认是可以选择投一次票,复本成员会默认投给优先级高的复本。故障转移除了这一套选举机制,还有回滚机制。当数据 数据回滚在故障转移后进行。

  2. 水平扩展——sharding

    当存储到数据库的数据越来越多,一台机器无法满足良好的服务。MongoDB的分片(Sharding)功能以水平扩展地方式来解决这个问题。一台服务器的CPU不足以支持大量的查询请求,最终导致超过内存容量的数据量拖垮磁盘的IO。我们可以通过垂直扩展,直接增加机器的CPU、RAM和存储资源,抛开理论最大数量限制不说,高昂的大型机费用也是不相称的。与垂直扩展不同,分片采用的是水平扩展,将数据分布在不同的机器(分片)上。每个分片运行独立的数据库,逻辑上,分片以一个整体给上层应用提高数据服务。每个分片只存储集群的一部分数据并只接收这一部以及新存入的数据的相关请求,处理这一小部分的操作能减少单机的压力并整体分担服务。这种方式解决吞吐量和容量需求的问题是比较合理的。

    首先,分片集群的成员分为三种:Shards、Query Routers和Config Servers。

    • Shards:存储实际集群数据子集的节点,每一个分片运行一个mongod实例。每个数据库都有一个primary分片用来存储不进行分片的数据集合。
    • Query Routers:与外部应用进行通信,并将数据操作分配给各个分片。路由服务器运行的是mongos实例。
    • Config Servers:存储集群的元数据,元数据包括集群数据到分片的映射。配置服务器运行的是也mongos实例。

    当我们搭建完一个最小的分片集群(至少有一个Shard、Router和Config)后,来探索一下集群的一下行为方式。集群通过Shard key记录了分片中集合文档的位置,当数据第一次插入后生成Shard key。使用单调增加的数字充当shard key能提高集群写的能力,当Shard key使用类似于_id的数字,所有的写操作都会将数据插入同一个块,并存储到同一个分片。这样的写操作是高效的。当路由节点收到应用的查询操作请求,通过配置节点得知目标分片,shard key在这个过程中就充当来定位的作用。当然查询还牵扯到隔离性的问题,当查询语句没有描述shard key时,路由节点会向所有的分片查询,并等待返回。这种方式会消耗相当长的时间。

今次就写这么多,这些MongoDB的特性都是摘自MongoDB的官网,其是否能应对实际应用的需求还需要检验。并且,MongoDB的最大的问题还是稳定性,目前,作为开发者的解决方法是通过集群来提高可用性。但对于单机就能满足需求场景(比如决策树),还是存在比较大的宕机风险。不过,开发最重要的还是人员的经验能力,我想,能灵活使用工具的特性的开发者才是一个开发团队的保障。以后在实际开发中遇到问题,再好好补充。


Reference:

  1. A Brief History of Databases
  2. 数据库技术发展历史
  3. World of the NoSQL databases
  4. 关系数据库的第一第二第三范式
  5. PDF 架构设计基础
  6. ACID VS BASE
  7. 非关系型数据库NoSQL理论基础之CAP理论
  8. 如何正确理解CAP理论
  9. 当下NoSQL类型、适用场景及使用公司
  10. 视觉中国的MongoDB技术交流
  11. 盛拓的MongoDB技术交流
  12. NoSQL 数据建模技术
  13. MongoDB官方文档

以及其他

个人博客地址

Nodejs的事件驱动模型

我为Node.js”大吹水”系列:Node利用Javascript的特性,比如Continuation Passing Style(CPS)以及Event Loop,使得程序运行时表现优秀,CPS和Event Loop一套机制是用libuv库(libuv又根据OS的不同抽象了Unix下libev和Windos下ICOP)提供的。JS中处理IO业务的方法将回调函数当参数传递的编程风格就是CPS。而成为参数的回调函数的调用问题就得需要叫事件循环来控制。这一切就实现了Node的异步IO。


  1. 经典的服务器执行模型经典的的服务器执行模型有同步式、进程式、线程式。同步式模型就是每次值处理一个请求,迭代地将所有请求处理完毕。无并发可言,处理效率当然很低。

    进程式模型为每一个Web请求开启一个进程,这样可以同时处理多个请求,但式当请求量太大时有限的系统资源就比较吃紧了。并且为每个请求现场分配一个子进程比较消耗cpu时间。

    线程式模型为每一个ieWeb请求开启一个线程处理。线程式又包括单线程模型、多线程模型,也就是一个进程中几个个线程,像Apache就可以选择Worker MPM(多进程多线程)还是Prefork MPM(多进程单线程)。多线程模型处理并发的方式是将每一个IO操作分配到单独的线程中。当客户端发出请求给服务器,服务器会对请求处理并准备好响应回传给客户端,服务器通过维持一个有限的线程池来执行能分离开的处理任务。

    每一个线程分担一个任务听起来很不错,但这会冒出两个问题:其一,线程的数量是有限制的,当你的任务量大于可调度的线程时,就会发生等待处理资源的情况;其二,线程上的任务是共用资源,当线程面对堵塞IO的事务很长时间时,通过线程锁来阻止资源的使用。对于数据密集性的应用来说,这两个问题可能会导致低效的Web服务。建立一个单独的线程需要一些资源配置(runtime、heap、memory)以及处理线程之间的上下文。像图中这种web请求可能还好,文件请求和数据库请求基还是正交的,可以单独处理执行;但这还是避免不了在等待数据结果时当前线程的空闲等待,异步处理就能提高程序运行的效率。

  2. 事件驱动与Nginx的服务原理类似,Node采用事件驱动的运行方式。不过nginx式多进程单线程,而Node通过事件驱动的方式处理请求时无需为每一个请求创建额外的线程。在事件驱动的模型当中,每一个IO工作被添加到事件队列中,线程循环地处理队列上的工作任务,当执行过程中遇到来堵塞(读取文件、查询数据库)时,线程不会停下来等待结果,而是留下一个处理结果的回调函数,转而继续执行队列中的下一个任务。这个传递到队列中的回调函数在堵塞任务运行结束后才被线程调用。

    前面也说过Node Async IO = CPS + Callback,这一套实现开始于Node开始启动的进程,在这个进程中Node会创建一个循环,每次循环运行就是一个Tick周期,每个Tick周期中会从事件队列查看是否有事件需要处理,如果有就取出事件并执行相关的回调函数。事件队列事件全部执行完毕,node应用就会终止。Node对于堵塞IO的处理在幕后使用线程池来确保工作的执行。Node从池中取得一个线程来执行复杂任务,而不占用主循环线程。这样就防止堵塞IO占用空闲资源。当堵塞任务执行完毕通过添加到事件队列中的回调函数来处理接下来的工作。

    当然这么华丽的运行机制就能解决前面说的两个弊端。node基于事件的工作调度能很自然地将主要的调度工作限制到了一个线程,应用能很高效地处理多任务。程序每一时刻也只需管理一个工作中的任务。当必须处理堵塞IO时,通过将这个部分的IO控制权交给池中的线程,能最小地影响到应用处理事件,快速地反应web请求。 当然对机器方便的事情对于写代码的人来说就需要更小心地划分业务逻辑,我们需要将工作划分为合理大小的任务来适配事件模型这一套机制。

  3. 事件队列调度Node可以通过传递回调函数将任务添加到事件队列中,这种异步的调度可以通过5种方式来实现这个目标:异步堵塞IO库(db处理、fs处理),Node内置的事件和事件监听器(http、server的一些预定义事件),开发者自定义的事件和监听器、定时器以及Node全局对象process的.nextTick()API。3.1 异步堵塞IO库

    其IO库提供的API有Node自带的Module(比如fs)和数据库驱动API,比如mongoose的.save(doc, callback)就是将繁重的数据库Insert操作以及回调函数交给子线程来操作,主线程只负责任务的调度。当MongoDB返回给Node操作结果后,回调函数才开始执行。

    比如这段处理Dtree存储的回调函数只有当事件队列中的接收到来自堵塞IO处理线程的执行完毕才会被执行。

    3.2 Node内置的事件和事件监听器

    Node原生的模块都预定义来一些事件,比如NET模块的一套服务状态事件。当Net中的Socket检测到close就会调用放置在事件循环中的回调函数,下例中就是将sockets数组中删除相应的socket连接对象。

    3.3 开发者自定义的事件

    Node自身和很多模块都支持开发者自定义事件和处理持戟处理函数,当然既然是自定义,那么触发事件也是显性地需要开发者。在Socket.io编程中就有很好的例子,开发者可以自定义消息事件来处理端对端的交互。

    3.4 计时器(Timers)

    Node使用前端一致的Timeout和Interval计时器,他们的区别在Timeout是延时执行,Interval是间隔一段事件执行。值得注意的是这组函数其实不属于JS语言标准,他们只是扩展。在浏览器中,他们属于BOM,即它的确切定义为:window.setTimeout和window.setInterval;与window.alert, window.open等函数处于同一层次。Node把这组函数放置于全局范围中。

    除了这两个函数,Node还添加Immediate计时器,setImmediate()函数是没有事件参数的,在事件队列中的当前任务执行结束后执行,并且优先级比Timeout、Interbal高。

    计时器的问题在于它在事件循环中并非精确的执行回调函数。《深入浅出Node.js》举了一个例子:当通过setTimeout()设定一个任务在10毫秒后执行,但是如果在9毫秒后,有一个任务占用了5毫秒的CPU,再次炖老定时器执行时,事件就已经过期了。

    3.5 Node全局对象process的.nextTick()API

    这个延时执行函数函数是在添加任务到队列的开头,下一次Tick周期开始时就执行,也就是在其他任务前调度。

    nextTick的优先级是高于immediate的。并且每轮循环,nextTick中的回调函数全部都会执行完,而Immediate只会执行一个回调函数。这里有得说明每个Tick过程中,判断事件循环中是否有事件要处理的观察者。在Node的底层libuv,事件循环是一个典型的生产者/消费者模型。异步IO、网络请求是事件的生产者,回调函数是事件的消费者,而观察者则是在中间将传递过来的事件暂存起来。回调函数的idle观察者在每轮事件循环开始被检查,而check观察者后于idle观察者检查,两者之间被检查的就是IO操作的观察者。

  4. 事件驱动与高性能服务器前面大致介绍了Node的事件驱动模型,事件驱动的实质就是主循环线程+事件触发的方式来运行程序。Node的异步IO成功地使得IO操作与CPU操作分离成为一套高性能平台,既可以像Nginx一样构建服务器平台,也可以处理具体的业务。虽然Node没有Nginx在Web服务器方面那么专业,但不错的性能和更多的使用场景使得在实际开发中能够达到优异的性能。这一切也都归功与异步IO实现的核心——事件循环。在实际的项目中,我们可以结合不同工具的优点达到应用的最优性能。

references:

  1. Answer of Async
  2. Continuation-passing Style
  3. 关于Timeout计时器的文章
  4. An Introduction to libuv
  5. process.nextTick() vs. setImmediate()
  6. Book: 深入浅出Node.js
  7. Book: Node.js, MongoDB, and AngularJS Web Development

——————————

个人博客Edwardesire