Browsed by
分类:未分类

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
16 views
橘子洲

橘子洲

橘子洲

1,地理位置

橘子洲景区位于长沙市区的湘江之中(北纬N28°10′23.40″ 东经E112°57′18.36″),橘子洲大桥从上横跨而过,离长沙市中心1公里,桔子洲地形平坦,位于湘江水面中央;长达5公里,最宽处仅300余米,岛形狭长,形似长龙,属湘江水回环、泥沙淤沉堆积而成的江洲风景区。

2,历史沿革

橘子洲生成于晋惠帝永兴二年(305年),为激流回旋、沙石堆积而成。原有桔洲、织洲、誓洲、泉洲四岛,至清时只有上洲、中洲、下洲三岛,“望之若带,实不相连”。今演变成一串长岛,上为牛头洲,中为水陆洲,下为傅家洲。橘洲以盛产美桔而得名。湘江水流平缓,河床宽阔,由于下游受洞庭湖水顶托,因而形成绿洲片片。橘子洲久负盛名,春来,明光潋滟,沙鸥点点;秋至,柚黄橘红,清香一片;深冬,凌寒剪冰,江风戏雪,是潇湘八景之一“江天暮雪”的所在地。

1925年,毛泽东从广州回到湖南领导农民运动,寒秋时节,重游橘子洲,写下了《沁园春·长沙》,因此,橘子洲声名大振。

1950年5月是,周恩来和毛泽东曾作过对联,上联:“橘子洲,洲旁舟,舟走洲不走”,下联:“天心阁,阁中鸽,鸽飞阁不飞”。

3,艺术雕塑

毛泽东青年艺术雕塑伫立在长沙橘子洲头,作为长沙橘子洲景区最大景观工程的青年毛泽东艺术雕塑以1925年青年时期的毛泽东形象为基础,突出表现毛泽东胸怀大志、风华正茂的形象。雕像总高度32米,长83米,宽41米,以1925年青年时期的毛泽东形象为基础。

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
13 views
非法集资与资金池之个人浅见

非法集资与资金池之个人浅见

1.非法集资的定义:

指单位或者个人未依照法定程序经有关部门批准,以发行股票、债券、彩票、投资基金证券或者其他债权凭证的方式向社会公众筹集资金,并承诺在一定期限内以货币、实物以及其他方式向出资人还本付息或给予回报的行为。表现形式有非法吸收公众存款与集资诈骗两种。

2.个人浅见:

简单说,非法集资是平台拿着大家的钱自己用,或用来个人奢侈消费、或用来填补公司财务漏洞、或用来经营,如果是用来经营,赚钱了大家一起分,亏了大家一起倒霉,投资者无法判断自己资金的来龙去脉。

对网贷平台而言,判断非法集资最重要的方法是借款者是否真实有效,真实性的判断标准是投资者的钱是否借给了借款者,有效性的判断标准是借款者抵押手续是否完整合法。

相对而言,资金池的定义就没有严格的法律标准,我在网络上找了半天也没有找到标准,判断起来难度非常大。

最常见的资金池就是银行的活期存款,由于银行的政府背书,普通老百姓认为银行是安全的,活期存款是可以随时取款,由于活期存款的体量非常大,总体而言不断有现金存入,也有现金取出,只要这个池子不干涸,大家都是安全的。

对网贷平台而言,我在网络上找到一个比较有意思的判断方法:资金流动是否先于信息流动,简单说就是投资者资金先流出到平台指定的账户,然后再去匹配项目。这两个动作之间的时间差里,资金停留在平台账户上,资金池就形成了。

3.资金池的最大风险是:某个时间段投资者是对自己的资金失去控制,因为平台是有能力动用这笔钱的。

按照这个判断标准,网贷平台的操作方式应该是先找借款者,然后把借款者信息发布到网站上,投资者看到信息后,再充值投标,如果是采用第三方充值,借款者需要在第二个工作日才可以拿到投资者的资金,换句话说投资者资金的计息时间应该是等到借款者拿到资金后才开始。

这样的操作方式给借款者带来不确定性:如果投资者对借款者不认可而不投标,那么借款者可能出现把自己的信息提供给了平台,但却借不到钱的尴尬,容易造成借款者开发乏力;

给投资者也带来了不便:计息应该是从借款者拿到资金才开始的,而且需要时刻关注网站信息的变化,不能够提前把钱准备好放在平台上。

4.个人的浅见:

p2p行业的两个p,一端是投资者,另一端是借款者,网贷平台到底应该先有投资者还是先有借款者,一直是个悖论(这个就像母鸡与鸡蛋的故事一样),如果按照上述资金池的标准,应该是先有借款者,后有投资者。按照这样的流程,我们先找借款者,等一切抵押手续都办好了,再去找投资者,投资者对借款者认可,ok,双方交易完成,大家都高兴;要是投资者不买账,没有投满,我们只好抱歉地对借款者说:您改天再来吧!同时抱歉地对已经投标的投资者说:您的钱太少了,借款者不要,您等下一个借款者吧!(目前也许只有ppd可以坚持这个模式,其他平台要是这种模式,估计业务根本无法开展)。

5.自动投标功能算不算资金池?

目前大部分网贷平台都有自动投标功能,基本流程是投资者先在平台上准备好资金,然后等网站发布借款者信息,根据约定好的投标规则,自动完成交易的撮合过程。自动投标功能是先有资金,再有信息的一个过程,如果自动投标功能算是资金池,几乎目前所有的网贷平台都是资金池模式。

如果全部网贷平台都是资金池模式,监管层早就动手了。所以我认为资金池不应该从信息与资金出现的先后顺序来判断。

6.那么如何区分资金池这个政策风险?我认为应该从计息的周期来判断资金池的问题。

投资者可以先准备好钱,平台公布自动投标规则,如果投资者接受自动投标规则,一旦平台发布借款标,自动匹配,被匹配上的资金开始计息,没有匹配上的资金不计息,用户有权把没有匹配的资金取回到自己的银行卡账户,但需要给平台一定的成本(也许有投资者认为站岗被平台占了便宜,平台白拿投资者的资金在使用,却不用给投资者利息。正常的平台是不敢挪用投资者的站岗资金,因为站岗资金投资者可以有权随时提现,如果挪用了,那么无法给站岗资金提供提现服务。当然站岗对平台是有好处的,可以减少流动资金的储备,减低运营成本)。

这里面非常重要的是自动投标规则是否公平与透明;用户是否有知情权和决策权。

公平性体现为对所有的用户一视同仁,按照先来后到原则排队等候;透明化体现在用户可以大致判断需要等待是时间;知情权体现在用户资金流向的何处;决策权是用户可以决定是否参与自动投标。

source:http://www.p2peye.com/thread-59804-1-1.html

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
17 views
为什么我要用 Node.js? 案例逐一介绍

为什么我要用 Node.js? 案例逐一介绍

#介绍

JavaScript 高涨的人气带来了很多变化,以至于如今使用其进行网络开发的形式也变得截然不同了。就如同在浏览器中一样,现在我们也可以在服务器上运行 JavaScript ,从前端跨越到后端,这样巨大的反差让人难以想象,因为仅仅在几年前 Javascript 还如同 Flash 或者 Java applet 那样嵌入网页在沙箱环境中运行。

在深入Node.js之前,你可能需要阅读和了解使用跨栈式 JavaScript( JavaScript across the stack)带来的好处,它统一了编程语言和数据格式(JSON),让你能最佳地重用开发人员资源。由于这更多的是关于 JavaScript 的特点,这里就不过多讨论它。但它确实是一个让人在开发环节中使用 Node 的关键的优点。

正如维基百科所说:“Node.js 是谷歌 V8 引擎、libuv平台抽象层 以及主体使用 Javscript 编写的核心库三者集合的一个包装外壳。” 除此之外,值得注意的是,Node.js 的作者瑞恩·达尔 (Ryan Dahl) 的目标是创建具有实时推送能力的网站。在 Node.js 中,他给了开发者一个使用事件驱动来实现异步开发的优秀解决方案。(注:V8是谷歌开发的,目前公认最快的 Javascript 解析引擎,libuv 是一个开源的、为 Node 定制而生的跨平台的异步 IO 库。)

简而言之:Node.js 在实时的 Web应用上采用了基于 WebSocket 的推送技术。这意味着什么样的革命性?Well,在经过了20多年的基于无状态的请求-返机制的无状态交互之后,我们终于有了实时的,双向连接的web应用,客户端和服务器端都可以发起通信,能够自由地交换数据。与此形成鲜明对比的是传统的 web响应模式,客户端总是主动发起通信而服务端被动返回。此外,这些都是基于运行在标准80端口上的开放Web组件(HTML、CSS和JS)。

可能有人会说,我们已经使用 Flash 和 Java Applet 的形式很多年了——但实际上,这些方式只是使用网络将数据传递到客户端上的沙箱环境。他们都是隔离运行的,而且经常操作到需要额外的权限之类的非标准端口。

凭借其独特的优势,Node.js的现在已经在许多著名公司的产品中起到了关键作用。

在这篇文章中,我们不仅将讨论这些优势是如何实现的,而且也会讨论为什么你使用 Node.js 来替代一些经典的Web应用程序模型。

#Node.js 是如何工作的?

Node.js 的主要思路是:使用非阻塞的,事件驱动的 I/O 操作来保持在处理跨平台 (across distributed devices) 数据密集型实时应用时的轻巧高效。这听起来有点绕口。

它的真正含义是,Node.js 不是一个即将主导Web开发的世界的银弹级的平台。相反,它是一个满足特别需求的平台。你肯定不会希望使用 Node.js 去做 CPU密集型操作。事实上,使用它进行繁重的计算等于摒弃 Node 几乎所有的优点。Node 真正的亮点在于建设高性能,高扩展性的互联网应用——因为它能够处理庞大的并且高吞吐量的并发连接。

它的工作原理是相当有趣的。传统的网络服务技术,是每个新增一个连接(请求)便生成一个新的线程,这个新的线程会占用系统内存,最终会占掉所有的可用内存。而 Node.js 仅仅只运行在一个单线程中,使用非阻塞的异步 I/O 调用,所有连接都由该线程处理,在 libuv 的加分下,可以允许其支持数万并发连接(全部挂在该线程的事件循环中)。

