• 关于和leader沟通

    处理了一点儿杂事后什么也不想干,干脆写点儿东西好了。 前两天下班后在地铁上和同事聊了些关于和leader沟通的话题。这两天总结了下,一般情况下有如下几个原则: 经常沟通,让leader知道你在做什么; 遇到问题时最好带上自己的解决方案; 挑恰当时间; 尽量周到简洁; 意识到leader更关注结果; 保持一致,避免无意义的冲突。 随时保持沟通不只是可以让leader知道你工作的进度,也可以和他交流工作中遇到问题、解决的方案,让他意识到你对工作的态度和看法,更重要的是可以顺便学习他思考问题的方式、处理问题的手段。哪怕是为了让周报更好写一些,这也是该做的事情。 遇到问题时只是简单的将问题上报给leader并不是一件负责的行为,这在某程度上也可以视为甩锅给他。遇到了问题能先主动解决最好,如果不能解决,负责一些的做法是将自己的思路和方案说出来,这表示:我很重视这个问题,并且愿意承担责任,不过需要多些时间和资源。 第三条没什么好说的,其实不只是和leader,和任何同事交流都需要尊重对方的时间。 沟通工作的事情时应该做到周到简洁清晰,不产生歧义。就事说事,不确定的部分不要说,没弄清楚的老实说不清楚。(这一点我需要注意) 最后两条可以放在一起考虑。leader不太可能事必躬亲,所以他通常只是分配任务制定好目标。我们要做的事情就是和他在目标上保持一致。在实现的过程中可能会出现一些分歧,此时需要提出建议,考虑修正方案,但是目标还是最重要的。在实现目标前,无意义的固执大可以放弃,最愚蠢的就是产生冲突了。 先就这样。 #########

    [阅读更多...]
  • HBase自定义Filter

    必需要提前说明下:不建议使用自定义的Filter。所有的Filter都是在服务端生效:就是说需要将自定义的Filter封装为jar,上传到HBase的类路径下,并重启HBase使之生效。对于生产环境的HBase来说,重启通常是不能接受的。 Filter的设置是在客户端完成的,而Filter的逻辑是在HBase的服务端完成的,中间需要一次序列化。我试过几种序列化方案,不过protobuffer以外的其他几种效果不算好。HBase自带的Filter也是用protobuffer进行的序列化,因此使用protobuffer还可以少传几个包。 需要提前说明的已经说完了,开始进入正题。这次从一个案例开始说起:在HBase中存储着用户行为记录,行键设计为“uid(6位)+etime(时间戳/1000)+tid(7位)+顺序号(8位)”。其中uid为用户ID、etime为事件时间、tid为行为标签。目标是检索出某个用户在指定时间范围内的几种行为数据。 针对这个案例我们自定义一个CustomRowKeyFilter,并将一个用户ID、事件起止时间以及多个行为ID作为CustomRowKeyFilter的成员变量。 代码中继承了FilterBase类,可以减少一些结构性的代码工作。至于Filter是如何工作的,在网上找到的这张图应该描述得很清楚了: 前面的代码只是实现了Filter的处理逻辑。要想使用这个Filter还需要做一些序列化处理。如前面所说序列化方案选择的是protobuffer,这里需要先定义一个描述文件CustomRowKeyFilterProto.proto,内容如下: 定义完成后,执行protoc命令: 其中“-I”指定了proto描述文件的父目录, “—java_out”指定了java类的类路径,具体请根据自己的情况进行设置。执行命令后会在包com.zhyea.dev.hbase.filter.proto下生成序列化工具类CustomRowKeyFilterProto.java。 接下来在CustomRowKeyFilter中重写Filter类的toByteArray()方法和parseFrom()方法: 这样自定义Filter就完成了。剩下的事情就是将之打包并上传到HBase(每个RegionServer)的类路径下。然后就可以在程序中使用了。 现在再仔细想想这个程序,是否一定需要一个自定义Filter呢!我们已经将查询需要的所有元素都定义在行键里了。那么可以使用“uid+起始时间”作为startRow,“uid+结束时间”作为stopRow完成时间范围的匹配,使用RegexStringComparator来处理tid的匹配,这样直接使用HBase提供的RowFilter就能解决问题了。唯一需要注意的事情就是在设计表时多花些心思在行键上罢了。 就是这样。 参考文档 HBase Filter介绍及执行流程:https://my.oschina.net/cloudcoder/blog/289649

    [阅读更多...]
  • 通过HA访问Hdfs获取ActiveNode

    通过HA访问Hdfs的时候如何获取到活跃节点是一个稍稍有些麻烦的事情。 目前使用过两种方案:一是通过webhdfs接口逐一访问测试,找到状态为可用的节点;一是在zookeeper上直接获取当前活跃的节点。 简单说下第二种方案。ha的ActiveNode在zookeeper上的存储节点为:/hadoop-ha/dcnameservice/ActiveStandbyElectorLock。只需要通过ZooKeeper的API监听获取这个节点的信息即可。不过这个节点保存的信息不能当做字符串来读取,它是一个序列化后的对象,需要反序列化才能使用。 实现的代码如下: 就这样。

    [阅读更多...]
  • HBase RowKey设计

    热点现象 HBase中的记录行按行键的字典顺序进行排序。这种设计有利于扫描(scan)记录。因此我们可以合理的设计行键,将相关的行或者需要一起读取的行放得靠近一些。不过设计得不好的行键也是热点现象的常见来源。当大量客户端流量指向集群中一个或少数几个节点时就容易产生热点现象。这里说的流量指的是读、写或其他操作。这些客户端流量可能会压倒负责托管某个区域(region)的机器,并因此导致性能下降甚至该区域的不可用。因为无法响应请求,这台主机上托管的其他region也可能会受到不良影响。因此如何设计数据访问模式以使集群全面均衡地发挥作用就很重要了。 为防止写数据的热点出现,就需要调整行键设计。原本有些数据非常需要写入同一个region,但是从更高的角度上来看,这些数据就应该分发到集群的多个region上,而非是一次性写入某一个region。下面会介绍一些常用的避免热点的方案,以及这些方案的优点和缺点。 盐化 这里说的盐与加密盐并无关系,只是将一串随机数添加到行键的头部。这里说的盐化指的是将一串随机分配的前缀添加到行键以使其按与原本不同的方式进行排序。不同前缀的数量应该和设计的region的数量一致。如果在均匀分布的行模式中反复出现了少量热点行模式,此时使用盐化的方式是很有用的。看一下下面的实例,在实例中演示了是如何使用盐化将数据分散到不同的regionServer中的,以及盐化对读取数据的影响。 假设我们有如下行键列表。存储记录的表按字母表给每个字母分配一个region,比如前缀“a”是一个region,前缀“b”就是另一个region。在表中所有以“f”开头的行键的记录就会被写入到相同的region中。在这个例子中我们有如下这样的一批行键: 现在我们想把这些行键对应的记录分发到不同的region中。我们将使用4个不同的盐a、b、c和d作为前缀。这样,包含盐前缀的记录就会被写到对应的region上。使用盐以后,我们会得到下面这样的行键。因为现在是将记录写到四个不同的region,相比只写入到一个region理论上会有四倍的吞吐量。 如果要添加一行新的记录,在现有的行键上随机填上四个盐中的一个作为前缀。 因为这种分配是随机的,同一条记录有可能会分配到不同的前缀,这样要想按字典顺序检索出记录就得做更多的工作。 使用盐化的方式扩大了写入的吞吐量,却也增加了读取数据的开销。 哈希 要替代随机分配盐的方案,可以使用单向哈希。这使用单向哈希每个行都会分配到一个固定的前缀。这样记录仍可以被写到不同的region上,不过在读的时候可以保证做到精确读取。使用确定性哈希允许客户端重建完整的行键并使用Get操作准确检索到记录。 仍然是前面盐化方案中的例子,可以使用单向哈希给“foo0003”这条记录始终分配一个可以预知的前缀“a”。这样在检索这一行记录的时候就可以就可以准确计算出行键。基于这个方案我们可以对行键做一些优化,比如让特定的一对行键总是被分配到相同的region中。 反转行键 第三个常见的防止热点的技巧是反转行键。也就是将固定宽度或数值类型的行键反转,这样行键中变化最频繁的部分(最低有效位)就会排到第一位。这有效地对行键进行了随机化,不过却牺牲了行排序属性。 单调递增行键或时间序列数据 在使用HBase时要警惕一个现象:所有的客户端集中锁定访问表的一个region(也就是hbase的一个节点),然后又全部移动向下一个region,就这样推进并形成循环。在使用单调递增的行键(比如使用时间戳)时就容易发生这样的情况。在BigTable类的数据存储中使用单调递增的行键是一个坏的选择。这里有一幅漫画描述了为什么单调递增是不好的。虽然可以采用随机化的方式打乱输入记录的排序顺序来减轻单调递增行键造成的单个region的访问热点的问题,但是通常也应尽量避免使用时间戳或者一个自增序列(比如1、2、3)作为行键。 如果确实需要上传时间序列数据到HBase,可以学习一个成功案例:OpenTSDB。这个项目有一个页面专门描述了它在HBase中使用的schema。OpenTSDB实际上使用的行键格式是:[metric_type][event_timestamp]。乍一看这和我们前面说的不建议使用时间戳作为行键是冲突的。差别就在于这里没有将时间戳置于行键的前导位置,并且在设计中假设存在几十几百个(或者更多)不同的metric_type。因此,尽管有不间断的输入数据流,相关的Puts操作也会被分发向不同的region。 尝试最小化行键和列的大小 在HBase中,每一个值都有与其对应的坐标。比如HBase系统中的一个cell value,它的坐标就是行键名、列名还有时间戳。如果行键名和列名很大——尤其是与cell value一起比较的时候——就可能会遇到一些有趣得情况。在HBASE-3551中Marc Limotte就描述了这样的一种情形。这里描述的问题是保存在HBase存储文件(HFile)上以便于随机访问的索引占用了大部分分配给HBase的RAM,原因就是cell value的坐标太大了。刚才引用的这个问题,在评论中有人建议调大blocksize,这样为HBase存储记录新建索引的时间间隔就会大一些,或者是修改表设计使用更小的列名和行键名。此外,压缩也能导致索引变大,在这里提到了一个相关的案例。 大多数时候这种小的低效率并没有大的影响。不幸的是它们确实有发生的契机,尤其是在需要访问几十亿条数据的时候。 行键长度 行键长度固然是越短越好,但是应尽量保证行键有意义,以便于对数据进行访问(包括Get和scan)。一个短的行键并不比一个虽然长却有利于scan或get的行键更好。因此在设计行键时需要反复权衡。 关于列族 列族名称应该尽量的小,最好是1个字母,比如使用d来替换default或data。 关于属性名称 尽管冗长的属性名称(比如:myVeryImportantAttribute)更容易理解,但是保存在HBase中的属性名称应该是越短越好(比如使用via)。 字节模式 一个长整型值的长度是8个字节。在这8个字节里我们可以保存一个最大为18,446,744,073,709,551,615的整数值。如果我们将这个整数值保存在字符串里——假设使用一个单词代表一个字节——我们将需要3倍的字节数。 不相信?下面是一段示例代码,可以自己运行一下试试看。 使用字节代表一个类型有一个缺点:将会使数据在代码以外的地方很难阅读。下面这个例子演示了在hbase shell中对一个值执行increase操作的结果: hbase shell会尽最大努力打印一个字符串。在这种情况下,它只会打印16进制的值。同样的情况也会发生在region内的行键上。通常如果知道正在写入的数据是什么是最好的,不过写入cell内的可以是任何内容,包括可读的和不可读的。这点在设计时需要仔细衡量。 反向时间戳 数据库中一个常见的问题是如何快速的找到一个值的最新版本。在某些特殊情况下使用反向时间戳技术作为行键的一部分可以很好地处理这个问题。反向时间戳即(Long.MAX_VALUE – timestamp)。通常是将反向时间戳追加在行键末尾,即[行键][reverse_timestamp]。这样,在通过一个原始的行键执行scan搜索时检索到的第一个值就是这个原始行键所对应的最新的值。 使用反向时间戳替换HBase的Versions管理的目的是永久保留一些值的所有版本,并在使用相同的Scan方案的时候仍然能够保持快速访问其它任何版本的能力。 行键和列族 行键的作用域是列族。因此一个表中的列族可以有相同的行键而不冲突。 行键的不变性 行键是不可变的。一个表中的行键可以被“改变”的方式就是将记录删除后再重新插入。 行键和region split的关系 如果要对表进行预分区,那么弄清楚region边界处的行键分布就很重要了。下面的例子说明了为什么这很重要,在实例中使用了可视化十六进制字符作为行键(比如”0000000000000000″ 到 “ffffffffffffffff”)。使用Bytes.split(这也是用Admin.createTable(byte[] startKey, byte[] endKey, numRegions)方法创建分区时使用的split策略)处理这个范围内的行键,计划创建10个分区,目前有如下的splits数组作为splitKey: (注意:右侧的注释说明了前导字节的值)。如上面所示的,第一个split字节是字符‘0’,最后一个split字节是字符‘f’。看起来就是这样,没毛病,是不是?先别着急下定论。 问题是所有的数据将会堆积在前两个region以及最后一个region处,并因此产生lumpy region(也有可能是hot region)问题。要理解为什么,可以先参考ASCII表。字符‘0’的byte值是48,字符‘f’的byte值是102。但是在字节值中有一个巨大的间隙(字节58 到 96)永远不会出现在行键空间中,因为有效值是[0-9]和[a-f]。因此中间部分的region将永远不会出现。要使示例的行键设计在预分区工作中生效,需要使用一个自定义的split策略(而不仅仅是依赖内置的split方法)。 从刚才描述的问题,我们能够获得如下两条经验: 经验一:预分区通常是一个不错的方案,但是需要确保预分区得到的所有regions是对行键可达的。在这个例子里演示了使用十六进制字符作为行键前缀造成的问题,类似的问题也可能发生在其他类型的行键设计上。因此首先要做的一件事情就是充分了解要写入HBase的数据。 经验二:尽管通常不建议,使用十六进制字节(通常建议使用可视化数据)作为预分区表的行键前缀也是可行的——只要创建的所有分区对于设计的行键是可达的即可。 为了好好结束这个例子,下面提供了一种适合十六进制行键的预分区split生成方案: 参考文档 Configuring The Blocksize For HBase ##################

    [阅读更多...]
  • 魔术师or建筑师

    前段时间(额,至少是六个月前)接手了一个应用。看了一圈代码心里满是郁闷。应用要处理的事情很简单,但是代码一点儿也不简单。给人的感觉就是为了要使用java8的一个特性而生生将程序扭曲成了一个奇怪的东西。当时就有心吐槽一番,不过拖延症发作才等到了今天。这也是一件好事情,因为在这段时间里多少经过了一些思考和反思——满是负面情绪吐槽只是一时痛快了,思考和反思却是对以后有益的事情。 回到问题上来。可以将一个开发者设计开发应用的心态分成两种:魔术师和建筑师。魔术师是想法最多的那一批人,他们会搬出各种令人眼花缭乱的技术,随口说出各种专业术语,拿出的方案有各种理论的支持。建筑师则相对保守,他们通常是从曾经使用的方案上衍生出方案,没有新意却有经验的支撑。需要在这两者间选择的时候该怎么办呢?可以从三个方面考虑:目标、一致性还有简洁性。 在设计和开发的时候,至少要思量两个目标:怎样完成任务需求以及这个任务对自己有哪些提升?这两个目标一个是对职业的负责、一个是对自己的负责。对职业负责,就是尽量将一切做好,做到问心无愧。对自己负责就是在工作中学习更多的东西、掌握更多的技术、开拓自己的人脉。最好的做法当然是将这两个目标拧到一个方向上。最不好的做法就是一心偷懒敷衍任务。还有一种做法是将任务做成一块试验田,试验田的意思就是更重视过程而不计收成。魔术师最容易犯这个毛病,虽然有时候不是有意的,但是经常会走到为技术而开发的路子上。 在团队开发中需要考虑到一致性。这里的一致性指的是在使用的技术、方案以致编码风格上做到统一。这样做有如下好处:降低风险,减少学习成本和沟通成本。如果不是有明确的需要,就不要打破这种一致性。建筑师很容易加入到一致的队伍中。魔术师则可能会对这种一致性产生反感。 要避免和魔术师的冲突只需要用事实说话。这里有一种思路:列出应用中需要商榷的部分,逐一进行验证,应该保留的就保留,可以简化的就简化、可以删除的就删除。这样最后剩下来的部分就是最该留下来的部分。最后保留的部分也成了一致性的构成。 这里说的魔术师和建筑师的衡量是从技术追求和业务需求两个方向进行的衡量。可能是因为之前想要吐槽的缘故,所以前面多少显得对技术追求型做法有些不认同。不过作为开发就得凭技术说话,对新技术的学习永远也不该停止。但是否要以及怎样将一项技术引入到工作中,就得全面考虑了。 就这样。

    [阅读更多...]
  • 使用sbt-assembly提示unresolved dependency

    在使用sbt-assembly打包的时候遇到失败。报的错误信息如下: 疑惑了好久。 后来在StackOverflow找到了说明。这个问题是scalaVersion导致的。sbt在build应用的时候会使用scalaVersion这个配置项指定的scala版本执行构建。然而sbt0.13自己是用scala2.10构建的,同样sbt.13的插件也是用scala2.10构建的。如果应用的scalaVersion不是2.10,使用sbt的插件的时候就有可能报上面的这个错。可以简单将build.sbt中的scalaVersion删除,然后再使用sbt assembly就能看到问题解决了。 不过这样并不是最好的解决方案。这个问题的根源是因为我添加sbt-assembly插件的时候是将assembly.sbt放在了根目录下。如果将assembly.sbt这个文件放在应用的project目录下就可以避免这个错误。或者将添加sbt-assemble插件的语句放到./project/plugins.sbt中也是可以的。引用sbt-assemble插件的语句如下: 这个问题纯粹是因为阅读文档不仔细导致的。官网上说的很清楚。以后需要注意。 ########

    [阅读更多...]
  • HBase Shell整理

    简单整理了下HBase Shell的常见命令。 version 查看HBase版本。 status 查看HBase集群的状态。 create 创建表。建表时需要指定表名及列族的名称。 list 列出HBase中创建的所有表的信息。在当前的HBase中创建了两个表,分别是test和test2。 describe 查看某个表的表结构。使用时表名需要用括号括起来。 这个命令也可以使用简写的“desc”来代替。 disable和enable 用来启用禁用表。通常和表结构调整命令或者删除命令一起使用。 is_disabled和is_enabled 用来查看是否已经禁用或启用表。 put 向表里写数据,格式是:put ‘表名’, ‘行键’, ‘列族:限定符’, ‘值’。 get 从表中取一行数据,格式是:get ‘表名’, ‘行键’。 结合上面的put命令,我们可以看出hbase数据更新的方式就是向表里put新的记录。 scan 扫描全表。查看表里的全部数据。 alter 用来调整表结构。 执行前先看一下目标表test2的结构: 表中目前只有一个列族“cf2”,使用如下命令来新增列族“cf1”: 执行结果如下: 使用如下命令来删除列族“cf2”: 执行结果如下: 使用如下命令改变列族“cf1”单元数目: 执行结果如下: count 查看表中有多少条记录,格式是:count  ‘表名’: delete 删除指定行键和列族的命令。hbase的删除并不是立刻删除,而是将要删除的记录标记为可以删除,而后在下一次region合并分裂时将标记为删除的记录删除。 命令格式为:delete ‘表名’, ‘行键名’, ‘列族名’, [时间戳]。 执行结果: 可以看到虽然执行了删除命令,但是记录没有被立即删除掉。 deleteall 删除整行数据的命令。 删除命令格式:deleteall ‘表名’,  ‘行键名’。 执行示例如下: truncate 删除表中的全部记录。命令格式:truncate ‘表名’。 执行示例如下: 在执行过程中看到truncate表前不需要执行disable命令,truncate命令在执行前就自动执行disable了。 exists 检查表是否存在。命令格式如下:exists ‘表名’。 执行示例如下: drop 删除表的命令。命令格式:drop ‘表名’。 drop前需要先disable。 执行实例: 更新(update) hbase没有提供更新命令。hbase的更新是通过put新的数据覆盖旧的数据来完成的。 hbase是通过行键、列族、时间戳这三个维度来作为数据的唯一性的标准的。所以要更新数据只需要put一条这个维度都一致的数据即可。 参考文档 HBase的数据的update:  http://punishzhou.iteye.com/blog/1266341 ######################

    [阅读更多...]
  • 解决Gson 处理Map将整型处理为浮点型的问题

    gson一直是我用着非常得心应手的json处理工具。但是最近遇到了一个坑,就是在处理java.util.Map型json字符串的时候会把整型转为浮点型。 示例程序如下: 执行结果如下: 原因在于json的语法中关于数值只有一个number类型,而不会去判断这个number具体是整型是浮点型还是长整型。而Gson处理的时候也确实是偷懒了,统一将之视为浮点型。可以在Gson的ObjectTypeAdapter类中看到: 在case NUMBER这一行可以看到,数值型的数据都被处理为Double型的值。此外因为这里使用的Map的value类型是Object,在示例代码中就涉及到了Integer、Double和String。所以也不能使用TypeToken这样的解决方案。使用TypeToken的方式如下,适用于有明确的泛型说明的情况: 其它的解决方案也简单:1. 使用其他的json库,比如jackson和fastjson,亲测过,都没有这种问题;2. 添加自定义模块,修改这个问题。 详细说明下方案2。Gson支持添加自定义解析方案,可以使用GsonBuilder的registerTypeAdapter和registerTypeHierarchyAdapter。前者只针对设置的类进行序列化及反序列化,后者可以对设置的类及其子类进行序列化。可以添加的解析类的类型包括JsonSerializer、JsonDeserializer和TypeAdapter这三个接口的实现类。下面是一个使用自定义的JsonDeserializer方案: 这个方案也是有局限性的,只适用于Map的key和value都是数值型和字符串的情况(也可以添加对boolean的支持)。对于复杂的Map结构就有些无力了。 此外另一个方案是根据ObjectTypeAdapter自定义TypeAdapter。这个我也试过,功能支持还算可以,它解析json的方式是分层实现的。不过如果目标是具体类还可以,对于抽象类或接口的适用性就差一些。因为关键是json最外层的解析,如果是json对应的是对象的话,也就只能封装成一个目标类型对象或其超类对象。 先就这样。 #############

    [阅读更多...]
  • Hello Akka

    Akka是一个工具,用来在JVM上构建高并发、分布式、容错的事件驱动的应用。Akka支持java和scala两种语言。Akka最强大的一个特性是使用了并发Actor模型。 这一次本文会同时使用java和scala来进行说明。 示例代码 下面是一个非常简单使用akka的的例子。 java(做了折叠): scala(做了折叠): 在上面的例子中定义了一个Greetor  Actor,这个Actor可以获取最新的greeting消息,并对两种行为作出响应:设置一个新的greeting字符串;返回最新的greeting字符串。 接下来会详细解释下这个例子。 定义消息类 Actor并没有提供让开发者调用的公共API。它是通过Actor处理的消息来提供公共API的。消息可以是任意类型(java中指的是继承了Object的类,在scala中指的是继承了Any的类)的对象。也就是说我们可以使用包装类来发送直接类型的消息,也可以发送一些数据结构比如数组或者集合类的对象。然而因为消息是Actor的公共API,所以定义消息类的时候需要保证消息类的名称有一定的语义,或者在指定领域内有意义,尽管有时仅仅是将已有的类简单封装了下。不过这样可以构建简单易懂的Actor系统,在调试的时候也可以更容易一些。 在代码中我们定义了三个消息类: WhoToGreet,重新定义新的greeting消息; Greet,向Actor请求最新的消息; Greeting,返回最新的greeting消息。 在示例中这三个消息类定义在外部类HelloAkka中。这里要注意在消息对象中保存的消息应该是不可变的,否则的话就可能会出现两个不同的Actor共享状态的危险,这违反了Actor模型的设计原则。 虽然在这个示例中我们没有使用远程连接,但是在定义消息类时实现序列化是个好习惯——这样我们使用akka扩展到多个节点时就不需要再回过来修改代码。 消息类的定义: 下面是使用scala定义消息类的代码。scala的case类和case对象对Actor消息的支持非常好,因为它们本就是不可变的且支持模式匹配(这在对Actor接收的消息进行匹配时很有用)。使用case类的另一个好处就是其默认支持序列化。 定义Actor Actor是Akka的执行单元。Actor是面向对象的,因为它也封装了状态和行为。但是它要比Java或Scala中的普通对象有更强的隔离性。Actor模型禁止两个或多个Actor间共享状态的行为。一个Actor观察另一个Actor的状态的方式就是向其发送一条消息来请求状态。Actor是非常轻量级的——它只受到内存的限制。每个Actor只会消耗几百个字节的内存,这意味着在一个应用中可以轻松地创建数百万个并发的Actor。Actor模型强大的隔离原则以及事件驱动模型(稍后会说到)和位置透明等特性让我们可以用直观的方式轻松解决并发问题和伸缩性问题。 在java中创建Actor类需要继承抽象类UntypedActor并实现onReceive方法。使用scala也是差不多的方式,需要继承Actor trait并实现receive方法。在onReceive方法中定义了Actor的行为,在这个方法中Actor可以对它收到的不同的消息做出不同的反馈。一个Actor可以有或者说是通常都会有状态。访问或者是改变一个Actor的状态是完全线程安全的,因为这受到Actor模型的保护。 现在开始创建一个Greeter Actor,并使用一个变量greeting作为这个Actor的状态。greeting代表获取的最新定义的greeting消息。在Greeter的onReceive方法里面我们添加了一些行为用来对Greet和WhoToGreet两种消息分别作出反馈。先来看Java的实现: 代码中定义的Actor继承了抽象类UntypedActor,意味着其接收的消息是没有类型约束的,如代码中就是Object。此外也有类限制的Actor,不过目前暂时不关注这些。通常使用的Actor都是没有类型约束的。 暂时先不要考虑代码中定义的getSender()、tell(…)和getSelf()等API。稍后说到发送和响应消息时会详细解释这些API。 现在再看一下scala的实现。如代码中所示,scala case类的模式匹配的特性可以很大程度地简化Actor的receive方法。不过除此以外的内容还是还是和java版本很相似的: 有没有注意到scala版本的Greetor和java版本的一处不同:在scala版本的代码中并没有将未知类型的消息传递给unhandled方法。在scala中这不是必需的,因为scala将receive方法的行为解释为一个偏应用函数,也就是说匹配不上的语句会被默认为不处理,并由Akka自动将之传递给unhandled()方法。 另外一个不同就是Scala版本中继承的trait是Actor,而非是UntypedActor。因为这是scala的API,而不是Java的API,尽管二者本质上是同一种Actor。 创建Actor 到现在我们已经说过了如何创建Actor和消息。接下来我们会说一下如何创建Actor的实例。在Akka中创建Actor实例不能像平常一样直接使用new关键字,需要通过一个工厂来创建。这个工厂也不会直接返回一个目标Actor的实例,而是返回一个指向Actor实例的ActorRef对象。使用ActorRef看起来像是隔了一层,但是却增加了许多功能和灵活性。比如说位置透明:在相同语义的情况下,ActorRef表示的正在运行的实例既可以是在当前进程下也可以是在远端机器上。也就是说,位置并不重要。这也意味着,如果需要的话,可以在运行时改变Actor的位置或者调整应用的拓扑结构来优化系统。ActorRef这种间隔带来的另一个特性是使用“let it crash”模型来进行故障管理:系统可以主动crash故障的Actor并重启以实现自我治愈。 Akka中的这个工厂是ActorSystem。ActorSystem在某种程度上类似于Spring的BeanFactory,它也可以作为所有Actor的容器,执行管理这些Actor的生命周期等工作。可以通过一个名为actorOf的工厂方法创建Actor实例。这个方法需要一个Props的配置实例和一个名称。Actor(和ActorSystem)的名称在Akka中很重要,可以在查看Actor信息和在配置文件中添加配置时使用它们。因此完全有必要花些时间为Actor和ActorSystem起一个好名称。 下面是用java写的代码: scala的代码也没有太多不同: 现在我们已经创建了一个Greeter Actor的运行实例。接下来我们要看一下如何与之进行通信。 告诉Actor去做一些事情 和Actor的所有通信都是通过异步消息传递完成的。这也是如何使Actor做出反应以及事件驱动的方式。Actor不会主动做任何事情,除非它被通知做某事。开发者可以通过发送消息通知Actor做某些事。异步发送消息意味着发件方不会坚持等待接收方处理完消息。相反的,发件方只是将消息发送到接收方的收件箱里,然后就可以自由地去做一些比等待接收方处理消息更重要的事情了。接收方的收件箱本质上是一个队列,且是有序的。这保证了同一个Actor发送的多条消息的排序会被保留,不过却有可能会与另一个Actor发送的消息交治。 你可能会想知道当一个Actor在不处理任何消息时会做些什么事情。会做一些其他的具体的工作么?其实不会,此时Actor处于完全暂停的状态,它不会消耗除了内存以外的任何资源。就像牵线木偶一样。 我们可以通过传递消息到ActorRef的tell方法中告诉Actor去做一些事情。这个方法将消息放到Actor的收件箱以后就会立即退回。 java代码: scala代码: 使用scala还可以写得更简洁一些: 这里使用的“!”是一个绑定操作。 向Actor做出回复 发件方的自引用 在上面的代码里,没有等待Actor作出回复。有时候通信不是简单地单向通信模式,而是倾向于请求应答模式。这时一个直接的方式是添加一个对发件方的引用作为消息的一部分,以便接收方可以通过这个引用发送回复给发件方。这是一个常见的情况,它由Akka直接支持。对于发送的每一条消息,开发者都可以选择是否传递发件方的引用(Actor对应的ActorRef)。如果是从一个Actor内发送消息,那么就可以用过该Actor的自引用访问到这个Actor的ActorRef。然而请注意,不应该这样使用。在Java中可以通过getSelf()访问自引用,在scala中则可以通过self()方法。 java代码: scala的代码就会简单一些了。scala有一个隐式参数列表的特性,它允许自动透明地将参数传递给方法。我们在向另一个Actor发送消息时,可以利用这个特性自动传递对发件方的引用。 下面这段代码如果是在Actor A内部调用的,会在发送消息时将Actor A的ActorRef作为消息的发件方一起传递出去: 如果选择在tell方法中不传递对发件方的引用,或者说是忘记了,就会默认使用一个被称为“dead-letter”的Actor的引用。“dead-letter”是所有未处理的消息的结尾标识,可以使用Akka的事件总线( Event Bus )来订阅这种消息。 引用发件方 发件方的引用将会在接收方Actor处理消息时可用。因为每条消息都有一个与之唯一配对的发件方引用,也就是说接收方处理的每条消息的发件方引用是一直在变化的。因此,如果开发者出于某种原因想要在处理消息后继续使用某个特定的发件方引用,就需要保证持有它——可以考虑将之保存在一个成员变量或类似的结构中。要访问发件方引用,在java中可以使用getSender()方法,在scala中则可以直接使用sender: scala代码如下: 使用收件箱 当前大部分实际应用的Actor应用都会使用不止一个Actor。Actor模型的发明者,Carl Hewitt,最近在一个采访中说道:“One Actor is no Actor. Actors come in systems”。这是很重要且富有智慧的一个评论。要真正利用Actor模型,就应该使用大量的Actor。Actor编程中的每一个难题都可以通过添加更多的Actor来解决——通过将这个难题拆分成更细的子任务并将之委托给新的Actor。 为了简单起见,我们在这个示例中只使用了一个Actor。这意味着如果我们是从主程序与这唯一的一个Actor进行通信,我们就没有发件方,因为我们没有从另一个Actor的内部发送消息。幸运的是Akka提供了一个很好地解决方案:Inbox(收件箱)。 Inbox允许开发者创建一个“actor-in-a-box”。也就是说在Inbox中可以包含一个傀儡式的Actor,通过这个傀儡Actor可以向其他Actor发送消息并接收它们的回复。可以使用Inbox.create()方法创建一个Inbox实例,并使用inbox.send()方法从中发送消息。Inbox内置的傀儡Actor会把收到的所有消息放到一个队列里面,而后可以使用inbox.receive()方法将消息取出来。如果取消息的时候队列为空,那么调用的receive方法将会阻塞——直到有一条消息可以取出位置。很简单是吧。 开发者应该都知道:阻塞非常容易影响性能和扩展性,使用阻塞应当慎之又慎。我们在这实例中使用阻塞方法是因为它可以简化消息流,方便大家理解Actor模型。 现在我们将通过编写Greeter Actor的驱动程序代码来结束这篇文章。 java版本: scala版本: 就这样! 参考文档 http://www.lightbend.com/activator/template/hello-akka

    [阅读更多...]
  • 需要class,interface或enum

    引入github上的一个项目后在编译时收到了“需要class,interface或enum”这样的错误。就像下图: 图片是我随便找的,不过错误是一样的。 这样的错误通常是由编码问题导致的,只需要修改文件编码或项目编码就可以了,然而随之而来的另一个问题就是代码中的中文可能会丢失。后来找的解决方案是这样的,在pom中指定项目编码: 还有就是在编译时指定编码格式,具体怎么做也忘了,请自行百度或谷歌吧。

    [阅读更多...]