做一个简单的计算: 假设是普通的Web程序,新接入一个连接会占用 2M 的内存,在有 8GB RAM的系统上运行时, 算上线程之间上下文切换的成本,并发连接的最大理论值则为 4000 个。这是在传统 Web服务端技术下的处理情况。而 Node.js 则达到了约 1M 一个并发连接的拓展级别 (相关证明).

当然,在所有客户端的请求共享单一线程时也会有问题, 这也是一个编写 Node.js 应用的潜在缺陷. 首先, 大量的计算可能会使得 Node 的单线程暂时失去反应, 并导致所有的其他客户端的请求一直阻塞, 直到计算结束才恢复正常。 其次,开发人员需要非常小心,不要让一个 Exception 阻塞核心的事件循环,因为这将导致 Node.js 实例的终止(实际上就是程序崩溃)。( 笔者注:如 PHP 中某个页面挂掉是不会影响网站运行的,但是 Nodejs 是一个线程一个线程来处理所有的链接,所以不论是计算卡了或者是被异常阻塞了都可能会影响到其他所有的链接。解决方案在稍后讨论。)

用来避免异常抛出时中断进程的方法是将异常使用回调传递出去(而不是抛出他们,就像在其他环境中一样)。即使一些未处理的异常阻塞了程序,依旧有多种应对的解决方案,而且也有很多可用于监视 Node 进程来执行必要的崩溃后恢复工作的策略和工具(虽然你将无法恢复用户的 Session ),最常见的是使用 Forever 模块,或者采用其他的外部系统工具如 upstart and monit。

#NPM: The Node Package Manager

当我们讨论 Node.js 的时候,一个绝对不应该忽略地方就是默认内置的模块管理工具 —— NPM。 其灵感来源与 Ruby Gems(具有版本和依赖管理功能,可以通过在线资料库便捷安装可重用的组件的管理工具)。

一个完整的公用模块列表可以在 NPM 的网站上找到https://npmjs.org/,或者通过使用与 Node.js 一同安装的 NPM CLI 工具放问到。该模块的生态系统向所有人开放,任何人都可以发布自己的模块,所有的模块都可以在 NPM 资料库中找到。你可以在 http://howtonode.org/introduction-to-npm 页面找到 NPM 的一个简要介绍(有点旧,但依旧能看)。

目前非常流行的一些 NPM 模块有:

  • express – Express.js,是一个简洁而灵活的 node.js Web应用框架, 并且已经是现在大多数 Node.js 应用的标准框架,你已经可以在很多 Node.js 的书籍中看到它了。
  • connect – Connect 是一个 Node.js 的 HTTP 服务拓展框架,提供一个高性能的“插件”集合,以中间件闻名,是 Express 的基础部分之一。
  • socket.iosockjs – 目前服务端最流行的两个 websocket 组件。
  • Jade – 流行的模板引擎之一,并且是 Express.js 的默认模板引擎。其灵感来源于 HAML。
  • mongomongojs – 封装了 MongoDB 的的各种 API,不过笔者平常工作用的是 mongoose 也很推荐。
  • redisRedis (https://github.com/antirez/redis)的客户端函数库.
  • coffee-scriptCoffeeScript 编译器,允许开发者使用 Coffee 来编写他们的 Node.js 程序。
  • underscore (lodash, lazy) – 最流行的 JavaScript 工具库 , 用于 Node.js 的封装包,以及两个采取略有不同的实现方法来获得更好性能的同行。
  • forever – 可能是用来确保 node 脚本持续运行的最流行的工具。

还有很多好的模块,这里就不一一列举了(希望没有冒犯到没列举的)。

#Node.js 应该用在什么地方

聊天

聊天是最典型的多用户实时交互的应用。从 IRC 开始,有许多开源或者不开源的协议都运行在非标准端口上,而现在,使用 Node.js 则可以解决这些问题——在标准的80端口运行 WebSockets。

聊天应用程序是最能体现 Node.js 优点的例子:轻量级、高流量并且能良好的应对跨平台设备上运行密集型数据(虽然计算能力低)。同时,聊天也是一个非常值得学习的用例,因为它很简单,并且涵盖了目前为止一个典型的 Node.js 会用到的大部分解决方案。

让我们试着来描绘它如何工作。

在最简单的情况下,我们布置了一个聊天室在我们的网站上,用户可以在上面发消息,当然是一对多的形式。例如,假设总共有三个人连接到我们的网站上。

在服务端这边, 我们有一个使用 Express.js 搭建的简单站点,该站点实现了两件事 1) 处理路径为 ‘/’ 的GET请求时,下发包括一个留言板以及一个发送信息的 ‘发送’ 按钮的页面 2) 一个监听客户端发送新消息的 websockets 服务。

在客户端这边,我们有一个 HTML 页面,上面有个两个 js 方法,一个是用于触发事件的 “发送” 按钮,这会把把输入的消息通过 webscoket 发送,另一个方法是用 webscoket 在客户端上监听服务端来的推送(例如,其他用户发送的消息)。

当有一个客户端发送消息的时候,发生的事情是:

  • 浏览器上,点击发送按钮触发了 js 函数,将输入框中的文字通过 websocket 消息发送到服务器的 websocket 客户端(页面初始化加载的时候连接的)。
  • 服务端的 websocket 组件收到 消息,然后通过广播方法转发到其他所有连接的客户端。
  • 通过页面上运行的 websocket 客户端组件,所有的客户端都能收到这条推送的新消息。接着 js 处理函数可以把这个消息添加到文字框内。

这是一个最简单的例子。如果要更好的解决方案,你可以使用 Redis 数据库做一个简单的缓存。在一个更高级的解决方案中,你可能需要一个消息路由来专门处理消息队列,并且需要一个更强健的发送机制,比如发送的时候覆盖上暂时离线的用户或者为离线的注册用户存储尚未接收的消息等等。但是不论你做了怎么样的改进,Node.js 都将遵循一个基本原则:响应事件,处理多个并发连接,并保持流动性的用户体验。

对象数据库接口(API ON TOP OF AN OBJECT DB)

尽管,Node.js 确实非常擅长实时交互的应用,同时它也十分适合通过对象数据库(object DB)来查询数据(如 MongoDB)。以 JSON 格式存储的数据允许 Node.js 直接处理,不需要纠结数据转换和匹配的问题。

举个例子,如果你正在使用 Rails,你会将 JSON 数据转成 二进制的 model,当数据再被 Backbone.js, Angular.js 或者 jQuery AJAX 之类的调用又要转回 JSON。如果是 Nodejs 的话,你可以通过一个 REST API 简单的导出 JSON 对象以供客户端使用。另外,从数据库读写时候如果使用的是 MongoDB 的话,你也不用担心的 JSON 与任何数据之间的格式问题。总之,你可以避免多元的数据转换问题,不论是在客户端、服务端还是数据库。

队列输入

如果你正在接收一个高量并发的数据,你的数据库可能会成为你处理的瓶颈。正如上面的描述,Node.js 可以轻松的处理并发连接。 但是,由于数据库操作是一个阻塞的操作(在这种情况下),这就是麻烦的地方。Node.js的解决方案是,在数据真正的写入之前就承认客户端的数据是真实的。

用这种方法,在高负载的时候系统继续维持它的响应,这在当客户端不需要严格确认一个数据是否成功的被写入时特别有用。典型的例子包括:日志记录或者用户跟踪数据(user-tracking data)的记录,这会被分批处理并且在稍后才使用;同时也包括最终一致性(so, 常用于 NoSQL)可以接受,不需要立即反应的操作(例如 Facebook 上更新点赞的数目)。

数据通过某些缓存或者消息队列的基础组件(例如 RabbitMQ, ZeroMQ)进入队列,并且通过一个独立的数据库批量写入进程来一一消化,或者通过一个更高性能的计算密集型后端服务来进行处理。其他的语言/框架也可以实现相似的操作,但在相同的配置下是达不到 nodejs 的高吞吐量与高并发。

简单的说:使用 Node,你可以把数据库操作扔到一边并在稍后处理它们,假设他们成功了一样继续执行下去。(笔者注:在开发中通常的情况通常是,种耗时的操作通过回调函数来异步处理,主线程继续往下执行)

数据流

在较为传统的网络平台上,HTTP 的请求和响应更像是孤立的事件;然而事实上,他们都是数据流。这一观察结果在 Nodejs 上可以用来建立一些很酷的功能。因为数据通以流的形式接收,而我们可以在网站上在线处理正在上传中的文件。这样的话,就可以实现实时的音频和视频编码,以及在不同数据源之间进行代码(代理见下一段)。

(笔者注:Node 有代替如 apache 这样的 webserver 处理数据,所以开发者可以直接收到客户端一份一份上传的数据,并实时处理。上面这段话听起来有点抽象,不过各位可以简单的想象一下不需要开 YY 或者 QQ,打开网页就能进行语音视频的功能。

代理

Node.js 可以通过异步的方式处理大量的并发连接,所以很容易作为服务端的代理来使用。这在与不同响应时间的不同服务之间进行代理,或者是收集来自多个来源的数据时尤其有用。

举个例子:考虑一个服务器端的应用程序和第三方资源进行通信以更新自不同来源的数据,或者将服务端上的一些图像和视频资源存储到第三方云服务。

虽然专用代理服务器确实存在,但是如果你还没有专用的代理服务器,或者你需要一个本地开发的解决方案,那么使用 Node 来做代理可能是更好的选择。关于这个解决方案,我的意思是指当你在开发的时候,你可以使用Node.js的开发环境搭建一个服务来处理对资源和代理的请求,而在生产环境下,你可以使用专用的代理服务(比如nginx,HAProxy等)来处理这些交互。

股票操盘手的仪表盘

让我们继续讨论应用程序这块。实时网络的解决方案可以很轻松的实现证券交易软件——用于跟踪股票的价格,执行计算、做技术分析,同时生成报表。

使用一个实时的的基于网页的解决方案,将会允许操盘手轻松的切换工作软件以及工作地点。相信不久,我们或许会在 佛罗里达州、伊维萨岛又或者是巴厘岛的海滩上看到他们。

应用监听仪盘表

另一种常见的用例中,使用 Node+Web+Socket 非常适合:跟踪网站访问者并且可视化实时它们之间的实时交互。 (如果你有兴趣,可以去看看 Hummingbird

你可能需要采集用户的实时状态, 或者甚至当他们到达渠道中某个特定的点时, 打开一个交流频道, 通过有针对性的互动介绍移动到下一个阶段. (如果你感兴趣的话,推荐你看看 CANDDi

想象一下,如果你知道你的访客的实时操作,并能够形象化地看到他们的交互,这将对你的业务带来多大的提升。随着实时的、双向 socket 通信的 Node.js ,现在你可以做到了。

系统监控仪表

现在,让我们看看事情的基础设施方面。想象一下,比如,希望为其用户提供服务监控页面(例如,GitHub上的状态页)的 SaaS 运营商 。通过 Node.js 的事件循环,我们可以创建一个基于 Web 的功能强大的仪表板,以异步方式检查服务状态并且使用的 WebSockets 将数据推送到客户端。

内部(公司内部)和公共服务的状态都可以使用该项技术实现实时的上报。让我们把这一想法延伸的远一点,试着想象一个电信运营商中网络运营中心(NOC)的监控应用,云/网络/服务器运营商,或者一些金融机构,全都运行在这个由 Node.js 和 WebSocket 组成的应用上,而不是 Java 和/或 Java Applet。

注意:不要尝试使用 Node 打造硬实时系统(即,响应时间要求一致的系统)。 Erlang是可能是该类应用程序的更好的选择

#什么地方可以使用 Node.js

服务端 WEB 应用

通过 Node.js 使用 Express.js 也可以用来创建服务端上的典型的网页应用。然而,虽然有可能,使用 Node.js 来进行请求+响应的形式来呈现 HTML 并不是最典型的用例。有人赞成也有人反对这一做法。这里有一些看法以供参考:

优点:

  • 如果你不需要进行 CPU密集型计算,你可以从头到尾甚至是数据库(比如 MongoDB)都使用 Javascript 来开发。这显著地减轻了开发工序(包括成本)。
  • 对于一个使用 Node.js 作为服务端的单页应用或者 websocket 应用,爬虫可以收到一个完全 HTML 呈现的响应,这是更为SEO友好的。

    缺点:

  • 任何CPU密集型的计算都将阻碍 Node.js 的反应,所以使用多线程的平台是一个更好的方法。或者,您也可以尝试向外扩展的计算[*]。
  • Node.js 使用关系型数据库依旧十分痛苦(详细见下方)。拜托了,如果你想执行关系型数据操作,请考虑别的环境:Rails, Django 甚至 ASP.NET MVC 。。。。

【*】另一种解决方案是,为这些CPU密集型的计算建立一个高度可扩展的MQ支持的环境与后端处理,以保持 Node 作为一个前台专员来异步处理客户端请求。

#Node.js 不应该在什么地方使用

使用关系型数据库的服务端 WEB 应用

对比 Node.js 上的 Express.js 和 Ruby on Rails,当你使用关系型数据库的时候请毫不犹豫的选择后者。

Node.js 的关系数据库工具仍处于早期阶段,目前还没有成熟到让人能够愉快地使用它。而与此同时,Rails天生自带了数据访问组件,连同DB schema迁移的支持工具和一些Gems(一语双关,一指这些如同珍宝的工具,二指ruby的gems程序包)。Rails和它的搭档框架们拥有非常成熟且被证明了的活动记录(Active Record)或数据映射(Data Mapper)的数据访问层的实现,而这些是当你在使用纯 JavaScript来复制这些应用的时候会非常想要使用的东西。

不过,如果你真的倾向于全部使用 JS(并且做好可能抓狂的准备),那么请继续关注 Sequelize 和 Node ORM2 ,虽然这两者仍然不成熟的,但他们最终会迎头赶上。

[*] 使用 Node 光是作为前端而 Rails 做后端来连接关系型数据库,这是完全有可能也并不少见的。(笔者注:国外有种说法,PHP这一类程序员也可以算作是前端)

繁重的服务端的计算和处理

当涉及到大量的计算,Node.js 就不是最佳的解决方案。你肯定不希望使用Node.js建立一个斐波那契数的计算服务。一般情况下,任何 CPU密集型操作 会削弱掉 Node通过事件驱动, 异步 I/O 模型等等带来的在吞吐量上的优势,因为当线程被非异步的高计算量占用时任何传入的请求将被阻塞。

正如前面所说,Node.js 是单线程的,只使用一个单一的CPU核心。至于,涉及到服务器上多核并发处理,Node 的核心团队已经使用 cluster 模块的形式在这一方面做了一些工作 (参考:http://nodejs.org/api/cluster.html)。当然,您也可以很容易的通过nginx 的反向代理运行多个 Node.js 的服务器实例来避免单一线程阻塞的问题。

关于集群(clustering) ,你应该将所有繁重的计算转移到更合适的语言写的后台进程来处理,同时让他们通过像 RabbitMQ 那样通过消息队列服务器来进行通信。

即使你的后台处理可能最初运行在同一台服务器上时看不出什么优点,但是这样的做法具有非常高的可扩展性的潜力。这些后台处理服务可以容易地分割出去,作为单独的 worker 服务器,而不需要配置入口 web服务器的负载。

当然,你也可以在其他语言平台上用同样的方法,但使用 Node.js 你可以得到很高的吞吐量,每个请求都作为一个小任务非常迅速和高效地处理,这一点我们已经讨论过了。

#结论

我们已经从理论到实践讨论过 Node.js 了,从它的目标和野心,到其优点和缺点。在 Node.js 的开发中99%的问题是由误用阻塞操作而造成的。

请记住:Node.js 从来不是用于解决大规模计算问题而创建的。它的出现是为了解决大规模I/O 的问题,并且在这一点上做的非常好

综上,如果你项目需求中不包含CPU密集型操作,也不需要访问任何阻塞的资源,那么你就可以利用的 Node.js 的优点,尽情的享受快速、可扩展的网络应用。

source: http://blog.jobbole.com/53736

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
30 views
微信公共帐号机器人(Weixin Robot)

微信公共帐号机器人(Weixin Robot)

A node.js robot for wechat.

微信公众平台提供的开放信息接口的自动回复系统。

weixin-robot 是 webot 和 wechat-mp 的 高级包装。webot 负责定义回复规则,wechat-mp 负责与微信服务器通信。

功能特色:

  • 方便灵活的规则定义,轻松实现文本匹配流程控制
  • 基于正则表达式的对话设定,配置简单,可以给一句话随机回复不同内容
  • 支持等待后续操作模式,如可以提示用户“需要我执行xxx操作吗?”
  • 可直接从 yaml 或 json 文件中载入对话规则

快速入门 | FAQ | 示例

var express = require('express');
var webot = require('weixin-robot');

var app = express();

// 指定回复消息
webot.set('hi', '你好');

webot.set('subscribe', {
  pattern: function(info) {
    return info.is('event') && info.param.event === 'subscribe';
  },
  handler: function(info) {
    return '欢迎订阅微信机器人';
  }
});

webot.set('test', {
  pattern: /^test/i,
  handler: function(info, next) {
    next(null, 'roger that!')
  }
})

// 你可以获取已定义的 rule
//
// webot.get('subscribe') ->
//
// {
//   name: 'subscribe',
//   pattern: function(info) {
//     return info.is('event') && info.param.event === 'subscribe';
//   },
//   handler: function(info) {
//     return '欢迎订阅微信机器人';
//   }
// }
//

// 接管消息请求
webot.watch(app, { token: 'your1weixin2token', path: '/wechat' });

// 如果需要多个实例(即为多个微信账号提供不同回复):
var webot2 = new webot.Webot();
webot2.set({
  '/hi/i': 'Hello',
  '/who (are|r) (you|u)/i': 'I\'m a robot.'
});
webot2.watch(app, {
  token: 'token2',
  path: '/wechat_en', // 这个path不能为之前已经监听过的path的子目录
});

// 启动 Web 服务
// 微信后台只允许 80 端口
app.listen(80);

// 如果你不想让 node 应用直接监听 80 端口
// 可以尝试用 nginx 或 apache 自己做一层 proxy
// app.listen(process.env.PORT);
// app.enable('trust proxy');

然后你就可以在微信公众平台后台填入你的接口地址和 token , 或者使用 webot-cli 来调试消息。

如果一切顺利,你也搭建好了自己的机器人,欢迎到此项目的 Wiki 页面添加你的帐号。

命令行工具

提供可执行文件 webot 用于发送测试消息。 使用 npm 安装 webot-cli:

npm install webot-cli -g

微信公共账号自定义菜单

webot-cli 提供处理微信自定义菜单的功能,安装好之后执行:

webot help menu

微信自动回复API流程图

image

规则定义

具体的规则定义部分,请参考 webot 的文档。

主要API:

  • webot.set()
  • webot.waitRule()
  • webot.loads()

info 对象

webot rule 的 handler 接收到的 info 对象,包含请求消息内容和 session 支持。

请求消息属性

wexin-robot 的 info 把微信的请求内容包装为了更符合 js 命名规则的值,并根据 MsgType 的不同, 将额外参数存入了 info.param 对象。这样做能保证 info 对象的标准化,方便你在 不同平台使用相同的机器人。

你可以通过 info.raw 拿到与微信官方文档一致的参数对象。

原始请求参数与 info 属性的对照表:

image

注意

  • 大部分属性值只是把首字母大写换成了小写。地理信息的 Location_X 和 Location_Y 除外。
  • recognition 参数需要开通微信的语音识别功能,同时为方便调用,此文本也会直接存到 info.text 也就是说,语音识别消息与普通文本消息都有 info.text ,只不过 info.type 不同

例如,地理位置消息( MsgType === ‘location’) 会被转化为:

{
  uid: 'the_FromUserName',
  sp: 'the_ToUserName',
  id: 'the_MsgId',
  type: 'location',
  param: {
    lat: 'the_Location_X',
    lng: 'the_Location_Y',
    scale: 'the_Scale',
    label: 'the_Label'
  }
}

info.reply

大部分时候你并不需要直接给 info.reply 赋值。

你只需在 rule.handler 的返回值或 callbak 里提供回复消息的内容, webot.watch 自带的 express 中间件会自动给 info.reply 赋值, 并将其打包成 XML 发送给微信服务器。

info.reply 支持的数据类型:

  • {String} 直接回复文本消息,不能超过2048字节
  • {Object} 单条 图文消息/音乐消息
  • {Array} 多条图文消息

回复文本消息

info.reply = '收到你的消息了,谢谢'

回复图文消息

title        消息标题
url          消息网址
description  消息描述
picUrl       消息图片网址
info.reply = {
  title: '消息标题',
  url: 'http://example.com/...',
  picUrl: 'http://example.com/....a.jpg',
  description: '对消息的描述出现在这里',
}

// or

info.reply = [{
  title: '消息1',
  url: 'http://example.com/...',
  picUrl: 'http://example.com/....a.jpg',
  description: '对消息的描述出现在这里',
}, {
  title: '消息2',
  url: 'http://example.com/...',
  picUrl: 'http://example.com/....a.jpg',
  description: '对消息的描述出现在这里',
}]

回复音乐消息

title             标题
description       描述
musicUrl          音乐链接
hqMusicUrl        高质量音乐链接,wifi 环境下会优先使用该链接播放音乐

需指定 reply.type 为 ‘music’:

info.reply = {
  type: 'music',
  title: 'Music 101',
  musicUrl: 'http://....x.mp3',
  hqMusicUrl: 'http://....x.m4a'
}

Have fun with wechat, and enjoy being a robot!

info.noReply

如果对不想回复的消息,可设置 info.noReply = true

// 比如对于语音类型的消息不回复

webot.set('ignore', {
  pattern: function(info) {
    return info.is('voice');
  },
  handler: function(info) {
    info.noReply = true;
    return;
  }
}); 
好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
34 views
HBase介绍

HBase介绍

HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,利用HBase技术可在廉价PC Server上搭建起大规模结构化存储集群。

HBase简介

HBase是Google Bigtable的开源实现,类似Google Bigtable利用GFS作为其文件存储系统,HBase利用Hadoop HDFS作为其文件存储系统;Google运行MapReduce来处理Bigtable中的海量数据,HBase同样利用Hadoop MapReduce来处理HBase中的海量数据;Google Bigtable利用 Chubby作为协同服务,HBase利用Zookeeper作为对应。

image

上图描述了Hadoop EcoSystem中的各层系统,其中HBase位于结构化存储层,Hadoop HDFS为HBase提供了高可靠性的底层存储支持,Hadoop MapReduce为HBase提供了高性能的计算能力,Zookeeper为HBase提供了稳定服务和failover机制。

此外,Pig和Hive还为HBase提供了高层语言支持,使得在HBase上进行数据统计处理变的非常简单。 Sqoop则为HBase提供了方便的RDBMS数据导入功能,使得传统数据库数据向HBase中迁移变的非常方便。

HBase访问接口

  1. Native Java API,最常规和高效的访问方式,适合Hadoop MapReduce Job并行批处理HBase表数据
  2. HBase Shell,HBase的命令行工具,最简单的接口,适合HBase管理使用
  3. Thrift Gateway,利用Thrift序列化技术,支持C++,PHP,Python等多种语言,适合其他异构系统在线访问HBase表数据
  4. REST Gateway,支持REST 风格的Http API访问HBase, 解除了语言限制
  5. Pig,可以使用Pig Latin流式编程语言来操作HBase中的数据,和Hive类似,本质最终也是编译成MapReduce Job来处理HBase表数据,适合做数据统计
  6. Hive,当前Hive的Release版本尚没有加入对HBase的支持,但在下一个版本Hive 0.7.0中将会支持HBase,可以使用类似SQL语言来访问HBase

HBase数据模型

Table & Column Family

image

  • Row Key: 行键,Table的主键,Table中的记录按照Row Key排序
  • Timestamp: 时间戳,每次数据操作对应的时间戳,可以看作是数据的version number
  • Column Family:列簇,Table在水平方向有一个或者多个Column Family组成,一个Column Family中可以由任意多个Column组成,即Column Family支持动态扩展,无需预先定义Column的数量以及类型,所有Column均以二进制格式存储,用户需要自行进行类型转换。

Table & Region

当Table随着记录数不断增加而变大后,会逐渐分裂成多份splits,成为regions,一个region由[startkey,endkey)表示,不同的region会被Master分配给相应的RegionServer进行管理:

image

-ROOT- && .META. Table

HBase中有两张特殊的Table,-ROOT-和.META.

  • .META.:记录了用户表的Region信息,.META.可以有多个regoin
  • -ROOT-:记录了.META.表的Region信息,-ROOT-只有一个region
  • Zookeeper中记录了-ROOT-表的location

image

Client访问用户数据之前需要首先访问zookeeper,然后访问-ROOT-表,接着访问.META.表,最后才能找到用户数据的位置去访问,中间需要多次网络操作,不过client端会做cache缓存。

MapReduce on HBase

在HBase系统上运行批处理运算,最方便和实用的模型依然是MapReduce,如下图:

image

HBase Table和Region的关系,比较类似HDFS File和Block的关系,HBase提供了配套的TableInputFormat和TableOutputFormat API,可以方便的将HBase Table作为Hadoop MapReduce的Source和Sink,对于MapReduce Job应用开发人员来说,基本不需要关注HBase系统自身的细节。

HBase系统架构

image

Client

HBase Client使用HBase的RPC机制与HMaster和HRegionServer进行通信,对于管理类操作,Client与HMaster进行RPC;对于数据读写类操作,Client与HRegionServer进行RPC.

Zookeeper

Zookeeper Quorum中除了存储了-ROOT-表的地址和HMaster的地址,HRegionServer也会把自己以Ephemeral方式注册到 Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的健康状态。此外,Zookeeper也避免了HMaster的 单点问题,见下文描述.

HMaster

HMaster没有单点问题,HBase中可以启动多个HMaster,通过Zookeeper的Master Election机制保证总有一个Master运行,HMaster在功能上主要负责Table和Region的管理工作:

  1. 管理用户对Table的增、删、改、查操作
  2. 管理HRegionServer的负载均衡,调整Region分布
  3. 在Region Split后,负责新Region的分配
  4. 在HRegionServer停机后,负责失效HRegionServer 上的Regions迁移

HRegionServer

HRegionServer主要负责响应用户I/O请求,向HDFS文件系统中读写数据,是HBase中最核心的模块。

image

HRegionServer内部管理了一系列HRegion对象,每个HRegion对应了Table中的一个Region,HRegion中由多 个HStore组成。每个HStore对应了Table中的一个Column Family的存储,可以看出每个Column Family其实就是一个集中的存储单元,因此最好将具备共同IO特性的column放在一个Column Family中,这样最高效。

HStore存储是HBase存储的核心了,其中由两部分组成,一部分是MemStore,一部分是StoreFiles。MemStore是 Sorted Memory Buffer,用户写入的数据首先会放入MemStore,当MemStore满了以后会Flush成一个StoreFile(底层实现是HFile), 当StoreFile文件数量增长到一定阈值,会触发Compact合并操作,将多个StoreFiles合并成一个StoreFile,合并过程中会进 行版本合并和数据删除,因此可以看出HBase其实只有增加数据,所有的更新和删除操作都是在后续的compact过程中进行的,这使得用户的写操作只要 进入内存中就可以立即返回,保证了HBase I/O的高性能。当StoreFiles Compact后,会逐步形成越来越大的StoreFile,当单个StoreFile大小超过一定阈值后,会触发Split操作,同时把当前 Region Split成2个Region,父Region会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer 上,使得原先1个Region的压力得以分流到2个Region上。下图描述了Compaction和Split的过程:

image

在理解了上述HStore的基本原理后,还必须了解一下HLog的功能,因为上述的HStore在系统正常工作的前提下是没有问题的,但是在分布式 系统环境中,无法避免系统出错或者宕机,因此一旦HRegionServer意外退出,MemStore中的内存数据将会丢失,这就需要引入HLog了。 每个HRegionServer中都有一个HLog对象,HLog是一个实现Write Ahead Log的类,在每次用户操作写入MemStore的同时,也会写一份数据到HLog文件中(HLog文件格式见后续),HLog文件定期会滚动出新的,并 删除旧的文件(已持久化到StoreFile中的数据)。当HRegionServer意外终止后,HMaster会通过Zookeeper感知 到,HMaster首先会处理遗留的 HLog文件,将其中不同Region的Log数据进行拆分,分别放到相应region的目录下,然后再将失效的region重新分配,领取 到这些region的HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到MemStore中,然后flush到StoreFiles,完成数据恢复。

HBase存储格式

HBase中的所有数据文件都存储在Hadoop HDFS文件系统上,主要包括上述提出的两种文件类型:

  1. HFile, HBase中KeyValue数据的存储格式,HFile是Hadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile
  2. HLog File,HBase中WAL(Write Ahead Log) 的存储格式,物理上是Hadoop的Sequence File.

HFile

下图是HFile的存储格式:

image

首先HFile文件是不定长的,长度固定的只有其中的两块:Trailer和FileInfo。正如图中所示的,Trailer中有指针指向其他数 据块的起始点。File Info中记录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。Data Index和Meta Index块记录了每个Data块和Meta块的起始点。

Data Block是HBase I/O的基本单元,为了提高效率,HRegionServer中有基于LRU的Block Cache机制。每个Data块的大小可以在创建一个Table的时候通过参数指定,大号的Block有利于顺序Scan,小号Block利于随机查询。 每个Data块除了开头的Magic以外就是一个个KeyValue对拼接而成, Magic内容就是一些随机数字,目的是防止数据损坏。后面会详细介绍每个KeyValue对的内部构造。

HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

image

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是 RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。

HLogFile

image

上图中示意了HLog文件的结构,其实HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是“写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。

HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue,可参见上文描述。

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
20 views
MapReduce的运行原理

MapReduce的运行原理

江湖传说永流传:谷歌技术有”三宝”,GFS、MapReduce和大表(BigTable)!

谷歌在03到06年间连续发表了三篇很有影响力的文章,分别是03年SOSP的GFS,04年OSDI的MapReduce,和06年OSDI的BigTable。SOSP和OSDI都是操作系统领域的顶级会议,在计算机学会推荐会议里属于A类。SOSP在单数年举办,而OSDI在双数年举办。

MapReduce是干啥的

image

Hadoop实际上就是谷歌三宝的开源实现,Hadoop MapReduce对应Google MapReduce,HBase对应BigTable,HDFS对应GFS。HDFS(或GFS)为上层提供高效的非结构化存储服务,HBase(或BigTable)是提供结构化数据服务的分布式数据库,Hadoop MapReduce(或Google MapReduce)是一种并行计算的编程模型,用于作业调度。 GFS和BigTable已经为我们提供了高性能、高并发的服务,但是并行编程可不是所有程序员都玩得转的活儿,如果我们的应用本身不能并发,那GFS、BigTable也都是没有意义的。MapReduce的伟大之处就在于让不熟悉并行编程的程序员也能充分发挥分布式系统的威力。

简单概括的说,MapReduce是将一个大作业拆分为多个小作业的框架(大作业和小作业应该本质是一样的,只是规模不同),用户需要做的就是决定拆成多少份,以及定义作业本身。

下面用一个贯穿全文的例子来解释MapReduce是如何工作的。

例子:统计词频

如果我想统计下过去10年计算机论文出现最多的几个单词,看看大家都在研究些什么,那我收集好论文后,该怎么办呢?

方法一:我可以写一个小程序,把所有论文按顺序遍历一遍,统计每一个遇到的单词的出现次数,最后就可以知道哪几个单词最热门了。

这种方法在数据集比较小时,是非常有效的,而且实现最简单,用来解决这个问题很合适。

方法二:写一个多线程程序,并发遍历论文。

这个问题理论上是可以高度并发的,因为统计一个文件时不会影响统计另一个文件。当我们的机器是多核或者多处理器,方法二肯定比方法一高效。但是写一个多线程程序要比方法一困难多了,我们必须自己同步共享数据,比如要防止两个线程重复统计文件。

方法三:把作业交给多个计算机去完成。

我们可以使用方法一的程序,部署到N台机器上去,然后把论文集分成N份,一台机器跑一个作业。这个方法跑得足够快,但是部署起来很麻烦,我们要人工把程序copy到别的机器,要人工把论文集分开,最痛苦的是还要把N个运行结果进行整合(当然我们也可以再写一个程序)。

方法四:让MapReduce来帮帮我们吧!

MapReduce本质上就是方法三,但是如何拆分文件集,如何copy程序,如何整合结果这些都是框架定义好的。我们只要定义好这个任务(用户程序),其它都交给MapReduce。

在介绍MapReduce如何工作之前,先讲讲两个核心函数map和reduce以及MapReduce的伪代码。

map函数和reduce函数

map函数和reduce函数是交给用户实现的,这两个函数定义了任务本身。

map函数:接受一个键值对(key-value pair),产生一组中间键值对。MapReduce框架会将map函数产生的中间键值对里键相同的值传递给一个reduce函数。

reduce函数:接受一个键,以及相关的一组值,将这组值进行合并产生一组规模更小的值(通常只有一个或零个值)。

统计词频的MapReduce函数的核心代码非常简短,主要就是实现这两个函数。

map(String key, String value):
    // key: document name
    // value: document contents
    for each word w in value:
        EmitIntermediate(w, "1");

reduce(String key, Iterator values):
    // key: a word
    // values: a list of counts
    int result = 0;
    for each v in values:
        result += ParseInt(v);
        Emit(AsString(result));

在统计词频的例子里,map函数接受的键是文件名,值是文件的内容,map逐个遍历单词,每遇到一个单词w,就产生一个中间键值对,这表示单词w咱又找到了一个;MapReduce将键相同(都是单词w)的键值对传给reduce函数,这样reduce函数接受的键就是单词w,值是一串”1”(最基本的实现是这样,但可以优化),个数等于键为w的键值对的个数,然后将这些“1”累加就得到单词w的出现次数。最后这些单词的出现次数会被写到用户定义的位置,存储在底层的分布式存储系统(GFS或HDFS)。

MapReduce是如何工作的

image

上图是论文里给出的流程图。一切都是从最上方的user program开始的,user program链接了MapReduce库,实现了最基本的Map函数和Reduce函数。图中执行的顺序都用数字标记了。

  • MapReduce库先把user program的输入文件划分为M份(M为用户定义),每一份通常有16MB到64MB,如图左方所示分成了split0~4;然后使用fork将用户进程拷贝到集群内其它机器上。
  • user program的副本中有一个称为master,其余称为worker,master是负责调度的,为空闲worker分配作业(Map作业或者Reduce作业),worker的数量也是可以由用户指定的。
  • 被分配了Map作业的worker,开始读取对应分片的输入数据,Map作业数量是由M决定的,和split一一对应;Map作业从输入数据中抽取出键值对,每一个键值对都作为参数传递给map函数,map函数产生的中间键值对被缓存在内存中。
  • 缓存的中间键值对会被定期写入本地磁盘,而且被分为R个区,R的大小是由用户定义的,将来每个区会对应一个Reduce作业;这些中间键值对的位置会被通报给master,master负责将信息转发给Reduce worker。
  • master通知分配了Reduce作业的worker它负责的分区在什么位置(肯定不止一个地方,每个Map作业产生的中间键值对都可能映射到所有R个不同分区),当Reduce worker把所有它负责的中间键值对都读过来后,先对它们进行排序,使得相同键的键值对聚集在一起。因为不同的键可能会映射到同一个分区也就是同一个Reduce作业(谁让分区少呢),所以排序是必须的。
  • reduce worker遍历排序后的中间键值对,对于每个唯一的键,都将键与关联的值传递给reduce函数,reduce函数产生的输出会添加到这个分区的输出文件中。
  • 当所有的Map和Reduce作业都完成了,master唤醒正版的user program,MapReduce函数调用返回user program的代码。

所有执行完毕后,MapReduce输出放在了R个分区的输出文件中(分别对应一个Reduce作业)。用户通常并不需要合并这R个文件,而是将其作为输入交给另一个MapReduce程序处理。整个过程中,输入数据是来自底层分布式文件系统(GFS)的,中间数据是放在本地文件系统的,最终输出数据是写入底层分布式文件系统(GFS)的。而且我们要注意Map/Reduce作业和map/reduce函数的区别:Map作业处理一个输入数据的分片,可能需要调用多次map函数来处理每个输入键值对;Reduce作业处理一个分区的中间键值对,期间要对每个不同的键调用一次reduce函数,Reduce作业最终也对应一个输出文件。

我更喜欢把流程分为三个阶段。第一阶段是准备阶段,包括1、2,主角是MapReduce库,完成拆分作业和拷贝用户程序等任务;第二阶段是运行阶段,包括3、4、5、6,主角是用户定义的map和reduce函数,每个小作业都独立运行着;第三阶段是扫尾阶段,这时作业已经完成,作业结果被放在输出文件里,就看用户想怎么处理这些输出了。

词频是怎么统计出来的

结合第四节,我们就可以知道第三节的代码是如何工作的了。假设咱们定义M=5,R=3,并且有6台机器,一台master。

image

这幅图描述了MapReduce如何处理词频统计。由于map worker数量不够,首先处理了分片1、3、4,并产生中间键值对;当所有中间值都准备好了,Reduce作业就开始读取对应分区,并输出统计结果。

用户的权利

用户最主要的任务是实现map和reduce接口,但还有一些有用的接口是向用户开放的。

  • an input reader。这个函数会将输入分为M个部分,并且定义了如何从数据中抽取最初的键值对,比如词频的例子中定义文件名和文件内容是键值对。
  • a partition function。这个函数用于将map函数产生的中间键值对映射到一个分区里去,最简单的实现就是将键求哈希再对R取模。
  • a compare function。这个函数用于Reduce作业排序,这个函数定义了键的大小关系。
  • an output writer。负责将结果写入底层分布式文件系统。
  • a combiner function。实际就是reduce函数,这是用于前面提到的优化的,比如统计词频时,如果每个要读一次,因为reduce和map通常不在一台机器,非常浪费时间,所以可以在map执行的地方先运行一次combiner,这样reduce只需要读一次了。

map和reduce函数就不多说了。

MapReduce的实现

目前MapReduce已经有多种实现,除了谷歌自己的实现外,还有著名的hadoop,区别是谷歌是c++,而hadoop是用 java。另外斯坦福大学实现了一个在多核/多处理器、共享内存环境内运行的MapReduce,称为Phoenix,相关的论文发表在07年的HPCA,是当年的最佳论文哦!

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
12 views
Gson – RegisterTypeAdapter

Gson – RegisterTypeAdapter

Gson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object. Gson can work with arbitrary Java objects including pre-existing objects that you do not have source-code of.

DateTypeConverter

Json Data:

{"memberId":"64","memberName":"Kotlin","gender":"1","birthDate":315504000000,"mobile":"13311888009","email":"pxn4@qq.com","registerDate":1470743343000}

birthDate,registerDate are Date in Long type. When convert with default Date Adapter, or set date format likeval gson: Gson = GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create() These methods cannot convert Long type Date. We must define a custom Date Adapter to treat the situation.

class DateTypeConverter : JsonSerializer, JsonDeserializer {
    override fun serialize(src: Date, srcType: Type, context: JsonSerializationContext): JsonElement {
        return JsonPrimitive(src.toString())
    }

    @Throws(JsonParseException::class)
    override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): Date {
        try {
            return Date(json.asLong)
        } catch (e: IllegalArgumentException) {
            val date = context.deserialize(json, Date::class. java)
            return date
        }

    }
}

fun convert(){
    val json = """{"memberId":"64","memberName":"Kotlin","gender":"1","birthDate":315504000000,"mobile":"13311888009","email":"pxn4@qq.com","registerDate":1470743343000}"""
    val adapter = DateTypeConverter()
    val gson = GsonBuilder().registerTypeAdapter(Date::class. java, adapter).create()

    val m = gson.fromJson(json,Member::class. java)
    println(m)
}

result:

Member(memberId=64, memberName=Kotlin, gender=1, birthDate=Tue Jan 01 00:00:00 CST 1980, mobile=13311888009, email=pxn4@qq.com,registerDate=Tue Aug 09 19:49:03 CST 2016}

As you see, use which method depends on the data format, if date in json like ‘1990-01-01 20:10:20’, use setDateFormat(), if date in json is Long type like 315504000000, use custom one .

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
18 views
Java Character Set Encoding

Java Character Set Encoding

Java Character Set Encoding

  • Java stores characters internally as UTF-16
  • Java uses translation tables to map between external encodings and UTF-16.
    • Map from external encoding to UTF-16 on input.
    • Map from UTF-16 to external encoding on output.
  • These translations can be lossy.
  • Java only deals with UTF-16 internally.
  • Inbound characters are converted to UTF-16.
  • Outbound characters are converted from UTF-16 to whatever the output encoding is.
  • Unless you’re reading and writing UTF-16, all character I/O requires conversion to and from Java’s canonical UTF-16 encoding.
  • This is a perfectly reasonable and sound approach.

Scenario

  • Web-based user interface in windows
  • backed by Java servlets in linux
  • Support for Oracle or SQL Server in linux

image

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
11 views
字符编码笔记:ASCII,Unicode 和 UTF-8

字符编码笔记:ASCII,Unicode 和 UTF-8

今天中午,我突然想搞清楚 Unicode 和 UTF-8 之间的关系,就开始查资料。

这个问题比我想象的复杂,午饭后一直看到晚上9点,才算初步搞清楚。

下面就是我的笔记,主要用来整理自己的思路。我尽量写得通俗易懂,希望能对其他朋友有用。毕竟,字符编码是计算机技术的基石,想要熟练使用计算机,就必须懂得一点字符编码的知识。

一、ASCII 码

我们知道,计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有01两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000011111111

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII 码一共规定了128个字符的编码,比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0

二、非 ASCII 编码

英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。

但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是一样的,不一样的只是128–255的这一段。

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。

中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的 Unicode 和 UTF-8 是毫无关系的。

三. Unicode

正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。

Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母AinU+0041表示英语的大写字母AU+4E25表示汉字。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表

四、Unicode 的问题

需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

比如,汉字的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

它们造成的结果是:1)出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。2)Unicode 在很长一段时间内无法推广,直到互联网的出现。

五、UTF-8

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

下表总结了编码规则,字母x表示可用编码的位。

Unicode符号范围     |        UTF-8编码方式
(十六进制)        |              (二进制)
----------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

下面,还是以汉字为例,演示如何实现 UTF-8 编码。

的 Unicode 是4E25100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5

六、Unicode 与 UTF-8 之间的转换

通过上一节的例子,可以看到的 Unicode码 是4E25,UTF-8 编码是E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。

Windows平台,有一个最简单的转化方法,就是使用内置的记事本小程序notepad.exe。打开文件后,点击文件菜单中的另存为命令,会跳出一个对话框,在最底部有一个编码的下拉条。

bg2007102801.jpg

里面有四个选项:ANSIUnicodeUnicode big endianUTF-8

1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对 Windows 简体中文版,如果是繁体中文版会采用 Big5 码)。

2)Unicode编码这里指的是notepad.exe使用的 UCS-2 编码方式,即直接用两个字节存入字符的 Unicode 码,这个选项用的 little endian 格式。

3)Unicode big endian编码与上一个选项相对应。我在下一节会解释 little endian 和 big endian 的涵义。

4)UTF-8编码,也就是上一节谈到的编码方法。

选择完”编码方式”后,点击”保存”按钮,文件的编码方式就立刻转换好了。

七、Little endian 和 Big endian

上一节已经提到,UCS-2 格式可以存储 Unicode 码(码点不超过0xFFFF)。以汉字为例,Unicode 码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,这就是 Big endian 方式;25在前,4E在后,这是 Little endian 方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

第一个字节在前,就是”大头方式”(Big endian),第二个字节在前就是”小头方式”(Little endian)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(zero width no-break space),用FEFF表示。这正好是两个字节,而且FFFE1

如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

八、实例

下面,举一个实例。

打开”记事本”程序notepad.exe,新建一个文本文件,内容就是一个字,依次采用ANSIUnicodeUnicode big endianUTF-8编码方式保存。

然后,用文本编辑软件UltraEdit 中的”十六进制功能”,观察该文件的内部编码方式。

1)ANSI:文件的编码就是两个字节D1 CF,这正是的 GB2312 编码,这也暗示 GB2312 是采用大头方式存储的。

2)Unicode:编码是四个字节FF FE 25 4E,其中FF FE表明是小头方式存储,真正的编码是4E25

3)Unicode big endian:编码是四个字节FE FF 4E 25,其中FE FF表明是大头方式存储。

4)UTF-8:编码是六个字节EF BB BF E4 B8 A5,前三个字节EF BB BF表示这是UTF-8编码,后三个E4B8A5就是的具体编码,它的存储顺序与编码顺序是一致的。

九、延伸阅读

(完)

文章来源:

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

 

好烂啊有点差凑合看看还不错很精彩 (No Ratings Yet)
Loading...
14 views
深入理解Java 8 Lambda (类库篇——Streams API,Collectors和并行)

深入理解Java 8 Lambda (类库篇——Streams API,Collectors和并行)

自从lambda表达式成为 Java语言的一部分之后, Java集合(Collections)API就面临着大幅变化。而 JSR 355(规定了 Java lambda 表达式的标准)的正式启用更是使得 Java 集合 API 变的过时不堪。尽管我们可以从头实现一个新的集合框架(比如“Collection II”),但取代现有的集合框架是一项非常艰难的工作,因为集合接口渗透了 Java 生态系统的每个角落,将它们一一换成新类库需要相当长的时间。因此,我们决定采取演化的策略(而非推倒重来)以改进集合 API:

  • 为现有的接口(例如 Collection,List 和 Stream)增加扩展方法;
  • 在类库中增加新的 流(stream,即 java.util.stream.Stream)抽象以便进行聚集(aggregation)操作;
  • 改造现有的类型使之可以提供流视图(stream view);

改造现有的类型使之可以容易的使用新的编程模式,这样用户就不必抛弃使用以久的类库,例如 ArrayList 和 HashMap(当然这并不是说集合 API 会常驻永存,毕竟集合 API 在设计之初并没有考虑到 lambda 表达式。我们可能会在未来的 JDK 中添加一个更现代的集合类库)。

除了上面的改进,还有一项重要工作就是提供更加易用的并行(Parallelism)库。尽管 Java 平台已经对并行和并发提供了强有力的支持,然而开发者在实际工作(将串行代码并行化)中仍然会碰到很多问题。因此,我们希望 Java 类库能够既便于编写串行代码也便于编写并行代码,因此我们把编程的重点从具体执行细节(how computation should be formed)转移到抽象执行步骤(what computation should be perfomed)。除此之外,我们还需要在将并行变的 容易(easier)和将并行变的 不可见(invisible)之间做出抉择,我们选择了一个折中的路线:提供 显式(explicit)但 非侵入(unobstrusive)的并行。(如果把并行变的透明,那么很可能会引入不确定性(nondeterminism)以及各种数据竞争(data race)问题).

内部迭代和外部迭代(Internal vs external iteration)

集合类库主要依赖于 外部迭代(external iteration)。Collection 实现 Iterable 接口,从而使得用户可以依次遍历集合的元素。比如我们需要把一个集合中的形状都设置成红色,那么可以这么写:

for (Shape shape : shapes) {
  shape.setColor(RED);
}

这个例子演示了外部迭代:for-each 循环调用 shapes 的 iterator() 方法进行依次遍历。外部循环的代码非常直接,但它有如下问题:

  • Java 的 for 循环是串行的,而且必须按照集合中元素的顺序进行依次处理;
  • 集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。

尽管有时 for-each 循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。

我们可以使用 内部迭代(internal iteration)替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。

下面是前例的内部迭代代码:

shapes.forEach(s -> s.setColor(RED));

尽管看起来只是一个小小的语法改动,但是它们的实际差别非常巨大。用户把对操作的控制权交还给类库,从而允许类库进行各种各样的优化(例如乱序执行、惰性求值和并行等等)。总的来说,内部迭代使得外部迭代中不可能实现的优化成为可能。

外部迭代同时承担了 做什么(把形状设为红色)和 怎么做(得到 Iterator 实例然后依次遍历)两项职责,而内部迭代只负责 做什么,而把 怎么做 留给类库。通过这样的职责转变:用户的代码会变得更加清晰,而类库则可以进行各种优化,从而使所有用户都从中受益。

流(Stream)

流 是 Java SE 8 类库中新增的关键抽象,它被定义于 java.util.stream(这个包里有若干流类型:Stream 代表对象引用流,此外还有一系列特化(specialization)流,比如 IntStream 代表整形数字流)。每个流代表一个值序列,流提供一系列常用的聚集操作,使得我们可以便捷的在它上面进行各种运算。集合类库也提供了便捷的方式使我们可以以操作流的方式使用集合、数组以及其它数据结构。

流的操作可以被组合成 流水线(Pipeline)。以前面的例子为例,如果我们只想把蓝色改成红色:

shapes.stream()
      .filter(s -> s.getColor() == BLUE)
      .forEach(s -> s.setColor(RED));

在 Collection 上调用 stream() 会生成该集合元素的流视图(stream view),接下来 filter() 操作会产生只包含蓝色形状的流,最后,这些蓝色形状会被 forEach 操作设为红色。

如果我们想把蓝色的形状提取到新的 List 里,则可以:

List blue =
    shapes.stream()
          .filter(s -> s.getColor() == BLUE)
          .collect(Collectors.toList());

collect() 操作会把其接收的元素聚集(aggregate)到一起(这里是 List),collect() 方法的参数则被用来指定如何进行聚集操作。在这里我们使用 toList() 以把元素输出到 List 中。(如需更多 collect() 方法的细节,请阅读 Collectors 一节)

如果每个形状都被保存在 Box 里,然后我们想知道哪个盒子至少包含一个蓝色形状,我们可以这么写:

Set hasBlueShape =
    shapes.stream()
          .filter(s -> s.getColor() == BLUE)
          .map(s -> s.getContainingBox())
          .collect(Collectors.toSet());

map() 操作通过映射函数(这里的映射函数接收一个形状,然后返回包含它的盒子)对输入流里面的元素进行依次转换,然后产生新流。

如果我们需要得到蓝色物体的总重量,我们可以这样表达:

int sum =
    shapes.stream()
          .filter(s -> s.getColor() == BLUE)
          .mapToInt(s -> s.getWeight())
          .sum();

这些例子演示了流框架的设计,以及如何使用流框架解决实际问题。

流和集合(Streams vs Collections)

集合和流尽管在表面上看起来很相似,但它们的设计目标是不同的:集合主要用来对其元素进行有效(effective)的管理和访问(access),而流并不支持对其元素进行直接操作或直接访问,而只支持通过声明式操作在其上进行运算然后得到结果。除此之外,流和集合还有一些其它不同:

  • 无存储:流并不存储值;流的元素源自数据源(可能是某个数据结构、生成函数或 I/O 通道等等),通过一系列计算步骤得到;
  • 天然的函数式风格(Functional in nature):对流的操作会产生一个结果,但流的数据源不会被修改;
  • 惰性求值:多数流操作(包括过滤、映射、排序以及去重)都可以以惰性方式实现。这使得我们可以用一遍遍历完成整个流水线操作,并可以用短路操作提供更高效的实现;
  • 无需上界(Bounds optional):不少问题都可以被表达为无限流(infinite stream):用户不停地读取流直到满意的结果出现为止(比如说,枚举 完美数 这个操作可以被表达为在所有整数上进行过滤)。集合是有限的,但流不是(操作无限流时我们必需使用短路操作,以确保操作可以在有限时间内完成);

从API的角度来看,流和集合完全互相独立,不过我们可以既把集合作为流的数据源(Collection 拥有 stream() 和 parallelStream() 方法),也可以通过流产生一个集合(使用前例的 collect() 方法)。Collection 以外的类型也可以作为 stream 的数据源,比如JDK中的 BufferedReader、Random 和 BitSet 已经被改造可以用做流的数据源,Arrays.stream() 则产生给定数组的流视图。事实上,任何可以用 Iterator 描述的对象都可以成为流的数据源,如果有额外的信息(比如大小、是否有序等特性),库还可以进行进一步的优化。

惰性(Laziness)

过滤和映射这样的操作既可以被 急性求值(以 filter 为例,急性求值需要在方法返回前完成对所有元素的过滤),也可以被 惰性求值(用 Stream 代表过滤结果,当且仅当需要时才进行过滤操作)在实际中进行惰性运算可以带来很多好处。比如说,如果我们进行惰性过滤,我们就可以把过滤和流水线里的其它操作混合在一起,从而不需要对数据进行多遍遍历。相类似的,如果我们在一个大型集合里搜索第一个满足某个条件的元素,我们可以在找到后直接停止,而不是继续处理整个集合。(这一点对无限数据源是很重要,惰性求值对于有限数据源起到的是优化作用,但对无限数据源起到的是决定作用,没有惰性求值,对无限数据源的操作将无法终止)

对于过滤和映射这样的操作,我们很自然的会把它当成是惰性求值操作,不过它们是否真的是惰性取决于它们的具体实现。另外,像 sum() 这样生成值的操作和 forEach() 这样产生副作用的操作都是“天然急性求值”,因为它们必须要产生具体的结果。

以下面的流水线为例:

int sum =
    shapes.stream()
          .filter(s -> s.getColor() == BLUE)
          .mapToInt(s -> s.getWeight())
          .sum();

这里的过滤操作和映射操作是惰性的,这意味着在调用 sum() 之前,我们不会从数据源提取任何元素。在 sum 操作开始之后,我们把过滤、映射以及求和混合在对数据源的一遍遍历之中。这样可以大大减少维持中间结果所带来的开销。

大多数循环都可以用数据源(数组、集合、生成函数以及I/O管道)上的聚合操作来表示:进行一系列惰性操作(过滤和映射等操作),然后用一个急性求值操作(forEach,toArray 和 collect 等操作)得到最终结果——例如过滤—映射—累积,过滤—映射—排序—遍历等组合操作。惰性操作一般被用来计算中间结果,这在Streams API设计中得到了很好的体现——与其让 filter 和 map 返回一个集合,我们选择让它们返回一个新的流。在 Streams API 中,返回流对象的操作都是惰性操作,而返回非流对象的操作(或者无返回值的操作,例如 forEach())都是急性操作。绝大多数情况下,潜在的惰性操作会被用于聚合,这正是我们想要的——流水线中的每一轮操作都会接收输入流中的元素,进行转换,然后把转换结果传给下一轮操作。

在使用这种 数据源—惰性操作—惰性操作—急性操作 流水线时,流水线中的惰性几乎是不可见的,因为计算过程被夹在数据源和最终结果(或副作用操作)之间。这使得API的可用性和性能得到了改善。

对于 anyMatch(Predicate) 和 findFirst() 这些急性求值操作,我们可以使用短路(short-circuiting)来终止不必要的运算。以下面的流水线为例:

Optional firstBlue =
    shapes.stream()
          .filter(s -> s.getColor() == BLUE)
          .findFirst();

由于过滤这一步是惰性的,findFirst 在从其上游得到一个元素之后就会终止,这意味着我们只会处理这个元素及其之前的元素,而不是所有元素。findFirst() 方法返回 Optional 对象,因为集合中有可能不存在满足条件的元素。Optional 是一种用于描述可缺失值的类型。

在这种设计下,用户并不需要显式进行惰性求值,甚至他们都不需要了解惰性求值。类库自己会选择最优化的计算方式。

并行(Parallelism)

流水线既可以串行执行也可以并行执行,并行或串行是流的属性。除非你显式要求使用并行流,否则JDK总会返回串行流。(串行流可以通过 parallel() 方法被转化为并行流)

尽管并行是显式的,但它并不需要成为侵入式的。利用 parallelStream(),我们可以轻松的把之前重量求和的代码并行化:

int sum =
    shapes.parallelStream()
          .filter(s -> s.getColor = BLUE)
          .mapToInt(s -> s.getWeight())
          .sum();

并行化之后和之前的代码区别并不大,然而我们可以很容易看出它是并行的(此外我们并不需要自己去实现并行代码)。

因为流的数据源可能是一个可变集合,如果在遍历流时数据源被修改,就会产生干扰(interference)。所以在进行流操作时,流的数据源应保持不变(held constant)。这个条件并不难维持,如果集合只属于当前线程,只要 lambda 表达式不修改流的数据源就可以。(这个条件和遍历集合时所需的条件相似,如果集合在遍历时被修改,绝大多数的集合实现都会抛出ConcurrentModificationException)我们把这个条件称为无干扰性(non-interference)。

我们应避免在传递给流方法的 lambda 产生副作用。一般来说,打印调试语句这种输出变量的操作是安全的,然而在 lambda 表达式里访问可变变量就有可能造成数据竞争或是其它意想不到的问题,因为 lambda 在执行时可能会同时运行在多个线程上,因而它们所看到的元素有可能和正常的顺序不一致。无干扰性有两层含义:

  • 不要干扰数据源;
  • 不要干扰其它 lambda 表达式,当一个 lambda 在修改某个可变状态而另一个 lambda 在读取该状态时就会产生这种干扰。

只要满足无干扰性,我们就可以安全的进行并行操作并得到可预测的结果,即便对线程不安全的集合(例如 ArrayList)也是一样。

实例(Examples)

下面的代码源自 JDK 中的 Class 类型(getEnclosingMethod 方法),这段代码会遍历所有声明的方法,然后根据方法名称、返回类型以及参数的数量和类型进行匹配:

for (Method method : enclosingInfo.getEnclosingClass().getDeclaredMethods()) {
  if (method.getName().equals(enclosingInfo.getName())) {
    Class>[] candidateParamClasses = method.getParameterTypes();
    if (candidateParamClasses.length == parameterClasses.length) {
      boolean matches = true;
      for (int i = 0; i 

通过使用流,我们不但可以消除上面代码里面所有的临时变量,还可以把控制逻辑交给类库处理。通过反射得到方法列表之后,我们利用 Arrays.stream 将它转化为 Stream,然后利用一系列过滤器去除类型不符、参数不符以及返回值不符的方法,然后通过调用 findFirst 得到 Optional,最后利用 orElseThrow 返回目标值或者抛出异常。

return Arrays.stream(enclosingInfo.getEnclosingClass().getDeclaredMethods())
             .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName()))
             .filter(m -> Arrays.equals(m.getParameterTypes(), parameterClasses))
             .filter(m -> Objects.equals(m.getReturnType(), returnType))
             .findFirst()
             .orElseThrow(() -> new InternalError("Enclosing method not found"));

相对于未使用流的代码,这段代码更加紧凑,可读性更好,也不容易出错。

流操作特别适合对集合进行查询操作。假设有一个“音乐库”应用,这个应用里每个库都有一个专辑列表,每张专辑都有其名称和音轨列表,每首音轨表都有名称、艺术家和评分。

假设我们需要得到一个按名字排序的专辑列表,专辑列表里面的每张专辑都至少包含一首四星及四星以上的音轨,为了构建这个专辑列表,我们可以这么写:

List favs = new ArrayList();
for (Album album : albums) {
  boolean hasFavorite = false;
  for (Track track : album.tracks) {
    if (track.rating >= 4) {
      hasFavorite = true;
      break;
    }
  }
  if (hasFavorite)
    favs.add(album);
}
Collections.sort(favs, new Comparator() {
  public int compare(Album a1, Album a2) {
    return a1.name.compareTo(a2.name);
  }
});

我们可以用流操作来完成上面代码中的三个主要步骤——识别一张专辑是否包含一首评分大于等于四星的音轨(使用 anyMatch);按名字排序;以及把满足条件的专辑放在一个 List 中:

List sortedFavs =
    albums.stream()
          .filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4)))
          .sorted(Comparator.comparing(a -> a.name))
          .collect(Collectors.toList());

Compartor.comparing 方法接收一个函数(该函数返回一个实现了 Comparable 接口的排序键值),然后返回一个利用该键值进行排序的 Comparator(请参考下面的 比较器工厂 一节)。

收集器(Collectors)

在之前的例子中,我们利用 collect() 方法把流中的元素聚合到 List 或 Set 中。collect() 接收一个类型为 Collector 的参数,这个参数决定了如何把流中的元素聚合到其它数据结构中。Collectors 类包含了大量常用收集器的工厂方法,toList() 和 toSet() 就是其中最常见的两个,除了它们还有很多收集器,用来对数据进行对复杂的转换。

Collector 的类型由其输入类型和输出类型决定。以 toList() 收集器为例,它的输入类型为 T,输出类型为 List,toMap 是另外一个较为复杂的 Collector,它有若干个版本。最简单的版本接收一对函数作为输入,其中一个函数用来生成键(key),另一个函数用来生成值(value)。toMap 的输入类型是 T,输出类型是 Map,其中 K 和 V 分别是前面两个函数所生成的键类型和值类型。(复杂版本的 toMap 收集器则允许你指定目标 Map 的类型或解决键冲突)。举例来说,下面的代码以目录数字为键值创建一个倒排索引:

Map albumsByCatalogNumber =
    albums.stream()
          .collect(Collectors.toMap(a -> a.getCatalogNumber(), a -> a));

groupingBy 是一个与 toMap 相类似的收集器,比如说我们想要把我们最喜欢的音乐按歌手列出来,这时我们就需要这样的 Collector:它以 Track 作为输入,以 Map 作为输出。groupingBy 收集器就可以胜任这个工作,它接收分类函数(classification function),然后根据这个函数生成 Map,该 Map 的键是分类函数的返回结果,值是该分类下的元素列表。

Map> favsByArtist =
    tracks.stream()
          .filter(t -> t.rating >= 4)
          .collect(Collectors.groupingBy(t -> t.artist));

收集器可以通过组合和复用来生成更加复杂的收集器,简单版本的 groupingBy 收集器把元素按照分类函数为每个元素计算出分类键值,然后把输入元素输出到对应的分类列表中。除了这个版本,还有一个更加通用(general)的版本允许你使用 其它 收集器来整理输入元素:它接收一个分类函数以及一个下流(downstream)收集器(单参数版本的 groupingBy 使用 toList() 作为其默认下流收集器)。举例来说,如果我们想把每首歌曲的演唱者收集到 Set 而非 List 中,我们可以使用 toSet 收集器:

Map> favsByArtist =
    tracks.stream()
          .filter(t -> t.rating >= 4)
          .collect(Collectors.groupingBy(t -> t.artist,
                                         Collectors.toSet()));

如果我们需要按照歌手和评分来管理歌曲,我们可以生成多级 Map:

Map>> byArtistAndRating =
    tracks.stream()
          .collect(groupingBy(t -> t.artist,
                              groupingBy(t -> t.rating)));

在最后的例子里,我们创建了一个歌曲标题里面的词频分布。我们首先使用 Stream.flatMap() 得到一个歌曲流,然后用 Pattern.splitAsStream 把每首歌曲的标题打散成词流;接下来我们用 groupingBy 和 String.toUpperCase 对这些词进行不区分大小写的分组,最后使用 counting() 收集器计算每个词出现的次数(从而无需创建中间集合)。

Pattern pattern = Pattern.compile("\\s+");
Map wordFreq =
    tracks.stream()
          .flatMap(t -> pattern.splitAsStream(t.name)) // Stream
          .collect(groupingBy(s -> s.toUpperCase(), counting()));

flatMap 接收一个返回流(这里是歌曲标题里的词)的函数。它利用这个函数将输入流中的每个元素转换为对应的流,然后把这些流拼接到一个流中。所以上面代码中的 flatMap 会返回所有歌曲标题里面的词,接下来我们不区分大小写的把这些词分组,并把词频作为值(value)储存。

Collectors 类包含大量的方法,这些方法被用来创造各式各样的收集器,以便进行查询、列表(tabulation)和分组等工作,当然你也可以实现一个自定义 Collector。

并行的实质(Parallelism under the hood)

Java SE 7 引入了 Fork/Join 模型,以便高效实现并行计算。不过,通过 Fork/Join 编写的并行代码和同功能的串行代码的差别非常巨大,这使改写串行代码变的非常困难。通过提供串行流和并行流,用户可以在串行操作和并行操作之间进行便捷的切换(无需重写代码),从而使得编写正确的并行代码变的更加容易。

为了实现并行计算,我们一般要把计算过程递归分解(recursive decompose)为若干步:

  • 把问题分解为子问题;
  • 串行解决子问题从而得到部分结果(partial result);
  • 合并部分结果合为最终结果。

这也是 Fork/Join 的实现原理。

为了能够并行化任意流上的所有操作,我们把流抽象为 Spliterator,Spliterator 是对传统迭代器概念的一个泛化。分割迭代器(spliterator)既支持顺序依次访问数据,也支持分解数据:就像 Iterator 允许你跳过一个元素然后保留剩下的元素,Spliterator 允许你把输入元素的一部分(一般来说是一半)转移(carve off)到另一个新的 Spliterator 中,而剩下的数据则会被保存在原来的 Spliterator 里。(这两个分割迭代器还可以被进一步分解)除此之外,分割迭代器还可以提供源的元数据(比如元素的数量,如果已知的话)和其它一系列布尔值特征(比如说“元素是否被排序”这样的特征),Streams 框架可以利用这些数据来进行优化。

上面的分解方法也同样适用于其它数据结构,数据结构的作者只需要提供分解逻辑,然后就可以直接享用并行流操作带来的遍历。

大多数用户无需去实现 Spliterator 接口,因为集合上的 stream() 方法往往就足够了。但如果你需要实现一个集合或一个流,那么你可能需要手动实现 Spliterator 接口。Spliterator 接口的API如下所示:

public interface Spliterator {
  // Element access
  boolean tryAdvance(Consumer action);
  void forEachRemaining(Consumer action);
  // Decomposition
  Spliterator trySplit();
  //Optional metadata
  long estimateSize();
  int characteristics();
  Comparator getComparator();
}

集合库中的基础接口 Collection 和 Iterable 都实现了正确但相对低效的 spliterator() 实现,但派生接口(例如 Set)和具体实现类(例如 ArrayList)均提供了高效的分割迭代器实现。分割迭代器的实现质量会影响到流操作的执行效率;如果在 split() 方法中进行良好(平衡)的划分,CPU 的利用率会得到改善;此外,提供正确的特性(characteristics)和大小(size)这些元数据有利于进一步优化。

出现顺序(Encounter order)

多数数据结构(例如列表,数组和I/O通道)都拥有 自然出现顺序(natural encounter order),这意味着它们的元素出现顺序是可预测的。其它的数据结构(例如 HashSet)则没有一个明确定义的出现顺序(这也是 HashSet 的 Iterator 实现中不保证元素出现顺序的原因)。

是否具有明确定义的出现顺序是 Spliterator 检查的特性之一(这个特性也被流使用)。除了少数例外(比如 Stream.forEach() 和 Stream.findAny()),并行操作一般都会受到出现顺序的限制。这意味着下面的流水线:

List names =
    people.parallelStream()
          .map(Person::getName)
          .collect(toList());

代码中名字出现的顺序必须要和流中的 Person 出现的顺序一致。一般来说,这是我们所期待的结果,而且它对多大多数的流实现都不会造成明显的性能损耗。从另外的角度来说,如果源数据是 HashSet,那么上面代码中名字就可以以任意顺序出现。

JDK 中的流和 lambda(Streams and lambdas in JDK)

Stream 在 Java SE 8 中非常重要,我们希望可以在 JDK 中尽可能广的使用 Stream。我们为 Collection 提供了 stream() 和 parallelStream(),以便把集合转化为流;此外数组可以通过 Arrays.stream() 被转化为流。

除此之外,Stream 中还有一些静态工厂方法(以及相关的原始类型流实现),这些方法被用来创建流,例如 Stream.of(),Stream.generate 以及 IntStream.range。其它的常用类型也提供了流相关的方法,例如 String.chars,BufferedReader.lines,Pattern.splitAsStream,Random.ints 和 BitSet.stream。

最后,我们提供了一系列API用于构建流,类库的编写者可以利用这些API来在流上实现其它聚集操作。实现 Stream 至少需要一个 Iterator,不过如果编写者还拥有其它元数据(例如数据大小),类库就可以通过 Spliterator 提供一个更加高效的实现(就像 JDK 中所有的集合一样)。

比较器工厂(Comparator factories)

我们在 Comparator 接口中新增了若干用于生成比较器的实用方法:

静态方法 Comparator.comparing() 接收一个函数(该函数返回一个实现 Comparable 接口的比较键值),返回一个 Comparator,它的实现十分简洁:

public static > Compartor comparing(
    Function keyExtractor) {
  return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

我们把这种方法称为 高阶函数 ——以函数作为参数或是返回值的函数。我们可以使用高阶函数简化代码:

List people = ...
people.sort(comparing(p -> p.getLastName()));

这段代码比“过去的代码”(一般要定义一个实现 Comparator 接口的匿名类)要简洁很多。但是它真正的威力在于它大大改进了可组合性(composability)。举例来说,Comparator 拥有一个用于逆序的默认方法。于是,如果想把列表按照姓进行反序排序,我们只需要创建一个和之前一样的比较器,然后调用反序方法即可:

people.sort(comparing(p -> p.getLastName()).reversed());

与之类似,默认方法 thenComparing 允许你去改进一个已有的 Comparator:在原比较器返回相等的结果时进行进一步比较。下面的代码演示了如何按照姓和名进行排序:

Comparator c =
    Comparator.comparing(p -> p.getLastName())
              .thenComparing(p -> p.getFirstName());
people.sort(c);

可变的集合操作(Mutative collection operation)

集合上的流操作一般会生成一个新的值或集合。不过有时我们希望就地修改集合,所以我们为集合(例如 Collection,List 和 Map)提供了一些新的方法,比如 Iterable.forEach(Consumer),Collection.removeAll(Predicate),List.replaceAll(UnaryOperator),List.sort(Comparator) 和 Map.computeIfAbsent()。除此之外,ConcurrentMap 中的一些非原子方法(例如 replace 和 putIfAbsent)被提升到 Map 之中。

小结(Summary)

引入 lambda 表达式是 Java 语言的巨大进步,但这还不够——开发者每天都要使用核心类库,为了开发者能够尽可能方便的使用语言的新特性,语言的演化和类库的演化是不可分割的。Stream 抽象作为新增类库特性的核心,提供了强大的数据集合操作功能,并被深入整合到现有的集合类和其它的 JDK 类型中。

跳至工具栏