昊天

2011年5月24日

HBase数据模型

Row Key

  1. 类似于MySQL中的主键,HBase天然自带的,在创建时不需要显示指定
  2. HBase不支持条件查询和Order by等查询,读取记录只有三种方式:①. 通过单个Row key访问【速度很快,因为存在着索引机制②. 给定Row key的range ③. 全表扫描
  3. Row key按照字典序存储,要充分考虑排序存储这个特性,将经常一起读取的行存储放到一起(位置相关性)。设计方法参见HBase表设计
  • 字典序对int排序的结果是1,10,100,11,2,20,21,…,9。要保持整形的自然序,行键必须用0作左填充
  • 行的一次读写是原子操作 (不论一次读写多少列),使得多用户不能并发对同一个行进行更新操作。

Column Family

  1. 建表时手动指定,包含一个或者多个列
  2. 列族中的数据都是以二进制的形式保存在hdfs上,没有数据类型
  3. 增加新的列族:先disable 'users' / alter 'users','info' / enable 'users'。
  4. 删除列族方式:先disable 'users' alter 'users',{NAME=>'info',METHOD=>'delete'} / enable 'users'
  5. 不能重命名列族:通常做法是使用API创建一个有着期望名称的新的列族,然后将数据复制过去,最后再删除旧的列族。
  6. 每个列族存储在HDFS上的一个单独文件中,空值不会被保存。
  7. 同一Column Family的Columns会群聚在一个存储文件上,并依Column key排序,因此设计时:读写相关性较高的数据,存在同一列族中

Column

  1. 列名在添加数据时动态添加,无需在建表时指定。没有具体的数据类型,以二进制方式存储在HDFS上。
  2. 设置列值:put 'users','xiaoming','info:age','18'
  3. 读取列值:get 'users','info:age' 

TimeStamp

  1. 默认值使用系统时间戳,如果应用程序要避免数据时间戳冲突,就必须自己生成具有唯一性的时间戳。
  2. 每个cell中,不同版本的数据按照时间倒序排列,即最新的数据排在最前面。
  3. 为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,HBase提供了两种数据版本回收方式。一是保存数据的最后n个版本二是保存最近一段时间内的版本(即设置HColumnDescriptor.setTimeToLive(); 比如最近七天)。用户可以针对每个列族单独进行设置

Cell

  1. HBase中通过" tableName + RowKey + ColumnKey "确定的唯一存贮单元称为Cell。
  2. 每个Cell都保存着同一份数据的多个版本,每个版本通过时间戳Time Stamp来索引。
  3. Cell的每个值通过4个键唯一索引,tableName + RowKey + ColumnKey + Timestamp => value

存储类型

  • TableName 是字符串
  • RowKey 和 ColumnFamily 是二进制值(Java 类型 byte[])
  • Timestamp 是一个 64 位整数(Java 类型 long)
  • Value 是一个字节数组(Java类型 byte[])。 


HBase是一种专门为半结构化数据(semistructured)和水平可扩展性(horizontalscalability)设计的数据库。
它把数据存储在表里。在表里,数据按照一个四维坐标系统来组织:行键、列族、列限定符和时间版本。
HBase 是无模式数据库,只需要提前定义列族。它也是无类型数据库,把所有数据不加解释地按照字节数组存储。
有5个基本命令用来访问HBase中的数据,即GetPutDeleteScan 和Increment
基于非行键值查询HBase的唯一办法是通过带过滤器的扫描Scan


HBase 不是一个ACID 兼容数据库。但是HBase 提供一些保证,当你的应用系统访问HBase系统时,你可以用其来使你的应用系统的行为更加合理。这些保证具体如下。
1.操作是低级原子不可分的。换句话说,指定行的Put()要么整体成功要么整体失败回到操作开始前的状态,永远不会部分行写入而另一部分没有。这个要素和操作执行的列族的数量无关。
2.行间操作不是原子性的。不能保证所有操作整体成功或者失败。所有单行操作如上一点所述是原子性的。
3.checkAnd*和increment*操作是原子不可分的。
4.对于给定行的多个写操作,总是以每个写操作为整体彼此独立的。这是第一点的延伸。
5.对于给定行的任何Get()操作,返回系统当时所保存的完整行。
6.全表扫描不是对某个时间点表的快照的扫描。如果扫描已经开始,但是在行R 被扫描器对象读出之前,行R 被改变了,那么扫描器读出行R 更新后的版本。但是扫描器读出的数据是一致的,得到行R 更新后的完整行。


当你搭建使用HBase 的应用系统时,这些背景信息是你需要注意的要点。

  • 数据模型从逻辑上可以分类为键值存储或有序映射的映射。物理数据模型是基于列族的列式数据库,单个记录以键值形式存储。HBase 把数据记录保存在HFile里,这是一种不能更改的文件格式。因为记录一旦写入就不能修改,新值将保存在新HFile里。在读取数据和数据合并时,数据视图需要在内存中重新衔接。
  • HBase Java API 通过HTableInterface 来使用表。表连接可以直接通过构造HTable 实例来建立。使用HTable 实例系统开销大,优选方式是使用HTablePool,因为它可以重复使用连接。表通过HbaseAdmin 、HTableDescriptor 和HColumnDescriptor 类的实例来新建和操作。5 个命令通过相应的命令对象来使用:Get、Put、Delete、Scan 和Increment。命令送到HtableInterface 实例来执行。递增Increment 有另外一种用法,使用HTableInterface.incrementColumnValue()方法。执行Get、Scan 和Increment 命令的结果返回到Result和ResultScanner 对象的实例。一个KeyValue 实例代表一条返回记录。所有这些操作也可以通过HBase Shell 以命令行方式执行。 
  • 预期的数据访问模式对HBase 的模式设计有很大的影响。理想情况下,HBase 中的表根据预期的模式来组织。行键是HBase 中唯一的全局索引坐标,因此查询经常通过行键扫描实现。复合行键是支持这种扫描的常见做法。行键值经常希望是均衡分布的。诸如MD5 或SHA1 等散列算法通常用来实现这种均衡分布。
posted @ 2016-12-30 11:44 昊天 阅读(338) | 评论 (0)编辑 收藏

HBase简介

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

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

上图描述了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

Row KeyTimestampColumn Family
URIParser
r1t3url=http://www.taobao.comtitle=天天特价
t2host=taobao.com
t1

r2t5url=http://www.alibaba.comcontent=每天…
t4host=alibaba.com

Ø  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进行管理:

-ROOT- && .META. Table

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

Ø  .META.:记录了用户表的Region信息,.META.可以有多个regoin

Ø  -ROOT-:记录了.META.表的Region信息,-ROOT-只有一个region

Ø  Zookeeper中记录了-ROOT-表的location

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

MapReduce on HBase

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

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

HBase系统架构

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中最核心的模块。

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的过程:

在理解了上述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的存储格式:

首先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数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

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

HLogFile

上图中示意了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,可参见上文描述。

结束

本文对HBase技术在功能和设计上进行了大致的介绍,由于篇幅有限,本文没有过多深入地描述HBase的一些细节技术。目前一淘的存储系统就是基于HBase技术搭建的,后续将介绍“一淘分布式存储系统”,通过实际案例来更多的介绍HBase应用。

posted @ 2016-12-30 11:43 昊天 阅读(130) | 评论 (0)编辑 收藏

Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera、Apache Storm、Spark都支持与Kafka集成。InfoQ一直在紧密关注Kafka的应用以及发展,“Kafka剖析”专栏将会从架构设计、实现、应用场景、性能等方面深度解析Kafka。

背景介绍

Kafka创建背景

Kafka是一个消息系统,原本开发自LinkedIn,用作LinkedIn的活动流(Activity Stream)和运营数据处理管道(Pipeline)的基础。现在它已被多家不同类型的公司 作为多种类型的数据管道和消息系统使用。

活动流数据是几乎所有站点在对其网站使用情况做报表时都要用到的数据中最常规的部分。活动数据包括页面访问量(Page View)、被查看内容方面的信息以及搜索情况等内容。这种数据通常的处理方式是先把各种活动以日志的形式写入某种文件,然后周期性地对这些文件进行统计分析。运营数据指的是服务器的性能数据(CPU、IO使用率、请求时间、服务日志等等数据)。运营数据的统计方法种类繁多。



近年来,活动和运营数据处理已经成为了网站软件产品特性中一个至关重要的组成部分,这就需要一套稍微更加复杂的基础设施对其提供支持。

Kafka简介

Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下:

  • 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。
  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输。
  • 支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输。
  • 同时支持离线数据处理和实时数据处理。
  • Scale out:支持在线水平扩展。

为何使用消息系统

  • 解耦

    在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

  • 冗余

    有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。

  • 扩展性

    因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。

  • 灵活性 & 峰值处理能力

    在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

  • 可恢复性

    系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

  • 顺序保证

    在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka保证一个Partition内的消息的有序性。

  • 缓冲

    在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。

  • 异步通信

    很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

常用Message Queue对比

  • RabbitMQ

    RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。

  • Redis

    Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。

  • ZeroMQ

    ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter的Storm 0.9.0以前的版本中默认使用ZeroMQ作为数据流的传输(Storm从0.9版本开始同时支持ZeroMQ和Netty作为传输模块)。

  • ActiveMQ

    ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。

  • Kafka/Jafka

    Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。

Kafka架构

Terminology

  • Broker

    Kafka集群包含一个或多个服务器,这种服务器被称为broker

  • Topic

    每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)

  • Partition

    Parition是物理上的概念,每个Topic包含一个或多个Partition.

  • Producer

    负责发布消息到Kafka broker

  • Consumer

    消息消费者,向Kafka broker读取消息的客户端。

  • Consumer Group

    每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。

Kafka拓扑结构

如上图所示,一个典型的Kafka集群中包含若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。

Topic & Partition

Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。若创建topic1和topic2两个topic,且分别有13个和19个分区,则整个集群上会相应会生成共32个文件夹(本文所用集群共8个节点,此处topic1和topic2 replication-factor均为1),如下图所示。

每个日志文件都是一个log entrie序列,每个log entrie包含一个4字节整型数值(值为N+5),1个字节的"magic value",4个字节的CRC校验码,其后跟N个字节的消息体。每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下:

message length : 4 bytes (value: 1+4+n) "magic" value : 1 byte  crc : 4 bytes  payload : n bytes 

这个log entries并非由一个文件构成,而是分成多个segment,每个segment以该segment第一条消息的offset命名并以“.kafka”为后缀。另外会有一个索引文件,它标明了每个segment下包含的log entry的offset范围,如下图所示。

因为每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。

对于传统的message queue而言,一般会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此Kafka提供两种策略删除旧数据。一是基于时间,二是基于Partition文件大小。例如可以通过配置$KAFKA_HOME/config/server.properties,让Kafka删除一周前的数据,也可在Partition文件超过1GB时删除旧数据,配置如下所示。

   # The minimum age of a log file to be eligible for deletion log.retention.hours=168 # The maximum size of a log segment file. When this size is reached a new log segment will be created. log.segment.bytes=1073741824 # The interval at which log segments are checked to see if they can be deleted according to the retention policies log.retention.check.interval.ms=300000 # If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction. log.cleaner.enable=false 

这里要注意,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高Kafka性能无关。选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka会为每一个Consumer Group保留一些metadata信息——当前消费的消息的position,也即offset。这个offset由Consumer控制。正常情况下Consumer会在消费完一条消息后递增该offset。当然,Consumer也可将offset设成一个较小的值,重新消费一些消息。因为offet由Consumer控制,所以Kafka broker是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过broker去保证同一个Consumer Group只有一个Consumer能消费某一条消息,因此也就不需要锁机制,这也为Kafka的高吞吐率提供了有力保障。

Producer消息路由

Producer发送消息到broker时,会根据Paritition机制选择将其存储到哪一个Partition。如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。如果一个Topic对应一个文件,那这个文件所在的机器I/O将会成为这个Topic的性能瓶颈,而有了Partition后,不同的消息可以并行写入不同broker的不同Partition里,极大的提高了吞吐率。可以在$KAFKA_HOME/config/server.properties中通过配置项num.partitions来指定新建Topic的默认Partition数量,也可在创建Topic时通过参数指定,同时也可以在Topic创建之后通过Kafka提供的工具修改。

在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Parition。Paritition机制可以通过指定Producer的paritition. class这一参数来指定,该class必须实现kafka.producer.Partitioner接口。本例中如果key可以被解析为整数则将对应的整数与Partition总数取余,该消息会被发送到该数对应的Partition。(每个Parition都会有个序号,序号从0开始)

import kafka.producer.Partitioner; import kafka.utils.VerifiableProperties;  public class JasonPartitioner<T> implements Partitioner {      public JasonPartitioner(VerifiableProperties verifiableProperties) {}      @Override     public int partition(Object key, int numPartitions) {         try {             int partitionNum = Integer.parseInt((String) key);             return Math.abs(Integer.parseInt((String) key) % numPartitions);         } catch (Exception e) {             return Math.abs(key.hashCode() % numPartitions);         }     } } 

如果将上例中的类作为partition.class,并通过如下代码发送20条消息(key分别为0,1,2,3)至topic3(包含4个Partition)。

public void sendMessage() throws InterruptedException{   for(int i = 1; i <= 5; i++){         List messageList = new ArrayList<KeyedMessage<String, String>>();         for(int j = 0; j < 4; j++){             messageList.add(new KeyedMessage<String, String>("topic2", j+"", "The " + i + " message for key " + j));         }         producer.send(messageList);     }   producer.close(); } 

则key相同的消息会被发送并存储到同一个partition里,而且key的序号正好和Partition序号相同。(Partition序号从0开始,本例中的key也从0开始)。下图所示是通过Java程序调用Consumer后打印出的消息列表。

Consumer Group

(本节所有描述都是基于Consumer hight level API而非low level API)。

使用Consumer high level API时,同一Topic的一条消息只能被同一个Consumer Group内的一个Consumer消费,但多个Consumer Group可同时消费这一消息。

这是Kafka用来实现一个Topic消息的广播(发给所有的Consumer)和单播(发给某一个Consumer)的手段。一个Topic可以对应多个Consumer Group。如果需要实现广播,只要每个Consumer有一个独立的Group就可以了。要实现单播只要所有的Consumer在同一个Group里。用Consumer Group还可以将Consumer进行自由的分组而不需要多次发送消息到不同的Topic。

实际上,Kafka的设计理念之一就是同时提供离线处理和实时处理。根据这一特性,可以使用Storm这种实时流处理系统对消息进行实时在线处理,同时使用Hadoop这种批处理系统进行离线处理,还可以同时将数据实时备份到另一个数据中心,只需要保证这三个操作所使用的Consumer属于不同的Consumer Group即可。下图是Kafka在Linkedin的一种简化部署示意图。

下面这个例子更清晰地展示了Kafka Consumer Group的特性。首先创建一个Topic (名为topic1,包含3个Partition),然后创建一个属于group1的Consumer实例,并创建三个属于group2的Consumer实例,最后通过Producer向topic1发送key分别为1,2,3的消息。结果发现属于group1的Consumer收到了所有的这三条消息,同时group2中的3个Consumer分别收到了key为1,2,3的消息。如下图所示。

Push vs. Pull

作为一个消息系统,Kafka遵循了传统的方式,选择由Producer向broker push消息并由Consumer从broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用push模式。事实上,push模式和pull模式各有优劣。

push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成Consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据Consumer的消费能力以适当的速率消费消息。

对于Kafka而言,pull模式更合适。pull模式可简化broker的设计,Consumer可自主控制消费消息的速率,同时Consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

Kafka delivery guarantee

有这么几种可能的delivery guarantee:

  • At most once 消息可能会丢,但绝不会重复传输
  • At least one 消息绝不会丢,但可能会重复传输
  • Exactly once 每条消息肯定会被传输一次且仅传输一次,很多时候这是用户所想要的。

    当Producer向broker发送消息时,一旦这条消息被commit,因数replication的存在,它就不会丢。但是如果Producer发送数据给broker后,遇到网络问题而造成通信中断,那Producer就无法判断该条消息是否已经commit。虽然Kafka无法确定网络故障期间发生了什么,但是Producer可以生成一种类似于主键的东西,发生故障时幂等性的重试多次,这样就做到了Exactly once。截止到目前(Kafka 0.8.2版本,2015-03-04),这一Feature还并未实现,有希望在Kafka未来的版本中实现。(所以目前默认情况下一条消息从Producer到broker是确保了At least once,可通过设置Producer异步发送实现At most once)。

    接下来讨论的是消息从broker到Consumer的delivery guarantee语义。(仅针对Kafka consumer high level API)。Consumer在从broker读取消息后,可以选择commit,该操作会在Zookeeper中保存该Consumer在该Partition中读取的消息的offset。该Consumer下一次再读该Partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然可以将Consumer设置为autocommit,即Consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了Exactly once。但实际使用中应用程序并非在Consumer读取完数据就结束了,而是要进行进一步处理,而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。

  • 读完消息先commit再处理消息。这种模式下,如果Consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于At most once

  • 读完消息先处理再commit。这种模式下,如果在处理完消息之后commit之前Consumer crash了,下次重新开始工作时还会处理刚刚未commit的消息,实际上该消息已经被处理过了。这就对应于At least once。在很多使用场景下,消息都有一个主键,所以消息的处理往往具有幂等性,即多次处理这一条消息跟只处理一次是等效的,那就可以认为是Exactly once。(笔者认为这种说法比较牵强,毕竟它不是Kafka本身提供的机制,主键本身也并不能完全保证操作的幂等性。而且实际上我们说delivery guarantee 语义是讨论被处理多少次,而非处理结果怎样,因为处理方式多种多样,我们不应该把处理过程的特性——如是否幂等性,当成Kafka本身的Feature)

  • 如果一定要做到Exactly once,就需要协调offset和实际操作的输出。精典的做法是引入两阶段提交。如果能让offset和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,Consumer拿到数据后可能把数据放到HDFS,如果把最新的offset和数据本身一起写到HDFS,那就可以保证数据的输出和offset的更新要么都完成,要么都不完成,间接实现Exactly once。(目前就high level API而言,offset是存于Zookeeper中的,无法存于HDFS,而low level API的offset是由自己去维护的,可以将之存于HDFS中)

总之,Kafka默认保证At least once,并且允许通过设置Producer异步提交来实现At most once。而Exactly once要求与外部存储系统协作,幸运的是Kafka提供的offset可以非常直接非常容易得使用这种方式。

posted @ 2016-12-30 11:41 昊天 阅读(186) | 评论 (0)编辑 收藏

鉴于自己的毕设是与视频检索有关,而在图像和视频检索领域中,D.Lowe和他的“亲儿子”——SIFT( Scale Invariant Feature Transform )算法是不能错过的经典论题,我在之前闲逛过的一个技术博客站点中找到了介绍这一经典算法基本概念的文章,原文地址:http://www.dakaren.com/index.php/archives/639.htm   

1、SIFT算法基本概念

Sift是David Lowe于1999年提出的局部特征描述子,可以处理两幅图像之间发生平移、旋转、仿射变换情况下的匹配问题,具有良好的不变性和很强的匹配能力。SIFT算法是一种提取局部特征的算法,也是一种模式识别技术,其基本思想是在尺度空间寻找极值点,提取位置,尺度,旋转不变量,它主要包括两个阶段,一个是Sift特征的生成,即从多幅图像中提取对尺度缩放、旋转、亮度变化无关的特征向量;第二阶段是Sift特征向量的匹配。Sift及其扩展算法已被证实在同类描述子中具有最强的健壮性,目前是国内外研究的热点。

2、SIFT算法的主要特点:

a) SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变 性,对视角变化、仿射变换、噪声也保持一定程度的稳定性,而对物体运动、遮 挡、噪声等因素也保持较好的可匹配性,从而可以实现差异较大的两幅图像之间 特征的匹配。

b) 独特性(Distinctiveness)好,信息量丰富,适用于在海量特征数据库中进行 快速、准确的匹配,比原有的harris点匹配方式具有更高的匹配准确度。

c) 多量性,即使少数的几个物体也可以产生大量SIFT特征向量。

d) 高速性,经优化的SIFT匹配算法甚至可以达到实时的要求。

e) 可扩展性,可以很方便的与其他形式的特征向量进行联合。

SIFT算法基于图像特征尺度选择的思想,建立图像的多尺度空间,在不同尺度下检测到同一个特征点,确定特征点位置的同时确定其所在尺度,以达到尺度抗缩放的目的。剔除一些对比度较低的点以及边缘响应点,并提取旋转不变特征描述符以达到抗仿射变换的目的。

3、SIFT算法步骤:

1) 构建尺度空间,检测极值点,获得尺度不变性;

2) 特征点过滤并进行精确定位;

3) 为每个关键点指定方向参数

4) 生成关键点的描述子

5) 当两幅图像的Sift特征向量生成以后,下一步就可以采用关键点特征向 量的欧式距离来作为两幅图像中关键点的相似性判定度量。取一幅图中的某个关键点,通过遍历找到另一幅图中的距离最近的两个关键点。在这两个关键点中,如果次近距离除以最近距离小于某个阙值,则判定为一对匹配点。降低这个比例阈值,SIFT匹配点数目会减少,但更加稳定。

4、SIFT算法发展历程:

Sift算子最早是由David.G.Lowe于1999年提出的,当时主要用于对象识别。2004年David.G.Lowe对该算子做了全面的总结及更深入的发展和完善,正式提出了一种基于尺度空间的、对图像缩放、旋转甚至仿射变换保持不变性的图像局部特征描述算子——Sift( Scale Invariant Feature Transform )算子,即尺度不变特征变换。Rob Hess 基于GSL和Opencv编写了相应的C语言程序,后来Y.Ke将其描述子部分用PCA代替直方图的方式,对其进行改进。在Mikolajczyk对包括Sift算子在内的十种局部描述子所做的不变性对比实验中,Sift及其扩展算法已被证实在同类描述子中具有最强的健壮性。

主要文献:

1)David G. Lowe, “Object recognition from local scale-invariant features,” International Conference on Computer Vision, Corfu, Greece  2)David G. Lowe, “Distinctive image features from scale-invariant keypoints,” International Journal of Computer Vision,

3)Y. Ke and R. Sukthankar. PCA-SIFT: A More Distinctive Representation for Local Image Descriptors.Computer Vision and Pattern Recognition, 2004

5、关于局部不变特征

1)局部不变特征的概念

局部不变特征就是由局部邻域所构成的一个图像模式。局部不变特征可以是点集,也可以是边缘集合,或者一些小的图像块集合,甚至是上述集合的复合体。局部不变特征认为图像中总是存在一些特殊的区域,这些区域中的特征比其它图像区域的特征更加稳定,信息含量更高,能够表征图像的内容。局部不变特征的局部是指特征只是图像的局部区域,不变性是指该特征不会因为图像经历了各种变换而发生变化。

2)局部不变特征特点

局部不变特征的种类繁多,适合不同的特征提取场合,各自独立性较强,相互之间可以组合和借鉴。复合类型的局部不变特征可能会增加计算负担,但是能够取得更好的性能。

3)局部不变特征的应用

局部不变特征是一种十分有效的工具,大量研究表明它能够适应各种图像处理的应用场合,特别是在模拟人类视觉系统的物体识别领域,拥有强大的应用性。从直观的人类视觉印象来看,人类视觉对物体的描述也是局部化的,基于局部不变特征的图像识别方法十分接近于人类视觉机理,通过局部化的特征组合,形成对目标物体的整体印象,这就为局部不变特征提取方法提供了生物学上的解释,因此局部不变特征也得到了广泛应用。

4)特征描述符

特征描述符(Featrue Descriptors)指的是检测图像的局部特征(比如边缘、角点、轮廓等),然后根据匹配目标的需要进行特征的组合、变换,以形成易于匹配、稳定性好的特征向量,从而把图像匹配问题转化为特征的匹配问题,进而将特征的匹配问题转化为特征空间特征向量的聚类问题。

5)局部不变特征检测与局部不变特征区域的概念

局部不变特征检测就是从图像中检测出具有某种几何和光学不变性(geometric and photometric invariant)的局部不变特征区域。局部不变特征区域是以特征点(feature point or key point)为中心带有尺度信息的图像局部区域。局部不变特征认为,在大多数的图像中总能找到一些性质特殊的区域,它们可以稳定的提取,并且对各种图像变化具有良好的鲁棒性且包涵更多的图像内容信息。

6、基于局部不变特征的图像处理理论和技术主要包括四个部分:

1)图像尺度空间:图像数据包含大量混杂在一起的特征信息,按照局部不变特征的思想,这些特征信息是归属于不同类型不同属性的目标物体,其位置和控制区域各不相同,不同分布和参数的特征相互叠加和组合,这使得特征提取变得困难,所以需将这些特征进行一定的“分离”操作,将各类特征分散到整个图像数据空间中去,图像尺度空间就是为图像的各种不同类特征分离提供的一种数据表示法。

2)局部不变特征检测:在尺度空间内构造不变特征检测函数,生成对应尺度下的局部不变特征空间,检测其中具有一定特征显著性的局部不变特征区域,并把它作为特征描述的目标区域,确定每个特征的尺度系数,局部不变特征结构的位置和尺度范围。这些局部区域及其包含的信息形成对图像语义结构信息的表示,为进一步的特征描述提供图像内容的结构和范围信息。

3)局部不变特征描述:局部不变特征检测获得的特征仅仅给出了图像内容的结构信息,局部不变特征区域还需要从图像尺度空间表示的数据形式转化特征描述向量。局部不变特征描述就是用局部不变特征描述符(Local feature descriptor)去描述局部不变特征区域,用尽可能相互独立和完备的特征描述数据来表示复杂组合的目标物体,完整详细地描述图像内容,给出图像的语义信息。

4)特征匹配和检索:特征提取的最终目的是使用这些特征来进行目标识别和特征的检索,通过对特征描述空间中的特征数据进行分类、匹配和检索,实现各种图像识别应用。由于其良好的鲁棒性和抗干扰性,使的它作为目标识别中机器学习样本描述的首选特征,图像和视频检索方法也大都采用局部不变特征作为学习和检索的依据。

7、局部不变特征发展方向

目前,局部不变特征主要分为两个发展方向:

1) 结构化的局部不变特征提取模型,也就是特征提取模型分为四个较为清晰的处理模块(上文中有提到)。在局部不变特征检测方面,D.Lowe提出基于扩散方程的尺度不变的SIFT特征检测方法,以及由角点检测发展而来的Multi-scale Harris检测,具有仿射不变性的Harris-Laplace/Affine检测等,目前局部不变特征检测方法逐渐向着检测具有多种不变性和抗干扰性强的局部不变特征的方向发展。局部不变特征描述技术更加广泛,其中以SIFT,GLOH,Steerable Filters,Shape Context,Complex Filters等为主要特征描述符。特征检索和匹配模块一般是面向图像模式识别的具体应用场合,如图像检索,机器学习的样本特征集合,目标识别中的样本特征数据库等,同时在视频的检索领域也获得不错的效果。

2) 模仿人类的视觉系统,通过模仿人类视觉系统的运作原理提出了显著性区域理论。这一理论认为图像中的每个局部区域的重要性和影响范围并非同等重要,即特征不是同等显著的,其主要理论来源是Marr的计算机视觉理论和Treisman的特征整合理论,一般也称为“原子论”。该理论认为视觉的过程开始于对物体的特征性质和简单组成部分的分析,是从局部性

质到大范围性质,图像中的每个局部不变特征的视觉显著性是不同的,所以在局部不变特征的提取和描述时也遵循与人眼视觉注意选择原理相类似的机制(Visual Selective Attention Mechanism)。

8、软件:SIFT Keypoint Detector

该软件是可以在Linux或Windows系统中运行的汇编代码形式的SIFT特征点检测器, 它可以输出特征点和可以匹配到一个简单的ASCII格式文件需要的所有信息。 所提供的MATLAB程序和示例C代码可以读取特征点并根据它们对两幅图片进行匹配。

9、应用前景

SIFT算法是模式识别的一种高效手段,凡模式识别的应用方面都可以运用SIFT算法来改进识别速度。

医学:运动学人体机能研究

仿生学:人工模拟生物

人工智能:智能机器人、智能驾驶

刑侦技术:跟踪

军事用途:敌友识别(战机、战舰、潜艇、雷达跟踪等等)

posted @ 2013-08-09 16:33 昊天 阅读(1708) | 评论 (0)编辑 收藏

1、我先要给大家讲一个概念:spring 的多数据源事务,这是民间的说法。官方的说法是:spring 的分布式事务。明白了这个概念,问题就好解决了。

2、分布式事务的应用场景:工程中使用两个及以上数据库中,就要考虑使用分布式事务管理,否则事务不能回滚。

3、现有两种开源的第三方jar支持spring的分布式事务管理,它们分别是:jotm和Atomikos。通过google可以找到下载的链接,其中atomikos的下载需要先填写email信息,再登录email找到链接去下载。下载地址分别:

1、http://jotm.objectweb.org/

2、http://www.atomikos.com/Main/InstallingTransactionsEssentials

我使用的是jotm。

4、基于spring+ibatis的环境下配置jotm的方法很简单。只城要修改spring数据源的配置及事务的配置及可。以下是我的配置,供参考。

Xml代码  
  1.        
  2. <bean id="jotm" class="org.springframework.transaction.jta.JotmFactoryBean">  
  3.      <property name="defaultTimeout" value="500000"/>  
  4. </bean>    
  5.   
  6. <bean id="dataSource" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown">  
  7.      <property name="dataSource">  
  8.              <bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">  
  9.                      <property name="transactionManager" ref="jotm"/>  
  10.                      <property name="driverName" value="${driverClass}"/>  
  11.                      <property name="url" value="${jdbcUrl}"/>  
  12.              </bean>  
  13.      </property>  
  14.      <property name="user" value="${user}"/>  
  15.      <property name="password" value="${password}"/>  
  16. </bean>  
  17.   
  18. <bean id="dataSourceBbs" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown">  
  19.      <property name="dataSource">  
  20.              <bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">  
  21.                      <property name="transactionManager" ref="jotm"/>  
  22.                      <property name="driverName" value="${bbs.driverClass}"/>  
  23.                      <property name="url" value="${bbs.jdbcUrl}"/>  
  24.              </bean>  
  25.      </property>  
  26.      <property name="user" value="${bbs.user}"/>  
  27.      <property name="password" value="${bbs.password}"/>  
  28. </bean>      
  29.   
  30. <!-- JTA事务管理器 -->  
  31. <bean id="myJtaManager" class="org.springframework.transaction.jta.JtaTransactionManager">  
  32.         <property name="userTransaction" ref="jotm" />  
  33. </bean>    
  34.   
  35.  <!-- 事务切面配置 -->  
  36.  <aop:config>  
  37.          <aop:pointcut id="serviceOperation" expression="execution(* com.bohai.service.impl.*.*(..))"/>  
  38.          <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"  order="0" />  
  39.  </aop:config>  
  40.     
  41.  <!-- 通知配置 -->  
  42.  <tx:advice id="txAdvice" transaction-manager="myJtaManager">  
  43.          <tx:attributes>  
  44.                  <tx:method name="delete*" propagation="REQUIRED"  />  
  45.                  <tx:method name="save*" propagation="REQUIRED"  />  
  46.                  <tx:method name="update*" propagation="REQUIRED"  />  
  47.                  <tx:method name="*" propagation="REQUIRED"  />  
  48.                  <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>  
  49.     <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>  
  50.     <tx:method name="fetch*" propagation="SUPPORTS" read-only="true"/>  
  51.     <tx:method name="*_noTrans" propagation="NOT_SUPPORTED"/>  
  52.          </tx:attributes>  
  53.  </tx:advice>    
 5、jtom 还需要一个配置文件 carol.properties ,内容是:
Java代码  
  1. do not use CAROL JNDI wrapper           
  2. carol.start.jndi=false           
  3.            
  4. do not start a name server           
  5. carol.start.ns=false           
  6.            
  7. # Naming Factory        
  8. carol.jndi.java.naming.factory.url.pkgs=org.apache.naming   

 

文件放在classpath下面,也就是src下面。

6、需要jtom的以下jar:


 

7、经过测试,配置是成功的。事务可以回滚。
 

posted @ 2013-03-21 11:39 昊天 阅读(4364) | 评论 (2)编辑 收藏
无意中看到张子阳的博客中的这篇文章,个人觉得挺好的就转载过来跟大家分享了,就擅自转载过来跟大家分享了。——靳建通

收入是由什么决定的?
这位员工辞职的原因主要有两个:
  • 公司的薪水无法达到他的预期,未来一年在公司的收入前景也不是很明确。
  • 想要去做更底层的开发,方向是使用C/C++开发3D图形图像。而我们公司主要是.NET开发。
既然其中的一个原因是薪水无法符合预期,那么首先要搞清楚的就是收入是由什么决定的。
1.积累
首先要说的一点就是:积累。积累就是你在这家公司所创造的价值的积累。
你今天所领的薪水,并不是由你现在所创造的价值所决定的,而是包含了以前一段时期内其他同事所创造的价值。举个例子来说,公司目前排名前三的大客户:客户A、客户B、客户C。
  • 客户A是2008年接下来的,现在每年为公司贡献600万。
  • 客户B是2009年接下来的,现在每年为公司贡献500万。
  • 客户C是2010年接下来的,现在每年为公司贡献350万。
我的年薪是你的两倍还多。可我也承认,我现在所能创造的价值,靳建通和我的能力绝对不可能是你的两倍。可问题是:2008年、2009年、2010年这些年份我都在公司,上面的每一个大客户,都有我的贡献。靳建通而你2012年才新进公司,你并没有之前的积累。所以,新员工入职后,工资相较老员工会低一些是正常的。很多新员工总是认为自己的收入低了,吃亏了,实际上,很多情况下,新员工在加入公司的头一年,公司仅能维持平衡,即新员工创造的价值全当工资发给他了。靳建通直到第二年,有了上一年的积累之后,公司才有所盈余。而加入半年就离职的员工,对公司来说基本上是亏本的。靳建通这也就解释了为什么人员流动特别快的公司活不长,因为人力成本太高。
关于积累,我可以再举几个例子说明一下:
洪小莲,李嘉诚的秘书,几十年来一直追随李嘉诚,她从几千元的工薪族,做到身家上亿的工薪族,享受的是公司成长的回报。这种回报并非是她个人的学识和能力有了大幅的提高而得到的等价交换,很大程度上仅仅是因为她忠诚地待在这趟车上。
杨元庆,联想现在的CEO,研究生毕业后就一直追随柳传志,尽管一开始从事的是他并不很乐意的销售工作,但最终还是坚持了下来。上一次注意到他,是看到一则新闻,标题是“杨元庆自掏2000万奖励一线员工”。
上面只是正面的例子,也有反面的例子:
吴士宏,曾写了一本书叫做《逆风飞扬》,可谓是红极一时。1986年进入IBM,1998年离开IBM,进入微软,担任微软中国公司总经理,1999年进入TCL,2002年离开TCL。之后就离开了公众的视线。我特意去百度搜索靳建通“吴士宏现在在哪里”,没有任何的消息。我想如果她很成功的话,一定还属于“公众人物”,不至于连度娘都不知去向。
跳槽的话显然就要放弃先前的积累。比方说,当你跳槽到另一家公司以后,你曾经做过的系统、曾经服务过的客户仍然在为先前的公司创造着利润,可是跟你已经一毛钱关系都没有了(极少数公司有股票,另当别论)。所以跳槽之前要慎重考虑,跳得不好,有可能越跳越低。
既然新员工相对于老员工来说,收入低一些是正常的,那么老员工工资高也是合情合理的。但是有一些公司,我将其归为“无良公司”,它们会在老员工的收入高到一定程度的时候,将老员工砍掉,然后再招募低廉的新人来承担之前老员工的工作,以赚取更高的利润。我觉得这些都是小聪明,最后的结果就是,聪明能干一些的人,在看出公司的这些伎俩之后果断离职;能力一般的员工,也会把你这里当成培训基地,翅膀硬了就飞了,受损的最后还是公司,实在是得不偿失。还不如厚待老员工,也让新进的员工对未来有一个更好的预期。也有一些人向我抱怨说:“老员工待得久了,干劲都被磨光了,每天都是混日子,还不如新员工,不开他开谁?”。然后我反问他:“激励员工难道不正是你工作的一部分吗?”。这种情况的出现,更多时候,是管理者的责任,而非员工。
最后补充一点:我并不认为老员工工资比新员工高就一定是合理的。当公司对一个新员工开出很高的工资时,其实是出于这样一种期望:他能推动公司进步的更快。而如果他真的这样做到了,公司进步的更快了、收益更高了,可以反哺老员工,从而公司的整体待遇水平都提高了,不是皆大欢喜吗?可能一些老员工并不能明白这些,所以,靳建通当招一个新员工工资水平远高于现有的老员工时,为什么要这样做,最好能让老员工知晓。
2.老板
这个“老板”是宽泛的老板,不一定是公司最大的老板。有的时候,公司比较大,你的职位又比较低,大老板连有没有你这个人都不知道,此时的老板就是你的顶头上司。很多时候,你的收入与他也有着莫大的关系。靳建通
对于我来说,我的原则是:在我的能力范围内,我会为我的员工争取更好的待遇。表面上看,这样做很蠢,花6000块就能雇到一个人,为什么要花8000块?我不是这样认为的,我期望能和我的员工形成这样一种互动:我尽我的能力为你争取好的待遇,你也尽你的努力做好工作。
我不能要求员工“你先把工作做好,我自然会给你好的待遇”。总是要有人先迈出一步,总是要有一方先信任另一方,所以在你什么还没有做的时候,我就先信任你,并且给你尽可能好的待遇,那么我该做的事情都做了,我问心无愧,剩下的,就看你的表现了。
可能有人会想,都这样了怎么还会有人提出辞职?实际上,提出辞职的是一个毕业刚一年的小伙子,1989年生,毕业1年多,我给他的待遇是试用期9000,转正后9500。在给他这个待遇之前,我是进行过一些调研的,我打电话给我的一个表妹,她是西安电子科技大学的研究生(陕西省排名第三的学校,211院校),她和她的同学在今年毕业找工作的时候,多得是6000到8000的工资。所以从这方面来说,我并没有亏待你,而你要求12K的工资,我并不是不愿意给这么多,你的表现也说明了你是个很有潜力的人才。只是受经济环境的影响,今年公司的效益不及往年,要在一定程度上节省开支。其次,你让其他的老员工情何以堪?所以,综合起来,你的要求超出了我的能力范围之外,我无法开口向公司申请提高你的薪水。
3.门槛
除了积累和顶头上司两个决定因素以外,靳建通第三个决定因素就是你从事工作的门槛。为什么餐厅服务员的收入很低?为什么坐在前台收发快递的文员收入很低?因为这些工作的门槛很低,门槛低就意味着你不做有的是人能做,你不做有大批的“后备队伍”在等着做。靳建通由于庞大的后备队伍的竞争,你就无法提高自己的要价。而提升自己所从事工作的门槛,实际上就缩减了竞争者的规模。
程序开发也是一样。如果你想收入高,你就做一些别人做不了,又有市场的。
.NET在程序开发中就属于门槛比较低的一类。个中原因我想大家都懂的,就不在这里赘述了。做.NET不需要你科班出身,或许一点兴趣再加上一点时间,或许一个类似北大青鸟的培训,都可以让你开始从事.NET开发了。你可以不懂指针、不懂数据结构、不懂算法、不懂汇编、不懂很多东西,但照样可以做出一个.NET程序来。而这些人往往又是对薪资的要求没那么高的,这样无形中就拉低了.NET程序员的“身价”。.NET的易学易会,很大程度上是由于它的封装性比较好。底层的东西都屏蔽掉了,你只要知道学习一下命名空间,然后寻找相关的API去调用就好了。记得我们公司曾经开发过一个基于C语言的手持设备程序,没有任何的类库支持,连排序、链表这样.NET中的基本功能,都要自己来实现,更别提内存管理和程序逻辑了,和.NET比起来,门槛就相对高一些了。
所以,如果想收入高一些,那么就去做更高难度的技术工作,这里随便想了几个例子:
  • 百度、谷歌的搜索引擎算法。
  • 微软、谷歌、苹果的操作系统。
  • 网络游戏,例如《征途》的游戏引擎。
  • 大型企业的ERP,比方说SAP。
  • 软硬结合,比如单片机,电气自动化。
  • 以及我这位即将离职的同事说的,3D图形图像。
所以,从这个角度来看,这位同事的辞职是明智的,他很年轻,靳建通有的是机会重新选择自己的道路,所以我也祝愿他能有更好的发展。而这些好赚的钱,就留给我们来做了:-)。
4.平台
接下来要说的一个决定因素是平台。很多程序员觉得30岁就瓶颈了,30岁写程序就到头了,实际上,这只是你的平台比较小罢了。就拿我自己的公司来说,平台就不大,只要是踏踏实实工作过5年的程序员,基本上就能够胜任公司90%的技术工作了,剩下的10%,请教一下其他同事,进行一下技术交流,也完全能够解决。这样就存在一个问题:随着你年龄的增长,你的生活压力越来越大,要求越来越高,可是公司只要5年经验的程序员就够用了。假设市场上5年经验的程序员的平均要求是10K,凭什么要给你15K?你的优势在哪里?如果你没有突破,就会有“30岁写程序就到头了”的感觉。
而如果平台大一些情况就会不一样,比方说,你去了IBM,可能5年的经验不过刚刚入门而已。IBM有一个工程院,其中有5位院士(IBM Fellow)获得过诺贝尔奖,很多人钻研技术都超过20年或者更久。如果你对技术感兴趣,并执着去钻研的话,你可以不断地去挑战和攀登。
当然,你可能没那么好的运气和实力进入IBM,那么其他一些中型的平台也是不错的,比方说阿里巴巴、金蝶、百度、腾讯等等。在这里,至少你有足够的理由和需要再去进行深入学习。因为在这些地方,5年的经验是远远不够的,还需要进一步地学习和努力。
如果你和我一样,不巧没有那么大的平台,此时的选择大概有这么几种:
1. 你可以凭借你在公司的积累(第一节讲过的),过比较安逸的日子。如果比较幸运,押对了宝,公司发展得比较好,收入一样会变得非常可观;如果比较不幸,公司经营的状况不好,那就要承担比较大的风险了。说得难听一点,公司倒闭了你去哪里?你过去的积累已经一文不值,靳建通而你的年龄已经35,水平却相当于只有5年经验。你的竞争力在哪里?
2. 你可以凭自己的努力将现在所在的平台做大,换言之,把自己的小公司做大。这当然是比较积极的做法,也是我一直努力的方向。现在你看到的大公司,不也是从小公司一步一步做起的吗?不过这里还有两个问题:1、有的时候,你的力量在公司中的占比没那么大,你再怎么努力推进的速度也还是有限;2、你缺乏慧眼,选中的公司本身就缺乏长大的资质。我们往往只看到成功了的公司,却忽视了更多在竞争中倒下的公司。
3. 主动选择更大的平台,也就是跳槽了。但是跳槽也是有风险的,尤其是过了30岁的程序员,你在这家公司的收入高,是因为有之前的积累,换一家就没有积累了,等于从新人开始,而大多数的公司,5年经验的程序员就够用了。如果跳得不好,收入还可能越跳越低,如果还有老婆、孩子、房贷,那将面临更大的压力。所以当你想要从一个低的平台向更高的平台跳跃的时候,平时就要做足功夫,认真积累自己的实力。对于我来说,我缺乏大型项目的管理经验,但是没关系,我努力学习考一个PMP没什么问题吧?我缺乏大型软件的架构经验,但是没关系,我把.NET的基础知识和各种设计模式掰开了揉碎了没什么问题吧?我缺乏大型团队的管理经验,但是没关系,每次遇到管理方面的问题我都认真思考仔细总结没什么问题吧?有些人总是抱怨没有机会,运气不好,我想机会总是有的,但只属于有准备的人。
5.行业
我想说的最后一点就是行业。有时候你觉得已经万事俱备了,可是你所处的这个行业本身就属于极低利润率的,你再怎么努力也很难有很高的收入。很多情况下,可能公司也想提高你的待遇,但是由于缺乏利润的支撑,公司也是有心无力。所以,在选择公司,尤其是小公司的时候,要重点考察一下公司所处的行业如何?是不是前景比较好、利润比较高的行业?如果是大公司的话,靳建通这方面的问题就会少一些,因为如果方向有问题,它就无法做成大公司。
行业是有周期性的,可能在一段时期内这个行业好,下一段时期这个行业就不行了。最典型的一个例子就是软盘,我现在的老板在成立这家公司之前是做销售的,他有一个客户,做索尼软盘的,这种软盘我想很多80后都见过。当时生意做得很大,可是当光盘出来以后,软盘的市场是会急剧萎缩的,可是这家公司的领导层居然没有看到,或者是看到了但不愿意转变,像鸵鸟一样在危机来临时把头埋在土里,继续做它的软盘。几年以后,这家公司就倒掉了。
选择行业也不是选择暴利行业就一定好,比方说房地产。资本都是逐利的,当一个行业属于暴利,同时所有人都知道它是暴利的时候,危机就来了。这个危机就是会有大量的社会资源、人力物力投入到这个行业中企图分一杯羹。而全局上又没有一个统一的把控,这个行业究竟需要多少公司才是合适的?最后的结果就是过剩。就好像股票在崩盘时,也许跌到3000点是比较合理也比较正常的位置,靳建通但是由于人们的恐慌,它就跌到1600点了。
感谢阅读,希望这篇文章能给你带来收获。
不是所有一年工作经验的毕业生都有这样的待遇,我主要是看能力,而不是年龄、学历等。特别说明一下,以免误导。
posted @ 2013-03-15 17:57 昊天 阅读(381) | 评论 (0)编辑 收藏

当前很多大型的web系统为了减轻数据库服务器负载,会采用memchached作为缓存系统以提高响应速度。

目录:

  1. memchached简介
  2. hash
  3. 参考资料

1. memchached简介

memcached是一个开源的高性能分布式内存对象缓存系统。
其实思想还是比较简单的,实现包括server端(memcached开源项目一般只单指server端)和client端两部分:

  • server端本质是一个in-memory key-value store,通过在内存中维护一个大的hashmap用来存储小块的任意数据,对外通过统一的简单接口(memcached protocol)来提供操作。
  • client端是一个library,负责处理memcached protocol的网络通信细节,与memcached server通信,针对各种语言的不同实现分装了易用的API实现了与不同语言平台的集成。
  • web系统则通过client库来使用memcached进行对象缓存。

2. hash

memcached的分布式主要体现在client端,对于server端,仅仅是部署多个memcached server组成集群,每个server独自维护自己的数据(互相之间没有任何通信),通过daemon监听端口等待client端的请求。
而在client端,通过一致的hash算法,将要存储的数据分布到某个特定的server上进行存储,后续读取查询使用同样的hash算法即可定位。

client端可以采用各种hash算法来定位server:
取模
最简单的hash算法

targetServer = serverList[hash(key) % serverList.size]

直接用key的hash值(计算key的hash值的方法可以自由选择,比如算法CRC32、MD5,甚至本地hash系统,如java的hashcode)模上server总数来定位目标server。这种算法不仅简单,而且具有不错的随机分布特性。

但是问题也很明显,server总数不能轻易变化。因为如果增加/减少memcached server的数量,对原先存储的所有key的后续查询都将定位到别的server上,导致所有的cache都不能被命中而失效。

一致性hash
为了解决这个问题,需要采用一致性hash算法(consistent hash)
相对于取模的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。

为了方便理解,可以把这个有限值域理解成一个环,值顺时针递增。
circle space
如上图所示,集群中一共有5个memcached server,已通过server的hash值分布到环中。

如果现在有一个写入cache的请求,首先计算x=hash(key),映射到环中,然后从x顺时针查找,把找到的第一个server作为目标server来存储cache,如果超过了2^32仍然找不到,则命中第一个server。比如x的值介于A~B之间,那么命中的server节点应该是B节点
image
可以看到,通过这种算法,对于同一个key,存储和后续的查询都会定位到同一个memcached server上。

那么它是怎么解决增/删server导致的cache不能命中的问题呢?
假设,现在增加一个server F,如下图


此时,cache不能命中的问题仍然存在,但是只存在于B~F之间的位置(由C变成了F),其他位置(包括F~C)的cache的命中不受影响(删除server的情况类似)。尽管仍然有cache不能命中的存在,但是相对于取模的方式已经大幅减少了不能命中的cache数量。

虚拟节点
但是,这种算法相对于取模方式也有一个缺陷:当server数量很少时,很可能他们在环中的分布不是特别均匀,进而导致cache不能均匀分布到所有的server上。

如图,一共有3台server – 1,2,4。命中4的几率远远高于1和2。
为解决这个问题,需要使用虚拟节点的思想:为每个物理节点(server)在环上分配100~200个点,这样环上的节点较多,就能抑制分布不均匀。
当为cache定位目标server时,如果定位到虚拟节点上,就表示cache真正的存储位置是在该虚拟节点代表的实际物理server上。

另外,如果每个实际server的负载能力不同,可以赋予不同的权重,根据权重分配不同数量的虚拟节点。

源码解析
下面结合一个java的memcached client(gwhalin / Memcached-Java-Client)的源码来看一下consistent hash的实现。
首先看server的分布:

  1. // 采用有序map来模拟环   
  2. this.consistentBuckets = new TreeMap();  
  3.   
  4. MessageDigest md5 = MD5.get();//用MD5来计算key和server的hash值   
  5.   
  6. // 计算总权重   
  7. if ( this.totalWeight   for ( int i = 0; i < this.weights.length; i++ )  
  8.         this.totalWeight += ( this.weights[i] == null ) ? 1 : this.weights[i];  
  9. else if ( this.weights == null ) {  
  10.     this.totalWeight = this.servers.length;  
  11. }  
  12.   
  13. // 为每个server分配虚拟节点   
  14. for ( int i = 0; i < servers.length; i++ ) {  
  15.     // 计算当前server的权重   
  16.     int thisWeight = 1;  
  17.     if ( this.weights != null && this.weights[i] != null )  
  18.         thisWeight = this.weights[i];  
  19.   
  20.     // factor用来控制每个server分配的虚拟节点数量   
  21.     // 权重都相同时,factor=40   
  22.     // 权重不同时,factor=40*server总数*该server权重所占的百分比   
  23.     // 总的来说,权重越大,factor越大,可以分配越多的虚拟节点   
  24.     double factor = Math.floor( ((double)(40 * this.servers.length * thisWeight)) / (double)this.totalWeight );  
  25.   
  26.     for ( long j = 0; j < factor; j++ ) {  
  27.         // 每个server有factor个hash值   
  28.         // 使用server的域名或IP加上编号来计算hash值   
  29.         // 比如server - "172.45.155.25:11111"就有factor个数据用来生成hash值:   
  30.         // 172.45.155.25:11111-1, 172.45.155.25:11111-2, ..., 172.45.155.25:11111-factor   
  31.         byte[] d = md5.digest( ( servers[i] + "-" + j ).getBytes() );  
  32.   
  33.         // 每个hash值生成4个虚拟节点   
  34.         for ( int h = 0 ; h < 4; h++ ) {  
  35.             Long k =  
  36.                 ((long)(d[3+h*4]&0xFF) << 24)  
  37.                   | ((long)(d[2+h*4]&0xFF) << 16)  
  38.                   | ((long)(d[1+h*4]&0xFF) << 8 )  
  39.                   | ((long)(d[0+h*4]&0xFF));  
  40.   
  41.             // 在环上保存节点   
  42.             consistentBuckets.put( k, servers[i] );  
  43.         }  
  44.   
  45.     }  
  46.     // 每个server一共分配4*factor个虚拟节点   
  47. }  


每个server根据权重获得一个虚拟节点数量控制因子factor,然后由services[i]+”-”+i来生成factor个hash值,生成hash值时采用MD5算法。
由于MD5长度是16个字节,正好划分成4段,每段4字节,这样每段就对应一个虚拟节点。linex-liney通过把这一段的4字节拼装成连续的32bit,作为低32位拉升为一个Long。

为key定位cache存储的server:

  1. // 用MD5来计算key的hash值   
  2. MessageDigest md5 = MD5.get();  
  3. md5.reset();  
  4. md5.update( key.getBytes() );  
  5. byte[] bKey = md5.digest();  
  6.   
  7. // 取MD5值的低32位作为key的hash值   
  8. long hv = ((long)(bKey[3]&0xFF) << 24) | ((long)(bKey[2]&0xFF) << 16) | ((long)(bKey[1]&0xFF) << 8 ) | (long)(bKey[0]&0xFF);  
  9.   
  10. // hv的tailMap的第一个虚拟节点对应的即是目标server   
  11. SortedMap tmap = this.consistentBuckets.tailMap( hv );  
  12. return ( tmap.isEmpty() ) ? this.consistentBuckets.firstKey() : tmap.firstKey();  


3. 参考资料

  1. 首次提出consistent hash的论文 – “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web”
  2. Consistent hashing Wiki – http://en.wikipedia.org/wiki/Consistent_hashing
  3. Ketama: Consistent Hashing
  4. memcached开源项目主页
  5. memcached google code主页
  6. gwhalin / Memcached-Java-Client主页
posted @ 2013-01-29 11:26 昊天 阅读(11456) | 评论 (0)编辑 收藏

一致性 hash 算法( consistent hashing

张亮

consistent hashing 算法早在 1997 年就在论文 Consistent hashing and random trees 中被提出,目前在 cache 系统中应用越来越广泛;

1 基本场景

比如你有 N cache 服务器(后面简称 cache ),那么如何将一个对象 object 映射到 N cache 上呢,你很可能会采用类似下面的通用方法计算 object hash 值,然后均匀的映射到到 N cache

hash(object)%N

一切都运行正常,再考虑如下的两种情况;

1 一个 cache 服务器 m down 掉了(在实际应用中必须要考虑这种情况),这样所有映射到 cache m 的对象都会失效,怎么办,需要把 cache m cache 中移除,这时候 cache N-1 台,映射公式变成了 hash(object)%(N-1)

2 由于访问加重,需要添加 cache ,这时候 cache N+1 台,映射公式变成了 hash(object)%(N+1)

1 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器;

再来考虑第三个问题,由于硬件能力越来越强,你可能想让后面添加的节点多做点活,显然上面的 hash 算法也做不到。

  有什么方法可以改变这个状况呢,这就是 consistent hashing...

2 hash 算法和单调性

   Hash 算法的一个衡量指标是单调性( Monotonicity ),定义如下:

  单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

容易看到,上面的简单 hash 算法 hash(object)%N 难以满足单调性要求。

3 consistent hashing 算法的原理

consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。

下面就来按照 5 个步骤简单讲讲 consistent hashing 算法的基本原理。

3.1 环形hash 空间

考虑通常的 hash 算法都是将 value 映射到一个 32 为的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环,如下面图 1 所示的那样。

circle space

1 环形 hash 空间

3.2 把对象映射到hash 空间

接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash key 在环上的分布如图 2 所示。

hash(object1) = key1;

… …

hash(object4) = key4;

object

2 4 个对象的 key 值分布

3.3 cache 映射到hash 空间

Consistent hashing 的基本思想就是将对象和 cache 都映射到同一个 hash 数值空间中,并且使用相同的 hash 算法。

假设当前有 A,B C 3 cache ,那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash 值排列。

hash(cache A) = key A;

… …

hash(cache C) = key C;

cache

3 cache 和对象的 key 值分布

 

说到这里,顺便提一下 cache hash 计算,一般的方法可以使用 cache 机器的 IP 地址或者机器名作为 hash 输入。

3.4 把对象映射到cache

现在 cache 和对象都已经通过同一个 hash 算法映射到 hash 数值空间中了,接下来要考虑的就是如何将对象映射到 cache 上面了。

在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 cache ,那么就将该对象存储在这个 cache 上,因为对象和 cache hash 值是固定的,因此这个 cache 必然是唯一和确定的。这样不就找到了对象和 cache 的映射方法了吗?!

依然继续上面的例子(参见图 3 ),那么根据上面的方法,对象 object1 将被存储到 cache A 上; object2 object3 对应到 cache C object4 对应到 cache B

3.5 考察cache 的变动

前面讲过,通过 hash 然后求余的方法带来的最大问题就在于不能满足单调性,当 cache 有所变动时, cache 会失效,进而对后台服务器造成巨大的冲击,现在就来分析分析 consistent hashing 算法。

3.5.1 移除 cache

考虑假设 cache B 挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿 cache B 逆时针遍历直到下一个 cache cache C )之间的对象,也即是本来映射到 cache B 上的那些对象。

因此这里仅需要变动对象 object4 ,将其重新映射到 cache C 上即可;参见图 4

remove

4 Cache B 被移除后的 cache 映射

3.5.2 添加 cache

再考虑添加一台新的 cache D 的情况,假设在这个环形 hash 空间中, cache D 被映射在对象 object2 object3 之间。这时受影响的将仅是那些沿 cache D 逆时针遍历直到下一个 cache cache B )之间的对象(它们是也本来映射到 cache C 上对象的一部分),将这些对象重新映射到 cache D 上即可。

 

因此这里仅需要变动对象 object2 ,将其重新映射到 cache D 上;参见图 5

add

5 添加 cache D 后的映射关系

4 虚拟节点

考量 Hash 算法的另一个指标是平衡性 (Balance) ,定义如下:

平衡性

  平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。

hash 算法并不是保证绝对的平衡,如果 cache 较少的话,对象并不能被均匀的映射到 cache 上,比如在上面的例子中,仅部署 cache A cache C 的情况下,在 4 个对象中, cache A 仅存储了 object1 ,而 cache C 则存储了 object2 object3 object4 ;分布是很不均衡的。

为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:

“虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。

仍以仅部署 cache A cache C 的情况为例,在图 4 中我们已经看到, cache 分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了 cache A cache C1, cache C2 代表了 cache C ;假设一种比较理想的情况,参见图 6

virtual nodes

6 引入“虚拟节点”后的映射关系

 

此时,对象到“虚拟节点”的映射关系为:

objec1->cache A2 objec2->cache A1 objec3->cache C1 objec4->cache C2

因此对象 object1 object2 都被映射到了 cache A 上,而 object3 object4 映射到了 cache C 上;平衡性有了很大提高。

引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如图 7 所示。

map

7 查询对象所在 cache

 

“虚拟节点”的 hash 计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 cache A IP 地址为 202.168.14.241

引入“虚拟节点”前,计算 cache A hash 值:

Hash(“202.168.14.241”);

引入“虚拟节点”后,计算“虚拟节”点 cache A1 cache A2 hash 值:

Hash(“202.168.14.241#1”);  // cache A1

Hash(“202.168.14.241#2”);  // cache A2

5 小结

Consistent hashing 的基本原理就是这些,具体的分布性等理论分析应该是很复杂的,不过一般也用不到。

http://weblogs.java.net/blog/2007/11/27/consistent-hashing 上面有一个 java 版本的例子,可以参考。

http://blog.csdn.net/mayongzhan/archive/2009/06/25/4298834.aspx 转载了一个 PHP 版的实现代码。

http://www.codeproject.com/KB/recipes/lib-conhash.aspx C语言版本


 

一些参考资料地址:

http://portal.acm.org/citation.cfm?id=258660

http://en.wikipedia.org/wiki/Consistent_hashing

http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/

 http://weblogs.java.net/blog/2007/11/27/consistent-hashing

http://tech.idv2.com/2008/07/24/memcached-004/

http://blog.csdn.net/mayongzhan/archive/2009/06/25/4298834.aspx

 

posted @ 2013-01-29 11:26 昊天 阅读(457) | 评论 (0)编辑 收藏
1.memcached client for java
http://www.whalin.com/memcached

Java代码  
  1. import com.danga.MemCached.*;   
  2. import org.apache.log4j.*;   
  3. public class TestMemcached {   
  4.     public static void main(String[] args) {   
  5.         /*初始化SockIOPool,管理memcached的连接池*/  
  6.         String[] servers = { "192.168.1.20:12111" };   
  7.         SockIOPool pool = SockIOPool.getInstance();   
  8.         pool.setServers(servers);   
  9.         pool.setFailover(true);   
  10.         pool.setInitConn(10);   
  11.         pool.setMinConn(5);   
  12.         pool.setMaxConn(250);   
  13.         pool.setMaintSleep(30);   
  14.         pool.setNagle(false);   
  15.         pool.setSocketTO(3000);   
  16.         pool.setAliveCheck(true);   
  17.         pool.initialize();   
  18.         /*建立MemcachedClient实例*/  
  19.         MemCachedClient memCachedClient = new MemCachedClient();   
  20.         for (int i = 0; i < 10; i++) {   
  21.             /*将对象加入到memcached缓存*/  
  22.             boolean success = memCachedClient.set("" + i, "Hello!");   
  23.             /*从memcached缓存中按key值取对象*/  
  24.             String result = (String) memCachedClient.get("" + i);   
  25.             System.out.println(String.format("set( %d ): %s", i, success));   
  26.             System.out.println(String.format("get( %d ): %s", i, result));   
  27.         }   
  28.     }   
  29. }  


2.spymemcached
http://code.google.com/p/spymemcached/

用spymemcached将对象存入缓存
Java代码  
  1. import java.net.InetSocketAddress;   
  2. import java.util.concurrent.Future;   
  3.   
  4. import net.spy.memcached.MemcachedClient;   
  5.   
  6. public class MClient {   
  7.   
  8.     public static void main(String[] args){   
  9.         try{   
  10.             /*建立MemcachedClient 实例,并指定memcached服务的IP地址和端口号*/  
  11.             MemcachedClient mc = new MemcachedClient(new InetSocketAddress("192.168.1.20"12111));   
  12.             Future<Boolean> b = null;   
  13.             /*将key值,过期时间(秒)和要缓存的对象set到memcached中*/  
  14.             b = mc.set("neea:testDaF:ksIdno"900"someObject");   
  15.             if(b.get().booleanValue()==true){   
  16.                 mc.shutdown();   
  17.             }   
  18.         }   
  19.         catch(Exception ex){   
  20.             ex.printStackTrace();   
  21.         }   
  22.     }   
  23. }  

用spymemcached从缓存中取得对象
Java代码  
  1. import java.net.InetSocketAddress;   
  2. import java.util.concurrent.Future;   
  3.   
  4. import net.spy.memcached.MemcachedClient;   
  5.   
  6. public class MClient {   
  7.   
  8.     public static void main(String[] args){   
  9.         try{   
  10.             /*建立MemcachedClient 实例,并指定memcached服务的IP地址和端口号*/  
  11.             MemcachedClient mc = new MemcachedClient(new InetSocketAddress("192.168.1.20"12111));   
  12.             /*按照key值从memcached中查找缓存,不存在则返回null */  
  13. Object b = mc.get("neea:testDaF:ksIdno ");   
  14.             mc.shutdown();   
  15.         }   
  16.         catch(Exception ex){   
  17.             ex.printStackTrace();   
  18.         }   
  19.     }   
  20. }  

以上两种API比较
memcached client for java:较早推出的memcached JAVA客户端API,应用广泛,运行比较稳定。
spymemcached:A simple, asynchronous, single-threaded memcached client written in java. 支持异步,单线程的memcached客户端,用到了java1.5版本的concurrent和nio,存取速度会高于前者,但是稳定性不好,测试中常报timeOut等相关异常。
由于memcached client for java发布了新版本,性能上有所提高,并且运行稳定,所以建议使用memcached client for java.

3. Xmemcached
http://code.google.com/p/xmemcached/
引用

Xmemcached是基于java nio实现的高性能可扩展的memcached客户端。

实际上是基于一个nio框架 http://code.google.com/p/yanf4j/的基础上实现的(目前1.1.3是基于yanf4j 0.7.0,而1.2.0-RC1版本基于1.0-SNAPSHOT)。
posted @ 2013-01-28 17:33 昊天 阅读(640) | 评论 (0)编辑 收藏

Hadoop集群配置(最全面总结)  huangguisu

       通常,集群里的一台机器被指定为 NameNode,另一台不同的机器被指定为JobTracker。这些机器是masters。余下的机器即作为DataNode作为TaskTracker。这些机器是slaves\

官方地址:(http://hadoop.apache.org/common/docs/r0.19.2/cn/cluster_setup.html)

1 先决条件

  1. 确保在你集群中的每个节点上都安装了所有必需软件:sun-JDK  ,ssh,Hadoop

  2. JavaTM1.5.x,必须安装,建议选择Sun公司发行的Java版本。

  3. ssh 必须安装并且保证 sshd一直运行,以便用Hadoop 脚本管理远端Hadoop守护进程。

2 实验环境搭建

   2.1  准备工作

     操作系统:Ubuntu
     部署:Vmvare
     在vmvare安装好一台Ubuntu虚拟机后,可以导出或者克隆出另外两台虚拟机。
     说明:
    保证虚拟机的ip和主机的ip在同一个ip段,这样几个虚拟机和主机之间可以相互通信。
    为了保证虚拟机的ip和主机的ip在同一个ip段,虚拟机连接设置为桥连。

     准备机器:一台master,若干台slave,配置每台机器的/etc/hosts保证各台机器之间通过机器名可以互访,例如:
     10.64.56.76 node1(master)   
     10.64.56.77 node2 (slave1)   
     10.64.56.78 node3 (slave2)
     主机信息: 

机器名   IP地址 作用
Node1 10.64.56.76 NameNode、JobTracker
Node2 10.64.56.77 DataNode、TaskTracker
Node3 10.64.56.78 DataNode、TaskTracker
为保证环境一致先安装好JDK和ssh:

2.2 安装JDK

 

#安装JDK
$ sudo apt-get install sun-java6-jdk1.2.3     
这个安装,java执行文件自动添加到/usr/bin/目录。
验证 shell命令 :java -version 看是否与你的版本号一致。

 

2.3下载、创建用户

$ useradd hadoop
$ cd  /home/hadoop

在所有的机器上都建立相同的目录,也可以就建立相同的用户,最好是以该用户的home路径来做hadoop的安装路径。
例如在所有的机器上的安装路径都是:/home/hadoop/hadoop-0.20.203,这个不需要mkdir,在/home/hadoop/下解压hadoop包的时候,会自动生成)
(当然可以安装/usr/local/目录下,例如/usr/local/hadoop-0.20.203/
  chown -R hadoop /usr/local/hadoop-0.20.203/
  chgrp -R hadoop  /usr/local/hadoop-0.20.203/

(最好不要使用root安装,因为不推荐各个机器之间使用root访问 )

2.4 安装ssh和配置

1) 安装:sudo apt-get install ssh

 

     这个安装完后,可以直接使用ssh命令 了。
     执行$ netstat  -nat    查看22端口是否开启了。
     测试:ssh localhost。
     输入当前用户的密码,回车就ok了。说明安装成功,同时ssh登录需要密码。
   (这种默认安装方式完后,默认配置文件是在/etc/ssh/目录下。sshd配置文件是:/etc/ssh/sshd_config): 

 

    注意:在所有机子都需要安装ssh。

2)  配置

在Hadoop启动以后,Namenode是通过SSH(Secure Shell)来启动和停止各个datanode上的各种守护进程的,这就须要在节点之间执行指令的时候是不须要输入密码的形式,故我们须要配置SSH运用无密码公钥认证的形式。
以本文中的三台机器为例,现在node1是主节点,他须要连接node2和node3。须要确定每台机器上都安装了ssh,并且datanode机器上sshd服务已经启动。

( 说明:hadoop@hadoop~]$ssh-keygen  -t  rsa
这个命令将为hadoop上的用户hadoop生成其密钥对,询问其保存路径时直接回车采用默认路径,当提示要为生成的密钥输入passphrase的时候,直接回车,也就是将其设定为空密码。生成的密钥对id_rsa,id_rsa.pub,默认存储在/home/hadoop/.ssh目录下然后将id_rsa.pub的内容复制到每个机器(也包括本机)的/home/dbrg/.ssh/authorized_keys文件中,如果机器上已经有authorized_keys这个文件了,就在文件末尾加上id_rsa.pub中的内容,如果没有authorized_keys这个文件,直接复制过去就行.)

3) 首先设置namenode的ssh为无需密码的、自动登录。

切换到hadoop用户( 保证用户hadoop可以无需密码登录,因为我们后面安装的hadoop属主是hadoop用户。)

$ su hadoop

cd /home/hadoop

$ ssh-keygen -t rsa

然后一直按回车
完成后,在home跟目录下会产生隐藏文件夹.ssh

$ cd .ssh

之后ls 查看文件

cp id_rsa.pub  authorized_keys

测试:

$ssh localhost

或者:

$ ssh node1                                      

第一次ssh会有提示信息:

The authenticity of host ‘node1 (10.64.56.76)’ can’t be established.
RSA key fingerprint is 03:e0:30:cb:6e:13:a8:70:c9:7e:cf:ff:33:2a:67:30.
Are you sure you want to continue connecting (yes/no)?

输入 yes 来继续。这会把该服务器添加到你的已知主机的列表中

发现链接成功,并且无需密码。


4 ) 复制authorized_keys到node2 和node3 上

为了保证node1可以无需密码自动登录到node2和node3,先在node2和node3上执行

$ su hadoop

cd /home/hadoop

$ ssh-keygen -t rsa

 

一路按回车.
然后回到node1,复制authorized_keys到node2 和node3

[hadoop@hadoop .ssh]$ scp authorized_keys   node2:/home/hadoop/.ssh/

[hadoop@hadoop .ssh]$ scp authorized_keys   node3:/home/hadoop/.ssh/

 

这里会提示输入密码,输入hadoop账号密码就可以了。
改动你的 authorized_keys 文件的许可权限

[hadoop@hadoop .ssh]$chmod 644 authorized_keys

 

测试:ssh node2或者ssh node3(第一次需要输入yes)。
如果不须要输入密码则配置成功,如果还须要请检查上面的配置能不能正确。

 

2.5 安装Hadoop

#切换为hadoop用户

su hadoop

wgethttp://apache.mirrors.tds.net//hadoop/common/hadoop-0.20.203.0/hadoop-0.20.203.0rc1.tar.gz

下载安装包后,直接解压安装即可:

$ tar -zxvfhadoop-0.20.203.0rc1.tar.gz 

1 ) 安装Hadoop集群通常要将安装软件解压到集群内的所有机器上。并且安装路径要一致,如果我们用HADOOP_HOME指代安装的根路径,通常,集群里的所有机器的     
     HADOOP_HOME路径相同。
2 )  如果集群内机器的环境完全一样,可以在一台机器上配置好,然后把配置好的软件即hadoop-0.20.203整个文件夹拷贝到其他机器的相同位置即可。
3 )  可以将Master上的Hadoop通过scp拷贝到每一个Slave相同的目录下,同时根据每一个Slave的Java_HOME 的不同修改其hadoop-env.sh 。
4)   为了方便,使用hadoop命令或者start-all.sh等命令,修改Master上/etc/profile 新增以下内容:
    export HADOOP_HOME=/home/hadoop/hadoop-0.20.203
    exportPATH=$PATH:$HADOOP_HOME/bin
   修改完毕后,执行source /etc/profile 来使其生效。 

6)配置conf/hadoop-env.sh文件

配置conf/hadoop-env.sh文件
#添加

export JAVA_HOME=/usr/lib/jvm/java-6-sun/

这里修改为你的jdk的安装位置。

测试hadoop安装:

Bin/hadoop jar hadoop-0.20.2-examples.jarwordcount  conf/   /tmp/out

3. 集群配置(所有节点相同)

3.1配置文件:conf/core-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl"href="configuration.xsl"?>
<configuration>
<property>
 <name>fs.default.name</name>
  <value>hdfs://node1:49000</value>
</property>
<property>
  <name>hadoop.tmp.dir</name>
 <value>/home/hadoop/hadoop_home/var</value>
</property>
</configuration>

1)fs.default.name是NameNode的URI。hdfs://主机名:端口/
2)hadoop.tmp.dir :Hadoop的默认临时路径,这个最好配置,如果在新增节点或者其他情况下莫名其妙的DataNode启动不了,就删除此文件中的tmp目录即可。不过如果删除了NameNode机器的此目录,那么就需要重新执行NameNode格式化的命令。

3.2配置文件:conf/mapred-site.xml

 

<?xmlversion="1.0"?>
<?xml-stylesheettype="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
  <name>mapred.job.tracker</name>
  <value>node1:49001</value>
</property>
<property>
  <name>mapred.local.dir</name>
 <value>/home/hadoop/hadoop_home/var</value>
</property>
</configuration>

 

1)mapred.job.tracker是JobTracker的主机(或者IP)和端口。主机:端口。

3.3配置文件:conf/hdfs-site.xml

 

<?xmlversion="1.0"?>
<?xml-stylesheettype="text/xsl" href="configuration.xsl"?>
<configuration>
<property>
<name>dfs.name.dir</name>
<value>/home/hadoop/name1, /home/hadoop/name2</value> #hadoop的name目录路径
<description>  </description>
</property>
<property>
<name>dfs.data.dir</name>
<value>/home/hadoop/data1, /home/hadoop/data2</value>
<description> </description>
</property>
<property>
  <name>dfs.replication</name>
  <!-- 我们的集群又两个结点,所以rep两份 -->
  <value>2</vaue>
</property>
</configuration>

 

 

1)   dfs.name.dir是NameNode持久存储名字空间及事务日志的本地文件系统路径。 当这个值是一个逗号分割的目录列表时,nametable数据将会被复制到所有目录中做冗余备份。
2)   dfs.data.dir是DataNode存放块数据的本地文件系统路径,逗号分割的列表。 当这个值是逗号分割的目录列表时,数据将被存储在所有目录下,通常分布在不同设备上。
3)dfs.replication是数据需要备份的数量,默认是3,如果此数大于集群的机器数会出错。

注意:此处的name1、name2、data1、data2目录不能预先创建,hadoop格式化时会自动创建,如果预先创建反而会有问题。

3.4配置masters和slaves主从结点

配置conf/masters和conf/slaves来设置主从结点,注意最好使用主机名,并且保证机器之间通过主机名可以互相访问,每个主机名一行。

vi masters:
输入:

node1

vi slaves:

输入:
node2
node3

配置结束,把配置好的hadoop文件夹拷贝到其他集群的机器中,并且保证上面的配置对于其他机器而言正确,例如:如果其他机器的Java安装路径不一样,要修改conf/hadoop-env.sh 

$ scp -r /home/hadoop/hadoop-0.20.203 root@node2: /home/hadoop/

 

4 hadoop启动

4.1 格式化一个新的分布式文件系统

先格式化一个新的分布式文件系统

$ cd hadoop-0.20.203
$ bin/hadoop namenode -format

成功情况下系统输出:

12/02/06 00:46:50 INFO namenode.NameNode:STARTUP_MSG:
/************************************************************
STARTUP_MSG: Starting NameNode
STARTUP_MSG:   host = ubuntu/127.0.1.1
STARTUP_MSG:   args = [-format]
STARTUP_MSG:   version = 0.20.203.0
STARTUP_MSG:   build =http://svn.apache.org/repos/asf/hadoop/common/branches/branch-0.20-security-203-r 1099333; compiled by 'oom' on Wed May 4 07:57:50 PDT 2011
************************************************************/

12/02/0600:46:50 INFO namenode.FSNamesystem: fsOwner=root,root
12/02/06 00:46:50 INFO namenode.FSNamesystem:supergroup=supergroup
12/02/06 00:46:50 INFO namenode.FSNamesystem:isPermissionEnabled=true
12/02/06 00:46:50 INFO common.Storage: Imagefile of size 94 saved in 0 seconds.
12/02/06 00:46:50 INFO common.Storage: Storagedirectory /opt/hadoop/hadoopfs/name1 has been successfully formatted.
12/02/06 00:46:50 INFO common.Storage: Imagefile of size 94 saved in 0 seconds.
12/02/06 00:46:50 INFO common.Storage: Storagedirectory /opt/hadoop/hadoopfs/name2 has been successfully formatted.
12/02/06 00:46:50 INFO namenode.NameNode:SHUTDOWN_MSG:
/************************************************************
SHUTDOWN_MSG: Shutting down NameNode atv-jiwan-ubuntu-0/127.0.0.1
************************************************************/


查看输出保证分布式文件系统格式化成功
执行完后可以到master机器上看到/home/hadoop//name1和/home/hadoop//name2两个目录。在主节点master上面启动hadoop,主节点会启动所有从节点的hadoop。

4.2 启动所有节点

启动方式1

$ bin/start-all.sh (同时启动HDFS和Map/Reduce)
系统输出:

starting namenode, logging to /usr/local/hadoop/logs/hadoop-hadoop-namenode-ubuntu.out
node2: starting datanode, loggingto /usr/local/hadoop/logs/hadoop-hadoop-datanode-ubuntu.out
node3: starting datanode, loggingto /usr/local/hadoop/logs/hadoop-hadoop-datanode-ubuntu.out
node1: starting secondarynamenode,logging to /usr/local/hadoop/logs/hadoop-hadoop-secondarynamenode-ubuntu.out
starting jobtracker, logging to/usr/local/hadoop/logs/hadoop-hadoop-jobtracker-ubuntu.out
node2: starting tasktracker,logging to /usr/local/hadoop/logs/hadoop-hadoop-tasktracker-ubuntu.out
node3: starting tasktracker,logging to /usr/local/hadoop/logs/hadoop-hadoop-tasktracker-ubuntu.out
As you can see in slave's output above, it will automatically format it's storage directory(specified by dfs.data.dir) if it is not formattedalready. It will also create the directory if it does not exist yet.


执行完后可以到master(node1)和slave(node1,node2)机器上看到/home/hadoop/hadoopfs/data1和/home/hadoop/data2两个目录。

启动方式2

启动Hadoop集群需要启动HDFS集群和Map/Reduce集群。

在分配的NameNode上,运行下面的命令启动HDFS:
$ bin/start-dfs.sh(单独启动HDFS集群)

bin/start-dfs.sh脚本会参照NameNode上${HADOOP_CONF_DIR}/slaves文件的内容,在所有列出的slave上启动DataNode守护进程。

在分配的JobTracker上,运行下面的命令启动Map/Reduce:
$bin/start-mapred.sh (单独启动Map/Reduce)

bin/start-mapred.sh脚本会参照JobTracker上${HADOOP_CONF_DIR}/slaves文件的内容,在所有列出的slave上启动TaskTracker守护进程。

4.3 关闭所有节点

从主节点master关闭hadoop,主节点会关闭所有从节点的hadoop。

$ bin/stop-all.sh 

Hadoop守护进程的日志写入到 ${HADOOP_LOG_DIR} 目录 (默认是 ${HADOOP_HOME}/logs).

${HADOOP_HOME}就是安装路径.

5 .测试

1)浏览NameNode和JobTracker的网络接口,它们的地址默认为:

NameNode - http://node1:50070/
JobTracker - http://node2:50030/

3)   使用netstat  –nat查看端口49000和49001是否正在使用。

4)  使用jps查看进程

要想检查守护进程是否正在运行,可以使用 jps 命令(这是用于 JVM 进程的ps 实用程序)。这个命令列出 5 个守护进程及其进程标识符。

5)将输入文件拷贝到分布式文件系统:
$ bin/hadoop fs -mkdir input
$ bin/hadoop fs -put conf/core-site.xml input

运行发行版提供的示例程序:
$ bin/hadoop jar hadoop-0.20.2-examples.jar grep input output 'dfs[a-z.]+'

6.补充
Q: bin/hadoop jar hadoop-0.20.2-examples.jar grep input output 'dfs[a-z.]+' 什么意思啊?
A: bin/hadoop jar(使用hadoop运行jar包) hadoop-0.20.2_examples.jar(jar包的名字) grep (要使用的类,后边的是参数)input output 'dfs[a-z.]+'
整个就是运行hadoop示例程序中的grep,对应的hdfs上的输入目录为input、输出目录为output。
Q: 什么是grep?
A: A map/reduce program that counts the matches of a regex in the input.

查看输出文件:

将输出文件从分布式文件系统拷贝到本地文件系统查看:
$ bin/hadoop fs -get output output
$ cat output/*

或者

在分布式文件系统上查看输出文件:
$ bin/hadoop fs -cat output/*

统计结果:
root@v-jiwan-ubuntu-0:~/hadoop/hadoop-0.20.2-bak/hadoop-0.20.2#bin/hadoop fs -cat output/part-00000
3       dfs.class
2       dfs.period
1       dfs.file
1      dfs.replication
1       dfs.servers
1       dfsadmin

7. HDFS常用操作

 

hadoopdfs -ls 列出HDFS下的文件
hadoop dfs -ls in 列出HDFS下某个文档中的文件
hadoop dfs -put test1.txt test 上传文件到指定目录并且重新命名,只有所有的DataNode都接收完数据才算成功
hadoop dfs -get in getin 从HDFS获取文件并且重新命名为getin,同put一样可操作文件也可操作目录
hadoop dfs -rmr out 删除指定文件从HDFS上
hadoop dfs -cat in/* 查看HDFS上in目录的内容
hadoop dfsadmin -report 查看HDFS的基本统计信息,结果如下
hadoop dfsadmin -safemode leave 退出安全模式
hadoop dfsadmin -safemode enter 进入安全模式

8.添加节点

可扩展性是HDFS的一个重要特性,首先在新加的节点上安装hadoop,然后修改$HADOOP_HOME/conf/master文件,加入 NameNode主机名,然后在NameNode节点上修改$HADOOP_HOME/conf/slaves文件,加入新加节点主机名,再建立到新加节点无密码的SSH连接

运行启动命令:

start-all.sh

然后可以通过http://(Masternode的主机名):50070查看新添加的DataNode

9负载均衡

start-balancer.sh,可以使DataNode节点上选择策略重新平衡DataNode上的数据块的分布


结束语:遇到问题时,先查看logs,很有帮助。

10 SHell自动安装脚本


  1. #!/bin/bash  
  2.   
  3. #validate user or group  
  4. validate() {  
  5.  if [ 'id -u' == 0 ];then  
  6.    echo "must not be root!"  
  7.    exit 0  
  8.  else  
  9.    echo "---------welcome to hadoop---------"  
  10.  fi  
  11. }  
  12.   
  13. #hadoop install  
  14. hd-dir() {  
  15.  if [ ! -d /home/hadoop/ ];then  
  16.    mkdir /home/hadoop/  
  17.  else  
  18.    echo "download hadoop will begin"  
  19.  fi  
  20. }  
  21.   
  22. download-hd() {  
  23.  wget -c http://archive.apache.org/dist/hadoop/core/stable/hadoop-1.0.4.tar.gz -O /home/hadoop/hadoop-1.0.4.tar.gz  
  24.  tar -xzvf /home/hadoop/hadoop-1.0.4.tar.gz -C /home/hadoop  
  25.  rm /home/hadoop/hadoop-1.0.4.tar.gz  
  26.  Ln -s /home/hadoop/hadoop-1.0.4 /home/hadoop/hadoop1.0.4  
  27. }  
  28.   
  29. #hadoop conf  
  30. hd-conf() {  
  31.  echo "export JAVA_HOME=/usr/lib/jvm/java-6-openjdk-i386" >> /home/hadoop/hadoop1.0.4/conf/hadoop-env.sh  
  32.  echo "#set path jdk" >> /home/hadoop/.profile  
  33.  echo "export JAVA_HOME=/usr/lib/jvm/java-6-openjdk-i386" >> /home/hadoop/.profile  
  34.  echo "#hadoop path" >> /home/hadoop/.profile  
  35.  echo "export HADOOP_HOME=/home/hadoop/hadoop1.0.4" >> /home/hadoop/.profile  
  36.  echo "PATH=$PATH:$HADOOP_HOME/bin:$JAVA_HOME/bin" >> /home/hadoop/.profile  
  37.  echo "HADOOP_HOME_WARN_SUPPRESS=1" >> /home/hadoop/.profile  
  38. #hadoop core-site.xml  
  39.  echo "<configuration>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  40.  echo "<property>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  41.  echo "<name>fs.default.name</name>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  42.  echo "<value>hdfs://hadoop-master:9000" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  43.  echo "</property>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  44.  echo "<property>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  45.  echo "<name>hadoop.tmp.dir</name>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  46.  echo "<value>/home/hadoop/tmp</value>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  47.  echo "</property>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  48.  echo "</configuration>" >> /home/hadoop/hadoop1.0.4/conf/core-site.xml  
  49. #hadoop hdfs-site.xml  
  50.   
  51.  echo "<configuration>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  52.  echo "<property>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  53.  echo "<name>dfs.name.dir</name>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  54.  echo "<value>/home/hadoop/name</value>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  55.  echo "</property>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  56.  echo "<property>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  57.  echo "<name>dfs.data.dir</name>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  58.  echo "<value>/home/hadoop/data</value>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  59.  echo "</property>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  60.  echo "<property>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  61.  echo "<name>dfs.replication</name>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  62.  echo "<value>1</value>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  63.  echo "</property>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  64.  echo "</configuration>" >> /home/hadoop/hadoop1.0.4/conf/hdfs-site.xml  
  65. # hadoop mapred-site.xml  
  66.   
  67.  echo "<configuration>" >> /home/hadoop/hadoop1.0.4/conf/mapred-site.xml  
  68.  echo "<property>" >> /home/hadoop/hadoop1.0.4/conf/mapred-site.xml  
  69.  echo "<name>mapred.job.tracker</name>" >> /home/hadoop/hadoop1.0.4/conf/mapred-site.xml  
  70.  echo "<value>hadoop-master:9001</value>" >> /home/hadoop/hadoop1.0.4/conf/mapred-site.xml  
  71.  echo "</property>" >> /home/hadoop/hadoop1.0.4/conf/mapred-site.xml  
  72.  echo "</configuration>" >> /home/hadoop/hadoop1.0.4/conf/mapred-site.xml  
  73. #hadoop master  
  74.  echo "hadoop-master" >> /home/hadoop/hadoop1.0.4/conf/masters  
  75.   
  76. #hadoop slaves  
  77.  echo "hadoop-master" >> /home/hadoop/hadoop1.0.4/conf/slaves  
  78. source /home/hadoop/.profile  
  79. }  
  80.   
  81. hd-start() {  
  82. hadoop namenode -format  
  83. }  
  84.   
  85.   
  86. yes-or-no() {  
  87.   echo "Is your name $* ?"  
  88.   while true  
  89.   do  
  90.      echo -n "Enter yes or no: "  
  91.      read x  
  92.      case "$x" in  
  93.      y | yes ) return 0;;  
  94.      n | no ) return 1;;  
  95.      * ) echo "Answer yes or no";;  
  96.    esac  
  97.   done  
  98. }  
  99.   
  100. echo "Original params are $*"  
  101.   
  102. if yes-or-no "$1"  
  103. then  
  104.   echo "HI $1,nice name!"  
  105.   validate  
  106.   hd-dir  
  107.   download-hd  
  108.   hd-conf  
  109. else  
  110.   echo "Never mind!"  
  111. fi  

posted @ 2013-01-28 14:59 昊天 阅读(596) | 评论 (0)编辑 收藏

下面是《memcached全面剖析》的第三部分。

前几次的文章在这里:

memcached是缓存,所以数据不会永久保存在服务器上,这是向系统中引入memcached的前提。本次介绍memcached的数据删除机制,以及memcached的最新发展方向——二进制协议(Binary Protocol)和外部引擎支持。

memcached在数据删除方面有效利用资源

数据不会真正从memcached中消失

上次介绍过, memcached不会释放已分配的内存。记录超时后,客户端就无法再看见该记录(invisible,透明),其存储空间即可重复使用。

Lazy Expiration

memcached内部不会监视记录是否过期,而是在get时查看记录的时间戳,检查记录是否过期。这种技术被称为lazy(惰性)expiration。因此,memcached不会在过期监视上耗费CPU时间。

LRU:从缓存中有效删除数据的原理

memcached会优先使用已超时的记录的空间,但即使如此,也会发生追加新记录时空间不足的情况,此时就要使用名为 Least Recently Used(LRU)机制来分配空间。顾名思义,这是删除“最近最少使用”的记录的机制。因此,当memcached的内存空间不足时(无法从slab class 获取到新的空间时),就从最近未被使用的记录中搜索,并将其空间分配给新的记录。从缓存的实用角度来看,该模型十分理想。

不过,有些情况下LRU机制反倒会造成麻烦。memcached启动时通过“-M”参数可以禁止LRU,如下所示:

$ memcached -M -m 1024

启动时必须注意的是,小写的“-m”选项是用来指定最大内存大小的。不指定具体数值则使用默认值64MB。

指定“-M”参数启动后,内存用尽时memcached会返回错误。话说回来,memcached毕竟不是存储器,而是缓存,所以推荐使用LRU。

memcached的最新发展方向

memcached的roadmap上有两个大的目标。一个是二进制协议的策划和实现,另一个是外部引擎的加载功能。

关于二进制协议

使用二进制协议的理由是它不需要文本协议的解析处理,使得原本高速的memcached的性能更上一层楼,还能减少文本协议的漏洞。目前已大部分实现,开发用的代码库中已包含了该功能。 memcached的下载页面上有代码库的链接。

二进制协议的格式

协议的包为24字节的帧,其后面是键和无结构数据(Unstructured Data)。实际的格式如下(引自协议文档):

 Byte/     0       |       1       |       2       |       3       |   
    /              |               |               |               |   
   |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
   +---------------+---------------+---------------+---------------+
  0/ HEADER                                                        /   
   /                                                               /   
   /                                                               /   
   /                                                               /   
   +---------------+---------------+---------------+---------------+
 24/ COMMAND-SPECIFIC EXTRAS (as needed)                           /   
  +/  (note length in th extras length header field)               /   
   +---------------+---------------+---------------+---------------+
  m/ Key (as needed)                                               /   
  +/  (note length in key length header field)                     /   
   +---------------+---------------+---------------+---------------+
  n/ Value (as needed)                                             /   
  +/  (note length is total body length header field, minus        /   
  +/   sum of the extras and key length body fields)               /   
   +---------------+---------------+---------------+---------------+
  Total 24 bytes

如上所示,包格式十分简单。需要注意的是,占据了16字节的头部(HEADER)分为请求头(Request Header)和响应头(Response Header)两种。头部中包含了表示包的有效性的Magic字节、命令种类、键长度、值长度等信息,格式如下:

Request Header

 Byte/     0       |       1       |       2       |       3       |
    /              |               |               |               |
   |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
   +---------------+---------------+---------------+---------------+
  0| Magic         | Opcode        | Key length                    |
   +---------------+---------------+---------------+---------------+
  4| Extras length | Data type     | Reserved                      |
   +---------------+---------------+---------------+---------------+
  8| Total body length                                             |
   +---------------+---------------+---------------+---------------+
 12| Opaque                                                        |
   +---------------+---------------+---------------+---------------+
 16| CAS                                                           |
   |                                                               |
   +---------------+---------------+---------------+---------------+

Response Header

 Byte/     0       |       1       |       2       |       3       |
    /              |               |               |               |
   |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
   +---------------+---------------+---------------+---------------+
  0| Magic         | Opcode        | Key Length                    |
   +---------------+---------------+---------------+---------------+
  4| Extras length | Data type     | Status                        |
   +---------------+---------------+---------------+---------------+
  8| Total body length                                             |
   +---------------+---------------+---------------+---------------+
 12| Opaque                                                        |
   +---------------+---------------+---------------+---------------+
 16| CAS                                                           |
   |                                                               |
   +---------------+---------------+---------------+---------------+

如希望了解各个部分的详细内容,可以checkout出memcached的二进制协议的代码树,参考其中的docs文件夹中的protocol_binary.txt文档。

HEADER中引人注目的地方

看到HEADER格式后我的感想是,键的上限太大了!现在的memcached规格中,键长度最大为250字节,但二进制协议中键的大小用2字节表示。因此,理论上最大可使用65536字节(216)长的键。尽管250字节以上的键并不会太常用,二进制协议发布之后就可以使用巨大的键了。

二进制协议从下一版本1.3系列开始支持。

外部引擎支持

我去年曾经试验性地将memcached的存储层改造成了可扩展的(pluggable)。

MySQL的Brian Aker看到这个改造之后,就将代码发到了memcached的邮件列表。 memcached的开发者也十分感兴趣,就放到了roadmap中。现在由我和 memcached的开发者Trond Norbye协同开发(规格设计、实现和测试)。和国外协同开发时时差是个大问题,但抱着相同的愿景,最后终于可以将可扩展架构的原型公布了。代码库可以从memcached的下载页面 上访问。

外部引擎支持的必要性

世界上有许多memcached的派生软件,其理由是希望永久保存数据、实现数据冗余等,即使牺牲一些性能也在所不惜。我在开发memcached之前,在mixi的研发部也曾经考虑过重新发明memcached。

外部引擎的加载机制能封装memcached的网络功能、事件处理等复杂的处理。因此,现阶段通过强制手段或重新设计等方式使memcached和存储引擎合作的困难就会烟消云散,尝试各种引擎就会变得轻而易举了。

简单API设计的成功的关键

该项目中我们最重视的是API设计。函数过多,会使引擎开发者感到麻烦;过于复杂,实现引擎的门槛就会过高。因此,最初版本的接口函数只有13个。具体内容限于篇幅,这里就省略了,仅说明一下引擎应当完成的操作:

  • 引擎信息(版本等)
  • 引擎初始化
  • 引擎关闭
  • 引擎的统计信息
  • 在容量方面,测试给定记录能否保存
  • 为item(记录)结构分配内存
  • 释放item(记录)的内存
  • 删除记录
  • 保存记录
  • 回收记录
  • 更新记录的时间戳
  • 数学运算处理
  • 数据的flush

对详细规格有兴趣的读者,可以checkout engine项目的代码,阅读器中的engine.h。

重新审视现在的体系

memcached支持外部存储的难点是,网络和事件处理相关的代码(核心服务器)与内存存储的代码紧密关联。这种现象也称为tightly coupled(紧密耦合)。必须将内存存储的代码从核心服务器中独立出来,才能灵活地支持外部引擎。因此,基于我们设计的API,memcached被重构成下面的样子:

memcached-0003-001.png

重构之后,我们与1.2.5版、二进制协议支持版等进行了性能对比,证实了它不会造成性能影响。

在考虑如何支持外部引擎加载时,让memcached进行并行控制(concurrency control)的方案是最为容易的,但是对于引擎而言,并行控制正是性能的真谛,因此我们采用了将多线程支持完全交给引擎的设计方案。

以后的改进,会使得memcached的应用范围更为广泛。

总结

本次介绍了memcached的超时原理、内部如何删除数据等,在此之上又介绍了二进制协议和外部引擎支持等memcached的最新发展方向。这些功能要到1.3版才会支持,敬请期待!

这是我在本连载中的最后一篇。感谢大家阅读我的文章!

下次由长野来介绍memcached的应用知识和应用程序兼容性等内容。

posted @ 2013-01-26 11:54 昊天 阅读(389) | 评论 (0)编辑 收藏

下面是《memcached全面剖析》的第二部分。

我是mixi株式会社研究开发组的前坂徹。 上次的文章介绍了memcached是分布式的高速缓存服务器。本次将介绍memcached的内部构造的实现方式,以及内存的管理方式。另外,memcached的内部构造导致的弱点也将加以说明。

Slab Allocation机制:整理内存以便重复使用

最近的memcached默认情况下采用了名为Slab Allocator的机制分配、管理内存。在该机制出现以前,内存的分配是通过对所有记录简单地进行malloc和free来进行的。但是,这种方式会导致内存碎片,加重操作系统内存管理器的负担,最坏的情况下,会导致操作系统比memcached进程本身还慢。Slab Allocator就是为解决该问题而诞生的。

下面来看看Slab Allocator的原理。下面是memcached文档中的slab allocator的目标:

the primary goal of the slabs subsystem in memcached was to eliminate memory fragmentation issues totally by using fixed-size memory chunks coming from a few predetermined size classes.

也就是说,Slab Allocator的基本原理是按照预先规定的大小,将分配的内存分割成特定长度的块,以完全解决内存碎片问题。

Slab Allocation的原理相当简单。 将分配的内存分割成各种尺寸的块(chunk),并把尺寸相同的块分成组(chunk的集合)(图1)。

memcached-0002-01.png

图1 Slab Allocation的构造图

而且,slab allocator还有重复使用已分配的内存的目的。也就是说,分配到的内存不会释放,而是重复利用。

Slab Allocation的主要术语

Page

分配给Slab的内存空间,默认是1MB。分配给Slab之后根据slab的大小切分成chunk。

Chunk

用于缓存记录的内存空间。

Slab Class

特定大小的chunk的组。

在Slab中缓存记录的原理

下面说明memcached如何针对客户端发送的数据选择slab并缓存到chunk中。

memcached根据收到的数据的大小,选择最适合数据大小的slab(图2)。 memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk,然后将数据缓存于其中。

memcached-0002-02.png

图2 选择存储记录的组的方法

实际上,Slab Allocator也是有利也有弊。下面介绍一下它的缺点。

Slab Allocator的缺点

Slab Allocator解决了当初的内存碎片问题,但新的机制也给memcached带来了新的问题。

这个问题就是,由于分配的是特定长度的内存,因此无法有效利用分配的内存。例如,将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了(图3)。

memcached-0002-03.png

图3 chunk空间的使用

对于该问题目前还没有完美的解决方案,但在文档中记载了比较有效的解决方案。

The most efficient way to reduce the waste is to use a list of size classes that closely matches (if that’s at all possible) common sizes of objects that the clients of this particular installation of memcached are likely to store.

就是说,如果预先知道客户端发送的数据的公用大小,或者仅缓存大小相同的数据的情况下,只要使用适合数据大小的组的列表,就可以减少浪费。

但是很遗憾,现在还不能进行任何调优,只能期待以后的版本了。但是,我们可以调节slab class的大小的差别。接下来说明growth factor选项。

使用Growth Factor进行调优

memcached在启动时指定 Growth Factor因子(通过-f选项),就可以在某种程度上控制slab之间的差异。默认值为1.25。但是,在该选项出现之前,这个因子曾经固定为2,称为“powers of 2”策略。

让我们用以前的设置,以verbose模式启动memcached试试看:

$ memcached -f 2 -vv

下面是启动后的verbose输出:

slab class   1: chunk size    128 perslab  8192
slab class   2: chunk size    256 perslab  4096
slab class   3: chunk size    512 perslab  2048
slab class   4: chunk size   1024 perslab  1024
slab class   5: chunk size   2048 perslab   512
slab class   6: chunk size   4096 perslab   256
slab class   7: chunk size   8192 perslab   128
slab class   8: chunk size  16384 perslab    64
slab class   9: chunk size  32768 perslab    32
slab class  10: chunk size  65536 perslab    16
slab class  11: chunk size 131072 perslab     8
slab class  12: chunk size 262144 perslab     4
slab class  13: chunk size 524288 perslab     2

可见,从128字节的组开始,组的大小依次增大为原来的2倍。这样设置的问题是,slab之间的差别比较大,有些情况下就相当浪费内存。因此,为尽量减少内存浪费,两年前追加了growth factor这个选项。

来看看现在的默认设置(f=1.25)时的输出(篇幅所限,这里只写到第10组):

slab class   1: chunk size     88 perslab 11915
slab class   2: chunk size    112 perslab  9362
slab class   3: chunk size    144 perslab  7281
slab class   4: chunk size    184 perslab  5698
slab class   5: chunk size    232 perslab  4519
slab class   6: chunk size    296 perslab  3542
slab class   7: chunk size    376 perslab  2788
slab class   8: chunk size    472 perslab  2221
slab class   9: chunk size    592 perslab  1771
slab class  10: chunk size    744 perslab  1409

可见,组间差距比因子为2时小得多,更适合缓存几百字节的记录。从上面的输出结果来看,可能会觉得有些计算误差,这些误差是为了保持字节数的对齐而故意设置的。

将memcached引入产品,或是直接使用默认值进行部署时,最好是重新计算一下数据的预期平均长度,调整growth factor,以获得最恰当的设置。内存是珍贵的资源,浪费就太可惜了。

接下来介绍一下如何使用memcached的stats命令查看slabs的利用率等各种各样的信息。

查看memcached的内部状态

memcached有个名为stats的命令,使用它可以获得各种各样的信息。执行命令的方法很多,用telnet最为简单:

$ telnet 主机名 端口号

连接到memcached之后,输入stats再按回车,即可获得包括资源利用率在内的各种信息。此外,输入”stats slabs”或”stats items”还可以获得关于缓存记录的信息。结束程序请输入quit。

这些命令的详细信息可以参考memcached软件包内的protocol.txt文档。

$ telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stats
STAT pid 481
STAT uptime 16574
STAT time 1213687612
STAT version 1.2.5
STAT pointer_size 32
STAT rusage_user 0.102297
STAT rusage_system 0.214317
STAT curr_items 0
STAT total_items 0
STAT bytes 0
STAT curr_connections 6
STAT total_connections 8
STAT connection_structures 7
STAT cmd_get 0
STAT cmd_set 0
STAT get_hits 0
STAT get_misses 0
STAT evictions 0
STAT bytes_read 20
STAT bytes_written 465
STAT limit_maxbytes 67108864
STAT threads 4
END
quit

另外,如果安装了libmemcached这个面向C/C++语言的客户端库,就会安装 memstat 这个命令。使用方法很简单,可以用更少的步骤获得与telnet相同的信息,还能一次性从多台服务器获得信息。

$ memstat --servers=server1,server2,server3,...

libmemcached可以从下面的地址获得:

  • http://tangent.org/552/libmemcached.html

查看slabs的使用状况

使用memcached的创造着Brad写的名为memcached-tool的Perl脚本,可以方便地获得slab的使用情况(它将memcached的返回值整理成容易阅读的格式)。可以从下面的地址获得脚本:

  • http://code.sixapart.com/svn/memcached/trunk/server/scripts/memcached-tool

使用方法也极其简单:

$ memcached-tool 主机名:端口 选项

查看slabs使用状况时无需指定选项,因此用下面的命令即可:

$ memcached-tool 主机名:端口

获得的信息如下所示:

 #  Item_Size   Max_age  1MB_pages Count   Full?
 1     104 B  1394292 s    1215 12249628    yes
 2     136 B  1456795 s      52  400919     yes
 3     176 B  1339587 s      33  196567     yes
 4     224 B  1360926 s     109  510221     yes
 5     280 B  1570071 s      49  183452     yes
 6     352 B  1592051 s      77  229197     yes
 7     440 B  1517732 s      66  157183     yes
 8     552 B  1460821 s      62  117697     yes
 9     696 B  1521917 s     143  215308     yes
10     872 B  1695035 s     205  246162     yes
11     1.1 kB 1681650 s     233  221968     yes
12     1.3 kB 1603363 s     241  183621     yes
13     1.7 kB 1634218 s      94   57197     yes
14     2.1 kB 1695038 s      75   36488     yes
15     2.6 kB 1747075 s      65   25203     yes
16     3.3 kB 1760661 s      78   24167     yes

各列的含义为:

含义
# slab class编号
Item_Size Chunk大小
Max_age LRU内最旧的记录的生存时间
1MB_pages 分配给Slab的页数
Count Slab内的记录数
Full? Slab内是否含有空闲chunk

从这个脚本获得的信息对于调优非常方便,强烈推荐使用。

内存存储的总结

本次简单说明了memcached的缓存机制和调优方法。希望读者能理解memcached的内存管理原理及其优缺点。

下次将继续说明LRU和Expire等原理,以及memcached的最新发展方向—— 可扩充体系(pluggable architecher))。

posted @ 2013-01-26 11:49 昊天 阅读(438) | 评论 (0)编辑 收藏

翻译一篇技术评论社的文章,是讲memcached的连载。fcicq同学说这个东西很有用,希望大家喜欢。

我是mixi株式会社开发部系统运营组的长野。日常负责程序的运营。从今天开始,将分几次针对最近在Web应用的可扩展性领域的热门话题memcached,与我公司开发部研究开发组的前坂一起,说明其内部结构和使用。

memcached是什么?

memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 为首开发的一款软件。现在已成为 mixihatenaFacebookVox、LiveJournal等众多服务中提高Web应用扩展性的重要因素。

许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。

这时就该memcached大显身手了。memcached是高性能的分布式内存缓存服务器。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。

memcached-0001-01.png

图1 一般情况下memcached的用途

memcached的特征

memcached作为高速运行的分布式缓存服务器,具有以下的特点。

  • 协议简单
  • 基于libevent的事件处理
  • 内置内存存储方式
  • memcached不互相通信的分布式

协议简单

memcached的服务器客户端通信并不使用复杂的XML等格式,而使用简单的基于文本行的协议。因此,通过telnet 也能在memcached上保存数据、取得数据。下面是例子。

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
set foo 0 0 3     (保存命令)
bar               (数据)
STORED            (结果)
get foo           (取得命令)
VALUE foo 0 3     (数据)
bar               (数据)

协议文档位于memcached的源代码内,也可以参考以下的URL。

基于libevent的事件处理

libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥O(1)的性能。 memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能。关于事件处理这里就不再详细介绍,可以参考Dan Kegel的The C10K Problem。

内置内存存储方式

为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失。另外,内容容量达到指定值之后,就基于LRU(Least Recently Used)算法自动删除不使用的缓存。 memcached本身是为缓存而设计的服务器,因此并没有过多考虑数据的永久性问题。关于内存存储的详细信息,本连载的第二讲以后前坂会进行介绍,请届时参考。

memcached不互相通信的分布式

memcached尽管是“分布式”缓存服务器,但服务器端并没有分布式功能。各个memcached不会互相通信以共享信息。那么,怎样进行分布式呢?这完全取决于客户端的实现。本连载也将介绍memcached的分布式。

memcached-0001-02.png

图2 memcached的分布式

接下来简单介绍一下memcached的使用方法。

安装memcached

memcached的安装比较简单,这里稍加说明。

memcached支持许多平台。 * Linux * FreeBSD * Solaris (memcached 1.2.5以上版本) * Mac OS X

另外也能安装在Windows上。这里使用Fedora Core 8进行说明。

memcached的安装

运行memcached需要本文开头介绍的libevent库。Fedora 8中有现成的rpm包,通过yum命令安装即可。

$ sudo yum install libevent libevent-devel

memcached的源代码可以从memcached网站上下载。本文执笔时的最新版本为1.2.5。 Fedora 8虽然也包含了memcached的rpm,但版本比较老。因为源代码安装并不困难,这里就不使用rpm了。

memcached安装与一般应用程序相同,configure、make、make install就行了。

$ wget http://www.danga.com/memcached/dist/memcached-1.2.5.tar.gz
$ tar zxf memcached-1.2.5.tar.gz
$ cd memcached-1.2.5
$ ./configure
$ make
$ sudo make install

默认情况下memcached安装到/usr/local/bin下。

memcached的启动

从终端输入以下命令,启动memcached。

$ /usr/local/bin/memcached -p 11211 -m 64m -vv
slab class   1: chunk size     88 perslab 11915
slab class   2: chunk size    112 perslab  9362
slab class   3: chunk size    144 perslab  7281
中间省略
slab class  38: chunk size 391224 perslab     2
slab class  39: chunk size 489032 perslab     2
<23 server listening
<24 send buffer was 110592, now 268435456
<24 server listening (udp)
<24 server listening (udp)
<24 server listening (udp)
<24 server listening (udp)

这里显示了调试信息。这样就在前台启动了memcached,监听TCP端口11211 最大内存使用量为64M。调试信息的内容大部分是关于存储的信息,下次连载时具体说明。

作为daemon后台启动时,只需

$ /usr/local/bin/memcached -p 11211 -m 64m -d

这里使用的memcached启动选项的内容如下。

选项 说明
-p 使用的TCP端口。默认为11211
-m 最大内存大小。默认为64M
-vv 用very verbose模式启动,调试信息和错误输出到控制台
-d 作为daemon在后台启动

上面四个是常用的启动选项,其他还有很多,通过

$ /usr/local/bin/memcached -h

命令可以显示。许多选项可以改变memcached的各种行为,推荐读一读。

用客户端连接

许多语言都实现了连接memcached的客户端,其中以Perl、PHP为主。仅仅memcached网站上列出的语言就有

  • Perl
  • PHP
  • Python
  • Ruby
  • C#
  • C/C++
  • Lua

等等。

这里介绍通过mixi正在使用的Perl库链接memcached的方法。

使用Cache::Memcached

Perl的memcached客户端有

  • Cache::Memcached
  • Cache::Memcached::Fast
  • Cache::Memcached::libmemcached

等几个CPAN模块。这里介绍的Cache::Memcached是memcached的作者Brad Fitzpatric的作品,应该算是memcached的客户端中应用最为广泛的模块了。

使用Cache::Memcached连接memcached

下面的源代码为通过Cache::Memcached连接刚才启动的memcached的例子。

#!/usr/bin/perl

use strict;
use warnings;
use Cache::Memcached;

my $key = "foo";
my $value = "bar";
my $expires = 3600; # 1 hour
my $memcached = Cache::Memcached->new({
    servers => ["127.0.0.1:11211"],
    compress_threshold => 10_000
});

$memcached->add($key, $value, $expires);
my $ret = $memcached->get($key);
print "$ret\n";

在这里,为Cache::Memcached指定了memcached服务器的IP地址和一个选项,以生成实例。 Cache::Memcached常用的选项如下所示。

选项 说明
servers 用数组指定memcached服务器和端口
compress_threshold 数据压缩时使用的值
namespace 指定添加到键的前缀

另外,Cache::Memcached通过Storable模块可以将Perl的复杂数据序列化之后再保存,因此散列、数组、对象等都可以直接保存到memcached中。

保存数据

向memcached保存数据的方法有

  • add
  • replace
  • set

它们的使用方法都相同:

my $add = $memcached->add( '键', '值', '期限' );
my $replace = $memcached->replace( '键', '值', '期限' );
my $set = $memcached->set( '键', '值', '期限' );

向memcached保存数据时可以指定期限(秒)。不指定期限时,memcached按照LRU算法保存数据。这三个方法的区别如下:

选项 说明
add 仅当存储空间中不存在键相同的数据时才保存
replace 仅当存储空间中存在键相同的数据时才保存
set 与add和replace不同,无论何时都保存

获取数据

获取数据可以使用get和get_multi方法。

my $val = $memcached->get('键');
my $val = $memcached->get_multi('键1', '键2', '键3', '键4', '键5');

一次取得多条数据时使用getmulti。getmulti可以非同步地同时取得多个键值,其速度要比循环调用get快数十倍。

删除数据

删除数据使用delete方法,不过它有个独特的功能。

$memcached->delete('键', '阻塞时间(秒)');

删除第一个参数指定的键的数据。第二个参数指定一个时间值,可以禁止使用同样的键保存新数据。此功能可以用于防止缓存数据的不完整。但是要注意,set函数忽视该阻塞,照常保存数据

增一和减一操作

可以将memcached上特定的键值作为计数器使用。

my $ret = $memcached->incr('键');
$memcached->add('键', 0) unless defined $ret;

增一和减一是原子操作,但未设置初始值时,不会自动赋成0。因此,应当进行错误检查,必要时加入初始化操作。而且,服务器端也不会对超过2 SUP(32)时的行为进行检查。

总结

这次简单介绍了memcached,以及它的安装方法、Perl客户端Cache::Memcached的用法。只要知道,memcached的使用方法十分简单就足够了。

下次由前坂来说明memcached的内部结构。了解memcached的内部构造,就能知道如何使用memcached才能使Web应用的速度更上一层楼。欢迎继续阅读下一章。

posted @ 2013-01-26 11:47 昊天 阅读(462) | 评论 (0)编辑 收藏

常常思考一个问题:是不是考虑做2年开发,打2年酱油,然后结婚生子,这样到底行不行?无论你是男是女,人生是一场独自修行的道路。如果在可以选的时候,还是选择靠自己吧。

无论你是要养家还是要实现人生价值,如果你处在迷茫之中,希望这篇文章可以传达一些正能量,可以帮到你。--当然这篇文章是转的呀,但真的很有理啊,假如看了这篇文章,会影响到你的一生,那么我就觉得今天这一小时的时间花在这里值了。

 

你所有不曾料想过的问题,都会随着时间的推移而与你不期而遇;你所有曾经潇洒的随遇而安,同样也会随时间流逝而让你承担那些似乎命中已经注定的代价。在这个世界上,“唯一不可阻挡的是时间,它像一把利刃,无声地切开了坚硬和柔软的一切,恒定地向前推进着,没有任何东西能够使它的行进产生丝毫颠簸,它却改变着一切。”我始终相信一句话:出来混,迟早要还的。虽然你我皆是凡人,只是这芸芸众生中的普通一员,但我依然希望每个人的生命都能够迎着太阳开花结果。不管以你现在的阅历是否能够理解这段话的涵义,请先记下来。我相信总有一天,你会明白。因为,时间能解释一切,时间能证明一切,时间能解决一切。——题记

在最近的一年的职业规划咨询过程中,我明显地感觉到35岁以上人群对于职业生涯规划需求的迫切性。也正是从这些案例中,我们得以清晰地洞察到,时间点的把握对于一个人的成长如何起着决定性的作用。在我们的客户中,遇到的往往是两类较为极端的案例:一类是已经做到一定级别——至少是总监级以上,在公司具有一定地位,年薪不少于20万的人;另一类是工作多年,但依然处于一个相对低的位置,无论是职位层级和物质回报,还是个人的价值感,均无法得到较高认同。这两类案例虽然极端,但却给我们提供可以借鉴的思考。通过对比,我们发现,但凡那些在职业发展上获得一定成功的人,都有一个共同的特征,那就是在自己所熟悉且擅长的领域,至少精耕细作了10年以上。而那些在职场上找不到自己位置的人,往往属于每隔一两年换方向,从来没有在某一个方向上深入积累下去。当然,除了频繁跳槽的因素之外,还有另外一个因素,那就是:已经在一个方向上深入积累,但这种积累属于重复劳动式的,并没有上升,致使职业发展原地踏步。

不管你是否承认,你都必须重视“35岁现象”。很多企业在招募人才时,明确规定年龄在35岁以下。如果你的年龄到了35岁却还在通过招聘网站投递简历不断跳槽的话,你就应该反省一下自己到底哪里做错了。当然,根据我们的实践咨询经验来看,如果你真到了35岁甚至更高的年龄才去思考这个问题的时候,很有可能这个问题你已经无力解决了,很多现实的困难会让你有心无力,束手无策。到了这个时候,很多人会因为当初的选择后悔不迭,但却欲哭无泪。所以,无论是为了避免走更多的弯路,还是迈向更大的成功,你都必须提前思考你未来的谋生之路。已经有太多的案例证明:未雨绸缪会比临时抱佛脚有用得多。

在我们的客户中,有相当一部分属于80后,也是职业规划问题的高发区。年龄最大的一批80后,已经过了“三十而立”的年龄;但还有相当一部分80后,正在迈向三十而立的路上。如果说年轻是上帝给予你的犯错的资本,你还有时间去弥补;但如果你已经到了二十岁的尾巴上,这种资本将不再是你的专利。从大学毕业,到你的而立之年,这个时间已经足够长,让你有足够的机会去了解社会,适应社会,并反思自己的成长。

从30岁到35岁,这其中有5年的时间。假如给你足够犯错的时间,那么,为了不让你35岁以后的职业生涯变得一塌糊涂,你至少应该在30岁就确立明确的目标,并利用5年的时间去追赶。这可能是你成长的最后的最佳时机。错过了这个时机,你已不再年轻,社会也不会再以包容的心态去原谅你的年少轻狂。否则,你多走一步错路,就必定要在以后以十倍的代价补回来。从30岁到35岁,你应该学着为你芸芸众生般的生命,多积累一些厚度,以便让你下半生的职业生涯不要在“假如一切能够重来”的悔恨和遗憾中度过。    

30岁:你必须要面对的三大问题

第一个问题,就是家庭与责任的问题。

不要以为自己还年轻。不要以为有些问题离自己还很远。无论是颓废还是忙碌,你的时间都在飞快地逝去,你感觉自己在加速变老。有些问题,不管你是否愿意去面对,但你的成长阶段决定了你必须要承担与年龄相匹配的责任。大多数的人,到了这个年龄段,都必须要考虑一下家庭问题。如果你已经成家立业,你必须清醒地意识到:你的职业到底该如何发展,才能确保让你支撑整个家庭的负担?这不仅仅包括你的爱人,还包括你的孩子及父母(尤其是双方都是独生子女的时候,你要承担的是双方四位老人的供养)。到了这个阶段,你的压力是成倍增长的,但如果你在职场上的成长无法实现倍增,甚至还在以某种形式进行着倒退,那么,你以后的职业发展就会面临更多的压力。而这种压力,更多的时候,会使你没有余力去谋求更好的发展。很多人在跳槽时,往往会有一个很大的顾虑:如果我跳了,如果收入没有现在的高,那么,现在的工作,我还会轻易再动吗?说得更不客气一点:我还敢轻易再动吗?我还有那个胆量与勇气吗?所以,要想行动,就趁早,趁你还没有背上家庭的包袱的时候,轻装上阵,拼尽全力向前冲,这是你唯一的选择。千万不要在这个时候享受安逸,否则,你的后半生都将永远在碌碌无为中“被安逸”下去。

第二个问题,是能力与年龄的匹配度问题

为什么很多企业在招聘人才的时候,明确规定要5年经验、10年经验等等类似的工作经验要求呢?因为工作时间意味着与之匹配的能力等级。同样的工作,5年经验和10年经验所积累的能力是不一样的,所能承担的责任也是不一样的。你的工作年限越长,往往也意味着你的能力越强,这二者之间是一种正向倍增的关系。但如果你违背了这种关系,那么,你就无法获得用人单位的认可,进而丧失更好的职业发展机会。在我们的很多客户中,其中较为棘手的一种情况,就是能力与年龄不匹配的问题。很多工作3年、5年的人,甚至和工作一年的人在能力方面并没有什么太大的差别,所以当他们的职业想向上突破时,会遇到很多的阻力。导致这种情况的产生有两种原因:一是频繁跳槽,没有在一个方向上积累,无一技之长;二是虽然在一个方向上长期积累,但只有第一年是成长的,剩下的几年都是在做重复劳动,原地踏步。

所以,对于那些年龄30岁的朋友来说,从现在开始,你必须要慎重审视一个问题:从毕业到现在,我工作几年了?我身上所具备的能力是否与我的年龄相匹配?如果不匹配,那么,你一定要及早树立危机感,并跑步前进,以弥补与那些先知先觉者之间的差距,确保自己不会在竞争的过程中被他人挤下马。

第三个问题,是知识结构的构建与提升问题

我们曾经服务过一个客户,做销售做了五六年,也积累了相当丰富的实战操作经验。有一次跳槽去应聘某知名快消企业的区域营销经理的职位。在面试的过程中,所有关于具体操作层面的问题,他都能够对答如流,但上升到系统层面及战略层面的问题时,他的脑子就一片空白。我相信在职场上做过五六年的人都有这种感受:感觉在具体的操作层面,无论是流程,还是技巧与方法,都能够熟练掌握;但如果从更高一层的角度去看待问题,往往又不知道如何下手。

身在职场,不同职位等级的人,所做的事情是不一样的,他们所具备的眼光与思维模式同样也有差别。一个最基础的业务员,想着如何维护好终端,这是战术层面的东西,也是他的职责所在;但如果一个营销总监这样的角色,还和业务员一样天天想着如何去和终端老板打交道,那就是他的失职了。每一个企业的运营,都会由战略与战术所构成。大的战略会细分成小的战略,小的战略会细分成一个一个的执行战术,由基层人员去付诸实施。不同层级的人,分别负责不同高度的工作,各司其职,这也是团队协作的意义所在。同样,每一个不同层级的人,也存在不同的知识结构。层级越高,你看问题的眼光和思路就要越高,你的整体知识结构层次也要向上发展和突破。要不然,你的能力,永远只能停留在具体的基层操作层面上,不会有大的发展,职位上更不可能有上升。       

另外,从沟通的层面来说,你的职位越高,你与老板的距离就越近,你与公司最高领导接触的机会也就越多。如果你看待问题的思路无法与老板们保持在高度上的一致性,那么,你的能力是无法得到认可的。在这样的情况下,你向上晋升的可能性就微乎其微了,职业生涯也会就此止步。

30岁之前:如何打好基础

30 岁是一个承前启后的年龄,也是一个非常重要的时间点。我们在进行职业生涯规划咨询之前,会有一个评估环节,系统评估客户的综合基础,看看是否还适合做职业规划。其中有一个很重要的考察标准,就是年龄问题,重点是30岁左右的年龄。如果你30岁之前几乎没有任何积累,那么,30岁以后的职业生涯要想获得突破是十分困难的,有很多现实的问题无法解决。这是硬伤。但凡遇到有这样硬伤的客户,我们只能对他们说“很抱歉,我们无能为力。”所以,要想让你30岁以后的职业生涯不后悔,顺利渡过30岁这道坎,你就需要在30岁之前对自己负责,未雨绸缪。

你要做的第一件事,就是找准一个可以为之奋斗5年、10年甚至更久的目标。

这是一种最理想的状态。因为一个清晰的目标,可以让你的职业成长围绕着一个点去积累经验,而这种日积月累的经验积累,也是你日后升职加薪的筹码。虽然经验并不一定与能力相关,但如果没有经验的积累,你的能力也基本没有提升的可能。

关于目标与核心竞争力的问题——所谓核心竞争力,一定要有一个核心才行。围绕着这个核心,你才能构建你的竞争力。而这个核心,就是你的职业目标方向——我到底想成为什么样的人?这个问题解决了,方向明确了,哪怕你走得再慢,也可以比那些走弯路的人走得快,因为你明确你的目标,你一直在朝着你的目标前进,你是在走直线。捷径是什么?捷径就是不走弯路,永远走直线。

我们去年曾经咨询过一个客户,7年换了6家公司。按照通常的理解,大家往往会认为这个人的职业发展很糟糕。其实不是。这个人目前的税后月薪是1万元,属于还不错的那种水平。大家也许会纳闷:为什么频繁跳槽的一个人,会有这样的职业发展水平呢?有些人会觉得他很幸运,运气比别人好而已。其实不是。观察他的成长经历,我们发现, 每一次跳槽,他都没有偏离既定的方向。只要方向不错,那么,他的每一步行动,都是在接近这个方向,都是在为以后的职业发展积累更多的筹码。你积累的厚度,将最终决定你未来的发展高度。现在房价、物价这么高,很多人的生存压力也骤然增加,由此也导致很多人迫于生存的压力而变得浮躁和随波逐流。一份工作,往往坚持不了几个月,觉得看不到希望,工资无法提升,往往就选择跳槽。但实际上,跳槽并不能从根本上解决问题。现在的用人单位,都变得非常务实,做多少事,拿多少钱,天经地义(当然,特权阶层不在讨论范围之内)。如果你无法为公司发展贡献更大的价值,却还想着得到更多的回报,这样只赚不赔的买卖是没有人愿意干的。你为公司贡献多少价值,取决于你的能力到底有多强。而能力的积累,则源自于在一个方向上的长期坚持。既便是那些看起来令人很羡慕的成功者们,也少不了日积月累的修炼与成长。那些因为走了弯路却想在极短的时间内把损失补回来的急功近利的做法,只会让你失去更多的东西。      

大家可能听说过10000小时定律:不论你想在任何一个领域成功,你都必须至少付出10000个小时的磨练。Macolm Gladwell在他的新书《异类:成功人士的故事》中说道,无论是最优秀的运动员,企业家,音乐家还是科学家,经调查,你都会发现他们至少都在付出了长达十年,每天不低于三小时的努力之后才崭露头角的。书中有这样一个例子:34岁的世界顶级小提琴家马克西姆•文格罗夫出生在西伯利亚,在4岁的时候接触到第一把小提琴,从此就展现出了过人的天赋。然而他的天赋和敬业度是成正比的。他每天练习7小时,5岁就举办了独奏会,15岁就获得了国际大奖。文格罗夫说,“我的母亲每天晚上8点回到家,吃完晚饭之后就教我小提琴直到凌晨4点。对于一个4岁的小孩来说,这简直就是酷刑,但两年后我变成了小提琴手。”

很多人在抱怨没有机会。其实并非如此。肯特大学的社会学教授弗兰克•弗雷迪认为有效投入很多时间去练习的人自然会拥有自己的运气:“他们近乎苛求地努力,当运气来的时候,他们早就准备好了。”这正应了中国的那句老话——“机会只垂青有准备的头脑。”如果你没有遇到机会,不要抱怨,你首先要反思的是自身,到底哪些方面准备不足?根据我们的经验,只有你在一个方向上坚持至少5年以上,你才有可能得到回报,而且这种回报会对你以后的职业发展产生一个相对长期的影响。同时,也正是这么长时间的积累,让你的基础非常扎实和牢固,你的脚步也会站得更稳。

身在职场,无非要做两件事:一是做事,二是做人。

如果说找准目标为是了积累更多的核心技能,更好地做事的话,那么,学会做人,则是你要职场发展要考虑的第二件大事情。我们甚至认为,会做人比会做事还要重要百倍。找到一个合适的职业目标,对于很多迷茫中的人来说可能并不是容易的事;但学会做人,则是你在任何公司、任何时候都可以修炼的一种职业素养。即便你的职业技能没有多大的突破与成长,但良好的职业素养则会从另外一种层面为你的职业发展增添筹码。我们也遇到不少能力不足、但态度很好、很积极的客户,他们用自己的优秀职业素养成功征服了面试官,得到了不错的发展机会。有句话叫“要做事,先做人。”能力不足,可以慢慢培养;但如果不会做人,在任何组织中,都会成为人人喊打的过街老鼠。

在做人的修炼中,有四项要素,是能够对你以后的职业发展起到致命性影响的。第一是你的人际关系处理能力。这其中包括与你的上司的关系,以及与你的同事的关系。与上司的关系不好,直接决定着你的升迁。所以我经常对我们的客户说这样的一句话:“如果你与上司的关系不好,基本上也就意味着你在这个公司的职业生涯就此终结。” 而与同事的关系不好,即便你哪一天升迁了,成为了他们的领导,你也会因为缺乏“群众基础”而导致权力被架空,你的下属对你不是阳奉阴违,就是故意挑衅,你会发现领导这个位置其实很不好坐。

第二项要素,就是要树立不断学习的能力。我们应注重学习的重要性。我们在去年曾经遇到过一个客户,已经近40岁的人了,做会计做了14年,但仍然只是一个基层主管的职位,在公司中也看不到晋升的机会,也已经好多年没有加薪了。我们问了他一个问题,“在这14年中,你有没有进行过培训、进修或者任何其他形式的充电?”他说,“没有”。“从来没有吗?”“从来没有。”这是一个非常极端的例子,但这种现象在职场中却是非常普遍的。关于自己的学习与成长,你不妨问自己几个问题:与一年前相比,我的专业能力明显进步了吗?无论是理论还是实践,我有自己独特的见解吗?假如遇到职责范围内的事情,我一个人是否能够独立搞定,是否具备独当一面的能力?厚度决定高度。你所以会有厚度的积累,原因在于你不断强化的学习能力。没有学习,你的职业发展就会在原地踏步。你以为你没有退步,但实际上别人在进步,你与别人之间的差距在不知不觉中拉开了,你也就退步了。       

第三项要素,就是要树立职业化精神不管你有没有目标,不管这项工作是不是你愿意做的,只要你在这个岗位上,你就应该把事情做好。在其位谋其职,这是最起码的一种职业素养,也是任何岗位上都不可或缺的一种职业精神。既然在这个岗位上,你的岗位职责要求你把工作做好;如果不想做好,那就走人,别在这里浪费大家的时间。良好的职业素养,有时候比能力本身更重要。

良好的职业化精神,是很多优秀人士身上必备的成功素质。即便这份工作你不喜欢,也不要用消极的的方式去对待,因为你不仅在浪费公司的时间,更是在浪费你自己的时间。身在职场,你能收获的无非是两种东西:一是回报,二是成长。在没有明确的目标之前,把眼前的事情做到极致,把所有你能够在这个职位上得到的东西,统统用自己最大的努力去争取。以消极的态度对待工作,你会两手空空,什么也得不到。

第四项要素,就是要有强大的内心。我所遇到的很多人,其实内心里都很脆弱,无法经受漫长人生道路上所降临的各种苦难。要知道,每个人的成长都不是一帆风顺。即便是耶酥,又何尝不会遭受苦难的洗礼?强大的内心可以让一个人在绝望中看到希望,而消极悲观只会让你在希望面前错机良机。《肖申克的救赎》中有这样一句台词:“懦怯囚禁人的灵魂,希望可以感受自由。强者自救,圣者渡人。”你没有本事成为圣者,但一定要变成强者。“物竞天择,适者生存。”优胜劣汰永远是自然进步的永恒法则。你只有适应这个法则,才能在这样竞争激烈的社会中获得生存之地。

30岁之后:如何实现质的突破

30岁以后的职业发展,不应该再是原地踏步、停滞不前的状态。要想让你35岁以后的职业生涯实现一个比较大的发展,你应该从30岁开始,好好利用5年时间,着手以下3件至关重要的事情的准备。

第一件事就是完成你的专业化品牌构建。如何构建你自己的个人品牌。你到了30岁以后,再去频繁跳槽,肯定是不行的,最大的不利,就是你的专业知识成长,永远只是停留在一个相对低的层次上,而这又会影响你职位的晋升与薪水的增长。我该干什么,适合做什么,此时应该有一个清晰而明确的定位,并利用剩余5年的时间去积累。否则,“35岁现象”一定会在你身上上演。

“日本战略之父”大前研一在其专著《专业主义》中也提出了这样的观点:“你凭什么胜出?未来能够牵动世界大势的,是个人之间的竞争。能否独霸世界舞台,锻造他人无法超越的核心竞争力?你唯一的依恃,就是专业。” 你是一流的商务人士,还是一般的上班族?差别就在这里!给自己一个专业定位,树立自己的品牌形象,会为你以后的职业发展增加更多有价值的筹码。我们曾经遇到过5年跳槽13次、接触过10余个行业的客户。假如你也这样,就毫无专业性可言了。

当然,为了支撑你“专业化”的品牌形象,你还需要在以下两个方面做出努力:第一个方面,就是我们在前面提到过的知识结构的问题,要脱离具体的操作层面的问题,把你看待问题的眼光放得更高一些,更远一些,成为一个具有“远见卓识”而不是“鼠目寸光”的人。你要向这个行业里最优秀的人看齐,并以他们为目标,作为自己修炼成长的榜样,一步一步弥补与优秀之间的差距。这个时候,在职学习、进修、培训等等都需要提上日程,学会用新的知识充实自己的头脑。第二个方面,就是树立自己在行业内的影响力。每一个行业的优秀人才,都有自己聚集的圈子。比如哈佛商业评论网、职业经理人网、业务员网等等,都是相关人才聚集的地方。为什么有些人出去讲一次课就要好几万?为什么有些人能够用自己的力量推动一个行业的发展?原因就是他们的影响力太大了。你要想成为优秀的职场人士,也必须像那些最优秀的人看齐,树立在这个行业内的影响力,用你的思想去影响别人。我们去年遇到一个非常优秀的小伙子,至今印象深刻。他是一个印刷专业的大专生,毕业之后去印刷厂做销售。或许在有些人看来,这个专业、这个职位并没有什么特殊之处。但正是这样一个看起来没有什么含金量的职位,他却用自己独特的方式构建着自己的影响力。他给自己定了一个要求:每月都要写一篇文章,发表在这个行业内最具影响力的杂志上。当然,并不是每篇文章都能够达到发表的质量,但每个月必须要保证质量写一篇。 

这样做的好处,第一是树立了你在这个行业内的影响力;第二,可以让你形成自己的知识结构。我们前面提过知识结构的问题,这里需要再强调一下。你通过学习、培训等方式,得到的知识,终究是别人的,不是你自己的。要想形成你自己的知识结构,你需要将这些外来吸收的知识应用到工作实践中。在实践的过程中,同样的知识理论,在不同的情况下,每个人的感受和心得是不一样的。你写文章的过程,也就是对你自己在实践中的感悟和心得进行总结和反思的过程。在这个过程中形成的知识结构,才能真正称得上是你“自己的”知识。

第二件事情,就是把自己培养成一个优秀的管理者(这里指的是中层管理者,也是你的第一个管理者目标)。除了那些对于技术十分热衷的人,如果你要想在职场上获得更多的空间,职位上的晋升必不可少。有些人会说我不愿意成为一个领导者,我只希望做好自己的份内事,我就满足了。对于抱有这种想法的人,我只能说很抱歉,这不是你能选择的。原因很简单:你25岁的时候,可以做一名基础员工;30岁的时候,也可以做一名基础员工;但你到了35岁以上的时候,你如果还只想着做一名基础员工,那就不太现实了。我们前面提到过 “35岁现象”。如果一个已经35岁的人,他的能力还只是局限在基础岗位上,那么,这样的人基本上是没什么价值的(当然,落后的国企及事业单位除外)。所以,不管你是否愿意,你都必须把职位晋升作为你职业成长道路上的一个重要目标,并为之付出努力。     

走上了管理者岗位,是你职业成长的关键一步。以后的发展空间,都与此次的晋升密不可分。但这个位子并不是每个人都能够坐稳。盖洛普曾经做过一项调查,发现基础员工之所以离职频繁,很多时候问题并不在于公司,而是在于其上司——公司的中层管理者。在我们的职业规划咨询经历中,也经常遇到这样的问题:正因为中层管理的不善,而对于下属员工的发展与成长造成了非常不利的影响,导致他们职业发展无论是在专业能力上,还是在职业修养上,均不能得到有效的成长。
在这里我们暂且不去讨论“中层危机”对于公司的发展多么多么重要这样比较宏观的话题,单纯从小处来说,你必须要意识到:你的管理,将有可能改变你的下属的终身命运,是你错误的管理方式在不知不觉中伤害到你的下属,迫使他们离开公司。管理者在招聘员工的时候,对于频繁跳槽的人是深恶痛绝。但作为一个旁观者,我不得不说的是:当你们在抱怨求职者的不忠、素质低下时,你们是否考虑过建立一种良性的人才成长机制,让每个人都能够得到成长?你们在抱怨人才短缺的同时,是否考虑过如何去认真地培养人才?很多企业会说我们培养了,但留不住,到头来都是为他人做嫁衣,这是一件得不偿失的事情,所以对于人才培养也就不那么热衷了。但问题恰恰出在这里:你为什么留不住人?如果每个公司都不去培养人才,都想吃“现成的”,那么,最终的后果将是:所有的企业,都将面临无人可用的境地!

当你的成长因为你的上司而受到阻碍的时候,这是你的不幸,你可以选择“用脚投票”;当你某一天也成为管理者的时候,我们不希望把这种不幸继续传递下去。唯有如此,人才的培养才能进入正循环,才能够让每个人都能够在职场上找到自己的一席之地。这其中困难多多,但我希望有理想的人能够不断努力,慢慢改变这一切。

第三件事情,就是找一家成长中的公司,在相对长的时间内,伴随其成长。在成长的过程中,找一家具有成长潜力和发展空间的公司,并随着公司一起成长,是一件非常重要的事情。一是你可以亲眼目睹一家公司从小到大、由弱到强的成长历程,对于公司的运营也会有更加深入的理解,你也能够体会到你的角色在公司成长中的位置和作用。二是在公司成长的过程中,你的价值会有更大的发挥余地,会更容易展现出来。很多人在找工作时都倾向于寻找大公司,这也可以理解,但是要想快速的成长,寻找规模不太大的成长中的公司,其实是更好的一种选择。因为在成长的过程中,会对于人才的需求较为迫切,你的职位晋升也会更快。三是你的忠诚度会为你的发展带来更多的回报。企业用人,其中最重要的一条就是忠诚度。没有哪一个企业喜欢朝秦暮楚的员工。尤其是现在跳槽率、流失率在众多企业居高不下的情况下,忠诚就成为一种非常难能可贵的职业精神。

不论你现在已经“奔三”,还是正在“奔三”的路上,请始终告诫自己:不要虚度了大好年华,抓住所有能够抓住的时间,为自己芸芸众生般的生命多积累一些厚度。当你慢慢变老的时候,回想自己的一生,你希望是令人欣慰的一生,还是悔恨和懊恼的一生?

最后,借用我最喜欢的乔布斯语录,作为本文的结尾。“人这一辈子没法做太多的事情,所以每一件都要做得精彩绝伦。你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。”

当你觉得收入不如别人时,不要随便和别人攀比,因为你永远不可能知道别人在获得收入的背后付出了什么,同理,当我们生活不如别人时,亦无需格外羡慕,因为我们的生活质量是自己通过努力创造来的,如果你觉得自己生活不够好说明你的努力还不够。其实,我们只要坚持就好,坚持3天容易,难的是始终坚持! 

posted @ 2012-12-22 10:52 昊天 阅读(337) | 评论 (0)编辑 收藏

你对JVM调优是否熟悉,这里向大家描述一下,首先要了解几个概念,比如说Perm持久代,用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等。

JVM调优总结

Young(年轻代)

年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。

Tenured(年老代)

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

Perm(持久代)

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

JVM调优之年轻代大小选择

◆响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

◆吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

JVM调优之年老代大小选择

◆响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

1.并发垃圾收集信息

2.持久代并发收集次数

3.传统GC信息

4.花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

◆吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

JVM调优之较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

◆-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

◆-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次FullGC后,对年老代进行压缩

posted @ 2012-12-11 11:19 昊天 阅读(531) | 评论 (0)编辑 收藏
在使用Java程序从数据库中查询大量的数据或是应用服务器(如tomcat、jboss,weblogic)加载jar包时会出现java.lang.OutOfMemoryError异常。这主要是由于应用服务器的内存不足引起的。这种异常常有以下几种情况(以下以tomcat环境为例,其它WEB服务器如jboss,weblogic等是同一个道理):
  1. java.lang.OutOfMemoryError: PermGen space
  PermGen space的全称是Permanent Generation space,是指内存的永久保存区域OutOfMemoryError: PermGen space。从文字上看就是内存溢出,解决方法是加大内存。为什么会内存溢出,这是由于这块内存主要是被JVM存放Class和Meta信息的,Class在被Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。
  解决方法: 手动设置MaxPermSize大小
  a.如果tomcat是以bat方式启动的,则如下设置:
  修改TOMCAT_HOME/bin/catalina.sh
  在“echo "Using CATALINA_BASE: $CATALINA_BASE"”上面加入以下行:
  JAVA_OPTS="-server -XX:PermSize=64M -XX:MaxPermSize=128m
  b.如果tomcat是注册成了windows服务,以services方式启动的,则需要修改注册表中的相应键值。
  打开注册表,找到目录HKEY_LOCAL_MACHINE\SOFTWARE\Apache Software Foundation\Procrun 2.0\htfty\Parameters\Java,其中目录地址中红色标注的(如htfty)需要根据不同情况作修改,为tomcat服务注册成windows服务的名称。 可以看到JvmMs和JvmMx项,其中JvmMs设置最小的内存使用参数,JvmMx设置最大的内存使用参数。设置好JvmMs和JvmMx项的值,重启tomcat服务器即可生效。
  建议:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以达到减少jar 文档重复占用内存的目的。
  2. java.lang.OutOfMemoryError: Java heap space
  JVM堆的设置是指java程序运行过程中JVM可以调配使用的内存空间的设置。JVM在启动的时候会自动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。在JVM中如果98%的时间是用于GC且可用的Heap size 不足2%的时候将抛出此异常信息。
  解决方法:手动设置Heap size
  a.如果tomcat是以bat方式启动的,则如下设置:
  修改TOMCAT_HOME/bin/catalina.sh
  在“echo "Using CATALINA_BASE: $CATALINA_BASE"”上面加入以下行:
  JAVA_OPTS="-server -Xms800m -Xmx800m -XX:MaxNewSize=256m"
  b.如果tomcat是注册成了windows服务,以services方式启动的,则需要修改注册表中的相应键值。
  打开注册表,找到目录HKEY_LOCAL_MACHINE\SOFTWARE\Apache Software Foundation\Procrun 2.0\htfty\Parameters\Java,其中目录地址中红色标注的(如htfty)需要根据不同情况作修改,为tomcat服务注册成windows服务的名称。 可以看到JvmMs和JvmMx项,其中JvmMs设置最小的内存使用参数,JvmMx设置最大的内存使用参数。设置好JvmMs和JvmMx项的值,重启tomcat服务器即可生效。
  提示:Heap Size 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为1/4的-Xmx值。
posted @ 2012-12-11 10:28 昊天 阅读(714) | 评论 (0)编辑 收藏
  1. 堆大小设置
    JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
    典型设置:
    • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
      -
      Xmx3550m:设置JVM最大可用内存为3550M。
      -Xms3550m
      :设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
      -Xmn2g
      :设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
      -Xss128k
      :设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
    • java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
      -XX:NewRatio=4
      :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
      -XX:SurvivorRatio=4
      :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
      -XX:MaxPermSize=16m:设置持久代大小为16m。
      -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
  2. 回收器选择
    JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
    1. 吞吐量优先的并行收集器
      如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
      典型配置
      • java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
        -XX:+UseParallelGC
        :选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
        -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
        -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
        -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
        -XX:+UseAdaptiveSizePolicy
        :设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
    2. 响应时间优先的并发收集器
      如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
      典型配置
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
        -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
        -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
        -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
        -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
  3. 辅助信息
    JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
    • -XX:+PrintGC
      输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]

                      [Full GC 121376K->10414K(130112K), 0.0650971 secs]

    • -XX:+PrintGCDetails
      输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]

                      [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

    • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
      输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
    • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
      输出形式:Application time: 0.5291524 seconds
    • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
      输出形式:Total time for which application threads were stopped: 0.0468229 seconds
    • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
      输出形式:
      34.702: [GC {Heap before gc invocations=7:
       def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
      from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
        to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
       tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
       compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
         the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
          ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
          rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
       def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
        from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
        to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
       tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
       compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
         the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
          ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
          rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      }
      , 0.0757599 secs]
    • -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
  4. 常见配置汇总
    1. 堆设置
      • -Xms:初始堆大小
      • -Xmx:最大堆大小
      • -XX:NewSize=n:设置年轻代大小
      • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
      • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
      • -XX:MaxPermSize=n:设置持久代大小
    2. 收集器设置
      • -XX:+UseSerialGC:设置串行收集器
      • -XX:+UseParallelGC:设置并行收集器
      • -XX:+UseParalledlOldGC:设置并行年老代收集器
      • -XX:+UseConcMarkSweepGC:设置并发收集器
    3. 垃圾回收统计信息
      • -XX:+PrintGC
      • -XX:+PrintGCDetails
      • -XX:+PrintGCTimeStamps
      • -Xloggc:filename
    4. 并行收集器设置
      • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
      • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
      • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    5. 并发收集器设置
      • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
      • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


四、调优总结

  1. 年轻代大小选择
    • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  2. 年老代大小选择
    • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例
      减少年轻代和年老代花费的时间,一般会提高应用的效率
    • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
  3. 较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
    • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
posted @ 2012-12-11 10:28 昊天 阅读(357) | 评论 (0)编辑 收藏
/*==============================================================*/
/* 需要用user1账号执行下面的授权                                */
/*==============================================================*/ 
grant select any table to user;
grant create materialized view to user;
grant on commit refresh to user;  

/*==============================================================*/
/* 需要用sys账号执行下面的授权                                  */
/*==============================================================*/ 
grant exempt access policy to user;
grant exempt access policy to user;
grant exempt access policy to user;

drop materialized view log on AAA;
create materialized view log on AAA with primary key

drop materialized view AAA;
create materialized view AAA 
refresh fast 
on commit as 
select bindid, merchantid, userid, bindtime, bindtype, username, gsmsn
  
from AAA t;
  
drop materialized view log on DDD;
create materialized view log on DDD with primary key

drop materialized view BBB;
create materialized view  BBB
refresh fast 
on commit as 
select MERCHANTID,
       COMPANYNAME,
       ORGANIZATION
  
from DDD t; 
  
drop materialized view log on BBB;
drop materialized view log on CCC;
drop materialized view log on CCC;
create materialized view log on BBB WITH rowid ,SEQUENCE (userid,timinalid,ACCOUNTNO,IMSI,REGDATE,NICKNAME,SEX,INVALID,ISACTIVE,USERTYPE,STATUS,BIZTYPE,ACTIVEDATE,SIGNATURE,PHOTOID,CITYID,CARID,USERPHOTO,USERCARPHOTO,MOBILENO,EMAIL,LOGINDAYS,LASTLOGINDATE,USERSTATUS,LATELYCITY,LATELYCITYDATE) INCLUDING NEW VALUES;
create materialized view log on CCC WITH rowid ,SEQUENCE (IDENTIFYCARD,CARNUMBER,FDJNUMBER,FOURSSHOP,DELIVERDATE,BIRTHDATE,ADDRESS,MILEAGE,MILEAGETIME) INCLUDING NEW VALUES;
create materialized view log on CCC WITH rowid ,SEQUENCE (DEVICEID,IMSI,ISSUEMONTH,ICCID,SIMPHONE,GSMSN,DEVICESN,CURVER,DEVICETYPE,IS3GNET,IMSITYPE,TERMINALINFO,RATEID,CARDSTATUS) INCLUDING NEW VALUES;

drop materialized view DDD;
create materialized view DDD
refresh fast 
on commit as 
select a.rowid arowid,
       a.USERID,
       a.ACCOUNTNO,
       b.rowid browid,       
       b.USERNAME,
       c.rowid crowid,       
       c.DEVICEID 
  
from BBB a ,CCC b,CCC c 
 
where a.userid = b.userid(+
   
and a.timinalid = c.terminalid(+);
  
 
 
create index INDEX_DDD on DDD(merchantid)
  tablespace SSS
  pctfree 
10
  initrans 
2
  maxtrans 
255
  storage
  (
    initial 5M
    minextents 
5
    maxextents unlimited
  ); 
  
 
create index INDEX_AAA on AAA(userid)
  tablespace SSS
  pctfree 
10
  initrans 
2
  maxtrans 
255
  storage
  (
    initial 5M
    minextents 
5
    maxextents unlimited
  ); 
    
 
create index INDEX_BBB on BBB(merchantid)
  tablespace SSS
  pctfree 
10
  initrans 
2
  maxtrans 
255
  storage
  (
    initial 10M
    minextents 
5
    maxextents unlimited
  ); 
  

 
create index INDEX_CCC on CCC(userid)
  tablespace SSS
  pctfree 
10
  initrans 
2
  maxtrans 
255
  storage
  (
    initial 10M
    minextents 
5
    maxextents unlimited
  ); 
posted @ 2012-06-15 14:51 昊天 阅读(294) | 评论 (0)编辑 收藏

Apache的MINA是一个被用来构建高性能和高可伸缩性应用程序的网络应用框架,它提供了一套统一的建立在Java NIO之上的事件驱动的异步API。

对于MINA框架的了解,MINA官方的几篇文章是必须要看的,如下:

* Application Architecture http://mina.apache.org/mina-based-application-architecture.html
* Server Architecture http://mina.apache.org/server-architecture.html
* Client Architecture http://mina.apache.org/client-architecture.html

其中几个主要的组件如下:

I/O Service - 用来处理I/O流,对于Server端就是IOAcceptor的实现类接受Client端的连接请求,对于Client端就是IoConnector的实现类来建立到Server端的连接。

I/O Filter Chain - 用来过滤或转化数据。对于Server端和Client端来说都是IoFilter接口的实现类,MINA自己内建了很多IoFilter接口的实现类。具体可以参考官方文档。

I/O Handler - 用来处理真正业务逻辑的类。对于Server端和Client端来说都是IoHandler接口的实现类,通常来说需要自己编写。


由于Server端和Client端都是基于以上三个组件的,因此对于Server端和Client端编程来说就都有类似的代码结构。

对于Server端来说:

1. 创建I/O service - 这里就是创建IOAcceptor类监听端口。

2. 创建I/O Filter Chain - 这里就是告诉使用那些IoFilter。
3. 创建I/O Handler - 自己的业务逻辑。 

对于Client端来说:

1. 创建I/O service - 这里就是创建IOConnector类来建立到Server端的连接。

2. 创建I/O Filter Chain - 这里就是告诉使用那些IoFilter。

3. 创建I/O Handler - 自己的业务逻辑。

下面来通过一个例子看看MINA是怎样工作的。由于大多数应用都是基于TCP/IP的应用,所以这里也就不再说UDP/IP了。

这里我使用了Maven来创建了一个简单java应用程序,具体步骤请参Maven的官方手册。这里只是将我用到的maven配置文件pom.xml列出,方便下面及后续文章使用。具体pom.xml文件内容如下:

 

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  2.     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">  
  3.     <modelVersion>4.0.0</modelVersion>  
  4.     <groupId>com.google.code.garbagecan.minastudy</groupId>  
  5.     <artifactId>minastudy</artifactId>  
  6.     <packaging>jar</packaging>  
  7.     <version>1.0-SNAPSHOT</version>  
  8.     <name>minastudy</name>  
  9.     <url>http://maven.apache.org</url>  
  10.     <dependencies>  
  11.         <dependency>  
  12.             <groupId>org.apache.mina</groupId>  
  13.             <artifactId>mina-core</artifactId>  
  14.             <version>2.0.4</version>  
  15.         </dependency>  
  16.         <dependency>  
  17.             <groupId>org.apache.mina</groupId>  
  18.             <artifactId>mina-filter-compression</artifactId>  
  19.             <version>2.0.4</version>  
  20.         </dependency>  
  21.         <dependency>  
  22.             <groupId>org.slf4j</groupId>  
  23.             <artifactId>slf4j-api</artifactId>  
  24.             <version>1.3.0</version>  
  25.         </dependency>  
  26.         <dependency>  
  27.             <groupId>org.slf4j</groupId>  
  28.             <artifactId>slf4j-log4j12</artifactId>  
  29.             <version>1.3.0</version>  
  30.         </dependency>  
  31.     </dependencies>  
  32. </project>  
首先来看Server端的代码

 

 

  1. package com.google.code.garbagecan.minastudy.sample1;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.InetSocketAddress;  
  5. import java.nio.charset.Charset;  
  6.   
  7. import org.apache.mina.core.service.IoAcceptor;  
  8. import org.apache.mina.core.service.IoHandlerAdapter;  
  9. import org.apache.mina.core.session.IdleStatus;  
  10. import org.apache.mina.core.session.IoSession;  
  11. import org.apache.mina.filter.codec.ProtocolCodecFilter;  
  12. import org.apache.mina.filter.codec.textline.TextLineCodecFactory;  
  13. import org.apache.mina.filter.logging.LoggingFilter;  
  14. import org.apache.mina.transport.socket.nio.NioSocketAcceptor;  
  15. import org.slf4j.Logger;  
  16. import org.slf4j.LoggerFactory;  
  17.   
  18. public class MyServer {  
  19.       
  20.     private static final Logger logger = LoggerFactory.getLogger(MyServer.class);  
  21.       
  22.     public static void main(String[] args) {  
  23.         IoAcceptor acceptor = new NioSocketAcceptor();  
  24.   
  25.         acceptor.getFilterChain().addLast("logger"new LoggingFilter());  
  26.         acceptor.getFilterChain().addLast("codec"new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));  
  27.   
  28.         acceptor.setHandler(new IoHandlerAdapter() {  
  29.               
  30.             @Override  
  31.             public void sessionCreated(IoSession session) throws Exception {  
  32.             }  
  33.               
  34.             @Override  
  35.             public void sessionOpened(IoSession session) throws Exception {  
  36.             }  
  37.   
  38.             @Override  
  39.             public void sessionClosed(IoSession session) throws Exception {  
  40.             }  
  41.   
  42.             @Override  
  43.             public void sessionIdle(IoSession session, IdleStatus status) throws Exception {  
  44.             }  
  45.   
  46.             @Override  
  47.             public void exceptionCaught(IoSession session, Throwable cause) throws Exception {  
  48.                 logger.error(cause.getMessage(), cause);  
  49.                 session.close(true);  
  50.             }  
  51.   
  52.             @Override  
  53.             public void messageReceived(IoSession session, Object message) throws Exception {  
  54.                 logger.info("Received message " + message);  
  55.                 session.write(message);  
  56.             }  
  57.   
  58.             @Override  
  59.             public void messageSent(IoSession session, Object message) throws Exception {  
  60.                 logger.info("Sent message " + message);  
  61.             }  
  62.         });  
  63.           
  64.         try {  
  65.             acceptor.bind(new InetSocketAddress(10000));  
  66.         } catch (IOException ex) {  
  67.             logger.error(ex.getMessage(), ex);  
  68.         }  
  69.     }  
  70. }  
1. 首先创建I/O Service,这里使用的是NioSocketAcceptor类来创建了一个IoAcceptor实例。

 

2. 创建I/O Filter Chain,这里使用了两个IoFilter,一个是LoggingFilter用来记录日志和打印事件消息,另一个是ProtocolCodecFilter实例用来编码数据,这里其实就是将传递的数据编码成文本。

3. 创建I/O Handler,不要害怕,看起来代码多,其实就是一个实现了IoHandler接口的子类,自己需要实现其中的一些方法,这里方法比较多,但是我在这里只实现了messageSent,messageReceived和exceptionCaught方法。

4. 最后就是让IoAcceptor类实例绑定端口实现监听。

下面看看Client端的代码

  1. package com.google.code.garbagecan.minastudy.sample1;  
  2.   
  3. import java.net.InetSocketAddress;  
  4. import java.nio.charset.Charset;  
  5.   
  6. import org.apache.mina.core.RuntimeIoException;  
  7. import org.apache.mina.core.future.ConnectFuture;  
  8. import org.apache.mina.core.service.IoConnector;  
  9. import org.apache.mina.core.service.IoHandlerAdapter;  
  10. import org.apache.mina.core.session.IdleStatus;  
  11. import org.apache.mina.core.session.IoSession;  
  12. import org.apache.mina.filter.codec.ProtocolCodecFilter;  
  13. import org.apache.mina.filter.codec.textline.TextLineCodecFactory;  
  14. import org.apache.mina.filter.logging.LoggingFilter;  
  15. import org.apache.mina.transport.socket.nio.NioSocketConnector;  
  16. import org.slf4j.Logger;  
  17. import org.slf4j.LoggerFactory;  
  18.   
  19. public class MyClient {  
  20.       
  21.     private static final Logger logger = LoggerFactory.getLogger(MyClient.class);  
  22.       
  23.     public static void main(String[] args) {  
  24.         IoConnector connector = new NioSocketConnector();  
  25.         connector.setConnectTimeoutMillis(10 * 1000);  
  26.           
  27.         connector.getFilterChain().addLast("logger"new LoggingFilter());  
  28.         connector.getFilterChain().addLast("codec"new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));  
  29.           
  30.         connector.setHandler(new IoHandlerAdapter() {  
  31.               
  32.             @Override  
  33.             public void sessionCreated(IoSession session) throws Exception {  
  34.             }  
  35.   
  36.             @Override  
  37.             public void sessionOpened(IoSession session) throws Exception {  
  38.                 for (int i = 0; i < 10; i++) {  
  39.                     session.write("Hello user_" + i);  
  40.                 }  
  41.                 session.write("Bye");  
  42.             }  
  43.   
  44.             @Override  
  45.             public void sessionClosed(IoSession session) throws Exception {  
  46.             }  
  47.   
  48.             @Override  
  49.             public void sessionIdle(IoSession session, IdleStatus status) throws Exception {  
  50.             }  
  51.   
  52.             @Override  
  53.             public void exceptionCaught(IoSession session, Throwable cause) throws Exception {  
  54.                 logger.error(cause.getMessage(), cause);  
  55.                 session.close(true);  
  56.             }  
  57.   
  58.             @Override  
  59.             public void messageReceived(IoSession session, Object message) throws Exception {  
  60.                 logger.info("Received message " + message);  
  61.                 if (message.toString().equalsIgnoreCase("Bye")) {  
  62.                     session.close(true);  
  63.                 }  
  64.             }  
  65.   
  66.             @Override  
  67.             public void messageSent(IoSession session, Object message) throws Exception {  
  68.                 logger.info("Sent message " + message);  
  69.             }  
  70.         });  
  71.   
  72.         IoSession session = null;  
  73.         try {  
  74.             ConnectFuture future = connector.connect(new InetSocketAddress("localhost"10000));  
  75.             future.awaitUninterruptibly();  
  76.             session = future.getSession();  
  77.         } catch (RuntimeIoException e) {  
  78.             logger.error(e.getMessage(), e);  
  79.         }  
  80.   
  81.         session.getCloseFuture().awaitUninterruptibly();  
  82.         connector.dispose();  
  83.     }  
  84. }  

1. 首先创建I/O Service,这里使用的是NioSocketConnector类来创建了一个IoConnector实例,并设置连接超时为10秒。

2. 创建I/O Filter Chain,和服务器端同样设置了两个IoFilter,一个是LoggingFilter用来记录日志和打印事件消息,另一个是ProtocolCodecFilter实例用来编码数据,这里其实就是将传递的数据编码成文本。

3. 创建I/O Handler,也不要害怕,看起来代码多,其实也是一个实现了IoHandler接口的子类,并且自己实现了sessionOpened,messageSent,messageReceived和exceptionCaught方法。实现sessionOpened方法是为了在建立连接后向Server端发送消息。另外看一下messageReceived方法实现,在接收到服务器端的消息后关闭会话。从而可以使Client程序最终能够退出。

4. 最后就是IoConnector实例类连接远端的Server。

 

下面测试一下上面的程序,首先运行MyServer类,然后运行MyClient类,就可以分别在各自的终端上看到事件日志以及发送/接收的消息了。

posted @ 2012-06-12 16:30 昊天 阅读(813) | 评论 (0)编辑 收藏
ReentrantLock是jdk5引入的新的锁机制,它与内部锁(synchronize) 相同的并发性和内存语义,比如可重入加锁语义。在中等或者更高负荷下,ReentrantLock有更好的性能,并且拥有可轮询和可定时的请求锁等高级功能。这个程序简单对比了ReentrantLock公平锁、ReentrantLock非公平锁以及内部锁的性能,从结果上看,非公平的ReentrantLock表现最好。内部锁也仅仅是实现统计意义上的公平,结果也比公平的ReentrantLock好上很多。这个程序仅仅是计数,启动N个线程,对同一个Counter进行递增,显然,这个递增操作需要同步以保证原子性,采用不同的锁来实现同步,然后查看结果。
Counter接口:
package net.rubyeye.concurrency.chapter13;

public interface Counter {
    
public long getValue();

    
public void increment();

}

然后,首先使用我们熟悉的synchronize来实现同步:
package net.rubyeye.concurrency.chapter13;

public class SynchronizeBenchmark implements Counter {
    
private long count = 0;

    
public long getValue() {
        
return count;
    }

    
public synchronized void increment() {
        count
++;
    }
}

采用ReentrantLock的版本,切记要在finally中释放锁,这是与synchronize使用方式最大的不同,内部锁jvm会自动帮你释放锁,而ReentrantLock需要你自己来处理。
package net.rubyeye.concurrency.chapter13;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockBeanchmark implements Counter {

    
private volatile long count = 0;

    
private Lock lock;

    
public ReentrantLockBeanchmark() {
        
// 使用非公平锁,true就是公平锁
        lock = new ReentrantLock(false);
    }

    
public long getValue() {
        
// TODO Auto-generated method stub
        return count;
    }

    
public void increment() {
        lock.lock();
        
try {
            count
++;
        } 
finally {
            lock.unlock();
        }
    }

}

    写一个测试程序,使用CyclicBarrier来等待所有任务线程创建完毕以及所有任务线程计算完成,清单如下:
package net.rubyeye.concurrency.chapter13;

import java.util.concurrent.CyclicBarrier;

public class BenchmarkTest {
    
private Counter counter;

    
private CyclicBarrier barrier;

    
private int threadNum;

    
public BenchmarkTest(Counter counter, int threadNum) {
        
this.counter = counter;
        barrier 
= new CyclicBarrier(threadNum + 1); //关卡计数=线程数+1
        this.threadNum = threadNum;
    }

    
public static void main(String args[]) {
        
new BenchmarkTest(new SynchronizeBenchmark(), 5000).test();
       
//new BenchmarkTest(new ReentrantLockBeanchmark(), 5000).test();
        //new BenchmarkTest(new ReentrantLockBeanchmark(), 5000).test();   
    }

    
public void test() {
        
try {
            
for (int i = 0; i < threadNum; i++) {
                
new TestThread(counter).start();
            }
           
long start = System.currentTimeMillis();
            barrier.await(); // 等待所有任务线程创建
            barrier.await(); // 等待所有任务计算完成
            long end = System.currentTimeMillis();
            System.out.println(
"count value:" + counter.getValue());
            System.out.println(
"花费时间:" + (end - start) + "毫秒");
        } 
catch (Exception e) {
            
throw new RuntimeException(e);
        }
    }

    
class TestThread extends Thread {
        
private Counter counter;

        
public TestThread(final Counter counter) {
            
this.counter = counter;
        }

        
public void run() {
            
try {
                barrier.await();
                
for (int i = 0; i < 100; i++)
                    counter.increment();
                barrier.await();
            } 
catch (Exception e) {
                
throw new RuntimeException(e);
            }
        }
    }
}

分别测试一下,

将启动的线程数限定为500,结果为:
公平ReentrantLock:      210 毫秒
非公平ReentrantLock :   39  毫秒
内部锁:                          39 毫秒

将启动的线程数限定为1000,结果为:
公平ReentrantLock:      640 毫秒
非公平ReentrantLock :   81 毫秒
内部锁:                           60 毫秒

线程数不变,test方法中的循环增加到1000次,结果为:
公平ReentrantLock:      16715 毫秒
非公平ReentrantLock :   168 毫秒
内部锁:                           639  毫秒

将启动的线程数增加到2000,结果为:
公平ReentrantLock:      1100 毫秒
非公平ReentrantLock:   125 毫秒
内部锁:                           130 毫秒

将启动的线程数增加到3000,结果为:
公平ReentrantLock:      2461 毫秒
非公平ReentrantLock:   254 毫秒
内部锁:                           307 毫秒

启动5000个线程,结果如下:
公平ReentrantLock:      6154  毫秒
非公平ReentrantLock:   623   毫秒
内部锁:                           720 毫秒

非公平ReentrantLock和内部锁的差距,在jdk6上应该缩小了,据说jdk6的内部锁机制进行了调整。
posted @ 2012-04-12 10:31 昊天 阅读(525) | 评论 (1)编辑 收藏
     摘要: 这里演示的是Spring中使用AspectJ注解和XML配置两种方式实现AOP 下面是使用AspectJ注解实现AOP的Java Project 首先是位于classpath下的applicationContext.xml文件   Code highlighting produced by Actipro CodeHighlighter (freeware)http://www....  阅读全文
posted @ 2012-04-11 16:49 昊天 阅读(3097) | 评论 (0)编辑 收藏
访问量上升,数据库压力大,怎么办?好办法是在中间挡一层缓存!这个缓存要求高效,不能比数据库慢,否则服务质量受影响;如果能把数据用hash打散存储到硬盘,也是可以的,不过在内存越来越便宜的今天,还是使用内存吧!

    mysql也有自己的缓存,也是存储在内存的,但是有一个说法是:

以下是引用片段:

只能有一个实例
意味着你能存储内容的上限就是你服务器的可用内存,一台服务器能有多少内存?你又能存多少呢?

只要有写操作,mysql的query cache就失效
只要数据库内容稍有改变,那怕改变的是其他行,mysql的query cache也会失效

    再说,如果mysql都抗不住了,怎么还能指望它提供的缓存呢?

    所以我可以使用memcached了!他的好处和如何用可以参考:

以下是引用片段:

1:《Memcache和mysql交互流程操作原理

2:《让memcached和mysql更好的工作

    开发时面对需求是个麻烦事,更漫长而闹心的是维护,所以我更关心的是memcached运行中的情况。还好的是,memcached的作者给我们提供查看运行情况的命令。主要是“stats”,使用方法为 “telnet ip 端口号”,登录后使用“stats”命令。

    然后你可以看见很多内容,具体可以参考:《memcacche stats

以下是引用片段:

pid = process id
uptime = number of seconds since the process was started
time = current time
version = memcached version
rusage_user = seconds the cpu has devoted to the process as the user
rusage_system = seconds the cpu has devoted to the process as the system
curr_items = total number of items currently in memcache
total_items = total number of items that have passed through the cache
bytes = total number of bytes currently in use by curr_items
curr_connections = total number of open connections to memcached
connection_structures = ???
cmd_get = total GET commands issued to the server
cmd_set = total SET commands issued to the server
get_hits = total number of times a GET command was able to retrieve and
return data
get_misses = total number of times a GET command was unable to retrieve and
return data
bytes_read = total number of bytes input into the server
bytes_written = total number of bytes written by the server
limit_maxbytes = total storage bytes available to the server.

    着重说一下几个对观测很有用的项。

    limit_maxbytes、bytes

    memcached在存储的时候是可以设置失效时间的,但如果存储已经满了,那旧数据即使没有到过期时间,也会被移除。所以需要观察memcached存储是否已经满了,同时这对扩容也是有意义的参考。limit_maxbytes即总的存储大小,而bytes就是已经使用的大小,从这两个数据就可以看出在memcached启动时,我们为它分配的内存是否足够使用。

    cmd_get、cmd_set

    memcached启动后,我们对它一共做了多少次读取操作呢?从这两个参数可以观察出来。

    get_hits、get_misses

    使用memcached后,我们需要评估我们使用的策略是否合理。不能够使用中间缓存后,后端的数据库还是有较大的访问量,这样的话中间缓存就变得没有意义了。get_hits表示命中了多少次读取,即来memcached取到了多少有效数据;get_misses表示没有命中的次数,即此次来取数据的时候,memcached并没有你所查询的数据。如果没有清零统计数据的话,cmd_get = get_hits + get_misses。

    memcached 的状态查询还有其它的命令,可以参考:《Memcached的stats命令

    如下:

stats reset
清空统计数据

stats malloc
显示内存分配数据

stats maps
这个不太确定,看源代码是把/proc/self/maps的数据显示出来。

stats cachedump slab_id limit_num
显示某个slab中的前limit_num个key列表,显示格式如下
ITEM key_name [ value_length b; expire_time|access_time s]
其中,memcached 1.2.2及以前版本显示的是  访问时间(timestamp)
1.2.4以上版本,包括1.2.4显示 过期时间(timestamp)
如果是永不过期的key,expire_time会显示为服务器启动的时间

stats cachedump 7 2
ITEM copy_test1 [250 b; 1207795754 s]
ITEM copy_test [248 b; 1207793649 s]

stats slabs
显示各个slab的信息,包括chunk的大小、数目、使用情况等

stats items
显示各个slab中item的数目和最老item的年龄(最后一次访问距离现在的秒数)

stats detail [on|off|dump]
设置或者显示详细操作记录

参数为on,打开详细操作记录
参数为off,关闭详细操作记录
参数为dump,显示详细操作记录(每一个键值get、set、hit、del的次数)

stats detail dump
PREFIX copy_test2 get 1 hit 1 set 0 del 0
PREFIX copy_test1 get 1 hit 1 set 0 del 0
PREFIX cpy get 1 hit 0 set 0 del 0

memcached数据存储和取回相关的基本命令只有4条。
下面将采用telnet与memcached进行交互,并介绍这4条基本命令。
假设memcached服务器在本机上,并监听在默认端口11211上。

telnet连接到memcached:
telnet 127.0.0.1 11211

SET:添加一个新的条目到memcached,或是用新的数据替换掉已存在的条目

set test1 0 0 10
testing001
STORED

ADD:仅当key不存在的情况下存储数据。如果一个key已经存在,将得到NOT_STORED的响应

add test1 0 0 10
testing002
NOT_STORED
add test2 0 0 10
testing002
STORED

REPLACE:仅当key已经存在的情况下存储数据。如果一个key不存在,将得到NOT_STORED的响应

replace test1 0 0 10
testing003
STORED
replace test3 0 0 10
testing003
NOT_STORED

GET:从memcached中返回数据。从缓存中返回数据时,将在第一行得到key的名字,flag的值和返回的value的长度。真正的数据在第二行,最后返回END。如果key并不存在,那么在第一行就直接返回END。

get test1
VALUE test1 0 10
testing003
END
get test4
END
get test1 test2
VALUE test1 0 10
testing003
END

注:像上面那样你可以在一个请求中包含多个由空格分开的key。当请求多个key时,将只会得到那些有存储数据的key的响应。memcached将不会响应没有存储Data的key。

###################################################################

1、启动Memcache 常用参数
memcached 1.4.3
-p <num>      设置端口号(默认不设置为: 11211)
-U <num>      UDP监听端口 (默认: 11211, 0 时关闭)  
-l <ip_addr>  绑定地址 (默认:所有都允许,无论内外网或者本机更换IP,有安全隐患,若设置为127.0.0.1就只能本机访问)
-d            独立进程运行
-u <username> 绑定使用指定用于运行进程 <username>
-m <num>      允许最大内存用量,单位M (默认: 64 MB)
-P <file>     将PID写入文件<file>,这样可以使得后边进行快速进程终止, 需要与 -d 一起使用
如:
在linux下:./usr/local/bin/memcached -d -u jb-mc -l 192.168.1.197 -m 2048 -p 12121
在window下:d:\App_Serv\memcached\memcached.exe -d RunService -l 127.0.0.1 -p 11211 -m 500
在windows下注册为服务后运行:
sc.exe create jb-Memcached binpath= “d:\App_Serv\memcached\memcached.exe -d RunService -p 11211 -m 500″ start= auto
net start jb-Memcached

2、连接:telnet 127.0.0.1 11211
不要说不会用这个?

3、写入memcache
<command name> <key> <flags> <exptime> <bytes>\r\n <data block>\r\n
a) <command name> 可以是”set”, “add”, “replace”。
“set”表示按照相应的<key>存储该数据,没有的时候增加,有的覆盖。
“add”表示按照相应的<key>添加该数据,但是如果该<key>已经存在则会操作失败。
“replace”表示按照相应的<key>替换数据,但是如果该<key>不存在则操作失败

b) <key> 客户端需要保存数据的key。

c) <flags> 是一个16位的无符号的整数(以十进制的方式表示)。
该标志将和需要存储的数据一起存储,并在客户端get数据时返回。
客户可以将此标志用做特殊用途,此标志对服务器来说是不透明的。

d) <exptime> 过期的时间。
若为0表示存储的数据永远不过时(但可被服务器算法:LRU 等替换)。
如果非0(unix时间或者距离此时的秒数),当过期后,服务器可以保证用户得不到该数据(以服务器时间为标准)。

e) <bytes> 需要存储的字节数(不包含最后的”\r\n”),当用户希望存储空数据时,<bytes>可以为0

f) 最后客户端需要加上”\r\n”作为”命令头”的结束标志。
<data block>\r\n

紧接着”命令头”结束之后就要发送数据块(即希望存储的数据内容),最后加上”\r\n”作为此次通讯的结束。

结果响应:reply
当以上数据发送结束之后,服务器将返回一个应答。可能有如下的情况:

a) “STORED\r\n”:表示存储成功
b) “NOT_STORED\r\n” : 表示存储失败,但是该失败不是由于错误。
通常这是由于”add”或者”replace”命令本身的要求所引起的,或者该项在删除队列之中。

如: set key 33 0 4\r\n
ffff\r\n

4、获取/检查KeyValue
get <key>*\r\n
a) <key>* 表示一个或者多个key(以空格分开)
b) “\r\n” 命令头的结束

结果响应:reply
服务器端将返回0个或者多个的数据项。每个数据项都是由一个文本行和一个数据块组成。当所有的数据项都接收完毕将收到”END\r\n”
每一项的数据结构:
VALUE <key> <flags> <bytes>\r\n
<data block>\r\n

a) <key> 希望得到存储数据的key
b) <falg> 发送set命令时设置的标志项
c) <bytes> 发送数据块的长度(不包含”\r\n”)
d) “\r\n” 文本行的结束标志
e) <data block> 希望接收的数据项。
f) “\r\n” 接收一个数据项的结束标志。

如果有些key出现在get命令行中但是没有返回相应的数据,这意味着服务器中不存在这些项,这些项过时了,或者被删除了
如:get aa
VALUE aa 33 4
ffff
END

5、删除KeyValue:
delete <key> <time>\r\n

a) <key> 需要被删除数据的key
b) <time> 客户端希望服务器将该数据删除的时间(unix时间或者从现在开始的秒数)
c) “\r\n” 命令头的结束

6、检查Memcache服务器状态:
stats\r\n
在这里可以看到memcache的获取次数,当前连接数,写入次数,已经命中率等;

pid : 进程id
uptime :总的运行时间,秒数
time : 当前时间
version : 版本号
……
curr_items : 当前缓存中的KeyValue数量
total_items : 曾经总共经过缓存的KeyValue数量
bytes : 所有的缓存使用的内存量
curr_connections 当前连接数
….
cmd_get : 总获取次数
cmd_set : 总的写入次数
get_hits : 总的命中次数
miss_hits :  获取失败次数
…..
bytes_read : 总共读取的流量字节数
bytes_written : 总的写入流量字节
limit_maxbytes : 最大允许使用的内存量,字节

7、高级缓存细节查看方法:
stats reset
清空统计数据

stats malloc
显示内存分配数据

stats cachedump slab_id limit_num
显示某个slab中的前limit_num个key列表,显示格式如下
ITEM key_name [ value_length b; expire_time|access_time s]
其中,memcached 1.2.2及以前版本显示的是  访问时间(timestamp)
1.2.4以上版本,包括1.2.4显示 过期时间(timestamp)
如果是永不过期的key,expire_time会显示为服务器启动的时间

stats cachedump 7 2
ITEM copy_test1 [250 b; 1207795754 s]
ITEM copy_test [248 b; 1207793649 s]

stats slabs
显示各个slab的信息,包括chunk的大小、数目、使用情况等

stats items
显示各个slab中item的数目和最老item的年龄(最后一次访问距离现在的秒数)

stats detail [on|off|dump]
设置或者显示详细操作记录

参数为on,打开详细操作记录
参数为off,关闭详细操作记录
参数为dump,显示详细操作记录(每一个键值get、set、hit、del的次数)

8、清空所有键值
flush_all
注:flush并不会将items删除,只是将所有的items标记为expired,因此这时memcache依旧占用所有内存。

8、退出
quit\r\n

 

posted @ 2012-04-11 11:03 昊天 阅读(42389) | 评论 (0)编辑 收藏

今天要介绍的是Simple-Spring-Memcached,它封装了对MemCached的调用,使MemCached的客户端开发变得超乎寻常的简单,只要一行代码就行:

@ReadThroughAssignCache(assignedKey = "VETS", expiration = 300, namespace = "NELZ")

是不是很神奇?这行代码指定了MemCached的key,过期时间和命名空间。假设你的MemCached服务器IP是:196.168.10.101,端口是:12000,那么在数据调用的配置文件中只要加上下面配置代码就可以了:

 

1 <import resource="classpath:simplesm-context.xml" />
5 <bean id="memcachedConnectionBean" class="net.nelz.simplesm.config.MemcachedConnectionBean">
7 <property name="consistentHashing" value="true" />
9 <property name="nodeList" value="196.168.10.101:12000" />
5 </bean>

  

从simplesm-context.xml的内容中,可以看出它所封装的类和方法:

 

1 <bean id="memcachedClientFactory" class="net.nelz.simplesm.config.MemcachedClientFactory" >
3 property name="bean" ref="memcachedConnectionBean" />
5 </bean>
9 <bean id="memcachedClient" factory-bean="memcachedClientFactory" factory-method="createMemcachedClient" />
13 <bean id="methodStore" class="net.nelz.simplesm.aop.CacheKeyMethodStoreImpl" />
17 <bean id="net.nelz.simplesm.DefaultKeyProvider" class="net.nelz.simplesm.impl.DefaultKeyProvider">
19 <property name="methodStore" ref="methodStore" />
21 </bean>
25 <bean id="readThroughSingleCache" class="net.nelz.simplesm.aop.ReadThroughSingleCacheAdvice">
27 <property name="cache" ref="memcachedClient" />
29 <property name="methodStore" ref="methodStore" />
31 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
33 </bean>
35 <bean id="readThroughMultiCache" class="net.nelz.simplesm.aop.ReadThroughMultiCacheAdvice">
37 <property name="cache" ref="memcachedClient" />
39 <property name="methodStore" ref="methodStore" />
41 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
43 </bean>
45 <bean id="readThroughAssignCache" class="net.nelz.simplesm.aop.ReadThroughAssignCacheAdvice">
47 <property name="cache" ref="memcachedClient" />
49 <property name="methodStore" ref="methodStore" />
51 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
53 </bean>
54
55 <bean id="updateSingleCache" class="net.nelz.simplesm.aop.UpdateSingleCacheAdvice">
57 <property name="cache" ref="memcachedClient" />
59 <property name="methodStore" ref="methodStore" />
61 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
63 </bean>
64
65 <bean id="updateMultiCache" class="net.nelz.simplesm.aop.UpdateMultiCacheAdvice">
67 <property name="cache" ref="memcachedClient" />
69 <property name="methodStore" ref="methodStore" />
71 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
73 </bean>
75 <bean id="updateAssignCache" class="net.nelz.simplesm.aop.UpdateAssignCacheAdvice">
77 <property name="cache" ref="memcachedClient" />
79 <property name="methodStore" ref="methodStore" />
81 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
83 </bean>
85 <bean id="invalidateSingleCache" class="net.nelz.simplesm.aop.InvalidateSingleCacheAdvice">
87 <property name="cache" ref="memcachedClient" />
89 <property name="methodStore" ref="methodStore" />
91 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
93 </bean>
95 <bean id="invalidateMultiCache" class="net.nelz.simplesm.aop.InvalidateMultiCacheAdvice">
97 <property name="cache" ref="memcachedClient" />
99 <property name="methodStore" ref="methodStore" />
101 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
103 </bean>






105 <bean id="invalidateAssignCache" class="net.nelz.simplesm.aop.InvalidateAssignCacheAdvice">
107 <property name="cache" ref="memcachedClient" />
109 <property name="methodStore" ref="methodStore" />
111 <property name="defaultKeyProvider" ref="net.nelz.simplesm.DefaultKeyProvider" />
113 </bean>

 

Simple-Spring-Memcached还提供了一个例子,在spring的petClinic例子中加入了几行代码,就实现了对MemCached的调用:

 

1 import net.nelz.simplesm.annotations.ReadThroughAssignCache;
2
3 import net.nelz.simplesm.annotations.ReadThroughSingleCache;
4
5 @ReadThroughAssignCache(assignedKey = "VETS", expiration = 300, namespace = "NELZ")
6
7 public Collection<Vet> getVets() {
8
9 System.out.println("\n ! ! !Gonna wait a bit: " + new Date() + "\n");
10
11 try {
12
13 Thread.sleep(4000);
14
15 } catch (Exception ex) {}
16
17 return sessionFactory.getCurrentSession().createQuery("from Vet vet order by vet.lastName, vet.firstName").list();
18
19 }

  

为了加强测试的效果,在第一次读取数据时,故意停顿了一下(sleep)。

posted @ 2012-04-10 17:18 昊天 阅读(1070) | 评论 (0)编辑 收藏

    Memcached是高性能的,分布式的内存对象缓存系统,用于在动态应用中减少数据库负载,提升访问速度。Memcached由Danga Interactive开发,用于提升LiveJournal.com访问速度的。LJ每秒动态页面访问量几千次,用户700万。Memcached将数据库负载大幅度降低,更好的分配资源,更快速访问。

    上网baidu了很多东西,几乎都差不多,而且基于java的说的很少,所有只有在研究了各个其他语言类的应用后再来尝试在java上进行简单的操作应用。先从memcached上进行说明,memcached的最新版是采用c语言进行开发和设计的,据说旧版的是采用perl语言开发的,而且它是一个应用软件来的,是作为缓存服务器的服务器端运行在服务器上的,需要使用特定的语言编写客户端与其进行通信来进行数据的缓存和获取。通常我们是把memcached安装运行在web服务器上,然后通过对需要的数据进行缓存,据我目前所知,所有数据的缓存设置和存取操作,以及数据的更新后替换操作全部需要程序来进行,而不是自动进行的(自动不知道能不能成功,呵呵)。下面从一个实际的例子来应用memcached。

      首先到http://splinedancer.com/memcached-win32/下载windows版本的memcached,然后依次执行下面:
1、Unzip the binaries in your desired directory (eg. c:\memcached) ,
解压
2、Install the service using the command: 'c:\memcached\memcached.exe -d install' from the command line
cmd 通过 c:\memcached\memcached.exe -d install 进行安装 
3、Start the server from the Microsoft Management Console or by running the following command: 'c:\memcached\memcached.exe -d start'
cmd 通过 c:\memcached\memcached.exe -d install 启用服务
4、Use the server, by default listening to port 11211 
默认的端口为11211。
    执行完毕后,我们就可以在任务管理器中见到memcached.exe这个进程了。好了,我们的服务器已经正常运行了, 下面我们就来写java的客户端连接程序。到

    https://github.com/gwhalin/Memcached-Java-Client/downloads下载memcahce客户端。
    然后我们来编写代码,比如我提供的一个应用类如下:

package utils.cache;
import java.util.Date;
import com.meetup.memcached.SockIOPool;
import
com.meetup.memcached.MemcachedClient;

/**
* 使用memcached的缓存实用类.
*
*
@author hao446tian
*
*/
public class MemCached
{
   
// 创建全局的唯一实例
    protected static MemcachedClient mcc = new MemcachedClient();
   
   
protected static MemCached memCached = new MemCached();
   
   
// 设置与缓存服务器的连接池
    static {
       
// 服务器列表和其权重
        String[] servers = {"127.0.0.1:11211"};
        Integer[] weights
= {3};

       
// 获取socke连接池的实例对象
        SockIOPool pool = SockIOPool.getInstance();

       
// 设置服务器信息
        pool.setServers( servers );
        pool.setWeights( weights );

       
// 设置初始连接数、最小和最大连接数以及最大处理时间
        pool.setInitConn( 5 );
        pool.setMinConn(
5 );
        pool.setMaxConn(
250 );
        pool.setMaxIdle(
1000 * 60 * 60 * 6 );

       
// 设置主线程的睡眠时间
        pool.setMaintSleep( 30 );

       
// 设置TCP的参数,连接超时等
        pool.setNagle( false );
        pool.setSocketTO(
3000 );
        pool.setSocketConnectTO(
0 );

       
// 初始化连接池
        pool.initialize();

       
// 压缩设置,超过指定大小(单位为K)的数据都会被压缩
        mcc.setCompressEnable( true );
        mcc.setCompressThreshold(
64 * 1024 );
    }
   
   
/**
     * 保护型构造方法,不允许实例化!
     *
    
*/
   
protected MemCached()
    {
       
    }
   
   
/**
     * 获取唯一实例.
     *
@return
    
*/
   
public static MemCached getInstance()
    {
       
return memCached;
    }
   
   
/**
     * 添加一个指定的值到缓存中.
     *
@param key
     *
@param value
     *
@return
    
*/
   
public boolean add(String key, Object value)
    {
       
return mcc.add(key, value);
    }
   
   
public boolean add(String key, Object value, Date expiry)
    {
       
return mcc.add(key, value, expiry);
    }
   
   
public boolean replace(String key, Object value)
    {
       
return mcc.replace(key, value);
    }
   
   
public boolean replace(String key, Object value, Date expiry)
    {
       
return mcc.replace(key, value, expiry);
    }
   
   
/**
     * 根据指定的关键字获取对象.
     *
@param key
     *
@return
    
*/
   
public Object get(String key)
    {
       
return mcc.get(key);
    }
   
   
public static void main(String[] args)
    {
        MemCached cache
= MemCached.getInstance();
        cache.add(
"hello", 234);
        System.out.print(
"get value : " + cache.get("hello"));

    }
}

    那么我们就可以通过简单的像main方法中操作的一样存入一个变量,然后再取出进行查看,我们可以看到先调用了add,然后再进行get,我们运行一次后,234这个值已经被我们存入了memcached的缓存中的了,我们将main方法中红色的那一行注释掉后,我们再运行还是可以看到get到的value也是234,即缓存中我们已经存在了数据了。

    对基本的数据我们可以操作,对于普通的POJO而言,如果要进行存储的话,那么比如让其实现java.io.Serializable接口,因为memcached是一个分布式的缓存服务器,多台服务器间进行数据共享需要将对象序列化的,所以必须实现该接口,否则会报错的。比如我们写一个简单的测试Bean如下:

class TBean implements java.io.Serializable
{
   
private static final long serialVersionUID = 1945562032261336919L;
   
   
private String name;

   
public String getName()
    {
       
return name;
    }

   
public void setName(String name)
    {
       
this.name = name;
    }
}

    然后我们在main方法中加入如下几行代码:

TBean tb = new TBean();
tb.setName(
"铁木箱子");
cache.add(
"bean", tb);
TBean tb1
= (TBean)cache.get("bean");
System.out.print(
"name=" + tb1.getName());
tb1.setName(
"铁木箱子_修改的");
tb1
= (TBean)cache.get("bean");
System.out.print(
"name=" + tb1.getName());

    我们首先把TBean的一个实例放入缓存中,然后再取出来,并进行名称的修改,然后我们再取这个对象,我们再看其名称,发现修改的对象并不是缓存中的对象,而是通过序列化过来的一个实例对象,这样我们就无须担心对原生类的无意修改导致缓存数据失效了,呵呵~~看来我也是多此一想啊。所以这表明从缓存中获取的对象是存入对象的一个副本,对获取对象的修改并不能真正的修改缓存中的数据,而应该使用其提供的replace等方法来进行修改。

   

查看缓存区块,分析结果可调配-f参数
telnet 127.0.0.1 11212

查看缓存的各种状态

stats
====================================
查看缓存所分配的slabs

stats slabs

====================================
清除统计数据

stats reset====================================
是否显示详细操作记录(每一个键值get、set、hit、del的次数)
stats detail on,记录详细操作

stats detail dump,不记录详细操作
====================================
显示各个slab中item的数目和最老item的年龄(最后一次访问距离现在的秒数)
stats items
====================================
显示某个slab中的前limit_num个key列表,显示格式如下
stats cachedump slab_id 显示条数


参考资料
一般来说一个memcahced进程会预先将自己划分为若干个slab,每个slab下又有若干个page,每个page下又有多个chunk,如果我们把这3个咚咚看作是object得话,这是两个一对多得关系。再一般来说,slab得数量是有限得,几个,十几个,或者几十个,这个跟进程配置得内存有关。而每个slab下得page默认情况是1m,也就是说如果一个slab占用100m得内存得话,那么默认情况下这个slab所拥有得page得个数就是100,而chunk就是我们得数据存放得最终地方。chunk_size表示数据存放块得大小,chunks_per_page表示一个内存页page中拥有得chunk得数量,total_pages表示每个slab下page得个数。total_chunks表示这个slab下chunk得总数(=total_pages * chunks_per_page),used_chunks表示该slab下已经使用得chunk得数量,free_chunks表示该slab下还可以使用得chunks数量。

 

posted @ 2012-04-10 16:57 昊天 阅读(1041) | 评论 (0)编辑 收藏

企业中各项目中相互协作的时候可能用得到消息通知机制。比如有东西更新了,可以通知做索引。

在 Java 里有 JMS 的多个实现。其中 apache 下的 ActiveMQ 就是不错的选择。还有一个比较热的是 RabbitMQ (是 erlang 语言实现的)。这里示例下使用 ActiveMQ

用 ActiveMQ 最好还是了解下 JMS

JMS 公共 点对点域 发布/订阅域
ConnectionFactory QueueConnectionFactory TopicConnectionFactory
Connection QueueConnection TopicConnection
Destination Queue Topic
Session QueueSession TopicSession
MessageProducer QueueSender TopicPublisher
MessageConsumer QueueReceiver TopicSubscriber

JMS 定义了两种方式:Quere(点对点);Topic(发布/订阅)。

ConnectionFactory 是连接工厂,负责创建Connection。

Connection 负责创建 Session。

Session 创建 MessageProducer(用来发消息) 和 MessageConsumer(用来接收消息)。

Destination 是消息的目的地。

详细的可以网上找些 JMS 规范(有中文版)。

下载 apache-activemq-5.3.0。http://activemq.apache.org/download.html,解压,然后双击 bin/activemq.bat。运行后,可以在 http://localhost:8161/admin 观察。也有 demo, http://localhost:8161/demo。把 activemq-all-5.3.0.jar 加入 classpath。

Jms 发送 代码:

  1. public static void main(String[] args) throws Exception {   
  2.     ConnectionFactory connectionFactory = new ActiveMQConnectionFactory();   
  3.   
  4.     Connection connection = connectionFactory.createConnection();   
  5.     connection.start();   
  6.   
  7.     Session session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);   
  8.     Destination destination = session.createQueue("my-queue");   
  9.   
  10.     MessageProducer producer = session.createProducer(destination);   
  11.     for(int i=0; i<3; i++) {   
  12.         MapMessage message = session.createMapMessage();   
  13.         message.setLong("count"new Date().getTime());   
  14.         Thread.sleep(1000);   
  15.         //通过消息生产者发出消息   
  16.         producer.send(message);   
  17.     }   
  18.     session.commit();   
  19.     session.close();   
  20.     connection.close();   
  21. }  

Jms 接收代码:

  1. public static void main(String[] args) throws Exception {   
  2.     ConnectionFactory connectionFactory = new ActiveMQConnectionFactory();   
  3.   
  4.     Connection connection = connectionFactory.createConnection();   
  5.     connection.start();   
  6.   
  7.     final Session session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);   
  8.     Destination destination = session.createQueue("my-queue");   
  9.   
  10.     MessageConsumer consumer = session.createConsumer(destination);   
  11.     /*//listener 方式  
  12.     consumer.setMessageListener(new MessageListener() {  
  13.  
  14.         public void onMessage(Message msg) {  
  15.             MapMessage message = (MapMessage) msg;  
  16.             //TODO something....  
  17.             System.out.println("收到消息:" + new Date(message.getLong("count")));  
  18.             session.commit();  
  19.         }  
  20.  
  21.     });  
  22.     Thread.sleep(30000);  
  23.     */  
  24.     int i=0;   
  25.     while(i<3) {   
  26.         i++;   
  27.         MapMessage message = (MapMessage) consumer.receive();   
  28.         session.commit();   
  29.   
  30.         //TODO something....   
  31.         System.out.println("收到消息:" + new Date(message.getLong("count")));   
  32.     }   
  33.   
  34.     session.close();   
  35.     connection.close();   
  36. }  

启动 JmsReceiver 和 JmsSender 可以在看输出三条时间信息。当然 Jms 还指定有其它格式的数据,如 TextMessage

结合 Spring 的 JmsTemplate 方便用:

xml:

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  3.         xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">  
  4.   
  5. <!-- 在非 web / ejb 容器中使用 pool 时,要手动 stop,spring 不会为你执行 destroy-method 的方法   
  6.     <bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">  
  7.         <property name="connectionFactory">  
  8.             <bean class="org.apache.activemq.ActiveMQConnectionFactory">  
  9.                 <property name="brokerURL" value="tcp://localhost:61616" />  
  10.             </bean>  
  11.         </property>  
  12.     </bean>  
  13. -->  
  14.     <bean id="jmsFactory" class="org.apache.activemq.ActiveMQConnectionFactory">  
  15.         <property name="brokerURL" value="tcp://localhost:61616" />  
  16.     </bean>  
  17.     <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">  
  18.         <property name="connectionFactory" ref="jmsFactory" />  
  19.         <property name="defaultDestination" ref="destination" />  
  20.         <property name="messageConverter">  
  21.             <bean class="org.springframework.jms.support.converter.SimpleMessageConverter" />  
  22.         </property>  
  23.     </bean>  
  24.   
  25.     <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">  
  26.         <constructor-arg index="0" value="my-queue" />  
  27.     </bean>  
  28.   
  29. </beans>  

sender:

  1. public static void main(String[] args) {   
  2.     ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:app*.xml");   
  3.   
  4.     JmsTemplate jmsTemplate = (JmsTemplate) ctx.getBean("jmsTemplate");   
  5.   
  6.     jmsTemplate.send(new MessageCreator() {   
  7.   
  8.         public Message createMessage(Session session) throws JMSException {   
  9.             MapMessage mm = session.createMapMessage();   
  10.             mm.setLong("count"new Date().getTime());   
  11.             return mm;   
  12.         }   
  13.   
  14.     });   
  15. }  

receiver:

  1. public static void main(String[] args) {   
  2.     ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:app*.xml");   
  3.   
  4.     JmsTemplate jmsTemplate = (JmsTemplate) ctx.getBean("jmsTemplate");   
  5.     while(true) {   
  6.         Map<String, Object> mm =  (Map<String, Object>) jmsTemplate.receiveAndConvert();   
  7.         System.out.println("收到消息:" + new Date((Long)mm.get("count")));   
  8.     }   
  9. }  

注意:直接用 Jms 接口时接收了消息后要提交一下,否则下次启动接收者时还可以收到旧数据。有了 JmsTemplate 就不用自己提交 session.commit() 了。如果使用了 PooledConnectionFactory 要把 apache-activemq-5.3.0\lib\optional\activemq-pool-5.3.0.jar 加到 classpath

posted @ 2012-03-29 15:43 昊天 阅读(1327) | 评论 (0)编辑 收藏
JVM在加载类的时候,都是通过ClassLoader的loadClass()方法来加载class的,loadClass(String name)方法:
Java代码  
  1. public Class<?> loadClass(String name) throws ClassNotFoundException {   
  2. return loadClass(name, false);   
  3.    }  

loadClass(String name)方法再调用loadClass(String name, boolean resolve)方法:
     - name - 类的二进制名称
     - resolve - 如果该参数为 true,则分析这个类
Java代码  
  1. protected synchronized Class<?> loadClass(String name, boolean resolve)   
  2.     throws ClassNotFoundException   
  3.     {   
  4.     // First, check if the class has already been loaded   
  5.     //JVM 规范规定ClassLoader可以在缓存保留它所加载的Class,如果一个Class已经被加载过,则直接从缓存中获取   
  6.     Class c = findLoadedClass(name);   
  7.     if (c == null) {   
  8.         try {   
  9.         if (parent != null) {   
  10.             c = parent.loadClass(name, false);   
  11.         } else {   
  12.             c = findBootstrapClass0(name);   
  13.         }   
  14.         } catch (ClassNotFoundException e) {   
  15.             // If still not found, then invoke findClass in order   
  16.             // to find the class.   
  17.             c = findClass(name);   
  18.         }   
  19.     }   
  20.     if (resolve) {   
  21.         resolveClass(c);   
  22.     }   
  23.     return c;   
  24. }  

如果ClassLoader并没有加载这个class,则调用findBootstrapClass0:
Java代码  
  1. private Class findBootstrapClass0(String name)   
  2.     throws ClassNotFoundException   
  3.     {   
  4.     check();   
  5.     if (!checkName(name))   
  6.         throw new ClassNotFoundException(name);   
  7.     return findBootstrapClass(name);   
  8.     }  

该方法会调用check()方法来判断这个类是否已经初始化,并且通过checkName(name)来判断由name指定的这个类是否存在
最后调用findBootstrapClass(name):
Java代码  
  1. private native Class findBootstrapClass(String name)   
  2.     throws ClassNotFoundException;  

而这个findBootstrapClass方法是一个native方法,这是我们的root loader,这个载入方法并非是由JAVA所写,而是C++写的,它会最终调用JVM中的原生findBootstrapClass方法来完成类的加载。

如果上面两个都找不到,则使用findClass(name)来查找指定类名的Class:
Java代码  
  1. protected Class<?> findClass(String name) throws ClassNotFoundException {   
  2.     throw new ClassNotFoundException(name);   
  3. }  

JDK5.0中的说明:
使用指定的二进制名称查找类。此方法应该被类加载器的实现重写,该实现按照委托模型来加载类。在通过父类加载器检查所请求的类后,此方法将被 loadClass 方法调用。默认实现抛出一个 ClassNotFoundException。
所以,我们在自定义类中,只需要重写findClass()即可。
MyClassLoader类:
Java代码  
  1. public class MyClassLoader extends ClassLoader {   
  2.     private String fileName;   
  3.   
  4.     public MyClassLoader(String fileName) {   
  5.         this.fileName = fileName;   
  6.     }   
  7.   
  8.     protected Class<?> findClass(String className) throws ClassNotFoundException {   
  9.         Class clazz = this.findLoadedClass(className);   
  10.         if (null == clazz) {   
  11.             try {   
  12.                 String classFile = getClassFile(className);   
  13.                 FileInputStream fis = new FileInputStream(classFile);   
  14.                 FileChannel fileC = fis.getChannel();   
  15.                 ByteArrayOutputStream baos = new  
  16.                         ByteArrayOutputStream();   
  17.                 WritableByteChannel outC = Channels.newChannel(baos);   
  18.                 ByteBuffer buffer = ByteBuffer.allocateDirect(1024);   
  19.                 while (true) {   
  20.                     int i = fileC.read(buffer);   
  21.                     if (i == 0 || i == -1) {   
  22.                         break;   
  23.                     }   
  24.                     buffer.flip();   
  25.                     outC.write(buffer);   
  26.                     buffer.clear();   
  27.                 }   
  28.                 fis.close();   
  29.                 byte[] bytes = baos.toByteArray();   
  30.   
  31.                 clazz = defineClass(className, bytes, 0, bytes.length);   
  32.             } catch (FileNotFoundException e) {   
  33.                 e.printStackTrace();   
  34.             } catch (IOException e) {   
  35.                 e.printStackTrace();   
  36.             }   
  37.         }   
  38.         return clazz;   
  39.     }   
  40.     private byte[] loadClassBytes(String className) throws  
  41.             ClassNotFoundException {   
  42.         try {   
  43.             String classFile = getClassFile(className);   
  44.             FileInputStream fis = new FileInputStream(classFile);   
  45.             FileChannel fileC = fis.getChannel();   
  46.             ByteArrayOutputStream baos = new  
  47.                     ByteArrayOutputStream();   
  48.             WritableByteChannel outC = Channels.newChannel(baos);   
  49.             ByteBuffer buffer = ByteBuffer.allocateDirect(1024);   
  50.             while (true) {   
  51.                 int i = fileC.read(buffer);   
  52.                 if (i == 0 || i == -1) {   
  53.                     break;   
  54.                 }   
  55.                 buffer.flip();   
  56.                 outC.write(buffer);   
  57.                 buffer.clear();   
  58.             }   
  59.             fis.close();   
  60.             return baos.toByteArray();   
  61.         } catch (IOException fnfe) {   
  62.             throw new ClassNotFoundException(className);   
  63.         }   
  64.     }   
  65.     private String getClassFile(String name) {   
  66.         StringBuffer sb = new StringBuffer(fileName);   
  67.         name = name.replace('.', File.separatorChar) + ".class";   
  68.         sb.append(File.separator + name);   
  69.         return sb.toString();   
  70.     }   
  71. }  

该类中通过调用defineClass(String name, byte[] b, int off, int len)方法来定义一个类:
Java代码  
  1. protected final Class<?> defineClass(String name, byte[] b, int off, int len)   
  2. throws ClassFormatError   
  3.    {   
  4. return defineClass(name, b, off, len, null);   
  5.    }  

注:MyClassLoader加载类时有一个局限,必需指定.class文件,而不能指定.jar文件。MainClassLoader类:
Java代码  
  1. public class MainClassLoader {   
  2.     public static void main(String[] args) {   
  3.         try {   
  4.             MyClassLoader tc = new MyClassLoader("F:\\OpenLib\\");   
  5.             Class c = tc.findClass("Test");   
  6.             c.newInstance();   
  7.         } catch (ClassNotFoundException e) {   
  8.             e.printStackTrace();    
  9.         } catch (IllegalAccessException e) {   
  10.             e.printStackTrace();   
  11.         } catch (InstantiationException e) {   
  12.             e.printStackTrace();    
  13.         }   
  14.     }   
  15. }  

最后是一个简单的Test测试类:
Java代码  
  1. public class Test   
  2. {   
  3.     public Test() {   
  4.         System.out.println("Test");   
  5.     }   
  6.     public static void main(String[] args) {   
  7.         System.out.println("Hello World");   
  8.     }   
  9. }  
posted @ 2012-02-16 09:39 昊天 阅读(2157) | 评论 (0)编辑 收藏
最近使用Mina开发一个Java的NIO服务端程序,因此也特意学习了Apache的这个Mina框架。

  首先,Mina是个什么东西?看下官方网站(http://mina.apache.org/)对它的解释:

  Apache的Mina(Multipurpose Infrastructure Networked Applications)是一个网络应用框架,可以帮助用户开发高性能和高扩展性的网络应用程序;它提供了一个抽象的、事件驱动的异步API,使Java NIO在各种传输协议(如TCP/IP,UDP/IP协议等)下快速高效开发。

  Apache Mina也称为:

  • NIO框架
  • 客户端/服务端框架(典型的C/S架构)
  • 网络套接字(networking socket)类库
  • 事件驱动的异步API(注意:在JDK7中也新增了异步API)

总之:我们简单理解它是一个封装底层IO操作,提供高级操作API的通讯框架!

 

在Mina的官网、以及网上都有比较丰富的文档了,这里我就稍微简单说一下Mina的结构和示例代码。

因为Mina2.X改进了Mina的代码结构和包结构,降低了使用的复杂性和增强了健壮性,所以使得API发生了比较大的改变,有许多地方已经和Mina1.x不兼容了。

这里使用的是Mina2.0.4

1.Mina的结构

 

Mina的通信流程大致如上图所示,各个组件功能有:
(1.)  IoService:这个接口在一个线程上负责套接字的建立,拥有自己的Selector,监
听是否有连接被建立。

(Mina底层使用JAVA NIO, 因此它是典型的使用Reactor模式架构的,采用事件驱动编程 , Mina运行用户自定义线程模型,可以是单线程、多线程、线程池等 ,

   跟JAVA Socket不一样, Mina是非阻塞的Socket,它内部已经保证了对各个连接(session)的业务和数据的隔离,采用轮询机制为各个session分配CPU资源,

  所以,你就不需要再去考虑不同Socket连接需要用不同的线程去操纵的问题了。)

(2.)  IoProcessor:这个接口在另一个线程上负责检查是否有数据在通道上读写,也就是
说它也拥有自己的Selector,这是与我们使用JAVA NIO 编码时的一个不同之处,
通常在JAVA NIO 编码中,我们都是使用一个Selector,也就是不区分IoService
与 IoProcessor 两个功能接口。另外,IoProcessor 负责调用注册在IoService 上
的过滤器,并在过滤器链之后调用IoHandler。


(3.)  IoFilter:这个接口定义一组拦截器,这些拦截器可以包括日志输出、黑名单过滤、
数据的编码(write 方向)与解码(read 方向)等功能,其中数据的encode 与 decode
是最为重要的、也是你在使用Mina 时最主要关注的地方。


(4.)  IoHandler:这个接口负责编写业务逻辑,也就是接收、发送数据的地方。
 

2. Mina编程的大致过程.

    2.1 总体流程

        建立服务器端的资源: 包括 Acceptor的建立,之后为Acceptor配置相应的Filter(可以是Mina自带的Filter或者自定义的Filter),

之后再配置相应基于事件驱动的处理业务逻辑的IoHandler.

       建立客户端的资源: Mina采用了统一的编程模型,所以建立客户端的过程和建立服务器端的过程大致上是相似的,不过这里建立的是Connector.

   2.2 示例程序。(使用jar包为 mina-core-2.0.4.jar)

      下面通过一个简单的示例程序来进一步理解Mina的运行机制。

     该程序实现简单的即时通讯功能。 即,多个客户端可以同时脸上服务器,并进行类似于聊天室一样的通信。

         

       2.2.1 建立自定义的TextLineCodecFacotry

             为了了解Mina的代码功能以及运行机制,我们模拟实现了类似Mina自带TextLineCodecFactory。

    该CodecFactory功能是: 配合ProtocolCodecFilter 进行对底层数据(binary二进制数据流)和高层数据(特定类型的数据对象信息,例如String)之间的转换。

                这里实现了一个断行读取功能,即遇到'\n'的时候,就认为是一个String Line , 将这段数据流封装成String,之后再交给下一个Filter或者Handler处理。

                CodecFactory是一个工厂方法,底层通过一个Decoder和Encoder来提供对数据进行解、编码的操作,载体是IoBuffer

                (解码:将二进制数据转换成高层数据(对象)    编码:将高层数据(对象)转换成二进制数据流)  )

     1)建立Decoder (MyTextLineDecoder)

      实现了ProtocolDecoder接口

package com.mai.mina.diyCodecFilter;

import java.nio.charset.Charset;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

public class MyTextLineDecoder implements ProtocolDecoder{

Charset charset = Charset.forName("UTF-8");
IoBuffer buf = IoBuffer.allocate(100).setAutoExpand(true);

@Override
public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput output)
throws Exception {
// TODO Auto-generated method stub
while(in.hasRemaining()){
byte b = in.get();
if(b == '\n'){
buf.flip();
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);
String message = new String(bytes,charset);
buf = IoBuffer.allocate(100).setAutoExpand(true);

output.write(message);
}else{
buf.put(b);
}
}
}

@Override
public void dispose(IoSession arg0) throws Exception {
// TODO Auto-generated method stub

}

@Override
public void finishDecode(IoSession arg0, ProtocolDecoderOutput arg1)
throws Exception {
// TODO Auto-generated method stub

}

}

    2)建立Encoder (MyTextLineEncoder)

      实现了ProtocolEncoder接口

package com.mai.mina.diyCodecFilter;

import java.nio.charset.Charset;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoder;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.textline.LineDelimiter;

public class MyTextLineEncoder implements ProtocolEncoder{

Charset charset = Charset.forName("UTF-8");

@Override
public void dispose(IoSession session) throws Exception {
// TODO Auto-generated method stub

}

@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput output)
throws Exception {
// TODO Auto-generated method stub
IoBuffer buf = IoBuffer.allocate(100).setAutoExpand(true);

buf.putString(message.toString(), charset.newEncoder());

buf.putString(LineDelimiter.DEFAULT.getValue(), charset.newEncoder());
buf.flip();

output.write(buf);

}

}

    3)建立MyTextLineCodecFactory

      实现了ProtocolCodecFactory接口

package com.mai.mina.diyCodecFilter;

import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;

public class MyTextLineCodecFactory implements ProtocolCodecFactory{

@Override
public ProtocolDecoder getDecoder(IoSession arg0) throws Exception {
// TODO Auto-generated method stub
return new MyTextLineDecoder();
}

@Override
public ProtocolEncoder getEncoder(IoSession arg0) throws Exception {
// TODO Auto-generated method stub
return new MyTextLineEncoder();
}

}


  2.2.2 建立服务器端资源(包括Acceptor的配置、Handler建立)

    1). 建立自定义IoHandler(MyServerHandleDemo1)

      实现了IoHandler接口。

 

package com.mai.mina.diyChat;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.logging.Logger;

import org.apache.mina.core.future.CloseFuture;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;

public class MyServerHandleDemo1 implements IoHandler{

private Logger logger = Logger.getLogger(this.getClass().getName());

@Override
public void exceptionCaught(IoSession session, Throwable arg1)
throws Exception {
// TODO Auto-generated method stub
logger.warning("服务器启动发生异常,have a exception : " + arg1.getMessage());
}

@Override
public void messageReceived(IoSession session, Object message) throws Exception {
// TODO Auto-generated method stub
String messageStr = message.toString();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd H:m:s");
String dateStr = format.format(new Date());

logger.info(messageStr + "\t" + dateStr);

Collection<IoSession> sessions = session.getService().getManagedSessions().values();
for(IoSession tempSession : sessions){
tempSession.write(messageStr + "\t" + dateStr);
}
}

@Override
public void messageSent(IoSession session, Object message) throws Exception {
// TODO Auto-generated method stub
logger.info("服务器成功发送信息: " + message.toString());
}

@Override
public void sessionClosed(IoSession session) throws Exception {
// TODO Auto-generated method stub
logger.info("there is a session closed");
CloseFuture future = session.close(true);
future.addListener(new IoFutureListener(){
public void operationComplete(IoFuture future){
if(future instanceof CloseFuture){
((CloseFuture)future).setClosed();
logger.info("have do the future set to closed");
}
}
});
}

@Override
public void sessionCreated(IoSession session) throws Exception {
// TODO Auto-generated method stub
logger.info("there is a session created");
session.write("welcome to the chat room");
}

@Override
public void sessionIdle(IoSession session, IdleStatus arg1) throws Exception {
// TODO Auto-generated method stub
logger.info(session.getId() + "(SesssionID) is idle in the satate-->" + arg1);
}

@Override
public void sessionOpened(IoSession arg0) throws Exception {
// TODO Auto-generated method stub

}


} 
    2).建立Acceptor ,同时也充当Server端的启动类 (SimpleMinaServer)
package com.mai.mina.diyChat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LogLevel;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class SimpleMinaServer {

SocketAcceptor acceptor = null;

SimpleMinaServer(){
acceptor = new NioSocketAcceptor();
}

public boolean bind(){
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(
new MyTextLineCodecFactory()); //配置CodecFactory
LoggingFilter log = new LoggingFilter();
log.setMessageReceivedLogLevel(LogLevel.INFO);
acceptor.getFilterChain().addLast("logger", log);
acceptor.setHandler(new MyServerHandleDemo1());  //配置handler
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 30);

try {
acceptor.bind(new InetSocketAddress(8888));
return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}

}

public static void main(String args[]){
SimpleMinaServer server = new SimpleMinaServer();
if(!server.bind()){
System.out.println("服务器启动失败");
}else{
System.out.println("服务器启动成功");
}
}

}

  2.2.3 建立Client端资源:

    1)自定义IoHandler(MyClientHandleDemo1)

      实现IoHandler接口

package com.mai.mina.diyChat;

import java.util.logging.Logger;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class MyClientHandleDemo1 extends IoHandlerAdapter{
private ChatPanel messagePanel = null;
private Logger logger = Logger.getLogger(this.getClass().getName());

MyClientHandleDemo1(){

}

MyClientHandleDemo1(ChatPanel messagePanel){
this.messagePanel = messagePanel;
}


public void messageReceived(IoSession session, Object message) throws Exception {
// TODO Auto-generated method stub
String messageStr = message.toString();
logger.info("receive a message is : " + messageStr);

if(messagePanel != null)
messagePanel.showMsg(messageStr);
}

public void messageSent(IoSession session , Object message) throws Exception{
logger.info("客户端发了一个信息:" + message.toString());
}

}

    2) 建立Connector  (SimpleMinaClient)

 

View Code
package com.mai.mina.diyChat;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.future.CloseFuture;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LogLevel;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.SocketConnector;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

public class SimpleMinaClient {

public SocketConnector connector = null;
public ConnectFuture future;
public IoSession session = null;
private ChatPanel messagePanel = null;

SimpleMinaClient(){

}

SimpleMinaClient(ChatPanel messagePanel){
this.messagePanel = messagePanel;
}


boolean connect(){
try{
connector = new NioSocketConnector();
connector.setConnectTimeoutMillis(3000);
connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(
new MyTextLineCodecFactory());
LoggingFilter log = new LoggingFilter();
log.setMessageReceivedLogLevel(LogLevel.INFO);
connector.getFilterChain().addLast("logger", log);
connector.setHandler(new MyClientHandleDemo1(messagePanel));

future = connector.connect(new InetSocketAddress("127.0.0.1" , 8888));
future.awaitUninterruptibly();
session = future.getSession();
return true;
}catch(Exception e){
e.printStackTrace();
return false;
}

}


public void setAttribute(Object key , Object value){
session.setAttribute(key, value);
}


void sentMsg(String message){
session.write(message);
}


boolean close(){
CloseFuture future = session.getCloseFuture();
future.awaitUninterruptibly(1000);
connector.dispose();
return true;
}

public SocketConnector getConnector() {
return connector;
}

public IoSession getSession() {
return session;
}

/**
*
@param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
SimpleMinaClient client = new SimpleMinaClient();
if(client.connect()){
client.sentMsg("hello , sever !");
client.close();
}

}

}

  到这里,基本的Mina通信基础就建立好了。

  接下来实现一个客户端的GUI界面,方便实际功能的建立和信息交互的演示。

 

  2.2.4 Client Gui界面的建立。(ChatPanel -通过使用SimpleMinaClient来提供实际通信功能)

View Code
package com.mai.mina.diyChat;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

import org.apache.commons.lang.math.RandomUtils;

public class ChatPanel extends javax.swing.JPanel {
private JPanel northPanel;
private JLabel headLabel;
private JScrollPane jScrollPane1;
private JScrollPane jScrollPane2;
private JButton exitB;
private JButton clearMsgB;
private JButton sentB;
private JButton connectB;
private JTextArea messageText;
private JTextField nameText;
private JLabel nameLabel;
private JTextArea messageArea;
private JPanel southPanel;
private SimpleMinaClient client = null;
private boolean connected = false;
private String username = null;

{
//Set Look & Feel
try {
javax.swing.UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch(Exception e) {
e.printStackTrace();
}
}

public void connect(){
if(client.connect()){
username = nameText.getText().trim();
if(username == null || "".equals(username)){
username = "游客" + RandomUtils.nextInt(1000);
nameText.setText(username);
}
connected = true;
dealUIWithFlag();
}else{
connected = false;
dealUIWithFlag();
showMsg("连接服务器失败。。。");
}
}


public void showMsg(String msg){
messageArea.append(msg);
messageArea.append("\n");
messageArea.selectAll();
messageArea.lostFocus(null, this);
}


public void sentMsg(){
String message = username + ":" + messageText.getText();
client.sentMsg(message);
messageText.setText("");
messageText.requestFocus();
}

public void dealUIWithFlag(){
if(connected){
nameText.setEnabled(false);
connectB.setEnabled(false);
sentB.setEnabled(true);
clearMsgB.setEnabled(true);
exitB.setEnabled(true);
}else{
nameText.setEnabled(true);
connectB.setEnabled(true);
sentB.setEnabled(false);
clearMsgB.setEnabled(false);
exitB.setEnabled(false);
}
}


public void closeTheClient(){
if(client.close()){
showMsg("连接已断开...");
connected = false;
dealUIWithFlag();
}else{
showMsg("无法断开连接...");
}
}



public static void main(String[] args) {
JFrame frame = new JFrame();
frame.getContentPane().add(new ChatPanel());
frame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}

public ChatPanel() {
super();
client = new SimpleMinaClient(this);
initGUI();
dealUIWithFlag();
}

private void initGUI() {
try {
this.setPreferredSize(new java.awt.Dimension(400, 339));
this.setLayout(null);
{
northPanel = new JPanel();
BorderLayout northPanelLayout = new BorderLayout();
northPanel.setLayout(northPanelLayout);
this.add(northPanel);
northPanel.setBounds(0, 0, 400, 188);
{
headLabel = new JLabel();
northPanel.add(headLabel, BorderLayout.NORTH);
headLabel.setText("\u6b22\u8fce\u4f7f\u7528 (\u6d4b\u8bd5Ip:port --> 127.0.0.1:8888)");
headLabel.setPreferredSize(new java.awt.Dimension(397, 19));
}
{
jScrollPane1 = new JScrollPane();
northPanel.add(jScrollPane1, BorderLayout.CENTER);
jScrollPane1.setPreferredSize(new java.awt.Dimension(400, 169));
{
messageArea = new JTextArea();
jScrollPane1.setViewportView(messageArea);
messageArea.setPreferredSize(new java.awt.Dimension(398, 145));
messageArea.setEditable(false);
messageArea.setLineWrap(true);
messageArea.setWrapStyleWord(true);
}
}
}
{
southPanel = new JPanel();
this.add(southPanel);
southPanel.setBounds(0, 194, 400, 145);
southPanel.setLayout(null);
{
nameLabel = new JLabel();
southPanel.add(nameLabel);
nameLabel.setText("\u6635\u79f0:");
nameLabel.setBounds(10, 12, 35, 15);
}
{
nameText = new JTextField();
southPanel.add(nameText);
nameText.setText("\u6e38\u5ba2");
nameText.setBounds(45, 9, 96, 21);
}
{
jScrollPane2 = new JScrollPane();
southPanel.add(jScrollPane2);
jScrollPane2.setBounds(15, 37, 364, 69);
{
messageText = new JTextArea();
jScrollPane2.setViewportView(messageText);
messageText.setBounds(101, 72, 362, 75);
messageText.setPreferredSize(new java.awt.Dimension(362, 54));
messageText.setLineWrap(true);
messageText.setWrapStyleWord(true);
}
}
{
connectB = new JButton();
southPanel.add(connectB);
connectB.setText("\u8fde\u63a5\u670d\u52a1\u5668");
connectB.setBounds(179, 8, 93, 23);
connectB.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
System.out.println("connectB.actionPerformed, event="+evt);
//TODO add your code for connectB.actionPerformed
connect();
}
});
}
{
sentB = new JButton();
southPanel.add(sentB);
sentB.setText("\u53d1\u9001");
sentB.setBounds(261, 116, 57, 23);
sentB.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
System.out.println("sentB.actionPerformed, event="+evt);
//TODO add your code for sentB.actionPerformed
sentMsg();
}
});
}
{
clearMsgB = new JButton();
southPanel.add(clearMsgB);
clearMsgB.setText("\u6e05\u7a7a");
clearMsgB.setBounds(324, 116, 57, 23);
clearMsgB.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
System.out.println("clearMsgB.actionPerformed, event="+evt);
//TODO add your code for clearMsgB.actionPerformed
messageText.setText("");
}
});
}
{
exitB = new JButton();
southPanel.add(exitB);
exitB.setText("\u6ce8\u9500");
exitB.setBounds(282, 8, 57, 23);
exitB.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
System.out.println("exitB.actionPerformed, event="+evt);
//TODO add your code for exitB.actionPerformed
closeTheClient();
}
});
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

}


3. 运行结果

  首先启动服务器端,即运行SimpleMinaServer类 , 启动成功时会在控制台中打印出“服务器启动成功"

  接下来运行客户端ChatPanel。



note: 上面只是一个简单的信息交互,其实使用Mina比较常用的还是在自定义协议处理这块。

    所以,比较应该注重学习的是Filter这块。有时间大家可以去看看源码。

posted @ 2012-02-14 18:42 昊天 阅读(1343) | 评论 (0)编辑 收藏
Cron表达式是一个字符串,字符串以5或6个空格隔开,分开工6或7个域,每一个域代表一个含义,Cron有如下两种语法 
格式: 
Seconds Minutes Hours DayofMonth Month DayofWeek Year 或 
Seconds Minutes Hours DayofMonth Month DayofWeek 
每一个域可出现的字符如下:

代码
Seconds:可出现,-  *  / 四个字符,有效范围为0-59的整数    
Minutes:可出现,-  *  / 四个字符,有效范围为0-59的整数    
Hours:可出现,-  *  / 四个字符,有效范围为0-23的整数    
DayofMonth:可出现,-  *  / ? L W C八个字符,有效范围为0-31的整数     
Month:可出现,-  *  / 四个字符,有效范围为1-12的整数或JAN-DEc    
DayofWeek:可出现,-  *  / ? L C #四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推    
Year:可出现,-  *  / 四个字符,有效范围为1970-2099年   
 
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

代码
(1)*:表示匹配该域的任意值,假如在Minutes域使用*,即表示每分钟都会触发事件。    
   
(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13  13 15 20 * ?,其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。    
   
(3)-:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次    
   
(4)/:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.    
   
(5),:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。    
   
(6)L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。    
   
(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份    
   
(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。    
   
(9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。    
举几个例子:

代码
0 0  2  1 *  ? *  表示在每月的1日的凌晨2点调度任务     
0 15 10 ? *  MON-FRI 表示周一到周五每天上午10:15执行作业             
0 15 10 ? 6L 2002-2006 表示200-2006年的每个月的最后一个星期五上午10:15执行作业   
91linux

一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。
按顺序依次为 
秒(0~59) 
分钟(0~59)
小时(0~23)
天(月)(0~31,但是你需要考虑你月的天数)
月(0~11)
天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
7.年份(1970-2099)
其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?.
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点 
"0 0 12 * * ?" 每天中午12点触发 
"0 15 10 ? * *" 每天上午10:15触发 
"0 15 10 * * ?" 每天上午10:15触发 
"0 15 10 * * ? *" 每天上午10:15触发 
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发 
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 
"0 15 10 15 * ?" 每月15日上午10:15触发 
"0 15 10 L * ?" 每月最后一日的上午10:15触发 
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

有些子表达式能包含一些范围或列表
例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”
“*”字符代表所有可能的值
因此,“*”在子表达式(月)里表示每个月的含义,“*”在子表达式(天(星期))表示星期的每一天
 
“/”字符用来指定数值的增量
例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟
         在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样

“?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值
当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”
 
“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写
但是它在两个子表达式里的含义是不同的。
在天(月)子表达式中,“L”表示一个月的最后一天
在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT
如果在“L”前有具体的内容,它就具有其他的含义了
例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五
注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题
 
字段   允许值   允许的特殊字符 
秒    0-59    , - * / 
分    0-59    , - * / 
小时    0-23    , - * / 
日期    1-31    , - * ? / L W C 
月份    1-12 或者 JAN-DEC    , - * / 
星期    1-7 或者 SUN-SAT    , - * ? / L C # 
年(可选)    留空, 1970-2099    , - * /


<!--
  注意:日和星期是任先其一
  ?:代表可有可无
  *:代表每一年
  秒  分  时  日  月    星期几  年
  0  0  0  10  12    ?    2009      //代表:2009年12月10日0点0分0秒执行(星期几:'?'代表忽略)
  0  0  0  10  12    ?    *        //代表:每年12月10日0点0分0秒执行
  0  0  0  10  *    ?            //代表:每月10日0点0分0秒执行
  0  0  1  1  *    ?            //代表:每月1号1点0分0秒执行
  0  0  1  1  3,6,9    ?            //代表:3月 6月 9月,1号1点0分0秒执行
  0  0  1  1  2-5    ?
-->

posted @ 2012-02-13 15:07 昊天 阅读(44768) | 评论 (1)编辑 收藏
     摘要:  通过这几天的看书和学习,对Lucene有了更进一步的认识,所以总结一下这些天的学习成果把Lucene的学习心得也学出来。 1          Lucene的认识 提到Lucene很多人都知道这个开源的搜索工具,其魅力也是很大的。它让我们对搜索引擎的认识不在那么神秘,也不会在觉得百度和goo...  阅读全文
posted @ 2012-02-09 14:51 昊天 阅读(558) | 评论 (0)编辑 收藏
     摘要: Lucene是apache组织的一个用java实现全文搜索引擎的开源项目。 其功能非常的强大,api也很简单。总得来说用Lucene来进行建立 和搜索和操作数据库是差不多的(有点像),Document可以看作是 数据库的一行记录,Field可以看作是数据库的字段。用lucene实 现搜索引擎就像用JDBC实现连接数据库一样简单。 Lucene2.0,它与以前广泛应用和介绍的Lucene 1.4...  阅读全文
posted @ 2012-02-09 14:38 昊天 阅读(322) | 评论 (0)编辑 收藏
众所周知<textarea>文本框没有类似maxlength的属性,无法限制输入字符的个数。今天要推荐的Textarea Counter正是为了弥补这个缺点而出现的文本框字符限制插件。

jQuery文本框字符限制插件 - Textarea Counter

  Textarea Counter是一个轻量级的jQuery文本框字符限制插件,它也是简单易用的。字数限制的提示信息将会显示在文本框的下方,你也可以自定义提示信息的格式。当输入的字符超过字数限制时,提示信息将会相应的变为警告性的样式。

如何使用

  首先要加载jQuery库Textarea counter插件

<script src="jquery.min.js" type="text/javascript"></script>
<script src="jquery.textareaCounter.js" type="text/javascript"></script>

  在页面中增加一个textarea元素,如下

<textarea id="site" cols="60" rows="10"></textarea>

  最后初始化插件

var options = {
     'maxCharacterSize': 200,
     'originalStyle': 'originalTextareaInfo',
     'warningStyle' : 'warningTextareaInfo',
     'warningNumber': 40,
     'displayFormat' : '#input/#max | #words words'
   };
$('#site).textareaCount(options);

下载 演示


posted @ 2012-01-05 10:14 昊天 阅读(2018) | 评论 (3)编辑 收藏
     摘要: 50个必备的实用jQuery代码段 本文会给你们展示50个 jquery代码片段,这些代码能够给你的javascript项目提供帮助。其中的一些代码段是从jQuery1.4.2才开始支持的做法,另一些则是 真正有用的函数或方法,他们能够帮助你又快又好地把事情完成。如果你发现你任何可以做得更好的地方的话,欢迎把你的版本粘贴在评论中!1. 如何修改jQuery默认编码(例如默认UTF-8改成改GB...  阅读全文
posted @ 2012-01-04 15:01 昊天 阅读(354) | 评论 (0)编辑 收藏
     摘要: 第一步:新建一个webservice接口 @WebService  public interface IHelloWorld {       //@WebParam给参数命名,提高可代码可读性。此项可选   blic String&nb...  阅读全文
posted @ 2012-01-04 11:15 昊天 阅读(4178) | 评论 (0)编辑 收藏
     摘要: 本篇内容简要介绍BASE64、MD5、SHA、HMAC几种加密算法。     BASE64编码算法不算是真正的加密算法。     MD5、SHA、HMAC这三种加密算法,可谓是非可逆加密,就是不可解密的加密方法,我们称之为单向加密算法。我们通常只把他们作为加密的基础。单纯的以上三种的加密并不可靠。&...  阅读全文
posted @ 2011-11-12 15:19 昊天 阅读(1743) | 评论 (0)编辑 收藏

本文通过图表和实例的阐述在Oracle数据库创建新表时Storage的参数具体含义。


  可用于:表空间、回滚段、表、索引、分区、快照、快照日志


参数名称 缺省值 最小值 最大值 说明
INITIAL 5(数据块) 2(数据块) 操作系统限定 分配给Segment的第一个Extent的大小,以字节为单位,这个参数不能在alter语句中改变,如果指定的值小于最小值,则按最小值创建。
NEXT 5(数据块) 1(数据块) 操作系统限定 第二个Extent的大小等于NEXT的初值,以后的NEXT值=前一NEXT大小乘以(1+PCTINCREASE/100),如果指定的值小于最小值,则按最小值创建。
如果在alter语句中改变NEXT的值,则下一个分配的Extent将具有指定的大小,而不管上一次分配的Extent大小和PCTINCREASE参数值。
MINEXTENTS 1(Extent)
回滚段为2个Extent
1(Extent)
回滚段为2个Extent
操作系统限定 Segment第一次创建时分配的Extent数量
MAXEXTENTS 根据数据块大小而定 1(Extent)
回滚段为2个Extent
无限制 随着Segment中数据量的增长,最多可分配的Extent数量
PCTINCREASE 50%


(Oracle816中为0%)
0% 操作系统限定 指定第三个及其后的Extent相对于上一个Extent所增加的百分比,
如果PCTINCREASE为0,则Segment中所有新增加的Extent的大小都相同,等于NEXT的值,
如果PCTINCREASE大于0,则每次计算NEXT的值(用上面的公式),
PCTINCREASE不能为负数。
创建回滚段时,不可指定此参数,回滚段中此参数固定为0。
OPTIMAL ---- 不能小于回滚段初始分配空间 操作系统限定 仅与回滚段有关,当回滚段因为增长、扩展而超过此参数的设定范围时,Oracle系统会根据情况动态地重新分配Extents,试图收回多分配的Extent。
FREELISTS 1 1 数据块大小限制 只能在CREATE TABLE、CLUSTER、INDEX中指定FREELISTS和FREELIST GROUPS参数。
模式对象中每一个自由列表组中自由列表的数量
FREELIST GROUPS 1 1 取决于Oracle并行实例的数量 用户创建的数据库对象的自由列表组的数量,只有用OPS并行服务器选项时才使用这一参数,一个实例对应一个组。
BUFFER_POOL ---- ---- ---- 给模式对象定义缺省缓冲池(高速缓存),该对象的所有块都存储在指定的高速缓存中,对于表空间或回滚段无效。

  建议PCTINCREASE参数设置为0,可使碎片最小化,使每一个Extent都相同(等于NEXT值)
  一旦建立了某个对象,它的INITIAL和MINEXTENTS参数不能修改(Oracle 816中可修改MINEXTENTS参数)
  对于NEXT和PCTINCREASE的任何修改都只影响后来分配的那些Extent
  在分配一个新Extent时,系统直接按NEXT的值分配一个Extent,
  然后用公式:前一NEXT值*(1+PCTINCREASE/100) 计算出下一个应该分配的Extent的大小,
  并把计算结果保存到相关数据字典的NEXT_EXTENT列上,做为下一个应该分配的Extent的大小。
  
  CREATE TABLE test(a number)
  STORAGE(
  INITIAL 100K
  NEXT 100K
  MINEXTENTS 2
  MAXEXTENTS 100
  PCTINCREASE 100);
  解释:
  初始给test表分配两个Extent,
  第一个Extent是100K,因INITIAL=100K;
  第二个Extent是100K,因NEXT=100K;
  假如因表内数据增长,需要分配第三个Extent,因PCTINCREASE是100,则
  第三个Extent是200K=100K+100K;
  第四个Extent是400K=200K+200K。
  可通过数据字典表DBA_TABLES、ALL_TABLES、USER_TABLES查看参数设置情况,如:
  
  select table_name,initial_extent,next_extent,
  min_extents,max_extents,pct_increase from user_tables;
  TABLE_NAME INITIAL_EXTENT NEXT_EXTENT MIN_EXTENTS MAX_EXTENTS PCT_INCREASE
  ---------- -------------- ----------- ----------- ----------- ------------
  TEST 106496 212992 2 100 100
posted @ 2011-10-28 09:51 昊天 阅读(20338) | 评论 (0)编辑 收藏

实际上,HashSet 和 HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合元素;对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map 的 key-value 对。

在介绍集合存储之前需要指出一点:虽然集合号称存储的是 Java 对象,但实际上并不会真正将 Java 对象放入 Set 集合中,只是在 Set 集合中保留这些对象的引用而言。也就是说:Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。


HashMap 的存储实现

当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:

 HashMap<String , Double> map = new HashMap<String , Double>(); 
 map.put("语文" , 80.0); 
 map.put("数学" , 89.0); 
 map.put("英语" , 78.2); 

HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。

当程序执行 map.put("语文" , 80.0); 时,系统将调用"语文"的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。

我们可以看 HashMap 类的 put(K key , V value) 方法的源代码:

 public V put(K key, V value) 
 { 
	 // 如果 key 为 null,调用 putForNullKey 方法进行处理
	 if (key == null) 
		 return putForNullKey(value); 
	 // 根据 key 的 keyCode 计算 Hash 值
	 int hash = hash(key.hashCode()); 
	 // 搜索指定 hash 值在对应 table 中的索引
 	 int i = indexFor(hash, table.length);
	 // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素
	 for (Entry<K,V> e = table[i]; e != null; e = e.next) 
	 { 
		 Object k; 
		 // 找到指定 key 与需要放入的 key 相等(hash 值相同
		 // 通过 equals 比较放回 true)
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
		 { 
			 V oldValue = e.value; 
			 e.value = value; 
			 e.recordAccess(this); 
			 return oldValue; 
		 } 
	 } 
	 // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry 
	 modCount++; 
	 // 将 key、value 添加到 i 索引处
	 addEntry(hash, key, value, i); 
	 return null; 
 } 

JDK 源码

在 JDK 安装目录下可以找到一个 src.zip 压缩文件,该文件里包含了 Java 基础类库的所有源文件。只要读者有学习兴趣,随时可以打开这份压缩文件来阅读 Java 类库的源代码,这对提高读者的编程能力是非常有帮助的。需要指出的是:src.zip 中包含的源代码并没有包含像上文中的中文注释,这些注释是笔者自己添加进去的。

上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:

static int hash(int h) 
{ 
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4); 
} 

对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:

static int indexFor(int h, int length) 
{ 
    return h & (length-1); 
}

这个方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,这一点可参看后面关于 HashMap 构造器的介绍。

当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。

根据上面 put 方法的源代码可以看出,当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

当向 HashMap 中添加 key-value 对,由其 key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。

上面程序中还调用了 addEntry(hash, key, value, i); 代码,其中 addEntry 是 HashMap 提供的一个包访问权限的方法,该方法仅用于添加一个 key-value 对。下面是该方法的代码:

void addEntry(int hash, K key, V value, int bucketIndex) 
{ 
    // 获取指定 bucketIndex 索引处的 Entry 
    Entry<K,V> e = table[bucketIndex]; 	 // ①
    // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    // 如果 Map 中的 key-value 对的数量超过了极限
    if (size++ >= threshold) 
        // 把 table 对象的长度扩充到 2 倍。
        resize(2 * table.length); 	 // ②
} 

上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。


Hash 算法的性能选项

根据上面代码可以看出,在同一个 bucket 存储 Entry 链的情况下,新放入的 Entry 总是位于 bucket 中,而最早放入该 bucket 中的 Entry 则位于这个 Entry 链的最末端。

上面程序中还有这样两个变量:

  • size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
  • threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。

从上面程序中②号代码可以看出,当 size++ >= threshold 时,HashMap 会自动调用 resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍。

上面程序中使用的 table 其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是 HashMap 的容量。HashMap 包含如下几个构造器:

  • HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

当创建一个 HashMap 时,系统会自动创建一个 table 数组来保存 HashMap 中的 Entry,下面是 HashMap 中一个构造器的代码:

 // 以指定初始化容量、负载因子创建 HashMap 
 public HashMap(int initialCapacity, float loadFactor) 
 { 
	 // 初始容量不能为负数
	 if (initialCapacity < 0) 
		 throw new IllegalArgumentException( 
		"Illegal initial capacity: " + 
			 initialCapacity); 
	 // 如果初始容量大于最大容量,让出示容量
	 if (initialCapacity > MAXIMUM_CAPACITY) 
		 initialCapacity = MAXIMUM_CAPACITY; 
	 // 负载因子必须大于 0 的数值
	 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 
		 throw new IllegalArgumentException( 
		 loadFactor); 
	 // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
	 int capacity = 1; 
	 while (capacity < initialCapacity) 
		 capacity <<= 1; 
	 this.loadFactor = loadFactor; 
	 // 设置容量极限等于容量 * 负载因子
	 threshold = (int)(capacity * loadFactor); 
	 // 初始化 table 数组
	 table = new Entry[capacity]; 			 // ①
	 init(); 
 } 

上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。

initialCapacity 与 HashTable 的容量

创建 HashMap 时指定的 initialCapacity 并不等于 HashMap 的实际容量,通常来说,HashMap 的实际容量总比 initialCapacity 大一些,除非我们指定的 initialCapacity 参数值恰好是 2 的 n 次方。当然,掌握了 HashMap 容量分配的知识之后,应该在创建 HashMap 时将 initialCapacity 参数值指定为 2 的 n 次方,这样可以减少系统的计算开销。

程序①号代码处可以看到:table 的实质就是一个数组,一个长度为 capacity 的数组。

对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。

无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图 1 所示:


图 1. HashMap 的存储示意
图 1. HashMap 的存储示意

HashMap 的读取实现

当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,在根据该 hashCode 返回值找出该 key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:

 public V get(Object key) 
 { 
	 // 如果 key 是 null,调用 getForNullKey 取出对应的 value 
	 if (key == null) 
		 return getForNullKey(); 
	 // 根据该 key 的 hashCode 值计算它的 hash 码
	 int hash = hash(key.hashCode()); 
	 // 直接取出 table 数组中指定索引处的值,
	 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
		 e != null; 
		 // 搜索该 Entry 链的下一个 Entr 
		 e = e.next) 		 // ①
	 { 
		 Object k; 
		 // 如果该 Entry 的 key 与被搜索 key 相同
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
			 return e.value; 
	 } 
	 return null; 
 } 

从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 Hash 算法来决定其存储位置;当需要取出一个 Entry 时,也会根据 Hash 算法找到其存储位置,直接取出该 Entry。由此可见:HashMap 之所以能快速存、取它所包含的 Entry,完全类似于现实生活中母亲从小教我们的:不同的东西要放在不同的位置,需要时才能快速找到它。

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。

掌握了上面知识之后,我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。

如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap 中 Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity 的 Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。


HashSet 的实现

对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层采用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,查看 HashSet 的源代码,可以看到如下代码:

 public class HashSet<E> 
	 extends AbstractSet<E> 
	 implements Set<E>, Cloneable, java.io.Serializable 
 { 
	 // 使用 HashMap 的 key 保存 HashSet 中所有元素
	 private transient HashMap<E,Object> map; 
	 // 定义一个虚拟的 Object 对象作为 HashMap 的 value 
	 private static final Object PRESENT = new Object(); 
	 ... 
	 // 初始化 HashSet,底层会初始化一个 HashMap 
	 public HashSet() 
	 { 
		 map = new HashMap<E,Object>(); 
	 } 
	 // 以指定的 initialCapacity、loadFactor 创建 HashSet 
	 // 其实就是以相应的参数创建 HashMap 
	 public HashSet(int initialCapacity, float loadFactor) 
	 { 
		 map = new HashMap<E,Object>(initialCapacity, loadFactor); 
	 } 
	 public HashSet(int initialCapacity) 
	 { 
		 map = new HashMap<E,Object>(initialCapacity); 
	 } 
	 HashSet(int initialCapacity, float loadFactor, boolean dummy) 
	 { 
		 map = new LinkedHashMap<E,Object>(initialCapacity 
			 , loadFactor); 
	 } 
	 // 调用 map 的 keySet 来返回所有的 key 
	 public Iterator<E> iterator() 
	 { 
		 return map.keySet().iterator(); 
	 } 
	 // 调用 HashMap 的 size() 方法返回 Entry 的数量,就得到该 Set 里元素的个数
	 public int size() 
	 { 
		 return map.size(); 
	 } 
	 // 调用 HashMap 的 isEmpty() 判断该 HashSet 是否为空,
	 // 当 HashMap 为空时,对应的 HashSet 也为空
	 public boolean isEmpty() 
	 { 
		 return map.isEmpty(); 
	 } 
	 // 调用 HashMap 的 containsKey 判断是否包含指定 key 
	 //HashSet 的所有元素就是通过 HashMap 的 key 来保存的
	 public boolean contains(Object o) 
	 { 
		 return map.containsKey(o); 
	 } 
	 // 将指定元素放入 HashSet 中,也就是将该元素作为 key 放入 HashMap 
	 public boolean add(E e) 
	 { 
		 return map.put(e, PRESENT) == null; 
	 } 
	 // 调用 HashMap 的 remove 方法删除指定 Entry,也就删除了 HashSet 中对应的元素
	 public boolean remove(Object o) 
	 { 
		 return map.remove(o)==PRESENT; 
	 } 
	 // 调用 Map 的 clear 方法清空所有 Entry,也就清空了 HashSet 中所有元素
	 public void clear() 
	 { 
		 map.clear(); 
	 } 
	 ... 
 } 

由上面源程序可以看出,HashSet 的实现其实非常简单,它只是封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

HashSet 的绝大部分方法都是通过调用 HashMap 的方法来实现的,因此 HashSet 和 HashMap 两个集合在实现本质上是相同的。

HashMap 的 put 与 HashSet 的 add

由于 HashSet 的 add() 方法添加集合元素时实际上转变为调用 HashMap 的 put() 方法来添加 key-value 对,当新放入 HashMap 的 Entry 中 key 与集合中原有 Entry 的 key 相同(hashCode() 返回值相等,通过 equals 比较也返回 true),新添加的 Entry 的 value 将覆盖原来 Entry 的 value,但 key 不会有任何改变,因此如果向 HashSet 中添加一个已经存在的元素,新添加的集合元素(底层由 HashMap 的 key 保存)不会覆盖已有的集合元素。

掌握上面理论知识之后,接下来看一个示例程序,测试一下自己是否真正掌握了 HashMap 和 HashSet 集合的功能。

 class Name
{
    private String first; 
    private String last; 
    
    public Name(String first, String last) 
    { 
        this.first = first; 
        this.last = last; 
    } 

    public boolean equals(Object o) 
    { 
        if (this == o) 
        { 
            return true; 
        } 
        
	if (o.getClass() == Name.class) 
        { 
            Name n = (Name)o; 
            return n.first.equals(first) 
                && n.last.equals(last); 
        } 
        return false; 
    } 
}

public class HashSetTest
{
    public static void main(String[] args)
    { 
        Set<Name> s = new HashSet<Name>();
        s.add(new Name("abc", "123"));
        System.out.println(
            s.contains(new Name("abc", "123")));
    }
} 

上面程序中向 HashSet 里添加了一个 new Name("abc", "123") 对象之后,立即通过程序判断该 HashSet 是否包含一个 new Name("abc", "123") 对象。粗看上去,很容易以为该程序会输出 true。

实际运行上面程序将看到程序输出 false,这是因为 HashSet 判断两个对象相等的标准除了要求通过 equals() 方法比较返回 true 之外,还要求两个对象的 hashCode() 返回值相等。而上面程序没有重写 Name 类的 hashCode() 方法,两个 Name 对象的 hashCode() 返回值并不相同,因此 HashSet 会把它们当成 2 个对象处理,因此程序返回 false。

由此可见,当我们试图把某个类的对象当成 HashMap 的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的 equals(Object obj) 方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。

hashCode() 和 equals()

关于如何正确地重写某个类的 hashCode() 方法和 equals() 方法,请参考疯狂 Java 体系的《疯狂 Java 讲义》一书中相关内容。

如下程序就正确重写了 Name 类的 hashCode() 和 equals() 方法,程序如下:

class Name 
{ 
    private String first;
    private String last;
    public Name(String first, String last)
    { 
        this.first = first; 
        this.last = last; 
    } 
    // 根据 first 判断两个 Name 是否相等
    public boolean equals(Object o) 
    { 
        if (this == o) 
        { 
            return true; 
        } 
        if (o.getClass() == Name.class) 
        { 
            Name n = (Name)o; 
            return n.first.equals(first); 
        } 
        return false; 
    } 
	 
    // 根据 first 计算 Name 对象的 hashCode() 返回值
    public int hashCode() 
    { 
        return first.hashCode(); 
    }

    public String toString() 
    { 
        return "Name[first=" + first + ", last=" + last + "]"; 
    } 
 } 
 
 public class HashSetTest2 
 { 
    public static void main(String[] args) 
    { 
        HashSet<Name> set = new HashSet<Name>(); 
        set.add(new Name("abc" , "123")); 
        set.add(new Name("abc" , "456")); 
        System.out.println(set); 
    } 
}

上面程序中提供了一个 Name 类,该 Name 类重写了 equals() 和 toString() 两个方法,这两个方法都是根据 Name 类的 first 实例变量来判断的,当两个 Name 对象的 first 实例变量相等时,这两个 Name 对象的 hashCode() 返回值也相同,通过 equals() 比较也会返回 true。

程序主方法先将第一个 Name 对象添加到 HashSet 中,该 Name 对象的 first 实例变量值为"abc",接着程序再次试图将一个 first 为"abc"的 Name 对象添加到 HashSet 中,很明显,此时没法将新的 Name 对象添加到该 HashSet 中,因为此处试图添加的 Name 对象的 first 也是" abc",HashSet 会判断此处新增的 Name 对象与原有的 Name 对象相同,因此无法添加进入,程序在①号代码处输出 set 集合时将看到该集合里只包含一个 Name 对象,就是第一个、last 为"123"的 Name 对象。

posted @ 2011-10-25 16:37 昊天 阅读(382) | 评论 (0)编辑 收藏

基础

如先前所提到的,类图的目的是显示建模系统的类型。在大多数的 UML 模型中这些类型包括:

  • 接口
  • 数据类型
  • 组件

UML 为这些类型起了一个特别的名字:“分类器”。通常地,你可以把分类器当做类,但在技术上,分类器是更为普遍的术语,它还是引用上面的其它三种类型为好。

类名

类的 UML 表示是一个长方形,垂直地分为三个区,如图 1 所示。顶部区域显示类的名字。中间的区域列出类的属性。底部的区域列出类的操作。当在一个类图上画一个类元素时,你必须要有顶端的区域,下面的二个区域是可选择的(当图描述仅仅用于显示分类器间关系的高层细节时,下面的两个区域是不必要的)。图 1 显示一个航线班机如何作为 UML 类建模。正如我们所能见到的,名字是 Flight,我们可以在中间区域看到Flight类的3个属性:flightNumber,departureTime 和 flightDuration。在底部区域中我们可以看到Flight类有两个操作:delayFlight 和 getArrivalTime。

图 1: Flight类的类图

图 1: Flight类的类图

类属性列表

类的属性节(中部区域)在分隔线上列出每一个类的属性。属性节是可选择的,要是一用它,就包含类的列表显示的每个属性。该线用如下格式:

name : attribute type

flightNumber : Integer

继续我们的Flight类的例子,我们可以使用属性类型信息来描述类的属性,如表 1 所示。

表 1:具有关联类型的Flight类的属性名字

属性名称 属性类型
flightNumber Integer
departureTime Date
flightDuration Minutes

在业务类图中,属性类型通常与单位相符,这对于图的可能读者是有意义的(例如,分钟,美元,等等)。然而,用于生成代码的类图,要求类的属性类型必须限制在由程序语言提供的类型之中,或包含于在系统中实现的、模型的类型之中。

在类图上显示具有默认值的特定属性,有时是有用的(例如,在银行账户应用程序中,一个新的银行账户会以零为初始值)。UML 规范允许在属性列表节中,通过使用如下的记号作为默认值的标识:

name : attribute type = default value

举例来说:

balance : Dollars = 0

显示属性默认值是可选择的;图 2 显示一个银行账户类具有一个名为 balance的类型,它的默认值为0。

图 2:显示默认为0美元的balance属性值的银行账户类图。

图 2:显示默认为0美元的balance属性值的银行账户类图。

类操作列表

类操作记录在类图长方形的第三个(最低的)区域中,它也是可选择的。和属性一样,类的操作以列表格式显示,每个操作在它自己线上。操作使用下列记号表现:

	name(parameter list) : type of value returned

下面的表 2 中Flight类操作的映射。

表 2:从图 2 映射的Flight类的操作

操作名称 返回参数 值类型
delayFlight
Name Type
numberOfMinutes Minutes
N/A
getArrivalTime N/A Date

图3显示,delayFlight 操作有一个Minutes类型的输入参数 -- numberOfMinutes。然而,delayFlight 操作没有返回值。 1 当一个操作有参数时,参数被放在操作的括号内;每个参数都使用这样的格式:“参数名:参数类型”。

图 3:Flight类操作参数,包括可选择的“in”标识。

图 3:Flight类操作参数,包括可选择的“in”标识。

当文档化操作参数时,你可能使用一个可选择的指示器,以显示参数到操作的输入参数、或输出参数。这个可选择的指示器以“in”或“out”出现,如图3中的操作区域所示。一般来说,除非将使用一种早期的程序编程语言,如Fortran ,这些指示器可能会有所帮助,否则它们是不必要的。然而,在 C++和Java中,所有的参数是“in”参数,而且按照UML规范,既然“in”是参数的默认类型,大多数人将会遗漏输入/输出指示器。

继承

在面向对象的设计中一个非常重要的概念,继承,指的是一个类(子类)继承另外的一个类(超类)的同一功能,并增加它自己的新功能(一个非技术性的比喻,想象我继承了我母亲的一般的音乐能力,但是在我的家里,我是唯一一个玩电吉他的人)的能力。为了在一个类图上建模继承,从子类(要继承行为的类)拉出一条闭合的,单键头(或三角形)的实线指向超类。考虑银行账户的类型:图 4 显示 CheckingAccount 和 SavingsAccount 类如何从 BankAccount 类继承而来。

图 4: 继承通过指向超类的一条闭合的,单箭头的实线表示。

图 4: 继承通过指向超类的一条闭合的,单箭头的实线表示。

在图 4 中,继承关系由每个超类的单独的线画出,这是在IBM Rational Rose和IBM Rational XDE中使用的方法。然而,有一种称为 树标记的备选方法可以画出继承关系。当存在两个或更多子类时,如图 4 中所示,除了继承线象树枝一样混在一起外,你可以使用树形记号。图 5 是重绘的与图 4 一样的继承,但是这次使用了树形记号。

图 5: 一个使用树形记号的继承实例

图 5: 一个使用树形记号的继承实例

抽象类及操作
细心的读者会注意到,在图 4 和 图5 中的图中,类名BankAccount和withdrawal操作使用斜体。这表示,BankAccount 类是一个抽象类,而withdrawal方法是抽象的操作。换句话说,BankAccount 类使用withdrawal规定抽象操作,并且CheckingAccount 和 SavingsAccount 两个子类都分别地执行它们各自版本的操作。

然而,超类(父类)不一定要是抽象类。标准类作为超类是正常的。

关联
当你系统建模时,特定的对象间将会彼此关联,而且这些关联本身需要被清晰地建模。有五种关联。在这一部分中,我将会讨论它们中的两个 -- 双向的关联和单向的关联,而且我将会在Beyond the basics部分讨论剩下的三种关联类型。请注意,关于何时该使用每种类型关联的详细讨论,不属于本文的范围。相反的,我将会把重点集中在每种关联的用途,并说明如何在类图上画出关联。

双向(标准)的关联
关联是两个类间的联接。关联总是被假定是双向的;这意味着,两个类彼此知道它们间的联系,除非你限定一些其它类型的关联。回顾一下Flight 的例子,图 6 显示了在Flight类和Plane类之间的一个标准类型的关联。

图 6:在一个Flight类和Plane类之间的双向关联的实例

图 6:在一个Flight类和Plane类之间的双向关联的实例

一个双向关联用两个类间的实线表示。在线的任一端,你放置一个角色名和多重值。图 6 显示Flight与一个特定的Plane相关联,而且Flight类知道这个关联。因为角色名以Plane类表示,所以Plane承担关联中的“assignedPlane”角色。紧接于Plane类后面的多重值描述0...1表示,当一个Flight实体存在时,可以有一个或没有Plane与之关联(也就是,Plane可能还没有被分配)。图 6 也显示Plane知道它与Flight类的关联。在这个关联中,Flight承担“assignedFlights”角色;图 6 的图告诉我们,Plane实体可以不与flight关联(例如,它是一架全新的飞机)或与没有上限的flight(例如,一架已经服役5年的飞机)关联。

由于对那些在关联尾部可能出现的多重值描述感到疑惑,下面的表3列出了一些多重值及它们含义的例子。

表 3: 多重值和它们的表示

可能的多重值描述
表示 含义
0..1 0个或1个
1 只能1个
0..* 0个或多个
* 0个或多个
1..* 1个或我个
3 只能3个
0..5 0到5个
5..15 5到15个

单向关联
在一个单向关联中,两个类是相关的,但是只有一个类知道这种联系的存在。图 7 显示单向关联的透支财务报告的一个实例。

图 7: 单向关联一个实例:OverdrawnAccountsReport 类 BankAccount 类,而 BankAccount 类则对关联一无所知。

图 7: 单向关联一个实例:OverdrawnAccountsReport 类 BankAccount 类,而 BankAccount 类则对关联一无所知。

一个单向的关联,表示为一条带有指向已知类的开放箭头(不关闭的箭头或三角形,用于标志继承)的实线。如同标准关联,单向关联包括一个角色名和一个多重值描述,但是与标准的双向关联不同的时,单向关联只包含已知类的角色名和多重值描述。在图 7 中的例子中,OverdrawnAccountsReport 知道 BankAccount 类,而且知道 BankAccount 类扮演“overdrawnAccounts”的角色。然而,和标准关联不同,BankAccount 类并不知道它与 OverdrawnAccountsReport 相关联。 2

软件包
不可避免,如果你正在为一个大的系统或大的业务领域建模,在你的模型中将会有许多不同的分类器。管理所有的类将是一件令人生畏的任务;所以,UML 提供一个称为 软件包的组织元素。软件包使建模者能够组织模型分类器到名字空间中,这有些象文件系统中的文件夹。把一个系统分为多个软件包使系统变成容易理解,尤其是在每个软件包都表现系统的一个特定部分时。 3

在图中存在两种方法表示软件包。并没有规则要求使用哪种标记,除了用你个人的判断:哪种更便于阅读你画的类图。两种方法都是由一个较小的长方形(用于定位)嵌套在一个大的长方形中开始的,如图 8 所示。但是建模者必须决定包的成员如何表示,如下:

  • 如果建模者决定在大长方形中显示软件包的成员,则所有的那些成员 4 需要被放置在长方形里面。另外,所有软件包的名字需要放在软件包的较小长方形之内(如图 8 的显示)。

  • 如果建模者决定在大的长方形之外显示软件包成员,则所有将会在图上显示的成员都需要被置于长方形之外。为了显示属于软件包的分类器属于,从每个分类器画一条线到里面有加号的圆周,这些圆周粘附在软件包之上(图9)。
图 8:在软件包的长方形内显示软件包成员的软件包元素例子

图 8:在软件包的长方形内显示软件包成员的软件包元素例子

图 9:一个通过连接线表现软件包成员的软件包例子

图 9:一个通过连接线表现软件包成员的软件包例子

了解基础重要性

在 UML 2 中,了解类图的基础更为重要。这是因为类图为所有的其他结构图提供基本的构建块。如组件或对象图(仅仅是举了些例子)。


超过基础

到此为止,我已经介绍了类图的基础,但是请继续往下读!在下面的部分中,我将会引导你到你会使用的类图的更重要的方面。这些包括UML 2 规范中的接口,其它的三种关联类型,可见性和其他补充。

接口
在本文的前面,我建议你以类来考虑分类器。事实上,分类器是一个更为一般的概念,它包括数据类型和接口。

关于何时、以及如何高效地在系统结构图中使用数据类型和接口的完整讨论,不在本文的讨论范围之内。既然这样,我为什么要在这里提及数据类型和接口呢?你可能想在结构图上模仿这些分类器类型,在这个时候,使用正确的记号来表示,或者至少知道这些分类器类型是重要的。不正确地绘制这些分类器,很有可能将使你的结构图读者感到混乱,以后的系统将不能适应需求。

一个类和一个接口不同:一个类可以有它形态的真实实例,然而一个接口必须至少有一个类来实现它。在 UML 2 中,一个接口被认为是类建模元素的特殊化。因此,接口就象类那样绘制,但是长方形的顶部区域也有文本“interface”,如图 10 所示。 5

图 10:Professor类和Student类实现Person接口的类图实例

图 10:Professor类和Student类实现Person接口的类图实例

在图 10 中显示的图中,Professor和Student类都实现了Person的接口,但并不从它继承。我们知道这一点是由于下面两个原因:1) Person对象作为接口被定义 -- 它在对象的名字区域中有“interface”文本,而且我们看到由于Professor和Student对象根据画类对象的规则(在它们的名字区域中没有额外的分类器文本)标示,所以它们是 对象。 2) 我们知道继承在这里没有被显示,因为与带箭头的线是点线而不是实线。如图 10 所示,一条带有闭合的单向箭头的 线意味着实现(或实施);正如我们在图 4 中所见到的,一条带有闭合单向箭头的线表示继承。

更多的关联
在上面,我讨论了双向关联和单向关联。现在,我将会介绍剩下的三种类型的关联。

关联类
在关联建模中,存在一些情况下,你需要包括其它类,因为它包含了关于关联的有价值的信息。对于这种情况,你会使用 关联类 来绑定你的基本关联。关联类和一般类一样表示。不同的是,主类和关联类之间用一条相交的点线连接。图 11 显示一个航空工业实例的关联类。

图 11:增加关联类 MileageCredit

图 11:增加关联类 MileageCredit

在图 11 中显示的类图中,在Flight类和 FrequentFlyer 类之间的关联,产生了称为 MileageCredit的关联类。这意味当Flight类的一个实例关联到 FrequentFlyer 类的一个实例时,将会产生 MileageCredit 类的一个实例。

聚合
聚合是一种特别类型的关联,用于描述“总体到局部”的关系。在基本的聚合关系中, 部分类 的生命周期独立于 整体类 的生命周期。

举例来说,我们可以想象, 是一个整体实体,而 车轮 轮胎是整辆车的一部分。轮胎可以在安置到车时的前几个星期被制造,并放置于仓库中。在这个实例中,Wheel类实例清楚地独立地Car类实例而存在。然而,有些情况下, 部分 类的生命周期并 独立于 整体 类的生命周期 -- 这称为合成聚合。举例来说,考虑公司与部门的关系。 公司和部门 都建模成类,在公司存在之前,部门不能存在。这里Department类的实例依赖于Company类的实例而存在。

让我们更进一步探讨基本聚合和组合聚合。

基本聚合
有聚合关系的关联指出,某个类是另外某个类的一部分。在一个聚合关系中,子类实例可以比父类存在更长的时间。为了表现一个聚合关系,你画一条从父类到部分类的实线,并在父类的关联末端画一个未填充棱形。图 12 显示车和轮胎间的聚合关系的例子。

图 12: 一个聚合关联的例子

图 12: 一个聚合关联的例子

组合聚合
组合聚合关系是聚合关系的另一种形式,但是子类实例的生命周期依赖于父类实例的生命周期。在图13中,显示了Company类和Department类之间的组合关系,注意组合关系如聚合关系一样绘制,不过这次菱形是被填充的。

图 13: 一个组合关系的例子

图 13: 一个组合关系的例子

在图 13 中的关系建模中,一个Company类实例至少总有一个Department类实例。因为关系是组合关系,当Company实例被移除/销毁时,Department实例也将自动地被移除/销毁。组合聚合的另一个重要功能是部分类只能与父类的实例相关(举例来说,我们例子中的Company类)。

反射关联
现在我们已经讨论了所有的关联类型。就如你可能注意到的,我们的所有例子已经显示了两个不同类之间的关系。然而,类也可以使用反射关联与它本身相关联。起先,这可能没有意义,但是记住,类是抽象的。图 14 显示一个Employee类如何通过manager / manages角色与它本身相关。当一个类关联到它本身时,这并不意味着类的实例与它本身相关,而是类的一个实例与类的另一个实例相关。

图 14:一个反射关联关系的实例

图 14:一个反射关联关系的实例

图 14 描绘的关系说明一个Employee实例可能是另外一个Employee实例的经理。然而,因为“manages”的关系角色有 0..*的多重性描述;一个雇员可能不受任何其他雇员管理。

可见性
在面向对象的设计中,存在属性及操作可见性的记号。UML 识别四种类型的可见性:public,protected,private及package。

UML 规范并不要求属性及操作可见性必须显示在类图上,但是它要求为每个属性及操作定义可见性。为了在类图上的显示可见性,放置可见性标志于属性或操作的名字之前。虽然 UML 指定四种可见性类型,但是实际的编程语言可能增加额外的可见性,或不支持 UML 定义的可见性。表4显示了 UML 支持的可见性类型的不同标志。

表 4:UML 支持的可见性类型的标志

标志 可见性类型
+ Public
# Protected
- Private
~ Package

现在,让我们看一个类,以说明属性及操作的可见性类型。在图 15 中,所有的属性及操作都是public,除了 updateBalance 操作。updateBalance 操作是protected。

图 15:一个 BankAccount 类说明它的属性及操作的可见性

图 15:一个 BankAccount 类说明它的属性及操作的可见性


UML 2 补充

既然我们已经覆盖了基础和高级主题,我们将覆盖一些由UML 1. x增加的类图的新记号。

实例
当一个系统结构建模时,显示例子类实例有时候是有用的。为了这种结构建模,UML 2 提供 实例规范 元素,它显示在系统中使用例子(或现实)实例的值得注意的信息。

实例的记号和类一样,但是取代顶端区域中仅有的类名,它的名字是经过拼接的:

Instance Name : Class Name

举例来说:

Donald : Person

因为显示实例的目的是显示值得注意的或相关的信息,没必要在你的模型中包含整个实体属性及操作。相反地,仅仅显示感兴趣的属性及其值是完全恰当的。如图16所描述。

图 16:Plane类的一个实例例子(只显示感兴趣的属性值)

图 16:Plane类的一个实例例子(只显示感兴趣的属性值)

然而,仅仅表现一些实例而没有它们的关系不太实用;因此,UML 2 也允许在实体层的关系/关联建模。绘制关联与一般的类关系的规则一样,除了在建模关联时有一个附加的要求。附加的限制是,关联关系必须与类图的关系相一致,而且关联的角色名字也必须与类图相一致。它的一个例子显示于图 17 中。在这个例子中,实例是图 6 中类图的例子实例。

图 17:图 6 中用实例代替类的例子

图 17:图 6 中用实例代替类的例子

图 17 有Flight类的二个实例,因为类图指出了在Plane类和Flight类之间的关系是 0或多。因此,我们的例子给出了两个与NX0337 Plane实例相关的Flight实例。

角色
建模类的实例有时比期望的更为详细。有时,你可能仅仅想要在一个较多的一般层次做类关系的模型。在这种情况下,你应该使用 角色 记号。角色记号类似于实例记号。为了建立类的角色模型,你画一个方格,并在内部放置类的角色名及类名,作为实体记号,但是在这情况你不能加下划线。图 18 显示一个由图 14 中图描述的雇员类扮演的角色实例。在图 18 中,我们可以认为,即使雇员类与它本身相关,关系确实是关于雇员之间扮演经理及团队成员的角色。

图 18:一个类图显示图14中扮演不同角色的类

图 18:一个类图显示图14中扮演不同角色的类

注意,你不能在纯粹类图中做类角色的建模,即使图 18显示你可以这么做。为了使用角色记号,你将会需要使用下面讨论的内部结构记号。

内部的结构
UML 2 结构图的更有用的功能之一是新的内部结构记号。它允许你显示一个类或另外的一个分类器如何在内部构成。这在 UML 1. x 中是不可能的,因为记号限制你只能显示一个类所拥有的聚合关系。现在,在 UML 2 中,内部的结构记号让你更清楚地显示类的各个部分如何保持关系。

让我们看一个实例。在图 18 中我们有一个类图以表现一个Plane类如何由四个引擎和两个控制软件对象组成。从这个图中省略的东西是显示关于飞机部件如何被装配的一些信息。从图 18 的图,你无法说明,是每个控制软件对象控制两个引擎,还是一个控制软件对象控制三个引擎,而另一个控制一个引擎。

图 19: 只显示对象之间关系的类图

图 19: 只显示对象之间关系的类图

绘制类的内在结构将会改善这种状态。开始时,你通过用二个区域画一个方格。最顶端的区域包含类名字,而较低的区域包含类的内部结构,显示在它们父类中承担不同角色的部分类,角色中的每个部分类也关系到其它类。图 19 显示了Plane类的内部结构;注意内部结构如何澄清混乱性。

图 20:Plane类的内部结构例子。

图 20:Plane类的内部结构例子。

在图 20 中Plane有两个 ControlSoftware 对象,而且每个控制二个引擎。在图左边上的 ControlSoftware(control1)控制引擎 1 和 2 。在图右边的 ControlSoftware(control2)控制引擎 3 和 4 。


结论

至少存在两个了解类图的重要理由。第一个是它显示系统分类器的静态结构;第二个理由是图为UML描述的其他结构图提供了基本记号。开发者将会认为类图是为他们特别建立的;但是其他的团队成员将发现它们也是有用的。业务分析师可以用类图,为系统的业务远景建模。正如我们将会在本系列关于 UML 基础的文章中见到的,其他的图 -- 包括活动图,序列图和状态图——参考类图中的类建模和文档化。

关于“UML 基础”的本系列的后面的元件图。

posted @ 2011-10-25 15:23 昊天 阅读(340) | 评论 (0)编辑 收藏
一、什么是注释

    说起注释,得先提一提什么是元数据(metadata)。所谓元数据就是数据的数据。也就是说,元数据是描述数据的。就象数据表中的字段一样,每个字段描述了这个字段下的数据的含义。而J2SE5.0中提供的注释就是java源代码的元数据,也就是说注释是描述java源代码的。在J2SE5.0中可以自定义注释。使用时在@后面跟注释的名字。
                                                                                   
二、J2SE5.0中预定义的注释

    在J2SE5.0的java.lang包中预定义了三个注释。它们是Override、Deprecated和SuppressWarnings。下面分别解释它们的含义。

       1.Override注释:仅用于方法(不可用于类、包的生命或其他),指明注释的方法将覆盖超类中的方法(如果覆盖父类的方法而没有注
释就无法编译该类),注释还能确保注释父类方法的拼写是正确(错误的编写,编译器不认为是子类的新方法,而会报错)
       2.@Deprecated注释:对不应再使用的方法进行注释,与正在声明为过时的方法放在同一行。使用被     Deprecated注释的方法,编译器会
提示方法过时警告(”Warring”)
       3.@SuppressWarnings注释:单一注释,可以通过数组提供变量,变量值指明要阻止的特定类型警告(忽略某些警告)。数组中的变量指明要阻止的警告@SuppressWarnings(value={”unchecked”,”fallthrough”}))

三、自定义注释@interface

@interface:注释声明,定义注释类型(与默认的Override等三种注释类型类似)。请看下面实例:

注释类1:

package a.test;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FirstAnno {
String value() default "FirstAnno";
}

注释类2:

package a.test;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SecondAnnotation {
//  注释中含有两个参数
    String name() default "Hrmzone";
    String url() default "hrmzone.cn";

}

注释类3:

package a.test;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Kitto {
String value() default "kitto";
}

使用类:

package a.test;
@FirstAnno("http://hrmzone.cn")
public class Anno {
@Kitto("测试")
private String test = "";
//  不赋值注释中的参数,使用默认参数
    @SecondAnnotation()
    public String getDefault() {
            return "get default Annotation";
    }
    @SecondAnnotation(name="desktophrm",url="desktophrm.com")
    public String getDefine() {
            return "get define Annotation";
    }

}

测试类:

package a.test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class AnnoTest {
public static void main(String[] args) throws ClassNotFoundException {
  // 要使用到反射中的相关内容
  Class c = Class.forName("a.test.Anno");
  Method[] method = c.getMethods();
  boolean flag = c.isAnnotationPresent(FirstAnno.class);
  if (flag) {
   FirstAnno first = (FirstAnno) c.getAnnotation(FirstAnno.class);
   System.out.println("First Annotation:" + first.value() + "\n");
  }

  List<Method> list = new ArrayList<Method>();
  for (int i = 0; i < method.length; i++) {
    list.add(method[i]);
  }

  for (Method m : list) {
   SecondAnnotation anno = m.getAnnotation(SecondAnnotation.class);
   if(anno == null)
    continue;
  
   System.out.println("second annotation's\nname:\t" + anno.name()
     + "\nurl:\t" + anno.url());
  }
 
  List<Field> fieldList = new ArrayList<Field>();
  for(Field f : c.getDeclaredFields()){//访问所有字段
   Kitto k = f.getAnnotation(Kitto.class);
   System.out.println("----kitto anno: " + k.value());
  }
}

}


结合源文件中注释,想必对注释的应用有所了解。下面深入了解。
     深入注释:
     @Target:指定程序元定义的注释所使用的地方,它使用了另一个类:ElementType,是一个枚举类定义了注释类型可以应用到不同的程序元素以免使用者误用。看看java.lang.annotation 下的源代码:

@Documented 
@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.ANNOTATION_TYPE) 
public @interface Target { 
    ElementType[] value(); 
}


     ElementType是一个枚举类型,指明注释可以使用的地方,看看ElementType类:
public enum ElementType { 
     TYPE, // 指定适用点为 class, interface, enum 
     FIELD, // 指定适用点为 field 
     METHOD, // 指定适用点为 method 
     PARAMETER, // 指定适用点为 method 的 parameter 
     CONSTRUCTOR, // 指定适用点为 constructor 
     LOCAL_VARIABLE, // 指定使用点为 局部变量 
     ANNOTATION_TYPE, //指定适用点为 annotation 类型 
     PACKAGE // 指定适用点为 package 
}
     @Retention:这个元注释和java编译器处理注释的注释类型方式相关,告诉编译器在处理自定义注释类型的几种不同的选择,需要使用RetentionPolicy枚举类。此枚举类只有一个成员变量,可以不用指明成名名称而赋值,看Retention的源代码:

@Documented 
@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.ANNOTATION_TYPE) 
public @interface Retention { 
    RetentionPolicy value(); 
}
     类中有个RetentionPolicy类,也是一个枚举类,具体看代码:

public enum RetentionPolicy { 
     SOURCE, // 编译器处理完Annotation后不存储在class中 
     CLASS, // 编译器把Annotation存储在class中,这是默认值 
     RUNTIME // 编译器把Annotation存储在class中,可以由虚拟机读取,反射需要 
}
     @Documented:是一个标记注释,表示注释应该出现在类的javadoc中,因为在默认情况下注释时不包括在javadoc中的。

所以如果花费了大量的时间定义一个注释类型,并想描述注释类型的作用,可以使用它。

注意他与@Retention(RetentionPolicy.RUNTIME)配合使用,因为只有将注释保留在编译后的类文件中由虚拟机加载,

然后javadoc才能将其抽取出来添加至javadoc中。
     @Inherited:将注释同样继承至使用了该注释类型的方法中(表达有点问题,就是如果一个方法使用了的注释用了@inherited,

那么其子类的该方法同样继承了该注释)
注意事项:
     1.所有的Annotation自动继承java.lang.annotation接口
     2.自定义注释的成员变量访问类型只能是public、default;(所有的都能访问,源作者没用到函数:getDeclaredFields而已)
     3.成员变量的只能使用基本类型(byte、short、int、char、long、double、float、boolean和String、Enum、Class、annotations以及该类型的数据)(没有限制,大家可以修改测试一下,就清楚)
     4.如果只有一个成员变量,最好将参数名称设为value,赋值时不用制定名称而直接赋值
     5.在实际应用中,还可以使用注释读取和设置Bean中的变量。
posted @ 2011-10-25 10:07 昊天 阅读(7857) | 评论 (0)编辑 收藏

1 前言 2
2 总纲 2
3 降龙十八掌 3
第一掌 避免对列的操作 3
第二掌 避免不必要的类型转换 4
第三掌 增加查询的范围限制 4
第四掌 尽量去掉”IN”、”OR” 4
第五掌 尽量去掉 “<>” 5
第六掌 去掉Where子句中的IS NULL和IS NOT NULL 5
第七掌 索引提高数据分布不均匀时查询效率 5
第八掌 利用HINT强制指定索引 6
第九掌 屏蔽无用索引 6
第十掌 分解复杂查询,用常量代替变量 7
第十一掌 like子句尽量前端匹配 7
第十二掌 用Case语句合并多重扫描 7
第十三掌 使用nls_date_format 8
第十四掌 使用基于函数的索引 8
第十五掌 基于函数的索引要求等式匹配 9
第十六掌 使用分区索引 9
第十七掌 使用位图索引 9
第十八掌 决定使用全表扫描还是使用索引 9
4 总结 10

1 前言
客服业务受到SQL语句的影响非常大,在规模比较大的局点,往往因为一个小的SQL语句不够优化,导致数据库性能急剧下降,小型机idle所剩无几,应用 服务器断连、超时,严重影响业务的正常运行。因此,称低效的SQL语句为客服业务的‘恶龙’并不过分。数据库的优化方法有很多种,在应用层来说,主要是基 于索引的优化。本次秘笈根据实际的工作经验,在研发原来已有的方法的基础上,进行了一些扩充,总结了基于索引的SQL语句优化的降龙十八掌,希望有一天你 能用其中一掌来驯服客服业务中横行的‘恶龙’。
2 总纲
l 建立必要的索引
这次传授的降龙十八掌,总纲只有一句话:建立必要的索引,这就是后面降龙十八掌的内功基础。这一点看似容易实际却很难。难就难在如何判断哪些索引是必要 的,哪些又是不必要的。判断的最终标准是看这些索引是否对我们的数据库性能有所帮助。具体到方法上,就必须熟悉数据库应用程序中的所有SQL语句,从中统 计出常用的可能对性能有影响的部分SQL,分析、归纳出作为Where条件子句的字段及其组合方式;在这一基础上可以初步判断出哪些表的哪些字段应该建立 索引。其次,必须熟悉应用程序。必须了解哪些表是数据操作频繁的表;哪些表经常与其他表进行连接;哪些表中的数据量可能很大;对于数据量大的表,其中各个 字段的数据分布情况如何;等等。对于满足以上条件的这些表,必须重点关注,因为在这些表上的索引,将对SQL语句的性能产生举足轻重的影响。不过下面还是 总结了一下降龙十八掌内功的入门基础,建立索引常用的规则如下:
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
A、正确选择复合索引中的主列字段,一般是选择性较好的字段;
B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;
E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
8、频繁进行数据操作的表,不要建立太多的索引;
9、删除无用的索引,避免对执行计划造成负面影响;
以上是一些普遍的建立索引时的判断依据。一言以蔽之,索引的建立必须慎重,对每个索引的必要性都应该经过仔细分析,要有建立的依据。因为太多的索引与不充 分、不正确的索引对性能都毫无益处:在表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。 另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更 大。
3 降龙十八掌
第一掌 避免对列的操作
任何对列的操作都可能导致全表扫描,这里所谓的操作包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等式的右边,甚至去掉函数。
例1:下列SQL条件语句中的列都建有恰当的索引,但30万行数据情况下执行速度却非常慢:
select * from record where substrb(CardNo,1,4)=’5378′(13秒)
select * from record where amount/30< 1000(11秒)
select * from record where to_char(ActionTime,’yyyymmdd’)=’19991201′(10秒)
由于where子句中对列的任何操作结果都是在SQL运行时逐行计算得到的,因此它不得不进行表扫描,而没有使用该列上面的索引;如果这些结果在查询编译时就能得到,那么就可以被SQL优化器优化,使用索引,避免表扫描,因此将SQL重写如下:
select * from record where CardNo like ‘5378%’(< 1秒)
select * from record where amount < 1000*30(< 1秒)
select * from record where ActionTime= to_date (’19991201′ ,’yyyymmdd’)(< 1秒)
差别是很明显的!
第二掌 避免不必要的类型转换
需要注意的是,尽量避免潜在的数据类型转换。如将字符型数据与数值型数据比较,ORACLE会自动将字符型用to_number()函数进行转换,从而导致全表扫描。
例2:表tab1中的列col1是字符型(char),则以下语句存在类型转换:
select col1,col2 from tab1 where col1>10,
应该写为: select col1,col2 from tab1 where col1>’10′。
第三掌 增加查询的范围限制
增加查询的范围限制,避免全范围的搜索。
例3:以下查询表record 中时间ActionTime小于2001年3月1日的数据:
select * from record where ActionTime < to_date (’20010301′ ,’yyyymm’)
查询计划表明,上面的查询对表进行全表扫描,如果我们知道表中的最早的数据为2001年1月1日,那么,可以增加一个最小时间,使查询在一个完整的范围之内。修改如下: select * from record where
ActionTime < to_date (’20010301′ ,’yyyymm’)
and ActionTime > to_date (’20010101′ ,’yyyymm’)
后一种SQL语句将利用上ActionTime字段上的索引,从而提高查询效率。把’20010301′换成一个变量,根据取值的机率,可以有一半以上的 机会提高效率。同理,对于大于某个值的查询,如果知道当前可能的最大值,也可以在Where子句中加上 “AND 列名< MAX(最大值)”。
第四掌 尽量去掉”IN”、”OR”
含有”IN”、”OR”的Where子句常会使用工作表,使索引失效;如果不产生大量重复值,可以考虑把子句拆开;拆开的子句中应该包含索引。
例4: select count(*) from stuff where id_no in(’0′,’1′)(23秒)
可以考虑将or子句分开:
select count(*) from stuff where id_no=’0′
select count(*) from stuff where id_no=’1′
然后再做一个简单的加法,与原来的SQL语句相比,查询速度更快。
第五掌 尽量去掉 “<>”
尽量去掉 “<>”,避免全表扫描,如果数据是枚举值,且取值范围固定,则修改为”OR”方式。
例5:
UPDATE SERVICEINFO SET STATE=0 WHERE STATE<>0;
以上语句由于其中包含了”<>”,执行计划中用了全表扫描(TABLE ACCESS FULL),没有用到state字段上的索引。实际应用中,由于业务逻辑的限制,字段state为枚举值,只能等于0,1或2,而且,值等于=1,2的很 少,因此可以去掉”<>”,利用索引来提高效率。
修改为:UPDATE SERVICEINFO SET STATE=0 WHERE STATE = 1 OR STATE = 2 。进一步的修改可以参考第4种方法。
第六掌 去掉Where子句中的IS NULL和IS NOT NULL
Where字句中的IS NULL和IS NOT NULL将不会使用索引而是进行全表搜索,因此需要通过改变查询方式,分情况讨论等方法,去掉Where子句中的IS NULL和IS NOT NULL。
第七掌 索引提高数据分布不均匀时查询效率
索引的选择性低,但数据的值分布差异很大时,仍然可以利用索引提高效率。A、数据分布不均匀的特殊情况下,选择性不高的索引也要创建。
表ServiceInfo中数据量很大,假设有一百万行,其中有一个字段DisposalCourseFlag,取值范围为枚举值:[0,1,2,3, 4,5,6,7]。按照前面说的索引建立的规则,“选择性不高的字段不应该建立索引,该字段只有8种取值,索引值的重复率很高,索引选择性明显很低,因此 不建索引。然而,由于该字段上数据值的分布情况非常特殊,具体如下表:
取值范围 1~5 6 7
占总数据量的百分比 1% 98% 1%
而且,常用的查询中,查询DisposalCourseFlag<6 的情况既多又频繁,毫无疑问,如果能够建立索引,并且被应用,那么将大大提高这种情况的查询效率。因此,我们需要在该字段上建立索引。
第八掌 利用HINT强制指定索引
在ORACLE优化器无法用上合理索引的情况下,利用HINT强制指定索引。
继续上面7的例子,ORACLE缺省认定,表中列的值是在所有数据行中均匀分布的,也就是说,在一百万数据量下,每种 DisposalCourseFlag值各有12.5万数据行与之对应。假设SQL搜索条件DisposalCourseFlag=2,利用 DisposalCourseFlag列上的索引进行数据搜索效率,往往不比全表扫描的高,ORACLE因此对索引“视而不见”,从而在查询路径的选择 中,用其他字段上的索引甚至全表扫描。根据我们上面的分析,数据值的分布很特殊,严重的不均匀。为了利用索引提高效率,此时,一方面可以单独对该字段或该 表用analyze语句进行分析,对该列搜集足够的统计数据,使ORACLE在查询选择性较高的值时能用上索引;另一方面,可以利用HINT提示,在 SELECT关键字后面,加上“/*+ INDEX(表名称,索引名称)*/”的方式,强制ORACLE优化器用上该索引。
比如: select * from serviceinfo where DisposalCourseFlag=1 ;
上面的语句,实际执行中ORACLE用了全表扫描,加上蓝色提示部分后,用到索引查询。如下:
select /*+ INDEX(SERVICEINFO,IX_S_DISPOSALCOURSEFLAG) */ *
from serviceinfo where DisposalCourseFlag=1;
请注意,这种方法会加大代码维护的难度,而且该字段上索引的名称被改变之后,必须要同步所有指定索引的HINT代码,否则HINT提示将被ORACLE忽略掉。
第九掌 屏蔽无用索引
继续上面8的例子,由于实际查询中,还有涉及到DisposalCourseFlag=6的查询,而此时如果用上该字段上的索引,将是非常不明智的,效率 也极低。因此这种情况下,我们需要用特殊的方法屏蔽该索引,以便ORACLE选择其他字段上的索引。比如,如果字段为数值型的就在表达式的字段名后,添加 “+ 0”,为字符型的就并上空串:“||”"”
如: select * from serviceinfo where DisposalCourseFlag+ 0 = 6 and workNo = ‘36′ 。
不过,不要把该用的索引屏蔽掉了,否则同样会产生低效率的全表扫描。
第十掌 分解复杂查询,用常量代替变量
对于复杂的Where条件组合,Where中含有多个带索引的字段,考虑用IF语句分情况进行讨论;同时,去掉不必要的外来参数条件,减低复杂度,以便在不同情况下用不同字段上的索引。
继续上面9的例子,对于包含
Where (DisposalCourseFlag < v_DisPosalCourseFlag) or (v_DisPosalCourseFlag is null) and ….的查询,(这里v_DisPosalCourseFlag为一个输入变量,取值范围可能为[NULL,0,1,2,3,4,5,6,7]),可以 考虑分情况用IF语句进行讨论,类似:
IF v_DisPosalCourseFlag =1 THEN
Where DisposalCourseFlag = 1 and ….
ELSIF v_DisPosalCourseFlag =2 THEN
Where DisposalCourseFlag = 2 and ….
。。。。。。
第十一掌 like子句尽量前端匹配
因为like参数使用的非常频繁,因此如果能够对like子句使用索引,将很高的提高查询的效率。
例6:select * from city where name like ‘%S%’
以上查询的执行计划用了全表扫描(TABLE ACCESS FULL),如果能够修改为:
select * from city where name like ‘S%’
那么查询的执行计划将会变成(INDEX RANGE SCAN),成功的利用了name字段的索引。这意味着Oracle SQL优化器会识别出用于索引的like子句,只要该查询的匹配端是具体值。因此我们在做like查询时,应该尽量使查询的匹配端是具体值,即使用 like ‘S%’。
第十二掌 用Case语句合并多重扫描
我们常常必须基于多组数据表计算不同的聚集。例如下例通过三个独立查询:
例8:1)select count(*) from emp where sal<1000;
2)select count(*) from emp where sal between 1000 and 5000;
3)select count(*) from emp where sal>5000;
这样我们需要进行三次全表查询,但是如果我们使用case语句:
select
count (sale when sal <1000
then 1 else null end) count_poor,
count (sale when between 1000 and 5000
then 1 else null end) count_blue_collar,
count (sale when sal >5000
then 1 else null end) count_poor
from emp;
这样查询的结果一样,但是执行计划只进行了一次全表查询。
第十三掌 使用nls_date_format
例9:
select * from record where to_char(ActionTime,’mm’)=’12′
这个查询的执行计划将是全表查询,如果我们改变nls_date_format,
SQL>alert session set nls_date_formate=’MM’;
现在重新修改上面的查询:
select * from record where ActionTime=’12′
这样就能使用actiontime上的索引了,它的执行计划将是(INDEX RANGE SCAN)。
第十四掌 使用基于函数的索引
前面谈到任何对列的操作都可能导致全表扫描,例如:
select * from emp where substr(ename,1,2)=’SM’;
但是这种查询在客服系统又经常使用,我们可以创建一个带有substr函数的基于函数的索引,
create index emp_ename_substr on eemp ( substr(ename,1,2) );
这样在执行上面的查询语句时,这个基于函数的索引将排上用场,执行计划将是(INDEX RANGE SCAN)。
第十五掌 基于函数的索引要求等式匹配
上面的例子中,我们创建了基于函数的索引,但是如果执行下面的查询:
select * from emp where substr(ename,1,1)=’S’
得到的执行计划将还是(TABLE ACCESS FULL),因为只有当数据列能够等式匹配时,基于函数的索引才能生效,这样对于这种索引的计划和维护的要求都很高。请注意,向表中添加索引是非常危险的 操作,因为这将导致许多查询执行计划的变更。然而,如果我们使用基于函数的索引就不会产生这样的问题,因为Oracle只有在查询使用了匹配的内置函数时 才会使用这种类型的索引。
第十六掌 使用分区索引
在用分析命令对分区索引进行分析时,每一个分区的数据值的范围信息会放入Oracle的数据字典中。Oracle可以利用这个信息来提取出那些只与SQL查询相关的数据分区。
例如,假设你已经定义了一个分区索引,并且某个SQL语句需要在一个索引分区中进行一次索引扫描。Oracle会仅仅访问这个索引分区,而且会在这个分区上调用一个此索引范围的快速全扫描。因为不需要访问整个索引,所以提高了查询的速度。
第十七掌 使用位图索引
位图索引可以从本质上提高使用了小于1000个唯一数据值的数据列的查询速度,因为在位图索引中进行的检索是在RAM中完成的,而且也总是比传统的B树索引的速度要快。对于那些少于1000个唯一数据值的数据列建立位图索引,可以使执行效率更快。
第十八掌 决定使用全表扫描还是使用索引
和所有的秘笈一样,最后一招都会又回到起点,最后我们来讨论一下是否需要建立索引,也许进行全表扫描更快。在大多数情况下,全表扫描可能会导致更多的物理 磁盘输入输出,但是全表扫描有时又可能会因为高度并行化的存在而执行的更快。如果查询的表完全没有顺序,那么一个要返回记录数小于10%的查询可能会读取 表中大部分的数据块,这样使用索引会使查询效率提高很多。但是如果表非常有顺序,那么如果查询的记录数大于40%时,可能使用全表扫描更快。因此,有一个 索引范围扫描的总体原则是:
1)对于原始排序的表 仅读取少于表记录数40%的查询应该使用索引范围扫描。反之,读取记录数目多于表记录数的40%的查询应该使用全表扫描。
2)对于未排序的表 仅读取少于表记录数7%的查询应该使用索引范围扫描。反之,读取记录数目多于表记录数的7%的查询应该使用全表扫描。
4 总结
以上的招式,是完全可以相互结合同时运用的。而且各种方法之间相互影响,紧密联系。这种联系既存在一致性,也可能带来冲突,当冲突发生时,需要根据实际情况进行选择,没有固定的模式。最后决定SQL优化功力的因素就是对ORACLE内功的掌握程度了。
另外,值得注意的是:随着时间的推移和数据的累计与变化,ORACLE对SQL语句的执行计划也会改变,比如:基于代价的优化方法,随着数据量的增大,优 化器可能错误的不选择索引而采用全表扫描。这种情况可能是因为统计信息已经过时,在数据量变化很大后没有及时分析表;但如果对表进行分析之后,仍然没有用 上合理的索引,那么就有必要对SQL语句用HINT提示,强制用合理的索引。但这种HINT提示也不能滥用,因为这种方法过于复杂,缺乏通用性和应变能 力,同时也增加了维护上的代价;相对来说,基于函数右移、去掉“IN ,OR ,<> ,IS NOT NULL ”、分解复杂的SQL语句等等方法,却是“放之四海皆准”的,可以放心大胆的使用。
同时,优化也不是“一劳永逸”的,必须随着情况的改变进行相应的调整。当数据库设计发生变化,包括更改表结构:字段和索引的增加、删除或改名等;业务逻辑发生变化:如查询方式、取值范围发生改变等等。在这种情况下,也必须对原有的优化进行调整,以适应效率上的需求。
posted @ 2011-10-21 16:46 昊天 阅读(479) | 评论 (0)编辑 收藏
1 什么是单点登陆
      单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
      较大的企业内部,一般都有很多的业务支持系统为其提供相应的管理和IT服务。例如财务系统为财务人员提供财务的管理、计算和报表服务;人事系统为人事部门 提供全公司人员的维护服务;各种业务系统为公司内部不同的业务提供不同的服务等等。这些系统的目的都是让计算机来进行复杂繁琐的计算工作,来替代人力的手 工劳动,提高工作效率和质量。这些不同的系统往往是在不同的时期建设起来的,运行在不同的平台上;也许是由不同厂商开发,使用了各种不同的技术和标准。如 果举例说国内一著名的IT公司(名字隐去),内部共有60多个业务系统,这些系统包括两个不同版本的SAP的ERP系统,12个不同类型和版本的数据库系 统,8个不同类型和版本的操作系统,以及使用了3种不同的防火墙技术,还有数十种互相不能兼容的协议和标准,你相信吗?不要怀疑,这种情况其实非常普遍。 每一个应用系统在运行了数年以后,都会成为不可替换的企业IT架构的一部分,如下图所示。

      随着企业的发展,业务系统的数量在不断的增加,老的系统却不能轻易的替换,这会带来很多的开销。其一是管理上的开销,需要维护的系统越来越多。很多系统的 数据是相互冗余和重复的,数据的不一致性会给管理工作带来很大的压力。业务和业务之间的相关性也越来越大,例如公司的计费系统和财务系统,财务系统和人事 系统之间都不可避免的有着密切的关系。

      为了降低管理的消耗,最大限度的重用已有投资的系统,很多企业都在进行着企业应用集成(EAI)。企业应用集成可以在不同层面上进行:例如在数据存储层面 上的“数据大集中”,在传输层面上的“通用数据交换平台”,在应用层面上的“业务流程整合”,和用户界面上的“通用企业门户”等等。事实上,还用一个层面 上的集成变得越来越重要,那就是“身份认证”的整合,也就是“单点登录”。

      通常来说,每个单独的系统都会有自己的安全体系和身份认证系统。整合以前,进入每个系统都需要进行登录,这样的局面不仅给管理上带来了很大的困难,在安全方面也埋下了重大的隐患。下面是一些著名的调查公司显示的统计数据:
  • 用户每天平均16分钟花在身份验证任务上 - 资料来源:IDS
  • 频繁的IT用户平均有21个密码 - 资料来源:NTA Monitor Password Survey
  • 49%的人写下了其密码,而67%的人很少改变它们
  • 每79秒出现一起身份被窃事件 - 资料来源:National Small Business Travel Assoc
  • 全球欺骗损失每年约12B - 资料来源:Comm Fraud Control Assoc
  • 到2007年,身份管理市场将成倍增长至$4.5B - 资料来源:IDS

      使用“单点登录”整合后,只需要登录一次就可以进入多个系统,而不需要重新登录,这不仅仅带来了更好的用户体验,更重要的是降低了安全的风险和管理的消耗。请看下面的统计数据:
  • 提高IT效率:对于每1000个受管用户,每用户可节省$70K
  • 帮助台呼叫减少至少1/3,对于10K员工的公司,每年可以节省每用户$75,或者合计$648K
  • 生产力提高:每个新员工可节省$1K,每个老员工可节省$350 - 资料来源:Giga
  • ROI回报:7.5到13个月 - 资料来源:Gartner   

      另外,使用“单点登录”还是SOA时代的需求之一。在面向服务的架构中,服务和服务之间,程序和程序之间的通讯大量存在,服务之间的安全认证是SOA应用的难点之一,应此建立“单点登录”的系统体系能够大大简化SOA的安全问题,提高服务之间的合作效率。
   2 单点登陆的技术实现机制
      随着SSO技术的流行,SSO的产品也是满天飞扬。所有著名的软件厂商都提供了相应的解决方案。在这里我并不想介绍自己公司(Sun Microsystems)的产品,而是对SSO技术本身进行解析,并且提供自己开发这一类产品的方法和简单演示。有关我写这篇文章的目的,请参考我的博 客(http://yuwang881.blog.sohu.com/3184816.html)。

      单点登录的机制其实是比较简单的,用一个现实中的例子做比较。颐和园是北京著名的旅游景点,也是我常去的地方。在颐和园内部有许多独立的景点,例如“苏州 街”、“佛香阁”和“德和园”,都可以在各个景点门口单独买票。很多游客需要游玩所有德景点,这种买票方式很不方便,需要在每个景点门口排队买票,钱包拿 进拿出的,容易丢失,很不安全。于是绝大多数游客选择在大门口买一张通票(也叫套票),就可以玩遍所有的景点而不需要重新再买票。他们只需要在每个景点门 口出示一下刚才买的套票就能够被允许进入每个独立的景点。
      单点登录的机制也一样,如下图所示,当用户第一次访问应用系统1的时候,因为还没有登录,会被引导到认证系统中进行登录(1);根据用户提供的登录信息, 认证系统进行身份效验,如果通过效验,应该返回给用户一个认证的凭据--ticket(2);用户再访问别的应用的时候(3,5)就会将这个ticket 带上,作为自己认证的凭据,应用系统接受到请求之后会把ticket送到认证系统进行效验,检查ticket的合法性(4,6)。如果通过效验,用户就可 以在不用再次登录的情况下访问应用系统2和应用系统3了。
  
      从上面的视图可以看出,要实现SSO,需要以下主要的功能:
  • 所有应用系统共享一个身份认证系统。
    统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket),返还给用户。另外,认证系统还应该对ticket进行效验,判断其有效性。
  • 所有应用系统能够识别和提取ticket信息
    要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。   

      上面的功能只是一个非常简单的SSO架构,在现实情况下的SSO有着更加复杂的结构。有两点需要指出的是:
  • 单一的用户信息数据库并不是必须的,有许多系统不能将所有的用户信息都集中存储,应该允许用户信息放置在不同的存储中,如下图所示。事实上,只要统一认证系统,统一ticket的产生和效验,无论用户信息存储在什么地方,都能实现单点登录。    


  • 统一的认证系统并不是说只有单个的认证服务器,如下图所示,整个系统可以存在两个以上的认证服务器,这些服务器甚至可以是不同的产品。认证服务器 之间要通过标准的通讯协议,互相交换认证信息,就能完成更高级别的单点登录。如下图,当用户在访问应用系统1时,由第一个认证服务器进行认证后,得到由此 服务器产生的ticket。当他访问应用系统4的时候,认证服务器2能够识别此ticket是由第一个服务器产生的,通过认证服务器之间标准的通讯协议 (例如SAML)来交换认证信息,仍然能够完成SSO的功能。

     
    3 WEB-SSO的实现
      随着互联网的高速发展,WEB应用几乎统治了绝大部分的软件应用系统,因此WEB-SSO是SSO应用当中最为流行。WEB-SSO有其自身的特点和优 势,实现起来比较简单易用。很多商业软件和开源软件都有对WEB-SSO的实现。其中值得一提的是OpenSSO (https://opensso.dev.java.net),为用Java实现WEB-SSO提供架构指南和服务指南,为用户自己来实现WEB-SSO提供了理论的依据和实现的方法。  
      为什么说WEB-SSO比较容易实现呢?这是有WEB应用自身的特点决定的。
      众所周知,Web协议(也就是HTTP)是一个无状态的协议。一个Web应用由很多个Web页面组成,每个页面都有唯一的URL来定义。用户在浏览器的地 址栏输入页面的URL,浏览器就会向Web Server去发送请求。如下图,浏览器向Web服务器发送了两个请求,申请了两个页面。这两个页面的请求是分别使用了两个单独的HTTP连接。所谓无状 态的协议也就是表现在这里,浏览器和Web服务器会在第一个请求完成以后关闭连接通道,在第二个请求的时候重新建立连接。Web服务器并不区分哪个请求来 自哪个客户端,对所有的请求都一视同仁,都是单独的连接。这样的方式大大区别于传统的(Client/Server)C/S结构,在那样的应用中,客户端 和服务器端会建立一个长时间的专用的连接通道。正是因为有了无状态的特性,每个连接资源能够很快被其他客户端所重用,一台Web服务器才能够同时服务于成 千上万的客户端。

      但是我们通常的应用是有状态的。先不用提不同应用之间的SSO,在同一个应用中也需要保存用户的登录身份信息。例如用户在访问页面1的时候进行了登录,但 是刚才也提到,客户端的每个请求都是单独的连接,当客户再次访问页面2的时候,如何才能告诉Web服务器,客户刚才已经登录过了呢?浏览器和服务器之间有 约定:通过使用cookie技术来维护应用的状态。Cookie是可以被Web服务器设置的字符串,并且可以保存在浏览器中。如下图所示,当浏览器访问了 页面1时,web服务器设置了一个cookie,并将这个cookie和页面1一起返回给浏览器,浏览器接到cookie之后,就会保存起来,在它访问页 面2的时候会把这个cookie也带上,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状 态。

      Web-SSO完全可以利用Cookie结束来完成用户登录信息的保存,将浏览器中的Cookie和上文中的Ticket结合起来,完成SSO的功能。

      为了完成一个简单的SSO的功能,需要两个部分的合作:
  • 统一的身份认证服务。
  • 修改Web应用,使得每个应用都通过这个统一的认证服务来进行身份效验。

   3.1 Web SSO 的样例
      根据上面的原理,我用J2EE的技术(JSP和Servlet)完成了一个具有Web-SSO的简单样例。样例包含一个身份认证的服务器和两个简单的 Web应用,使得这两个 Web应用通过统一的身份认证服务来完成Web-SSO的功能。此样例所有的源代码和二进制代码都可以从网站地址http://gceclub.sun.com.cn/wangyu/下载。

      样例下载、安装部署和运行指南:
  • Web-SSO的样例是由三个标准Web应用组成,压缩成三个zip文件,从http://gceclub.sun.com.cn/wangyu/web-sso/中下载。其中SSOAuth(http://gceclub.sun.com.cn/wangyu/web-sso/SSOAuth.zip)是身份认证服务;SSOWebDemo1(http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo1.zip)和SSOWebDemo2(http://gceclub.sun.com.cn/wangyu/web-sso/SSOWebDemo2.zip) 是两个用来演示单点登录的Web应用。这三个Web应用之所以没有打成war包,是因为它们不能直接部署,根据读者的部署环境需要作出小小的修改。样例部 署和运行的环境有一定的要求,需要符合Servlet2.3以上标准的J2EE容器才能运行(例如Tomcat5,Sun Application Server 8, Jboss 4等)。另外,身份认证服务需要JDK1.5的运行环境。之所以要用JDK1.5是因为笔者使用了一个线程安全的高性能的Java集合类 “ConcurrentMap”,只有在JDK1.5中才有。
  • 这三个Web应用完全可以单独部署,它们可以分别部署在 不同的机器,不同的操作系统和不同的J2EE的产品上,它们完全是标准的和平台无关的应用。但是有一个限制,那两台部署应用(demo1、demo2)的 机器的域名需要相同,这在后面的章节中会解释到cookie和domain的关系以及如何制作跨域的WEB-SSO
    解压缩SSOAuth.zip文件,在/WEB-INF/下的web.xml中请修改“domainname”的属性以反映实际的应用部署情况, domainname需要设置为两个单点登录的应用(demo1和demo2)所属的域名。这个domainname和当前SSOAuth服务部署的机器 的域名没有关系。我缺省设置的是“.sun.com”。如果你部署demo1和demo2的机器没有域名,请输入IP地址或主机名(如 localhost),但是如果使用IP地址或主机名也就意味着demo1和demo2需要部署到一台机器上了。设置完后,根据你所选择的J2EE容器, 可能需要将SSOAuth这个目录压缩打包成war文件。用“jar -cvf SSOAuth.war SSOAuth/”就可以完成这个功能。
  • 解压缩SSOWebDemo1和SSOWebDemo2文件,分别在它们/WEB-INF/下找到web.xml文件,请修改其中的几个初始化参数

<init-param>
<param-name>SSOServiceURL</param-name>
<param-value>http://wangyu.prc.sun.com:8080/SSOAuth/SSOAuth</param-value>
</init-param>
<init-param>
<param-name>SSOLoginPage</param-name>
<param-value>http://wangyu.prc.sun.com:8080/SSOAuth/login.jsp</param-value>
</init-param>
  • 将其中的SSOServiceURL和SSOLoginPage修改成部署SSOAuth应用的机器名、端口号以及根路径(缺省是 SSOAuth)以反映实际的部署情况。设置完后,根据你所选择的J2EE容器,可能需要将SSOWebDemo1和SSOWebDemo2这两个目录压 缩打包成两个war文件。用“jar -cvf SSOWebDemo1.war SSOWebDemo1/”就可以完成这个功能。
  • 请输入第一个web应用的测试URL(test.jsp),例如http://wangyu.prc.sun.com:8080/SSOWebDemo1/test.jsp,如果是第一次访问,便会自动跳转到登录界面,如下图。

  • 使用系统自带的三个帐号之一登录(例如,用户名:wangyu,密码:wangyu),便能成功的看到test.jsp的内容:显示当前用户名和欢迎信息。

  • 请接着在同一个浏览器中输入第二个web应用的测试URL(test.jsp),例如http://wangyu.prc.sun.com:8080/SSOWebDemo2/test.jsp。你会发现,不需要再次登录就能看到test.jsp的内容,同样是显示当前用户名和欢迎信息,而且欢迎信息中明确的显示当前的应用名称(demo2)。   


   3.2 WEB-SSO代码讲解
      3.2.1身份认证服务代码解析
      Web-SSO的源代码可以从网站地址http://gceclub.sun.com.cn/wangyu/web-sso/websso_src.zip下 载。身份认证服务是一个标准的web应用,包括一个名为SSOAuth的Servlet,一个login.jsp文件和一个failed.html。身份 认证的所有服务几乎都由SSOAuth的Servlet来实现了;login.jsp用来显示登录的页面(如果发现用户还没有登录过); failed.html是用来显示登录失败的信息(如果用户的用户名和密码与信息数据库中的不一样)。
  
      SSOAuth的代码如下面的列表显示,结构非常简单,先看看这个Servlet的主体部分:
package DesktopSSO;

import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;

import javax.servlet.*;
import javax.servlet.http.*;


public class SSOAuth extends HttpServlet {
    
      static private ConcurrentMap accounts;
      static private ConcurrentMap SSOIDs;
      String cookiename="WangYuDesktopSSOID";
      String domainname;
    
      public void init(ServletConfig config) throws ServletException {
          super.init(config);
          domainname= config.getInitParameter("domainname");
          cookiename = config.getInitParameter("cookiename");
          SSOIDs = new ConcurrentHashMap();
          accounts=new ConcurrentHashMap();
          accounts.put("wangyu", "wangyu");
          accounts.put("paul", "paul");
          accounts.put("carol", "carol");
      }

      protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          PrintWriter ut = response.getWriter();
          String action = request.getParameter("action");
          String result="failed";
          if (action==null) {
              handlerFromLogin(request,response);
          } else if (action.equals("authcookie")){
              String myCookie = request.getParameter("cookiename");
              if (myCookie != null) result = authCookie(myCookie);
              out.print(result);
              out.close();
          } else if (action.equals("authuser")) {
             result=authNameAndPasswd(request,response);
              out.print(result);
              out.close();
          } else if (action.equals("logout")) {
              String myCookie = request.getParameter("cookiename");
              logout(myCookie);
              out.close();
          }
      }

.....

}

      从代码很容易看出,SSOAuth就是一个简单的Servlet。其中有两个静态成员变量:accounts和SSOIDs,这两个成员变量都使用了 JDK1.5中线程安全的MAP类: ConcurrentMap,所以这个样例一定要JDK1.5才能运行。Accounts用来存放用户的用户名和密码,在init()的方法中可以看到我 给系统添加了三个合法的用户。在实际应用中,accounts应该是去数据库中或LDAP中获得,为了简单起见,在本样例中我使用了 ConcurrentMap在内存中用程序创建了三个用户。而SSOIDs保存了在用户成功的登录后所产生的cookie和用户名的对应关系。它的功能显 而易见:当用户成功登录以后,再次访问别的系统,为了鉴别这个用户请求所带的cookie的有效性,需要到SSOIDs中检查这样的映射关系是否存在。

      在主要的请求处理方法processRequest()中,可以很清楚的看到SSOAuth的所有功能。
  • 如果用户还没有登录过,是第一次登录本系统,会被跳转到login.jsp页面(在后面会解释如何跳转)。用户在提供了用户名和密码以后,就会用handlerFromLogin()这个方法来验证。
  • 如果用户已经登录过本系统,再访问别的应用的时候,是不需要再次登录的。因为浏览器会将第一次登录时产生的cookie和请求一起发送。效验cookie的有效性是SSOAuth的主要功能之一。
  • SSOAuth还能直接效验非login.jsp页面过来的用户名和密码的效验请求。这个功能是用于非web应用的SSO,这在后面的桌面SSO中会用到。
  • SSOAuth还提供logout服务。   


      下面看看几个主要的功能函数:
private void handlerFromLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          String username = request.getParameter("username");
          String password = request.getParameter("password");
          String pass = (String)accounts.get(username);
          if ((pass==null)||(!pass.equals(password)))
              getServletContext().getRequestDispatcher("/failed.html").forward(request, response);
          else {
              String gotoURL = request.getParameter("goto");
              String newID = createUID();
              SSOIDs.put(newID, username);
              Cookie wangyu = new Cookie(cookiename, newID);
              wangyu.setDomain(domainname);
              wangyu.setMaxAge(60000);
              wangyu.setValue(newID);
              wangyu.setPath("/");
              response.addCookie(wangyu);
              System.out.println("login success, goto back url:" + gotoURL);
              if (gotoURL != null) {
                  PrintWriter ut = response.getWriter();
                  response.sendRedirect(gotoURL);
                  out.close();
              }
          }   
      }
      handlerFromLogin()这个方法是用来处理来自login.jsp的登录请求。它的逻辑很简单:将用户输入的用户名和密码与预先设定好的用 户集合(存放在accounts中)相比较,如果用户名或密码不匹配的话,则返回登录失败的页面(failed.html),如果登录成功的话,需要为用 户当前的session创建一个新的ID,并将这个ID和用户名的映射关系存放到SSOIDs中,最后还要将这个ID设置为浏览器能够保存的cookie 值。
      登录成功后,浏览器会到哪个页面呢?那我们回顾一下我们是如何使用身份认证服务的。一般来说我们不会直接访问身份服务的任何URL,包括 login.jsp。身份服务是用来保护其他应用服务的,用户一般在访问一个受SSOAuth保护的Web应用的某个URL时,当前这个应用会发现当前的 用户还没有登录,便强制将也页面转向SSOAuth的login.jsp,让用户登录。如果登录成功后,应该自动的将用户的浏览器指向用户最初想访问的那 个URL。在handlerFromLogin()这个方法中,我们通过接收“goto”这个参数来保存用户最初访问的URL,成功后便重新定向到这个页 面中。
      另外一个要说明的是,在设置cookie的时候,我使用了一个setMaxAge(6000)的方法。这个方法是用来设置cookie的有效期,单位是 秒。如果不使用这个方法或者参数为负数的话,当浏览器关闭的时候,这个cookie就失效了。在这里我给了很大的值(1000分钟),导致的行为是:当你 关闭浏览器(或者关机),下次再打开浏览器访问刚才的应用,只要在1000分钟之内,就不需要再登录了。我这样做是下面要介绍的桌面SSO中所需要的功 能。
      其他的方法更加简单,这里就不多解释了。

3.2.2具有SSO功能的web应用源代码解析         要实现WEB-SSO的功能,只有身份认证服务是不够的。这点很显然,要想使多个应用具有单点登录的功能,还需要每个应用本身的配合:将自己的身份认证的 服务交给一个统一的身份认证服务-SSOAuth。SSOAuth服务中提供的各个方法就是供每个加入SSO的Web应用来调用的。
























再转载点内容:


昨天和几位朋友探讨到了这个话题,发现虽然单点登录,或者叫做独立的passport登录虽然已经有了很多实现方法,但是能真正了解并实现的人却并不太多,所以些下此文,希望从原理到实现,能让大家了解的多一些


至于什么是单点登录,举个例子,如果你登录了msn messenger,访问hotmail邮件就不用在此登录。
一般单点登录都需要有一个独立的登录站点,一般具有独立的域名,专门的进行注册,登录,注销等操作

我们为了讨论方便,把这个登录站点叫做站点P,设其Url为http://passport.yizhu2000.com,需要提供服务的站点设为A和B,跨站点单点登录是指你在A网站进行登录后,使用B网站的服务就不需要再登录


从技术角度讲单点登录分为:

  • 跨子域单点登录
  • 完全跨单点域登录

跨子域单点登录

所谓跨子域登录,A,B站点和P站点位于同一个域下面,比如A站点为http://blog.yizhu2000.com     B站点为 http://forum.yizhu2000.com,他们和登录站点P的关系可以看到,都是属于同一个父域,yizhu2000.com,不同的是子域不同,一个为blog,一个为forum,一个是passport

我们先看看最常用的非跨站点普通登录的情况,一般登录验证通过后,一般会将你的用户名和一些用户信息,通过某一密钥进行加密,写在本地,也就是一个加密的cookie,我们把这个cookie叫做--票(ticket)。

需要判断用户是否登录的页面,需要读取这个ticket,并从其中解密出用户信息,如果ticket不存在,或者无法解密,意味着用户没有登录,或者登录信息不正确,这时就要跳转到登录页面进行登录,在这里加密的作用有两个,一是防止用户信息被不怀好意者看到,二是保证ticket不会被伪造,后者其实更为重要,加密后,各个应用需要采用与加密同样的密钥进行解密,如果不知道密钥,就不能伪造出ticket,

(注:加密和解密的密钥有可能不同,取决于采用什么加密算法,如果是对称加密,则为同一密钥,如果是非对称,就不同了,一般用私钥加密,公钥解密,但是无论怎样,密钥都只有内部知道,这样伪造者既无法伪造也无法解密ticket)

跨子域的单点登录,和上述普通登录的过程没有什么不同,唯一不同的是写cookie时,由于登录站点P和应用A处于不同的子域,P站写入的cookie的域为passport.yizhu2000.net,而A站点为forum.yizhu2000.net,A在判断用户登录时无法读到P站点的ticket

解决方法非常简单,当Login完成后P站点写ticket的时候,只需把cookie的域设为他们共同的父域,yizhu2000.net就可以了:cookie.domain="yizhu2000.net",A站点自然就可以读到这个ticket了

ASP。Net的form验证本身实现了这个机制,大家可以参考http://blog.csdn.net/octverve/archive/2007/09/22/1796338.aspx

ASP.NET身份验证信息跨域共享状态

在ASP.NET 2.0 中只需修改web.config文件即可,修改方法如下:

<authentication mode="Forms">
<forms name=".ASPNETFORM"   domain="imneio.com" loginUrl="/login.aspx" defaultUrl="/default.aspx" protection="All" timeout="30" path="/" requireSSL="false" slidingExpiration="true" enableCrossAppRedirects="false" cookieless="UseDeviceProfile" />
</authentication>

domain指定了cookie保存的域,只要保存的是 abc.com形式或者.abc.com的形式,那么其二级域名都可以共享此cookie。

此外,web.config标签中的<sessionState >也做相应修改,mode改为StateServer或者SqlServer,那么里面的session信息也就全部可以共享了。

StateServer需要在服务中开启“asp.net状态服务”的服务

http://www.imneio.com/2007/11/17/aspnetnote1/,以上斜体内容摘自此链接

完全跨单点域登录

完全跨域登录,是指A,B站点和P站点没有共同的父域,比如A站点为forum.yizhu1999.net,B站点为blog.yizhu1998.net,大家可以参考微软旗下的几个站点http://www.live.comwww.hotmail.com,这两个站点就没有共同的父域,而仍然可以共用登录,怎样才能实现呢?请参考下图,由于这种情况ticket比较复杂,我们暂时把P站点创建的的ticket叫做P-ticket,而A站点创建的ticket叫A-ticket,B的为B-ticket

login

 

由于站点A(forum.yizhu1999.com)不能读取到由站点P(passport.yizhu2000.com)创建的加密ticket,所以当用户访问A站点上需要登录才能访问的资源时,A站点会首先查看是否有A-ticket,如果没有,证明用户没有在A站点登录过,不过并不保证用户没有在B站点登录,(重复一下,既然是单点登录,当然无论你在A,B任意一个站点登录过,另外一个站点都要可以访问),请求会被重定向到p站点的验证页面,验证页面读取P-ticket,如果没有,或者解密不成功,就需要重定向登录页面,登录页面完成登录后,写一个加密cookie,也就是P-ticket,并且重定向到A站点的登录处理页,并把加密的用户信息作为参数传递给这个页面,这个页面接收登录页的用户信息,解密后也要写一个cookie,也就是A-ticket,今后用户再次访问A站点上需要登录权限才能访问的资源时,只需要检查这个A-cookie是否存在就可以了

当用户访问B站点时,会重复上面的过程,监测到没有B-ticket,就会重定向到P站点的验证页面,去检查P-ticket,如果没有,就登录,有则返回B的登录处理页面写B-ticket

 

注销的时候需要删除P-ticket和A-ticket

 

logout

 

怎么删除cookie:本来以为这个不是问题,不过还是有朋友问道,简单的说其实是创建一个和你要删除的cookie同名的cookie,并把cookie的expire设为当前时间之前的某个时间,不过在跨子域的删除cookie时有一点要注意:必须要把cookie的域设置为父域,在本文中为yizhu2000.com

为了保证各个环节的传输的安全性,最好使用https连接

posted @ 2011-10-19 14:57 昊天 阅读(668) | 评论 (0)编辑 收藏

很多人对二级缓存都不太了解,或者是有错误的认识,我一直想写一篇文章介绍一下hibernate的二级缓存的,今天终于忍不住了。
我的经验主要来自hibernate2.1版本,基本原理和3.0、3.1是一样的,请原谅我的顽固不化。

hibernate的session提供了一级缓存,每个session,对同一个id进行两次load,不会发送两条sql给数据库,但是session关闭的时候,一级缓存就失效了。

二级缓存是SessionFactory级别的全局缓存,它底下可以使用不同的缓存类库,比如ehcache、oscache等,需要设置hibernate.cache.provider_class,我们这里用ehcache,在2.1中就是
hibernate.cache.provider_class=net.sf.hibernate.cache.EhCacheProvider
如果使用查询缓存,加上
hibernate.cache.use_query_cache=true

缓存可以简单的看成一个Map,通过key在缓存里面找value。

Class的缓存
对于一条记录,也就是一个PO来说,是根据ID来找的,缓存的key就是ID,value是POJO。无论list,load还是 iterate,只要读出一个对象,都会填充缓存。但是list不会使用缓存,而iterate会先取数据库select id出来,然后一个id一个id的load,如果在缓存里面有,就从缓存取,没有的话就去数据库load。假设是读写缓存,需要设置:
<cache usage="read-write"/>
如果你使用的二级缓存实现是ehcache的话,需要配置ehcache.xml
<cache name="com.xxx.pojo.Foo" maxElementsInMemory="500" eternal="false" timeToLiveSeconds="7200" timeToIdleSeconds="3600" overflowToDisk="true" />
其中eternal表示缓存是不是永远不超时,timeToLiveSeconds是缓存中每个元素(这里也就是一个POJO)的超时时间,如果eternal="false",超过指定的时间,这个元素就被移走了。timeToIdleSeconds是发呆时间,是可选的。当往缓存里面put 的元素超过500个时,如果overflowToDisk="true",就会把缓存中的部分数据保存在硬盘上的临时文件里面。
每个需要缓存的class都要这样配置。如果你没有配置,hibernate会在启动的时候警告你,然后使用defaultCache的配置,这样多个class会共享一个配置。
当某个ID通过hibernate修改时,hibernate会知道,于是移除缓存。
这样大家可能会想,同样的查询条件,第一次先list,第二次再iterate,就可以使用到缓存了。实际上这是很难的,因为你无法判断什么时候是第一次,而且每次查询的条件通常是不一样的,假如数据库里面有100条记录,id从1到100,第一次list的时候出了前50个id,第二次 iterate的时候却查询到30至70号id,那么30-50是从缓存里面取的,51到70是从数据库取的,共发送1+20条sql。所以我一直认为 iterate没有什么用,总是会有1+N的问题。
(题外话:有说法说大型查询用list会把整个结果集装入内存,很慢,而iterate只select id比较好,但是大型查询总是要分页查的,谁也不会真的把整个结果集装进来,假如一页20条的话,iterate共需要执行21条语句,list虽然选择若干字段,比iterate第一条select id语句慢一些,但只有一条语句,不装入整个结果集hibernate还会根据数据库方言做优化,比如使用mysql的limit,整体看来应该还是 list快。)
如果想要对list或者iterate查询的结果缓存,就要用到查询缓存了

查询缓存
首先需要配置hibernate.cache.use_query_cache=true
如果用ehcache,配置ehcache.xml,注意hibernate3.0以后不是net.sf的包名了
<cache name="net.sf.hibernate.cache.StandardQueryCache"
maxElementsInMemory="50" eternal="false" timeToIdleSeconds="3600"
timeToLiveSeconds="7200" overflowToDisk="true"/>
<cache name="net.sf.hibernate.cache.UpdateTimestampsCache"
maxElementsInMemory="5000" eternal="true" overflowToDisk="true"/>
然后
query.setCacheable(true);//激活查询缓存
query.setCacheRegion("myCacheRegion");//指定要使用的cacheRegion,可选
第二行指定要使用的cacheRegion是myCacheRegion,即你可以给每个查询缓存做一个单独的配置,使用setCacheRegion来做这个指定,需要在ehcache.xml里面配置它:
<cache name="myCacheRegion" maxElementsInMemory="10" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true" />
如果省略第二行,不设置cacheRegion的话,那么会使用上面提到的标准查询缓存的配置,也就是net.sf.hibernate.cache.StandardQueryCache

对于查询缓存来说,缓存的key是根据hql生成的sql,再加上参数,分页等信息(可以通过日志输出看到,不过它的输出不是很可读,最好改一下它的代码)。
比如hql:
from Cat c where c.name like ?
生成大致如下的sql:
select * from cat c where c.name like ?
参数是"tiger%",那么查询缓存的key*大约*是这样的字符串(我是凭记忆写的,并不精确,不过看了也该明白了):
select * from cat c where c.name like ? , parameter:tiger%
这样,保证了同样的查询、同样的参数等条件下具有一样的key。
现在说说缓存的value,如果是list方式的话,value在这里并不是整个结果集,而是查询出来的这一串ID。也就是说,不管是list方法还是iterate方法,第一次查询的时候,它们的查询方式很它们平时的方式是一样的,list执行一条sql,iterate执行1+N条,多出来的行为是它们填充了缓存。但是到同样条件第二次查询的时候,就都和iterate的行为一样了,根据缓存的key去缓存里面查到了value,value是一串id,然后在到class的缓存里面去一个一个的load出来。这样做是为了节约内存。
可以看出来,查询缓存需要打开相关类的class缓存。list和iterate方法第一次执行的时候,都是既填充查询缓存又填充class缓存的。
这里还有一个很容易被忽视的重要问题,即打开查询缓存以后,即使是list方法也可能遇到1+N的问题!相同条件第一次list的时候,因为查询缓存中找不到,不管class缓存是否存在数据,总是发送一条sql语句到数据库获取全部数据,然后填充查询缓存和class缓存。但是第二次执行的时候,问题就来了,如果你的class缓存的超时时间比较短,现在class缓存都超时了,但是查询缓存还在,那么list方法在获取id串以后,将会一个一个去数据库load!因此,class缓存的超时时间一定不能短于查询缓存设置的超时时间!如果还设置了发呆时间的话,保证class缓存的发呆时间也大于查询的缓存的生存时间。这里还有其他情况,比如class缓存被程序强制evict了,这种情况就请自己注意了。

另外,如果hql查询包含select字句,那么查询缓存里面的value就是整个结果集了。

当hibernate更新数据库的时候,它怎么知道更新哪些查询缓存呢?
hibernate在一个地方维护每个表的最后更新时间,其实也就是放在上面net.sf.hibernate.cache.UpdateTimestampsCache所指定的缓存配置里面。
当通过hibernate更新的时候,hibernate会知道这次更新影响了哪些表。然后它更新这些表的最后更新时间。每个缓存都有一个生成时间和这个缓存所查询的表,当hibernate查询一个缓存是否存在的时候,如果缓存存在,它还要取出缓存的生成时间和这个缓存所查询的表,然后去查找这些表的最后更新时间,如果有一个表在生成时间后更新过了,那么这个缓存是无效的。
可以看出,只要更新过一个表,那么凡是涉及到这个表的查询缓存就失效了,因此查询缓存的命中率可能会比较低。

Collection缓存
需要在hbm的collection里面设置
<cache usage="read-write"/>
假如class是Cat,collection叫children,那么ehcache里面配置
<cache name="com.xxx.pojo.Cat.children"
maxElementsInMemory="20" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200"
overflowToDisk="true" />
Collection的缓存和前面查询缓存的list一样,也是只保持一串id,但它不会因为这个表更新过就失效,一个collection缓存仅在这个collection里面的元素有增删时才失效。
这样有一个问题,如果你的collection是根据某个字段排序的,当其中一个元素更新了该字段时,导致顺序改变时,collection缓存里面的顺序没有做更新。

缓存策略
只读缓存(read-only):没有什么好说的
读/写缓存(read-write):程序可能要的更新数据
不严格的读/写缓存(nonstrict-read-write):需要更新数据,但是两个事务更新同一条记录的可能性很小,性能比读写缓存好
事务缓存(transactional):缓存支持事务,发生异常的时候,缓存也能够回滚,只支持jta环境,这个我没有怎么研究过

读写缓存和不严格读写缓存在实现上的区别在于,读写缓存更新缓存的时候会把缓存里面的数据换成一个锁,其他事务如果去取相应的缓存数据,发现被锁住了,然后就直接取数据库查询。
在hibernate2.1的ehcache实现中,如果锁住部分缓存的事务发生了异常,那么缓存会一直被锁住,直到60秒后超时。
不严格读写缓存不锁定缓存中的数据。

使用二级缓存的前置条件
你的hibernate程序对数据库有独占的写访问权,其他的进程更新了数据库,hibernate是不可能知道的。你操作数据库必需直接通过 hibernate,如果你调用存储过程,或者自己使用jdbc更新数据库,hibernate也是不知道的。hibernate3.0的大批量更新和删除是不更新二级缓存的,但是据说3.1已经解决了这个问题。
这个限制相当的棘手,有时候hibernate做批量更新、删除很慢,但是你却不能自己写jdbc来优化,很郁闷吧。
SessionFactory也提供了移除缓存的方法,你一定要自己写一些JDBC的话,可以调用这些方法移除缓存,这些方法是:
void evict(Class persistentClass)
Evict all entries from the second-level cache.
void evict(Class persistentClass, Serializable id)
Evict an entry from the second-level cache.
void evictCollection(String roleName)
Evict all entries from the second-level cache.
void evictCollection(String roleName, Serializable id)
Evict an entry from the second-level cache.
void evictQueries()
Evict any query result sets cached in the default query cache region.
void evictQueries(String cacheRegion)
Evict any query result sets cached in the named query cache region.
不过我不建议这样做,因为这样很难维护。比如你现在用JDBC批量更新了某个表,有3个查询缓存会用到这个表,用evictQueries (String cacheRegion)移除了3个查询缓存,然后用evict(Class persistentClass)移除了class缓存,看上去好像完整了。不过哪天你添加了一个相关查询缓存,可能会忘记更新这里的移除代码。如果你的 jdbc代码到处都是,在你添加一个查询缓存的时候,还知道其他什么地方也要做相应的改动吗?

----------------------------------------------------

总结:
不要想当然的以为缓存一定能提高性能,仅仅在你能够驾驭它并且条件合适的情况下才是这样的。hibernate的二级缓存限制还是比较多的,不方便用jdbc可能会大大的降低更新性能。在不了解原理的情况下乱用,可能会有1+N的问题。不当的使用还可能导致读出脏数据。
如果受不了hibernate的诸多限制,那么还是自己在应用程序的层面上做缓存吧。
在越高的层面上做缓存,效果就会越好。就好像尽管磁盘有缓存,数据库还是要实现自己的缓存,尽管数据库有缓存,咱们的应用程序还是要做缓存。因为底层的缓存它并不知道高层要用这些数据干什么,只能做的比较通用,而高层可以有针对性的实现缓存,所以在更高的级别上做缓存,效果也要好些吧。

posted @ 2011-10-14 17:57 昊天 阅读(303) | 评论 (0)编辑 收藏
什么是Spring 的 IOC 容器呢 ?  可以说  BeanFactory 就是 我们看到的Spring IoC容器.

如何初始化 SPRING 的 IOC 容器 ?

一共三种方法 :

1 :
Resource resource = new FileSystemResource("beans.xml");
BeanFactory factory 
= new XmlBeanFactory(resource);


2:

ClassPathResource resource = new ClassPathResource("beans.xml");
BeanFactory factory 
= new XmlBeanFactory(resource);

 
3:

ApplicationContext context = new ClassPathXmlApplicationContextnew String[] {"applicationContext.xml""applicationContext-part2.xml"});
BeanFactory factory 
= (BeanFactory) context;


详细展开 :

一 :org.springframework.core.io包中主要是各样的Resource类



补充UML 知识 :

1:类名是斜体的表示是抽象类,正体的表示普通类

2 :

3 :

   

温昱说图

org.springframework.core.io包中主要是各样的Resource类,Spring的 Resource 接口是为了提供更强的访问底层资源能力的抽象。相当巧妙的地方在于,为何AbstractResource的子类有的override了getFile()而有的没有?这是因为在AbstractResource的getFile()方法设计为抛出异常,如果子类没有重写此方法,说明子类不支持通过绝对路径查找资源的方式,而override的子类则提供自己的实现。这里通过类的层次设计,充分利用继承带来的优点,避免了大量的条件语句。


二 :关于 org.springframework.beans.factory 包 和 org.springframework.context 包:

1 :BeanFactory 是个接口,LifecycleBean 是个类它负责管理bean的生命周期,它实现了4个接口,并且有一个 BeanFactory  类型的变量 owningFactory



补充一下 上文 的 XmlBeanFactory 不是

org.springframework.beans.factory

包下面的 而是

org.springframework.beans.factory.xml

下面的

2 。 ApplicationContext 也是个接口,它继承的 ListableBeanFactory  接口 又 继承 BeanFactory 接口

public interface ApplicationContext extends ListableBeanFactory, HierarchicalBeanFactory,  MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
}

public interface ListableBeanFactory extends BeanFactory {
}

        Spring的bean包支持通过编码方式管理和操作bean的基本功能,ApplicationContext则以Framework的方式提供BeanFactory的所有功能。使用
ApplicationContext,你可以让系统加载你的bean,例如:

在Servlet容器初始化ContextLoaderServlet时,通过ContextLoader类加载Spring Framework),而不是使用编码方式来加载。

       ApplicationContext接口是context包的基础,位于org.springframework.context包里,提供了BeanFactory的所有功能。除此之外, ApplicationContext为了支持Framework的工作方式,提供了以下的功能:

       l.MessageSource,提供了语言信息的国际化支持
      
2.提供资源(如URL和文件系统)的访问支持
      
3.为实现了ApplicationListener接口的bean提供了事件传播支持
      
4.为不同的应用环境提供不同的context,例如支持web应用的XmlWebApplicationContext类


posted @ 2011-10-14 17:43 昊天 阅读(1128) | 评论 (0)编辑 收藏

导言

 

 

 

 

 

 

 

 

Spring 1.1.1 开始, EHCache 就作为一种通用缓存解决方案集成进 Spring

 

 

 

 

 

 

 

 

我将示范拦截器的例子,它能把方法返回的结果缓存起来。

 


利用
Spring IoC 配置 EHCache

 

 

 

 

 

 

 

 

Spring 里配置 EHCache 很简单。你只需一个 ehcache.xml 文件,该文件用于配置 EHCache

 

 

<ehcache>

    <!—设置缓存文件 .data 的创建路径。

         如果该路径是 Java 系统参数,当前虚拟机会重新赋值。

         下面的参数这样解释:
         user.home – 用户主目录
         user.dir      – 用户当前工作目录
         java.io.tmpdir – 默认临时文件路径 -->
    <diskStore path="java.io.tmpdir"/>


    <!—缺省缓存配置。CacheManager 会把这些配置应用到程序中。

        下列属性是 defaultCache 必须的:

        maxInMemory           - 设定内存中创建对象的最大值。
        eternal                        - 设置元素(译注:内存中对象)是否永久驻留。如果是,将忽略超
                                              时限制且元素永不消亡。
        timeToIdleSeconds  - 设置某个元素消亡前的停顿时间。
                                              也就是在一个元素消亡之前,两次访问时间的最大时间间隔值。
                                              这只能在元素不是永久驻留时有效(译注:如果对象永恒不灭,则
                                              设置该属性也无用)。
                                              如果该值是 0 就意味着元素可以停顿无穷长的时间。
        timeToLiveSeconds - 为元素设置消亡前的生存时间。
                                               也就是一个元素从构建到消亡的最大时间间隔值。
                                               这只能在元素不是永久驻留时有效。
        overflowToDisk        - 设置当内存中缓存达到 maxInMemory 限制时元素是否可写到磁盘
                                               上。
        -->

    <cache name="org.taha.cache.METHOD_CACHE"
        maxElementsInMemory="300"
        eternal="false"
        timeToIdleSeconds="500"
        timeToLiveSeconds="500"
        overflowToDisk="true"
        />
</ehcache>

 

 

 

 

拦截器将使用 org.taha.cache.METHOD_CACHE区域缓存方法返回结果。下面利用 Spring IoC bean 来访问这一区域。

 

 

<!-- ======================   缓存   ======================= -->

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
  <property name="configLocation">
    <value>classpath:ehcache.xml</value>
  </property>
</bean>

<bean id="methodCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
  <property name="cacheManager">
    <ref local="cacheManager"/>
  </property>
  <property name="cacheName">
    <value>org.taha.cache.METHOD_CACHE</value>
  </property>
</bean>

   

 

 

 

构建我们的 MethodCacheInterceptor

 

 

 

 

 

 

 

 

该拦截器实现 org.aopalliance.intercept.MethodInterceptor 接口。一旦 运行起来 (kicks-in) ,它首先检查被拦截方法是否被配置为可缓存的。这将可选择性的配置想要缓存的 bean 方法。只要调用的方法配置为可缓存,拦截器将为该方法生成 cache key 并检查该方法返回的结果是否已缓存。如果已缓存,就返回缓存的结果,否则再次调用被拦截方法,并缓存结果供下次调用。

 

org.taha.interceptor.MethodCacheInterceptor

 

 

/*
 * Copyright 2002-2004 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.taha.interceptor;

import java.io.Serializable;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

/**
 * @author <a href=" mailto:irbouh@gmail.com">Omar Irbouh</a>
 * @since 2004.10.07
 */
public class MethodCacheInterceptor implements MethodInterceptor, InitializingBean {
  private static final Log logger = LogFactory.getLog(MethodCacheInterceptor.class);

  private Cache cache;

  /**
   * 设置缓存名
   */
  public void setCache(Cache cache) {
    this.cache = cache;
  }

  /**
   * 检查是否提供必要参数。
   */
  public void afterPropertiesSet() throws Exception {
    Assert.notNull(cache, "A cache is required. Use setCache(Cache) to provide one.");
  }

  /**
   * 主方法
   * 如果某方法可被缓存就缓存其结果
   * 方法结果必须是可序列化的(serializable)
   */
  public Object invoke(MethodInvocation invocation) throws Throwable {
    String targetName  = invocation.getThis().getClass().getName();
    String methodName  = invocation.getMethod().getName();
    Object[] arguments = invocation.getArguments();
    Object result;

    logger.debug("looking for method result in cache");
    String cacheKey = getCacheKey(targetName, methodName, arguments);
    Element element = cache.get(cacheKey);
    if (element == null) {
      //call target/sub-interceptor
      logger.debug("calling intercepted method");
      result = invocation.proceed();

      //cache method result
      logger.debug("caching result");
      element = new Element(cacheKey, (Serializable) result);
      cache.put(element);
    }
    return element.getValue();
  }

  /**
   * creates cache key: targetName.methodName.argument0.argument1...
   */
  private String getCacheKey(String targetName,
                             String methodName,
                             Object[] arguments) {
    StringBuffer sb = new StringBuffer();
    sb.append(targetName)
      .append(".").append(methodName);
    if ((arguments != null) && (arguments.length != 0)) {
      for (int i=0; i<arguments.length; i++) {
        sb.append(".")
          .append(arguments[i]);
      }
    }

    return sb.toString();
  }
}

 

 

 

 

MethodCacheInterceptor 代码说明了:

 

 

 

 

 

 

 

 

  • 默认条件下,所有方法返回结果都被缓存了( methodNames null

     

  • 缓存区利用 IoC 形成

     

  • cacheKey 的生成还包括方法参数的因素(译注:参数的改变会影响 cacheKey

     

使用 MethodCacheInterceptor

 

 

 

 

 

 

 

 

下面摘录了怎样配置 MethodCacheInterceptor

 

 

<bean id="methodCacheInterceptor" class="org.taha.interceptor.MethodCacheInterceptor">
  <property name="cache">
    <ref local="methodCache" />
  </property>
</bean>

<bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
  <property name="advice">
    <ref local="methodCacheInterceptor"/>
  </property>
  <property name="patterns">
    <list>
      <value>.*methodOne</value>
      <value>.*methodTwo</value>
    </list>
  </property>
</bean>

<bean id="myBean" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="target">
   <bean class="org.taha.beans.MyBean"/>
  </property>
  <property name="interceptorNames">
    <list>
      <value>methodCachePointCut</value>
    </list>
  </property>
</bean>

 

 

 

译注

 

 

 

 

 

 

 

 

如果你要缓存的方法是 findXXX,那么正则表达式应该这样写:“.*find.*”。

夏昕所著《
Hibernate 开发指南》,其中他这样描述 EHCache 配置文件的:

 

 

<ehcache>
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
        maxElementsInMemory="10000" //Cache中最大允许保存的数据数量
        eternal="false"                       //Cache中数据是否为常量
        timeToIdleSeconds="120"     //缓存数据钝化时间
        timeToLiveSeconds="120"     //缓存数据的生存时间
        overflowToDisk="true"       //内存不足时,是否启用磁盘缓存
    />
</ehcache>
 

请注意!引用、转贴本文应注明原译者:Rosen Jiang 以及出处:http://www.blogjava.net/rosen


 

 

     <!--  EHCache  -->
    
< bean  id ="cacheManager"  class ="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" >
        
< property  name ="configLocation" >
            
< value > classpath:ehcache.xml </ value >
        
</ property >
    
</ bean >
    
    
< bean  id ="methodCache"  class ="org.springframework.cache.ehcache.EhCacheFactoryBean" >
        
< property  name ="cacheManager" >
            
< ref  local ="cacheManager" />
        
</ property >
        
< property  name ="cacheName" >
            
< value > org.taha.cache.METHOD_CACHE </ value >
        
</ property >
    
</ bean >
    
    
< bean  id ="methodCacheInterceptor"  class ="com.cdmcs.util.MethodCacheInterceptor" >  
        
< property  name ="cache" >
            
< ref  local ="methodCache" />
        
</ property >
    
</ bean >
    
    
< bean  id ="cleanCacheAdvice"  class ="com.cdmcs.util.CleanCacheAdvice" >  
        
< property  name ="cache" >
            
< ref  local ="methodCache" />
        
</ property >
    
</ bean >
    
    
< bean  id ="advicePointCut"  class ="org.springframework.aop.support.RegexpMethodPointcutAdvisor" >
        
< property  name ="advice" >
            
< ref  local ="cleanCacheAdvice" />
        
</ property >
        
< property  name ="patterns" >
            
< list >
                
< value > .*save.* </ value >
                
< value > .*update.* </ value >
                
< value > .*delete.* </ value >
                
< value > .*valid.* </ value >
            
</ list >
        
</ property >
    
</ bean >

    
< bean  id ="methodCachePointCut"  class ="org.springframework.aop.support.RegexpMethodPointcutAdvisor" >
        
< property  name ="advice" >
            
< ref  local ="methodCacheInterceptor" />
        
</ property >
        
< property  name ="patterns" >
            
< list >
                
< value > .*find.* </ value >
            
</ list >
        
</ property >
    
</ bean >

   
<!--  EHCache End -->

    
< bean  id ="supplyDemandService"  parent ="baseTransactionProxy" >  
        
< property  name ="target" >  
            
< bean  class ="com.cdmcs.webbuilder.service.impl.SupplyDemandServiceImpl"  autowire ="byName" />  
        
</ property >  
        
< property  name ="preInterceptors" >
        
< list >
            
< ref  bean ="methodCachePointCut" />
            
< ref  bean ="advicePointCut" />
        
</ list >
        
</ property >
    
</ bean >
posted @ 2011-10-14 17:29 昊天 阅读(343) | 评论 (0)编辑 收藏
     摘要: Jeff Johnston 版本1.0.0 本文档允许在遵守以下两条原则的条件下被使用和传播: 1)不能凭借本文档索取任何费用 2)以任何方式(印刷物或电子版)使用和传播时本文档时,必须包含本版权申明 (更新中...) Table of Contents   前言 eXtremeComponents是一系列提供高级显示的开源JSP定制标...  阅读全文
posted @ 2011-09-28 15:35 昊天 阅读(360) | 评论 (0)编辑 收藏
jfreechart-1.0.1-US_developer_guide.pdf
posted @ 2011-07-12 10:40 昊天 阅读(313) | 评论 (0)编辑 收藏

在 JBoss 下配置 Oracle 数据源

1. 将 %JBOSS_HOME%\docs\examples\jca\oracle-ds.xml 复制到 %JBOSS_HOME%\server\default\deploy 目录下。

2. 打开 oracle-ds.xml,做如下编辑:

  • 修改jndi <jndi-name>OracleDS</jndi-name>
  • 修改url <connection-url>jdbc:oracle:thin:@localhost:1521:orcl</connection-url>
  • 修改驱动 <driver-class>oracle.jdbc.driver.OracleDriver</driver-class>
  • 修改用户名和密码  <user-name>root</user-name>  <password>123456</password>

<?xml version=”1.0″ encoding=”UTF-8″?>
<datasources>
<local-tx-datasource>
<jndi-name>OracleDS</jndi-name>   //jndi名字
<use-java-context>false</use-java-context>
<connection-url>jdbc:oracle:thin:@localhost:1521:orcl</connection-url>  //URL地址
<driver-class>oracle.jdbc.driver.OracleDriver</driver-class>  //驱动
<user-name>root</user-name>    //用户名
<password>123456</password>  //密码
<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter
</exception-sorter-class-name>
<metadata>
<type-mapping>Oracle9i</type-mapping>
</metadata>
<min-pool-size>10</min-pool-size>
<max-pool-size>30</max-pool-size>
<blocking-timeout-millis>60000</blocking-timeout-millis>
<idle-timeout-minutes>2</idle-timeout-minutes>
<new-connection-sql>SELECT COUNT(*) FROM dual</new-connection-sql>
<check-valid-connection-sql>SELECT COUNT(*) FROM dual</check-valid-connection-sql>
</local-tx-datasource>
</local-tx-datasource>
</datasources>

3. 打开 %JBOSS_HOME%\server\default\conf\standardjbosscmp-jdbc.xml 并编辑:

<jbosscmp-jdbc>
<defaults>
<datasource>java:/DefaultDS</datasource>
……
</defaults>
……
</jbosscmp-jdbc>

4. 打开 %JBOSS_HOME%\server\default\conf\login-config.xml, 将如下内容复制到文件最后:

<application-policy name = “OracleDbRealm”>
<authentication>
<login-module code = “org.jboss.resource.security.ConfiguredIdentityLoginModule” flag = “required”>
<module-option name = “principal”>OracleDS</module-option>
<module-option name = “userName”>root</module-option>
<module-option name = “password”>123456</module-option>
<module-option name = “managedConnectionFactoryName”>
jboss.jca:service=LocalTxCM,name=OracleDS
</module-option>
</login-module>
</authentication>
</application-policy>

5. 将 Oracle 驱动程序复制到 %JBOSS_HOME%\server\default\lib 目录下。

 

去除jboss的冲突包

由于jboss自身带的hibernate-annotations.jar版本与项目使用的版本存在冲突,因此去除hibernate-annotations.jar包

mv /opt/jboss-4.2.2.GA/server/default/lib/hibernate-annotations.jar /opt/jboss-4.2.2.GA/server/default/lib/hibernate-annotations.jar.bak  10. 部署esales.war到/opt/jboss-4.2.2.GA/server/default/deploy


以上的5步操作,完成了 JBoss 基本的数据源配置,但配置文件中有密码的明码,存在安全隐患。所以请使用如下的方法对密码进行加密的配置:

1. 生成加密密码:

  • 进入dos命令行模式
  • Set JBOSS_HOME环境变量
  • 执行如下命令:

java -cp %JBOSS_HOME%\lib\jboss-jmx.jar;%JBOSS_HOME%\lib\jboss-common.jar;%JBOSS_HOME%\server\default\lib\jboss-jca.jar;%JBOSS_HOME%\server\default\lib\jbosssx.jar org.jboss.resource.security.SecureIdentityLoginModule 123456

  • 生成 123456 的加密密码为: 64c5fd2979a86168

2. 修改 %JBOSS_HOME%\server\default\deploy\oracle-ds.xml 文件:

<?xml version=”1.0″ encoding=”UTF-8″?>
<datasources>
<local-tx-datasource>
<jndi-name>OracleDS</jndi-name>
<use-java-context>false</use-java-context>
<connection-url>jdbc:oracle:thin:@localhost:1521:orcl</connection-url>
<driver-class>oracle.jdbc.driver.OracleDriver</driver-class>
<security-domain>EncryptDBPassword</security-domain> //这里不用写上你的用户名与密码了,我们可以在 login-config.xml 里做点手脚,就OK了
<exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name>
<metadata>
<type-mapping>Oracle9i</type-mapping>
</metadata>
<min-pool-size>10</min-pool-size>
<max-pool-size>30</max-pool-size>
<blocking-timeout-millis>60000</blocking-timeout-millis>
<idle-timeout-minutes>2</idle-timeout-minutes>
<new-connection-sql>SELECT COUNT(*) FROM dual</new-connection-sql>
<check-valid-connection-sql>SELECT COUNT(*) FROM dual</check-valid-connection-sql>
</local-tx-datasource>
</local-tx-datasource>
</datasources>

3. 打开 %JBOSS_HOME%\server\default\conf\login-config.xml 文件,加上下面这一段配置文件:

<application-policy name=”EncryptDBPassword”> //这里的 name 应该是你在配置数据源时写的 security-domain 里的字符串
<authentication>
<login-module code=”org.jboss.resource.security.SecureIdentityLoginModule”
flag=”required”>
<module-option name=”username”>root</module-option> //数据库的用户名
<module-option name=”password”>64c5fd2979a86168</module-option> //数据库的密码,不过是加密过的了
<module-option name=”managedConnectionFactoryName”>
jboss.jca:service=LocalTxCM,name=OracleDS
</module-option> //注意 name 等于你的数据源的 jndi-name,这里是 OracleDS。
</login-module>
</authentication>
</application-policy>

posted @ 2011-07-12 09:58 昊天 阅读(3815) | 评论 (0)编辑 收藏
一.             下载与安装JBoss

在本文中,我们下载的JBoss版本为:4.2.1.GA

下载地址:

http://sourceforge.net/project/showfiles.php?group_id=22866&package_id=16942&release_id=523619

在如上的下载页中下载JBoss-4.2.1.GA.zip文件。

下载完成后,将其解压缩后即可完成安装,解压缩后将其放置到一个不带空格的目录(若目录带有空格,例如:C:"Program Files,日后可能会产生一些莫名的错误)egE:"JBoss4.2.1。同时在“环境变量设置”中设置名为JBOSS_HOME的环境变量,值为JBoss的安装路径,如下图所示:

在此,JBoss的安装工作已经结束,可通过如下方式测试安装是否成功:

运行JBoss安装目录"bin"run.bat,如果窗口中没有出现异常,且出现:10:16:19,765 INFO [Server] JBoss (MX MicroKernel) [4.2.1.GA (build: SVNTag=JBoss_4_2_1_GA date=200707131605)] Started in 30s:828ms字样,则表示安装成功。

我们可以通过访问:http://localhost:8080进入JBoss的欢迎界面,点击JBoss Management下的JMX Console可进入JBoss的控制台。

若启动失败,可能由以下原因引起:

1)              JBoss所用的端口(8080109910988083等)被占用。一般情况下为8080端口被占用(例如,Oracle占用了8080端口),此时需要修改JBoss的端口,方法为进入JBoss安装目录"server"default"deploy"jboss-web.deployer目录,修改其下的server.xml目录,在此文件中搜索8080,将其改成你想要的端口即可(例如8088);

2)              JDK安装不正确;

3)              JBoss下载不完全。

二.             JBoss的目录结构说明

目录

描述

bin

启动和关闭JBoss 的脚本(run.batwindows系统下的启动脚本,shutdown.batwindows系统下的关闭脚本)。

client

客户端与JBoss 通信所需的Java 库(JARs)。

docs

配置的样本文件(数据库配置等)。

docs/dtd

JBoss 中使用的各种XML 文件的DTD

lib

一些JARJBoss 启动时加载,且被所有JBoss 配置共享。(不要把你的库放在这里)

server 

各种JBoss 配置。每个配置必须放在不同的子目录。子目录的名字表示配置的名字。JBoss 包含3 个默认的配置:minimialdefault all,在你安装时可以进行选择。

server/all

JBoss 的完全配置,启动所有服务,包括集群和IIOP

server/default

JBoss 的默认配置。在没有在JBoss 命令行中指定配置名称时使用。(我们下载的4.2.1版本默认采用此配置

server/default/conf 

JBoss 的配置文件。

server/default/data

JBoss 的数据库文件。比如,嵌入的数据库,或者JBossMQ

server/default /deploy

JBoss 的热部署目录。放到这里的任何文件或目录会被JBoss 自动部署。EJBWAR EAR,甚至服务。

server/default /lib

一些JARJBoss 在启动特定配置时加载他们。(default minimial 配置也包含这个和下面两个目录。

server/default/log

JBoss 的日志文件。

server/default/tmp

JBoss 的临时文件。

三.             JBoss的配置

1.       日志文件设置

若需要修改JBoss默认的log4j设置,可修改JBoss安装目录"server"default"conf下的jboss-log4j.xml文件,在该文件中可以看到,log4j的日志输出在JBoss安装目录"server"default"log下的server.log文件中。对于log4j的设置,读者可以在网上搜索更加详细的信息。

2.       web服务的端口号的修改

这点在前文中有所提及,即修改JBoss安装目录"server"default"deploy"jboss-web.deployer下的server.xml文件,内容如下:

<Connector port="8080" address="${jboss.bind.address}"   

         maxThreads="250" maxHttpHeaderSize="8192"

         emptySessionPath="true" protocol="HTTP/1.1"

         enableLookups="false" redirectPort="8443" acceptCount="100"

         connectionTimeout="20000" disableUploadTimeout="true" />

将上面的8080端口修改为你想要的端口即可。重新启动JBoss后访问:http://localhost:新设置的端口,可看到JBoss的欢迎界面。

3.       JBoss的安全设置

1)jmx-console登录的用户名和密码设置

默认情况访问http://localhost:8080/jmx-console就可以浏览jboss的部署管理的一些信息,不需要输入用户名和密码,使用起来有点安全隐患。下面我们针对此问题对jboss进行配置,使得访问jmx-console也必须要知道用户名和密码才可进去访问。步骤如下:

i) 找到JBoss安装目录/server/default/deploy/jmx-console.war/WEB-INF/jboss-web.xml文件,去掉<security-domain>java:/jaas/jmx-console</security-domain>的注释。修改后的该文件内容为:

<jboss-web>

   <!-- Uncomment the security-domain to enable security. You will

      need to edit the htmladaptor login configuration to setup the

      login modules used to authentication users.-->

      <security-domain>java:/jaas/jmx-console</security-domain>

</jboss-web>

ii)修改与i)中的jboss-web.xml同级目录下的web.xml文件,查找到<security-constraint/>节点,去掉它的注释,修改后该部分内容为:

<!-- A security constraint that restricts access to the HTML JMX console

   to users with the role JBossAdmin. Edit the roles to what you want and

   uncomment the WEB-INF/jboss-web.xml/security-domain element to enable

   secured access to the HTML JMX console.-->

   <security-constraint>

     <web-resource-collection>

       <web-resource-name>HtmlAdaptor</web-resource-name>

       <description>An example security config that only allows users with the

         role JBossAdmin to access the HTML JMX console web application

       </description>

       <url-pattern>/*</url-pattern>

       <http-method>GET</http-method>

       <http-method>POST</http-method>

     </web-resource-collection>

     <auth-constraint>

       <role-name>JBossAdmin</role-name>

     </auth-constraint>

   </security-constraint>

   在此处可以看出,为登录配置了角色JBossAdmin

 iii 在第一步中的jmx-console安全域和第二步中的运行角色JBossAdmin都是在login-config.xml中配置,我们在JBoss安装目录/server/default/config下找到它。查找名字为:jmx-consoleapplication-policy

<application-policy name = "jmx-console">
       <authentication>
          <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule"
             flag = "required">
           <module-option name="usersProperties">props/jmx-console-users.properties</module-option>
           <module-option name="rolesProperties">props/jmx-console-roles.properties</module-option>
          </login-module>
       </authentication>
    </application-policy>

在此处可以看出,登录的角色、用户等的信息分别在props目录下的jmx-console-roles.propertiesjmx-console-users.properties文件中设置,分别打开这两个文件。

其中jmx-console-users.properties文件的内容如下:

# A sample users.properties file for use with the UsersRolesLoginModule

admin=admin

该文件定义的格式为:用户名=密码,在该文件中,默认定义了一个用户名为admin,密码也为admin的用户,读者可将其改成所需的用户名和密码。

jmx-console-roles.properties的内容如下:

# A sample roles.properties file for use with the UsersRolesLoginModule

admin=JBossAdmin, HttpInvoker

该文件定义的格式为:用户名=角色,多个角色以“,”隔开,该文件默认为admin用户定义了JBossAdminHttpInvoker这两个角色。

配置完成后读者可以通过访问:http://localhost:8088/jmx-console/,输入jmx-console-roles.properties文件中定义的用户名和密码,访问jmx-console的页面。

2)web-console登录的用户名和密码设置

默认情况下,用户访问JBossweb-console时,不需要输入用户名和密码,为了安全起见,我们通过修改配置来为其加上用户名和密码。步骤如下:

i)找到JBoss安装目录"server"default"deploy"management"console-mgr.sar"web-console.war"WEB-INF"jboss-web.xml文件,去掉<security-domain>java:/jaas/web-console</security-domain>的注释,修改后的文件内容为:

<?xml version='1.0' encoding='UTF-8' ?>

<!DOCTYPE jboss-web

    PUBLIC "-//JBoss//DTD Web Application 2.3V2//EN"

    "http://www.jboss.org/j2ee/dtd/jboss-web_3_2.dtd">

<jboss-web>

   <!-- Uncomment the security-domain to enable security. You will

   need to edit the htmladaptor login configuration to setup the

   login modules used to authentication users.-->

   <security-domain>java:/jaas/web-console</security-domain>

   <!-- The war depends on the -->

   <depends>jboss.admin:service=PluginManager</depends>

</jboss-web>

ii)打开i)中jboss-web.xml同目录下的web.xml文件,去掉<security-constraint>部分的注释,修改后的该部分内容为:

<!-- A security constraint that restricts access to the HTML JMX console

   to users with the role JBossAdmin. Edit the roles to what you want and

   uncomment the WEB-INF/jboss-web.xml/security-domain element to enable

   secured access to the HTML JMX console.-->

   <security-constraint>

   <web-resource-collection>

   <web-resource-name>HtmlAdaptor</web-resource-name>

   <description>An example security config that only allows users with the

   role JBossAdmin to access the HTML JMX console web application

   </description>

   <url-pattern>/*</url-pattern>

   <http-method>GET</http-method>

   <http-method>POST</http-method>

   </web-resource-collection>

   <auth-constraint>

   <role-name>JBossAdmin</role-name>

   </auth-constraint>

   </security-constraint>

iii)打开JBoss安装目录"server"default"conf下的login-config.xml文件,搜索web-console,可找到如下内容:

<application-policy name = "web-console">

       <authentication>

          <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule"

             flag = "required">

             <module-option name="usersProperties">web-console-users.properties</module-option>

             <module-option name="rolesProperties">web-console-roles.properties</module-option>

          </login-module>

       </authentication>

    </application-policy>

在文件中可以看到,设置登录web-console的用户名和角色等信息分别在login-config.xml文件所在目录下的web-console-users.propertiesweb-console-roles.properties文件中,但因为该目录下无这两个文件,我们在JBoss安装目录"server"default"conf"props目录下建立这两个文件,文件内容可参考在“jmx-console登录的用户名和密码设置”中的两个相应的配置文件的内容,web-console-users.properties文件的内容如下:

# A sample users.properties file for use with the UsersRolesLoginModule

admin=admin

web-console-roles.properties文件的内容如下:

# A sample roles.properties file for use with the UsersRolesLoginModule

admin=JBossAdmin,HttpInvoker

因为此时这两个文件不与login-config.xml同目录,所以login-config.xml文件需进行少许修改,修改后的<application-policy name = "web-console">元素的内容为:

<application-policy name = "web-console">

       <authentication>

          <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule"

             flag = "required">

             <module-option name="usersProperties">props/web-console-users.properties</module-option>

             <module-option name="rolesProperties">props/web-console-roles.properties</module-option>

          </login-module>

       </authentication>

    </application-policy>

四.             MyEclipse中配置JBoss

笔者的MyEclipse版本:5.1.1 GA

JBoss版本:4.2.1 GA

JDK版本:1.5

进入Window-> Preferences-> MyEclipse -> Application Servers -> JBoss4,进行如下设置:
    

选择JBoss 4下的JDK设置所用的JDK.

设置完成后,部署程序时,会发现多出JBoss 4部署的选择,如下图所示:

同时在中展开,可看到JBoss 4的启动图标。

参考文档:Jboss4.03 web-console jmx-console 登录安全设置

posted @ 2011-07-12 09:53 昊天 阅读(5369) | 评论 (0)编辑 收藏

到了21世纪--准确地说是2003年,UML已经获得了业界的认同。在我所见过的专业人员的简历中,75%都声称具备UML的知识。然而,在同绝大多数求职人员面谈之后,可以明显地看出他们并不真正了解UML。通常地,他们将UML用作一个术语,或对UML一知半解。大家对UML缺乏理解的这种状况,促进我撰写这篇关于UML 1.4的快速入门文章。当阅读完本文时,您还不具备足够的知识可以在简历上声称自己掌握了UML,但是您已具有了进一步钻研该语言的良好起点。


正如前面曾提到过的,UML的本意是要成为一种标准的统一语言,使得IT专业人员能够进行计算机应用程序的建模。UML的主要创始人是Jim Rumbaugh、Ivar Jacobson和Grady Booch,他们最初都有自己的建模方法(OMT、OOSE和Booch),彼此之间存在着竞争。最终,他们联合起来创造了一种开放的标准。(听起来是不是很熟悉?这个现象类似J2EE、SOAP和Linux的诞生。)UML成为"标准"建模语言的原因之一在于,它与程序设计语言无关。(IBM Rational的UML建模工具被广泛应用于J2EE和.NET开发。)而且,UML符号集只是一种语言而不是一种方法学。这点很重要,因为语言与方法学不同,它可以在不做任何更改的情况下很容易地适应任何公司的业务运作方式。

既然UML不是一种方法学,它就不需要任何正式的工作产品(即IBM Rational Unified Process?术语中所定义的"工件")。而且它还提供了多种类型的模型描述图(diagram),当在某种给定的方法学中使用这些图时,它使得开发中的应用程序的更易理解。UML的内涵远不只是这些模型描述图,但是对于入门来说,这些图对这门语言及其用法背后的基本原理提供了很好的介绍。通过把标准的UML图放进您的工作产品中,精通UML的人员就更加容易加入您的项目并迅速进入角色。最常用的UML图包括:用例图、类图、序列图、状态图、活动图、组件图和部署图。

深入讨论每类图的细节问题已超出了这篇入门文章的范围。因此,下面仅给出了每类图的简要说明,更详细的信息将在以后的文章中探讨。


用例图


用例图描述了系统提供的一个功能单元。用例图的主要目的是帮助开发团队以一种可视化的方式理解系统的功能需求,包括基于基本流程的"角色"(actors,也就是与系统交互的其他实体)关系,以及系统内用例之间的关系。用例图一般表示出用例的组织关系--要么是整个系统的全部用例,要么是完成具有功能(例如,所有安全管理相关的用例)的一组用例。要在用例图上显示某个用例,可绘制一个椭圆,然后将用例的名称放在椭圆的中心或椭圆下面的中间位置。要在用例图上绘制一个角色(表示一个系统用户),可绘制一个人形符号。角色和用例之间的关系使用简单的线段来描述,如图1所示。


示例用例图

图1:示例用例图

图字(从上到下):CD销售系统;查看乐队CD的销售统计;乐队经理;查看Billboard 200排行榜报告;唱片经理;查看特定CD的销售统计;检索最新的Billboard 200排行榜报告;排行榜报告服务

用例图通常用于表达系统或者系统范畴的高级功能。如图1所示,可以很容易看出该系统所提供的功能。这个系统允许乐队经理查看乐队CD的销售统计报告以及Billboard 200排行榜报告。它也允许唱片经理查看特定CD的销售统计报告和这些CD在Billboard 200排行榜的报告。这个图还告诉我们,系统将通过一个名为"排行榜报告服务"的外部系统提供Billboard排行榜报告。

此外,在用例图中,没有列出的用例表明了该系统不能完成的功能。例如,它不能提供给乐队经理收听Billboard 200上不同专辑中的歌曲的途径 -- 也就是说,系统没有引用一个叫做"收听Billboard 200上的歌曲"的用例。这种缺少不是一件小事。在用例图中提供清楚的、简要的用例描述,项目赞助商就很容易看出系统是否提供了必须的功能。


类图


类图表示不同的实体(人、事物和数据)如何彼此相关;换句话说,它显示了系统的静态结构。类图可用于表示逻辑类,逻辑类通常就是业务人员所谈及的事物种类--摇滚乐队、CD、广播剧;或者贷款、住房抵押、汽车信贷以及利率。类图还可用于表示实现类,实现类就是程序员处理的实体。实现类图或许会与逻辑类图显示一些相同的类。然而,实现类图不会使用相同的属性来描述,因为它很可能具有对诸如Vector和HashMap这种事物的引用。

类在类图上使用包含三个部分的矩形来描述,如图2所示。最上面的部分显示类的名称,中间部分包含类的属性,最下面的部分包含类的操作(或者说"方法")。


类图中的示例类对象

图2:类图中的示例类对象

根据我的经验,几乎每个开发人员都知道这个类图是什么,但是我发现大多数程序员都不能正确地描述类的关系。对于像图3这样的类图,您应该使用带有顶点指向父类的箭头的线段来绘制继承关系1,并且箭头应该是一个完全的三角形。如果两个类都彼此知道对方,则应该使用实线来表示关联关系;如果只有其中一个类知道该关联关系,则使用开箭头表示。


一个完整的类图

图3:一个完整的类图,包括了图2所示的类对象

在图3中,我们同时看到了继承关系和两个关联关系。CDSalesReport类继承自Report类。一个CDSalesReport类与一个CD类关联,但是CD类并不知道关于CDSalesReport类的任何信息。CD类和Band类都彼此知道对方,两个类彼此都可以与一个或者多个对方类相关联。

一个类图可以整合其他许多概念,这将在本系列文章的后续文章中介绍。


序列图


序列图显示具体用例(或者是用例的一部分)的详细流程。它几乎是自描述的,并且显示了流程中中不同对象之间的调用关系,同时还可以很详细地显示对不同对象的不同调用。

序列图有两个维度:垂直维度以发生的时间顺序显示消息/调用的序列;水平维度显示消息被发送到的对象实例。

序列图的绘制非常简单。横跨图的顶部,每个框(参见图4)表示每个类的实例(对象)。在框中,类实例名称和类名称之间用空格/冒号/空格来分隔,例如,myReportGenerator : ReportGenerator。如果某个类实例向另一个类实例发送一条消息,则绘制一条具有指向接收类实例的开箭头的连线,并把消息/方法的名称放在连线上面。对于某些特别重要的消息,您可以绘制一条具有指向发起类实例的开箭头的虚线,将返回值标注在虚线上。就我而言,我总喜欢绘制出包括返回值的虚线,这些额外的信息可以使得序列图更易于阅读。

阅读序列图也非常简单。从左上角启动序列的"驱动"类实例开始,然后顺着每条消息往下阅读。记住:虽然图4所示的例子序列图显示了每条被发送消息的返回消息,但这只是可选的。


一个示例序列图

图4:一个示例序列图

通过阅读图4中的示例序列图,您可以明白如何创建一个CD销售报告(CD Sales Report)。其中的aServlet对象表示驱动类实例。aServlet向名为gen的ReportGenerator类实例发送一条消息。该消息被标为generateCDSalesReport,表示ReportGenerator对象实现了这个消息处理程序。进一步理解可发现,generateCDSalesReport消息标签在括号中包括了一个cdId,表明aServlet随该消息传递一个名为cdId的参数。当gen实例接收到一条generateCDSalesReport消息时,它会接着调用CDSalesReport类,并返回一个aCDReport的实例。然后gen实例对返回的aCDReport实例进行调用,在每次消息调用时向它传递参数。在该序列的结尾,gen实例向它的调用者aServlet返回一个aCDReport。

请注意:图4中的序列图相对于典型的序列图来说太详细了。然而,我认为它才是足够易于理解的,并且它显示了如何表示嵌套的调用。对于初级开发人员来说,有时把一个序列分解到这种详细程度是很有必要的,这有助于他们理解相关的内容。


状态图


状态图表示某个类所处的不同状态和该类的状态转换信息。有人可能会争论说每个类都有状态,但不是每个类都应该有一个状态图。只对"感兴趣的"状态的类(也就是说,在系统活动期间具有三个或更多潜在状态的类)才进行状态图描述。

如图5所示,状态图的符号集包括5个基本元素:初始起点,它使用实心圆来绘制;状态之间的转换,它使用具有开箭头的线段来绘制;状态,它使用圆角矩形来绘制;判断点,它使用空心圆来绘制;以及一个或者多个终止点,它们使用内部包含实心圆的圆来绘制。要绘制状态图,首先绘制起点和一条指向该类的初始状态的转换线段。状态本身可以在图上的任意位置绘制,然后只需使用状态转换线条将它们连接起来。


显示类通过某个功能系统的各种状态的状态图

图5:显示类通过某个功能系统的各种状态的状态图

图5中的状态图显示了它们可以表达的一些潜在信息。例如,从中可以看出贷款处理系统最初处于Loan Application状态。当批准前(pre-approval)过程完成时,根据该过程的结果,或者转到Loan Pre-approved状态,或者转到Loan Rejected状态。这个判断(它是在转换过程期间做出的)使用一个判断点来表示--即转换线条间的空心圆。通过该状态图可知,如果没有经过Loan Closing状态,贷款不可能从Loan Pre-Approved状态进入Loan in Maintenance状态。而且,所有贷款都将结束于Loan Rejected或者Loan in Maintenance状态。


活动图


活动图表示在处理某个活动时,两个或者更多类对象之间的过程控制流。活动图可用于在业务单元的级别上对更高级别的业务过程进行建模,或者对低级别的内部类操作进行建模。根据我的经验,活动图最适合用于对较高级别的过程建模,比如公司当前在如何运作业务,或者业务如何运作等。这是因为与序列图相比,活动图在表示上"不够技术性的",但有业务头脑的人们往往能够更快速地理解它们。

活动图的符号集与状态图中使用的符号集类似。像状态图一样,活动图也从一个连接到初始活动的实心圆开始。活动是通过一个圆角矩形(活动的名称包含在其内)来表示的。活动可以通过转换线段连接到其他活动,或者连接到判断点,这些判断点连接到由判断点的条件所保护的不同活动。结束过程的活动连接到一个终止点(就像在状态图中一样)。作为一种选择,活动可以分组为泳道(swimlane),泳道用于表示实际执行活动的对象,如图6所示。


活动图

图6:活动图,具有两个泳道,表示两个对象的活动控制:乐队经理,以及报告工具

图字(沿箭头方向):乐队经理;报告工具;选择"查看乐队的销售报告";检索该乐队经理所管理的乐队;显示报告条件选择屏幕;选择要查看其销售报告的乐队;从销售数据库检索销售数据;显示销售报告。

该活动图中有两个泳道,因为有两个对象控制着各自的活动:乐队经理和报告工具。整个过程首先从乐队经理选择查看他的乐队销售报告开始。然后报告工具检索并显示他管理的所有乐队,并要求他从中选择一个乐队。在乐队经理选择一个乐队之后,报告工具就检索销售信息并显示销售报告。该活动图表明,显示报告是整个过程中的最后一步。


组件图


组件图提供系统的物理视图。它的用途是显示系统中的软件对其他软件组件(例如,库函数)的依赖关系。组件图可以在一个非常高的层次上显示,从而仅显示粗粒度的组件,也可以在组件包层次2上显示。

组件图的建模最适合通过例子来描述。图7显示了4个组件:Reporting Tool、Billboard Service、Servlet 2.2 API和JDBC API。从Reporting Tool组件指向Billboard Service、Servlet 2.2 API和JDBC API组件的带箭头的线段,表示Reporting Tool依赖于那三个组件。


组件图显示了系统中各种软件组件的依赖关系

图7:组件图显示了系统中各种软件组件的依赖关系


部署图


部署图表示该软件系统如何部署到硬件环境中。它的用途是显示该系统不同的组件将在何处物理地运行,以及它们将如何彼此通信。因为部署图是对物理运行情况进行建模,系统的生产人员就可以很好地利用这种图。

部署图中的符号包括组件图中所使用的符号元素,另外还增加了几个符号,包括节点的概念。一个节点可以代表一台物理机器,或代表一个虚拟机器节点(例如,一个大型机节点)。要对节点进行建模,只需绘制一个三维立方体,节点的名称位于立方体的顶部。所使用的命名约定与序列图中相同:[实例名称] : [实例类型](例如,"w3reporting.myco.com : Application Server")。


部署图

图8:部署图。由于Reporting Tool组件绘制在IBM WebSphere内部,后者又绘制在节点w3.reporting.myco.com内部,因而我们知道,用户将通过运行在本地机器上的浏览器来访问Reporting Tool,浏览器通过公司intranet上的HTTP协议与Reporting Tool建立连接。

图8中的部署图表明,用户使用运行在本地机器上的浏览器访问Reporting Tool,并通过公司intranet上的HTTP协议连接到Reporting Tool组件。这个工具实际运行在名为w3reporting.myco.com的Application Server上。这个图还表明Reporting Tool组件绘制在IBM WebSphere内部,后者又绘制在w3.reporting.myco.com节点内部。Reporting Tool使用Java语言通过IBM DB2数据库的JDBC接口连接到它的报告数据库上,然后该接口又使用本地DB2通信方式,与运行在名为db1.myco.com的服务器上实际的DB2数据库通信。除了与报告数据库通信外,Report Tool组件还通过HTTPS上的SOAP与Billboard Service进行通信。

posted @ 2011-05-24 17:08 昊天 阅读(274) | 评论 (0)编辑 收藏
hibernate的session提供了一级缓存,每个session,对同一个id进行两次load,不会发送两条sql给数据库,但是session关闭的时候,一级缓存就失效了。

二级缓存是SessionFactory级别的全局缓存,它底下可以使用不同的缓存类库,比如ehcache、oscache等,需要设置hibernate.cache.provider_class,我们这里用ehcache,在2.1中就是
hibernate.cache.provider_class=net.sf.hibernate.cache.EhCacheProvider
如果使用查询缓存,加上
hibernate.cache.use_query_cache=true


缓存可以简单的看成一个Map,通过key在缓存里面找value。

Class的缓存
对于一条记录,也就是一个PO来说,是根据ID来找的,缓存的key就是ID,value是POJO。无论list,load还是iterate,只要读出一个对象,都会填充缓存。但是list不会使用缓存,而iterate会先取数据库select id出来,然后一个id一个id的load,如果在缓存里面有,就从缓存取,没有的话就去数据库load。假设是读写缓存,需要设置:
&lt;cache usage="read-write"/&gt;
如果你使用的二级缓存实现是ehcache的话,需要配置ehcache.xml
&lt;cache name="com.xxx.pojo.Foo" maxElementsInMemory="500" eternal="false" timeToLiveSeconds="7200" timeToIdleSeconds="3600" overflowToDisk="true" /&gt;
其中eternal表示缓存是不是永远不超时,timeToLiveSeconds是缓存中每个元素(这里也就是一个POJO)的超时时间,如果eternal="false",超过指定的时间,这个元素就被移走了。timeToIdleSeconds是发呆时间,是可选的。当往缓存里面put的元素超过500个时,如果overflowToDisk="true",就会把缓存中的部分数据保存在硬盘上的临时文件里面。
每个需要缓存的class都要这样配置。如果你没有配置,hibernate会在启动的时候警告你,然后使用defaultCache的配置,这样多个class会共享一个配置。
当某个ID通过hibernate修改时,hibernate会知道,于是移除缓存。
这样大家可能会想,同样的查询条件,第一次先list,第二次再iterate,就可以使用到缓存了。实际上这是很难的,因为你无法判断什么时候是第一次,而且每次查询的条件通常是不一样的,假如数据库里面有100条记录,id从1到100,第一次list的时候出了前50个id,第二次iterate的时候却查询到30至70号id,那么30-50是从缓存里面取的,51到70是从数据库取的,共发送1+20条sql。所以我一直认为iterate没有什么用,总是会有1+N的问题。
(题外话:有说法说大型查询用list会把整个结果集装入内存,很慢,而iterate只select id比较好,但是大型查询总是要分页查的,谁也不会真的把整个结果集装进来,假如一页20条的话,iterate共需要执行21条语句,list虽然选择若干字段,比iterate第一条select id语句慢一些,但只有一条语句,不装入整个结果集hibernate还会根据数据库方言做优化,比如使用mysql的limit,整体看来应该还是list快。)
如果想要对list或者iterate查询的结果缓存,就要用到查询缓存了

查询缓存
首先需要配置hibernate.cache.use_query_cache=true
如果用ehcache,配置ehcache.xml,注意hibernate3.0以后不是net.sf的包名了
&lt;cache name="net.sf.hibernate.cache.StandardQueryCache"
   maxElementsInMemory="50" eternal="false" timeToIdleSeconds="3600"
   timeToLiveSeconds="7200" overflowToDisk="true"/&gt;
&lt;cache name="net.sf.hibernate.cache.UpdateTimestampsCache"
   maxElementsInMemory="5000" eternal="true" overflowToDisk="true"/&gt;
然后
query.setCacheable(true);//激活查询缓存
query.setCacheRegion("myCacheRegion");//指定要使用的cacheRegion,可选
第二行指定要使用的cacheRegion是myCacheRegion,即你可以给每个查询缓存做一个单独的配置,使用setCacheRegion来做这个指定,需要在ehcache.xml里面配置它:
&lt;cache name="myCacheRegion" maxElementsInMemory="10" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true" /&gt;
如果省略第二行,不设置cacheRegion的话,那么会使用上面提到的标准查询缓存的配置,也就是net.sf.hibernate.cache.StandardQueryCache

对于查询缓存来说,缓存的key是根据hql生成的sql,再加上参数,分页等信息(可以通过日志输出看到,不过它的输出不是很可读,最好改一下它的代码)。
比如hql:
from Cat c where c.name like ?
生成大致如下的sql:
select * from cat c where c.name like ?
参数是"tiger%",那么查询缓存的key*大约*是这样的字符串(我是凭记忆写的,并不精确,不过看了也该明白了):
select * from cat c where c.name like ? , parameter:tiger%
这样,保证了同样的查询、同样的参数等条件下具有一样的key。
现在说说缓存的value,如果是list方式的话,value在这里并不是整个结果集,而是查询出来的这一串ID。也就是说,不管是list方法还是iterate方法,第一次查询的时候,它们的查询方式很它们平时的方式是一样的,list执行一条sql,iterate执行1+N条,多出来的行为是它们填充了缓存。但是到同样条件第二次查询的时候,就都和iterate的行为一样了,根据缓存的key去缓存里面查到了value,value是一串id,然后在到class的缓存里面去一个一个的load出来。这样做是为了节约内存。
可以看出来,查询缓存需要打开相关类的class缓存。list和iterate方法第一次执行的时候,都是既填充查询缓存又填充class缓存的。
这里还有一个很容易被忽视的重要问题,即打开查询缓存以后,即使是list方法也可能遇到1+N的问题!相同条件第一次list的时候,因为查询缓存中找不到,不管class缓存是否存在数据,总是发送一条sql语句到数据库获取全部数据,然后填充查询缓存和class缓存。但是第二次执行的时候,问题就来了,如果你的class缓存的超时时间比较短,现在class缓存都超时了,但是查询缓存还在,那么list方法在获取id串以后,将会一个一个去数据库load!因此,class缓存的超时时间一定不能短于查询缓存设置的超时时间!如果还设置了发呆时间的话,保证class缓存的发呆时间也大于查询的缓存的生存时间。这里还有其他情况,比如class缓存被程序强制evict了,这种情况就请自己注意了。

另外,如果hql查询包含select字句,那么查询缓存里面的value就是整个结果集了。

当hibernate更新数据库的时候,它怎么知道更新哪些查询缓存呢?
hibernate在一个地方维护每个表的最后更新时间,其实也就是放在上面net.sf.hibernate.cache.UpdateTimestampsCache所指定的缓存配置里面。
当通过hibernate更新的时候,hibernate会知道这次更新影响了哪些表。然后它更新这些表的最后更新时间。每个缓存都有一个生成时间和这个缓存所查询的表,当hibernate查询一个缓存是否存在的时候,如果缓存存在,它还要取出缓存的生成时间和这个缓存所查询的表,然后去查找这些表的最后更新时间,如果有一个表在生成时间后更新过了,那么这个缓存是无效的。
可以看出,只要更新过一个表,那么凡是涉及到这个表的查询缓存就失效了,因此查询缓存的命中率可能会比较低。

Collection缓存
需要在hbm的collection里面设置
&lt;cache usage="read-write"/&gt;
假如class是Cat,collection叫children,那么ehcache里面配置
&lt;cache name="com.xxx.pojo.Cat.children"
   maxElementsInMemory="20" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200"
   overflowToDisk="true" /&gt;
Collection的缓存和前面查询缓存的list一样,也是只保持一串id,但它不会因为这个表更新过就失效,一个collection缓存仅在这个collection里面的元素有增删时才失效。
这样有一个问题,如果你的collection是根据某个字段排序的,当其中一个元素更新了该字段时,导致顺序改变时,collection缓存里面的顺序没有做更新。

缓存策略
只读缓存(read-only):没有什么好说的
读/写缓存(read-write):程序可能要的更新数据
不严格的读/写缓存(nonstrict-read-write):需要更新数据,但是两个事务更新同一条记录的可能性很小,性能比读写缓存好
事务缓存(transactional):缓存支持事务,发生异常的时候,缓存也能够回滚,只支持jta环境,这个我没有怎么研究过

读写缓存和不严格读写缓存在实现上的区别在于,读写缓存更新缓存的时候会把缓存里面的数据换成一个锁,其他事务如果去取相应的缓存数据,发现被锁住了,然后就直接取数据库查询。
在hibernate2.1的ehcache实现中,如果锁住部分缓存的事务发生了异常,那么缓存会一直被锁住,直到60秒后超时。
不严格读写缓存不锁定缓存中的数据。


使用二级缓存的前置条件
你的hibernate程序对数据库有独占的写访问权,其他的进程更新了数据库,hibernate是不可能知道的。你操作数据库必需直接通过hibernate,如果你调用存储过程,或者自己使用jdbc更新数据库,hibernate也是不知道的。hibernate3.0的大批量更新和删除是不更新二级缓存的,但是据说3.1已经解决了这个问题。
这个限制相当的棘手,有时候hibernate做批量更新、删除很慢,但是你却不能自己写jdbc来优化,很郁闷吧。
SessionFactory也提供了移除缓存的方法,你一定要自己写一些JDBC的话,可以调用这些方法移除缓存,这些方法是:
void evict(Class persistentClass)
          Evict all entries from the second-level cache.
void evict(Class persistentClass, Serializable id)
          Evict an entry from the second-level cache.
void evictCollection(String roleName)
          Evict all entries from the second-level cache.
void evictCollection(String roleName, Serializable id)
          Evict an entry from the second-level cache.
void evictQueries()
          Evict any query result sets cached in the default query cache region.
void evictQueries(String cacheRegion)
          Evict any query result sets cached in the named query cache region.
不过我不建议这样做,因为这样很难维护。比如你现在用JDBC批量更新了某个表,有3个查询缓存会用到这个表,用evictQueries(String cacheRegion)移除了3个查询缓存,然后用evict(Class persistentClass)移除了class缓存,看上去好像完整了。不过哪天你添加了一个相关查询缓存,可能会忘记更新这里的移除代码。如果你的jdbc代码到处都是,在你添加一个查询缓存的时候,还知道其他什么地方也要做相应的改动吗?

----------------------------------------------------

总结:
不要想当然的以为缓存一定能提高性能,仅仅在你能够驾驭它并且条件合适的情况下才是这样的。hibernate的二级缓存限制还是比较多的,不方便用jdbc可能会大大的降低更新性能。在不了解原理的情况下乱用,可能会有1+N的问题。不当的使用还可能导致读出脏数据。
如果受不了hibernate的诸多限制,那么还是自己在应用程序的层面上做缓存吧。
在越高的层面上做缓存,效果就会越好。就好像尽管磁盘有缓存,数据库还是要实现自己的缓存,尽管数据库有缓存,咱们的应用程序还是要做缓存。因为底层的缓存它并不知道高层要用这些数据干什么,只能做的比较通用,而高层可以有针对性的实现缓存,所以在更高的级别上做缓存,效果也要好些吧。
posted @ 2011-05-24 16:51 昊天 阅读(257) | 评论 (0)编辑 收藏

Ehcache中不仅可以用配置文件来配置缓存,而在代码中也可以实现同样的功能。
CacheManager singletonManager = CacheManager.create();
Cache memoryOnlyCache = new Cache(“testCache”, 50000, false, false, 8, 2);
Cache test = singletonManager.getCache(“testCache”);
删除只需要调用singletonManager.removeCache(“testCache”);
Shotdown CacheManager
在使用完Ehcache后,必须要shutdown缓存。Ehcache中有自己的关闭机制,不过最好在你的代码中显示调用CacheManager.getInstance().shutdown();

1.EhCache是什么
    EhCache是Hibernate的二级缓存技术之一,可以把查询出来的数据存储在内存或者磁盘,节省下次同样查询语句再次查询数据库,大幅减轻数据库压力;

2.EhCache的使用注意点
    当用Hibernate的方式修改表数据(save,update,delete等等),这时EhCache会自动把缓存中关于此表的所有缓存全部删除掉(这样能达到同步)。但对于数据经常修改的表来说,可能就失去缓存的意义了(不能减轻数据库压力);

3.EhCache使用的场合
    3.1比较少更新表数据
        EhCache一般要使用在比较少执行write操作的表(包括update,insert,delete等)[Hibernate的二级缓存也都是这样];
    3.2对并发要求不是很严格的情况
        两台机子中的缓存是不能实时同步的;

4.在项目做的实现
    4.1在工程的src目录下添加ehcache.xml文件,内容如下:
        <?xml version="1.0" encoding="UTF-8"?>
        <ehcache>    
            <diskStore path="java.io.tmpdir" />
          <defaultCache maxElementsInMemory="5"<!--缓存可以存储的总记录量-->
            eternal="false"<!--缓存是否永远不销毁-->
            overflowToDisk="true"<!--当缓存中的数据达到最大值时,是否把缓存数据写入磁盘-->
            timeToIdleSeconds="15"<!--当缓存闲置时间超过该值,则缓存自动销毁-->
                timeToLiveSeconds="120"<!--缓存创建之后,到达该缓存自动销毁-->
          />
        </ehcache>
    4.2在Hibernate.cfg.xml中的mapping标签上面加以下内容:
        <property name="show_sql">true</property>
        <property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
        <property name="hibernate.cache.use_query_cache">true</property>
    4.3在要缓存的bean的hbm.xml文件中的class标签下加入以下内容:
       <cache usage="read-only" /><!--也可读写-->
    4.4创建DAO,内容如下:
        Session s = HibernateSessionFactory.getSession();
        Criteria c = s.createCriteria(Xyz.class);
        c.setCacheable(true);//这句必须要有
        System.out.println("第一次读取");
        List l = c.list();
        System.out.println(l.size());
        HibernateSessionFactory.closeSession();

        s = HibernateSessionFactory.getSession();
        c = s.createCriteria(Xyz.class);
        c.setCacheable(true);//这句必须要有
        System.out.println("第二次读取");
        l = c.list();
        System.out.println(l.size());
        HibernateSessionFactory.closeSession();
   4.5这时你会看到打印出来的信息为(表示第二次并没有去读库):
        第一次读取
        Hibernate: *******
        13
        第二次读取
        13

配置Spring+hibernate使用ehcache作为second-level cache

大量数据流动是web应用性能问题常见的原因,而缓存被广泛的用于优化数据库应用。cache被设计为通过保存从数据库里load的数据来减少应用和数据库之间的数据流动。数据库访问只有当检索的数据不在cache里可用时才必要。hibernate可以用两种不同的对象缓存:first-level cache 和 second-level cache。first-level cache和Session对象关联,而second-level cache是和Session Factory对象关联。

        缺省地,hibernate已经使用基于每个事务的first-level cache。 Hibernate用first-level cache主要是减少在一个事务内的sql查询数量。例如,如果一个对象在同一个事务内被修改多次,hibernate将只生成一个包括所有修改的 UPDATE SQL语句。为了减少数据流动,second-level cache在Session Factory级的不同事务之间保持load的对象,这些对象对整个应用可用,不只是对当前用户正在运行的查询。这样,每次查询将返回已经load在缓存里的对象,避免一个或更多潜在的数据库事务。

下载ehcache,hibernate3.2必须要ehcache1.2以上才能支持。可以修改log4j配置文件log4j.logger.net.sf.hibernate.cache=debug查看日志

1.在类路径上ehcache.xml:

<ehcache>

     <!-- Sets the path to the directory where cache .data files are created.

          If the path is a Java System Property it is replaced by
          its value in the running VM.

          The following properties are translated:
          user.home - User's home directory
          user.dir - User's current working directory
          java.io.tmpdir - Default temp file path -->
     <diskStore path="java.io.tmpdir"/>


     <!--Default Cache configuration. These will applied to caches programmatically created through
         the CacheManager.

         The following attributes are required:

         maxElementsInMemory             - Sets the maximum number of objects that will be created in memory
         eternal                         - Sets whether elements are eternal. If eternal,   timeouts are ignored and the
                                          element is never expired.
         overflowToDisk                  - Sets whether elements can overflow to disk when the in-memory cache
                                          has reached the maxInMemory limit.

         The following attributes are optional:
         timeToIdleSeconds               - Sets the time to idle for an element before it expires.
                                          i.e. The maximum amount of time between accesses before an element expires
                                          Is only used if the element is not eternal.
                                          Optional attribute. A value of 0 means that an Element can idle for infinity.
                                          The default value is 0.
         timeToLiveSeconds               - Sets the time to live for an element before it expires.
                                          i.e. The maximum time between creation time and when an element expires.
                                          Is only used if the element is not eternal.
                                          Optional attribute. A value of 0 means that and Element can live for infinity.
                                          The default value is 0.
         diskPersistent                  - Whether the disk store persists between restarts of the Virtual Machine.
                                          The default value is false.
         diskExpiryThreadIntervalSeconds- The number of seconds between runs of the disk expiry thread. The default value
                                          is 120 seconds.
         -->

     <defaultCache
         maxElementsInMemory="10000"
         eternal="false"
         overflowToDisk="true"
         timeToIdleSeconds="120"
         timeToLiveSeconds="120"
         diskPersistent="false"
         diskExpiryThreadIntervalSeconds="120"/>
       
     <!-- See http://ehcache.sourceforge.net/documentation/#mozTocId258426 for how to configure caching for your objects -->
</ehcache>

2.applicationContext-hibernate.xml里Hibernate SessionFactory配置:

     <!-- Hibernate SessionFactory -->
     <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
         <property name="dataSource" ref="dataSource"/>
         <property name="configLocation"><value>classpath:hibernate.cfg.xml</value></property>
         <!-- The property below is commented out b/c it doesn't work when run via
              Ant in Eclipse.   It works fine for individual JUnit tests and in IDEA ??
         <property name="mappingJarLocations">
             <list><value>file:dist/appfuse-dao.jar</value></list>
         </property>
         -->
         <property name="hibernateProperties">
             <props>
                 <prop key="hibernate.dialect">@HIBERNATE-DIALECT@</prop>
                 <!--<prop key="hibernate.show_sql">true</prop>-->
                 <prop key="hibernate.max_fetch_depth">3</prop>
                 <prop key="hibernate.hibernate.use_outer_join">true</prop>
                 <prop key="hibernate.jdbc.batch_size">10</prop>
                 <prop key="hibernate.cache.use_query_cache">true</prop>
                 <prop key="hibernate.cache.use_second_level_cache">true</prop>
                 <prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>
                 <!--
                 <prop key="hibernate.use_sql_comments">false</prop>
                 -->
                 <!-- Create/update the database tables automatically when the JVM starts up
                 <prop key="hibernate.hbm2ddl.auto">update</prop> -->
                 <!-- Turn batching off for better error messages under PostgreSQL
                 <prop key="hibernate.jdbc.batch_size">0</prop> -->
             </props>
         </property>
         <property name="entityInterceptor">
            <ref local="auditLogInterceptor"/>
         </property>
     </bean>
说明:如果不设置“查询缓存”,那么hibernate只会缓存使用load()方法获得的单个持久化对象,如果想缓存使用findall()、 list()、Iterator()、createCriteria()、createQuery()等方法获得的数据结果集的话,就需要设置 hibernate.cache.use_query_cache true 才行

3.model类里采用Xdoclet生成*.hbm.xml里的cache xml标签,即<cache usage="read-only"/>

/**
* @hibernate.class table="WF_WORKITEM_HIS"
* @hibernate.cache usage="read-write"
*
*/


4.对于"query cache",需要在程序里编码:

         getHibernateTemplate().setCacheQueries(true);
         return getHibernateTemplate().find(hql);

使用spring和hibernate配置ehcache和query cache
1、applicationContext.xml
<prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>
<prop key="hibernate.cache.use_query_cache">true</prop>
这两句加到hibernateProperties中
<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.HibernateTemplate">
<property name="sessionFactory">
   <ref bean="sessionFactory" />
</property>
<property name="cacheQueries">
   <value>true</value>
</property>
</bean>

添加此bean到applicationcontext.xml中。在各个DAO的bean中,更改如下
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
改为
<property name="hibernateTemplate">
<ref bean="hibernateTemplate" />
</property>

2、ehcache.xml文件放在classes根目录即可

3、pojo与ehcache.xml的配置关系
以com.ce.ceblog.pojos.CeblogJournal为例子
在CeblogJournal.hbm.xml中配置:
<class name="CeblogJournal" table="CEBLOG_JOURNAL" lazy="false">
<cache usage="read-write" region="ehcache.xml中的name的属性值"/>
注意:这一句需要紧跟在class标签下面,其他位置无效。

Ehcache.xml文件主体如下
<defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="1" timeToLiveSeconds="1" overflowToDisk="true" />
<cache name="com.ce.ceblog.pojos.CeblogJournal" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true" />
hbm文件查找cache方法名的策略:如果不指定hbm文件中的region="ehcache.xml中的name的属性值",则使用name名为 com.ce.ceblog.pojos.CeblogJournal的cache,如果不存在与类名匹配的cache名称,则用 defaultCache。
如果CeblogJournal包含set集合,则需要另行指定其cache
例如CeblogJournal包含ceblogReplySet集合,则需要
添加如下配置到ehcache.xml中
<cache name="com.ce.ceblog.pojos.CeblogJournal.ceblogReplySet"
maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300"
timeToLiveSeconds="600" overflowToDisk="true" />

另,针对查询缓存的配置如下:
<cache name="org.hibernate.cache.UpdateTimestampsCache"
maxElementsInMemory="5000"
eternal="true"
overflowToDisk="true"/>
<cache name="org.hibernate.cache.StandardQueryCache"
maxElementsInMemory="10000"
eternal="false"
timeToLiveSeconds="120"
overflowToDisk="true"/>

4、选择缓存策略依据:
<cache usage="transactional|read-write|nonstrict-read-write|read-only" />
ehcache不支持transactional,其他三种可以支持。
read- only:无需修改, 那么就可以对其进行只读 缓存,注意,在此策略下,如果直接修改数据库,即使能够看到前台显示效果,但是将对象修改至cache中会报error,cache不会发生作用。另:删 除记录会报错,因为不能在read-only模式的对象从cache中删除。
read-write:需要更新数据,那么使用读/写缓存 比较合适,前提:数据库不可以为serializable transaction isolation level(序列化事务隔离级别)
nonstrict-read-write:只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离,那么比较适合使用非严格读/写缓存策略。

5、调试时候使用log4j的log4j.logger.org.hibernate.cache=debug,更方便看到ehcache的操作过程,主要用于调试过程,实际应用发布时候,请注释掉,以免影响性能。

6、 使用ehcache,打印sql语句是正常的,因为query cache设置为true将会创建两个缓存区域:一个用于保存查询结果集 (org.hibernate.cache.StandardQueryCache);另一个则用于保存最近查询的一系列表的时间戳(org.hibernate.cache.UpdateTimestampsCache)。请注意:在查询缓存中,它并不缓存结果集中所包含的实体的确切状态;它只缓存这些实体的标识符属性的值、以及各值类型的结果。需要将打印sql语句与最近的cache内容相比较,将不同之处修改到cache中,所以查询缓存通常会和二级缓存一起使用。

posted @ 2011-05-24 16:20 昊天 阅读(5956) | 评论 (1)编辑 收藏

Spring提供了对quartz的集成,这样在j2ee的应用中,可以很方便的实现我们的任务计划,比如:你可以设定每天半夜的时候,来实现备份数据库,记录日志,因为这个时候web的压力相对比较小。也可以用来定时的发EMAIL等。

1、Spring中集成quartz 首先需要在web.xml 中配置个quartz的监听器。这样,随着WEB程序的启动,会自动启动quartz的调度配置。

Web.xml 中加入 监听器:

<listener> 
<listener-class>com.yangxinyong.quartz.listener.QuartzServiceLoader</listener-class>  
 </listener>

2、QuartzServiceLoader 实现了 javax.servlet.ServletContextListener  ,里面有两个方法。

QuartzServiceLoader类:

Java代码  
  1. public class QuartzServiceLoader implements ServletContextListener {     
  2.     public void contextDestroyed(ServletContextEvent arg0) {             
  3.         try {     
  4.                 //监听器关闭时关闭Sheduler     
  5.          JobListener.stop();     
  6.         } catch (Exception ex) {     
  7.             System.out.println(ex.getMessage());     
  8.         }     
  9.          
  10.     }     
  11.     
  12.     public void contextInitialized(ServletContextEvent arg0) {     
  13.         try {    //监听器启动时启动Sheduler      
  14.          JobListener.run();     
  15.         } catch (Exception ex) {     
  16.             System.out.println(ex.getMessage());     
  17.         }     
  18.     }     
  19.     
  20. }    

 

3、此时,我们要新建个 JobListener 类 ,来读取配置文件applicationContext-quartz.xml,启动quartz的计划任务配置,以及关闭监听时的关闭quartz的操作。

Java代码  
  1. package com.yangxinyong.quartz.listener;   
  2.   
  3. import org.quartz.Scheduler;   
  4. import org.quartz.impl.StdSchedulerFactory;   
  5. import org.springframework.beans.factory.xml.XmlBeanFactory;   
  6. import org.springframework.core.io.ClassPathResource;   
  7. import org.springframework.scheduling.quartz.CronTriggerBean;   
  8. import org.springframework.scheduling.quartz.JobDetailBean;   
  9.   
  10.   
  11. /**  
  12.  * @author Seyo816@gmail.com  
  13.  */  
  14. public class JobListener   
  15. {   
  16.     
  17.  public static void run() throws Exception {     
  18.   ClassPathResource res = new ClassPathResource( "applicationContext-quartz.xml" );   
  19.    XmlBeanFactory factory = new XmlBeanFactory( res );   
  20.    JobDetailBean job = ( JobDetailBean ) factory   
  21.      .getBean( "Job" );   
  22.    CronTriggerBean trigger = ( CronTriggerBean ) factory   
  23.      .getBean( "cronTrigger" );   
  24.    Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler( );   
  25.    scheduler.start( );   
  26.    scheduler.scheduleJob( job, trigger );   
  27.  }     
  28.   
  29.   
  30.  public static void stop() throws Exception {     
  31.        
  32.  }     
  33. }  

 

其中applicationContext-quartz.xml 放在classpath路径下,应用程序才可以找到该配置文件。配置文件定义了一个job bean 以及 触发器cronTrigger bean ,最后 通过 SchedulerFactoryBean 添加 添加触发器 。

applicationContext-quartz.xml  代码:

 

Xml代码  
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"    
  3. "http://www.springframework.org/dtd/spring-beans.dtd">  
  4. <beans>  
  5.   
  6.  <bean name="Job" class="org.springframework.scheduling.quartz.JobDetailBean">  
  7.     
  8.   <property name="jobClass">  
  9.    <value>com.yangxinyong.quartz.listener.Job</value>  
  10.   </property>  
  11.     
  12.   <property name="jobDataAsMap">  
  13.    <map>  
  14.     <entry key="email"><value>seyo816@gmail.com</value></entry>  
  15.    </map>  
  16.   </property>  
  17.     
  18.     
  19.  </bean>  
  20.     
  21.  <bean id="cronTrigger" class="com.yangxinyong.quartz.listener.InitializingCronTrigger">  
  22.   <property name="jobDetail">  
  23.    <ref bean="Job"/>  
  24.   </property>  
  25.   <property name="cronExpression">  
  26.    <value>0 50 16 * * ?</value>  
  27.   </property>  
  28.  </bean>  
  29.     
  30.     
  31.  <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
  32.   
  33.   <!-- 添加触发器 -->  
  34.   <property name="triggers">  
  35.    <list>  
  36.     <ref local="cronTrigger"/>  
  37.    </list>  
  38.   </property>  
  39.  </bean>  
  40.     
  41. </beans>  

 

3、job bean  对应的类:com.yangxinyong.quartz.listener.Job 是spring的 QuartzJobBean接口的实现,用来定义 执行的任务。

Java代码  
  1. package com.yangxinyong.quartz.listener;   
  2.   
  3. import java.util.Date;   
  4.   
  5. import org.quartz.JobDetail;   
  6. import org.quartz.JobExecutionContext;   
  7. import org.quartz.JobExecutionException;   
  8. import org.springframework.scheduling.quartz.QuartzJobBean;   
  9.   
  10. /**  
  11.  * @author Seyo816@gmail.com  
  12.  */  
  13. public class Job extends QuartzJobBean {   
  14.  private String email;   
  15.   
  16.  protected void executeInternal(JobExecutionContext context)   
  17.    throws JobExecutionException {   
  18.   JobDetail job= context.getJobDetail();   
  19.   System.out.println(new Date()+":"+job.getName()+"["+email+"]");   
  20.  }   
  21.   
  22.   
  23.  public String getEmail() {   
  24.   return email;   
  25.  }   
  26.   
  27.  public void setEmail(String email) {   
  28.   this.email = email;   
  29.  }   
  30.   
  31. }  

 

该job只是简单的 在控制台中输出 一串字符串。

最后你可以 部署该 web应用,根据触发器中的时间配置,将实现 每天的16:50分的时候 执行操作。我们可以看到下面的输出字符串。

Tue Nov 18 16:50:00 CST 2008:Job[seyo816@gmail.com]

posted @ 2011-05-24 11:50 昊天 阅读(3708) | 评论 (1)编辑 收藏

Lucence是一个很容易上手,java语言的全文索引检索工具包。

    Lucene的作者是资深的全文索引/检索专家,最开始发布在他本人的主页上,200110月贡献给APACHE,成为APACHE基金jakarta的一个子项目。

    目前,lucene广泛用于全文索引/检索的项目中。 

    Lucene 原理

lucene的检索算法属于索引检索,即用空间来换取时间,对需要检索的文件、字符流进行全文索引,在检索的时候对索引进行快速的检索,得到检索位置,这个位置记录检索词出现的文件路径或者某个关键词。

    在使用数据库的项目中,不使用数据库进行检索的原因主要是:数据库在非精确查询的时候使用查询语言“like %keyword%”,对数据库进行查询是对所有记录遍历,并对字段进行“%keyword%”匹配,在数据库的数据庞大以及某个字段存储的数据量庞大的时候,这种遍历是致命的,它需要对所有的记录进行匹配查询。因此,lucene主要适用于文档集的全文检索,以及海量数据库的模糊检索,特别是对数据库的 xml或者大数据的字符类型。 

    全文检索的实现机制

LuceneAPI接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。

 

比较一下Lucene和数据库:

Lucene

数据库

索引数据源:doc(field1,field2...) doc(field1,field2...)

\ indexer /

| Lucene Index|

/ searcher \

结果输出:Hits(doc(field1,field2) doc(field1...))

索引数据源:record(field1,field2...) record(field1..)

\ SQL: insert/

| DB Index |

/ SQL: select \

结果输出:results(record(field1,field2..) record(field1...))

Document:一个需要进行索引的“单元”

一个Document由多个字段组成

Record:记录,包含多个字段

Field:字段

Field:字段

Hits:查询结果集,由匹配的Document组成

RecordSet:查询结果集,由多个Record组成

 

全文检索 like "%keyword%"

-------------------------------------------------------------------------

Lucene作为一个全文检索引擎,其具有如下突出的优点:

 

1)索引文件格式独立于应用平台。Lucene定义了一套以8位字节为基础的索引文件格式,使得兼容系统或者不同平台的应用能够共享建立的索引文件。

2)在传统全文检索引擎的倒排索引的基础上,实现了分块索引,能够针对新的文件建立小文件索引,提升索引速度。然后通过与原有索引的合并,达到优化的目的。

3)优秀的面向对象的系统架构,使得对于Lucene扩展的学习难度降低,方便扩充新功能。

4)设计了独立于语言和文件格式的文本分析接口,索引器通过接受Token流完成索引文件的创立,用户扩展新的语言和文件格式,只需要实现文本分析的接口

5)已经默认实现了一套强大的查询引擎,用户无需自己编写代码即使系统可获得强大的查询能力,Lucene的查询实现中默认实现了布尔操作、模糊查询(Fuzzy Search[11])、分组查询等等。

---------------------------------------------------------------------------------- 

lucence例子: 

Windows

    C:\file\1.txt; 内容:中华人民共和国,全国人民 2006 

    C:\file\2.txt;

    C:\file\3.txt;

    C:\index

step1.建立索引;step2.搜索文档 

建立索引 

为了对文档进行索引,Lucene 提供了五个基础的类,他们分别是 Document, Field, IndexWriter, Analyzer, Directory。下面我们分别介绍一下这五个类的用途: 

Document

Document 是用来描述文档的,这里的文档可以指一个 HTML 页面,一封电子邮件,或者是一个文本文件。一个 Document 对象由多个 Field 对象组成的。可以把一个 Document 对象想象成数据库中的一个记录,而每个 Field 对象就是记录的一个字段。 

Field

Field 对象是用来描述一个文档的某个属性的,比如一封电子邮件的标题和内容可以用两个 Field 对象分别描述。 

Analyzer

在一个文档被索引之前,首先需要对文档内容进行分词处理,这部分工作就是由 Analyzer 来做的。Analyzer 类是一个抽象类,它有多个实现。针对不同的语言和应用需要选择适合的 AnalyzerAnalyzer 把分词后的内容交给 IndexWriter 来建立索引 

IndexWriter

IndexWriter Lucene 用来创建索引的一个核心的类,他的作用是把一个个的 Document 对象加到索引中来。 

Directory

这个类代表了 Lucene 的索引的存储的位置,这是一个抽象类,它目前有两个实现,第一个是 FSDirectory,它表示一个存储在文件系统中的索引的位置。第二个是 RAMDirectory,它表示一个存储在内存当中的索引的位置。 

搜索文档 

利用Lucene进行搜索就像建立索引一样也是非常方便的。在上面一部分中,我们已经为一个目录下的文本文档建立好了索引,现在我们就要在这个索引上进行搜索以找到包含某个关键词或短语的文档。Lucene提供了几个基础的类来完成这个过程,它们分别是呢IndexSearcher, Term, Query, TermQuery, Hits. 下面我们分别介绍这几个类的功能。 

Query

这是一个抽象类,他有多个实现,比如TermQuery, BooleanQuery, PrefixQuery. 这个类的目的是把用户输入的查询字符串封装成Lucene能够识别的Query
Term

Term是搜索的基本单位,一个Term对象有两个String类型的域组成。生成一个Term对象可以有如下一条语句来完成:Term term = new Term(fieldName,queryWord); 其中第一个参数代表了要在文档的哪一个Field上进行查找,第二个参数代表了要查询的关键词。 

TermQuery

TermQuery是抽象类Query的一个子类,它同时也是Lucene支持的最为基本的一个查询类。生成一个TermQuery对象由如下语句完成: TermQuery termQuery = new TermQuery(new Term(fieldName,queryWord)); 它的构造函数只接受一个参数,那就是一个Term对象。 

IndexSearcher

IndexSearcher是用来在建立好的索引上进行搜索的。它只能以只读的方式打开一个索引,所以可以有多个IndexSearcher的实例在一个索引上进行操作。 

Hits

Hits是用来保存搜索的结果的。

————————————————————————

总结:

Lucene 其实很简单的,它最主要就是做两件事:建立索引和进行搜索
来看一些在lucene中使用的术语,这里并不打算作详细的介绍,只是点一下而已----因为这一个世界有一种好东西,叫搜索。

IndexWriter:lucene中最重要的的类之一,它主要是用来 将文档加入索引,同时控制索引过程中的一些参数使用。

Analyzer:分析器,主要用于分析搜索引擎遇到的各种文本。常用的 StandardAnalyzer分析器,StopAnalyzer分析器,WhitespaceAnalyzer分析器等。

Directory:索引存放的位置;lucene提供了两种索引存放的 位置,一种是磁盘,一种是内存。一般情况将索引放在磁盘上;相应地lucene提供了FSDirectoryRAMDirectory两个类。

Document:文档;Document相当于一个要进行索引的单元, 任何可以想要被索引的文件都必须转化为Document对象才能进行索引。

Field:字段。

IndexSearcher:lucene中最基本的检索工具,所有的 检索都会用到IndexSearcher工具;

Query:查询,lucene中支持模糊查询,语义查询,短语查询,组 合查询等等,如有TermQuery,BooleanQuery,RangeQuery,WildcardQuery等一些类。

QueryParser: 是一个解析用户输入的工具,可以通过扫描用户输入的字符串,生成Query对象。

Hits:在搜索完成之后,需要把搜索结果返回并显示给用户,只有这样才 算是完成搜索的目的。在lucene中,搜索的结果的集合是用Hits类的实例来表示的。

posted @ 2011-05-24 10:58 昊天 阅读(3705) | 评论 (0)编辑 收藏

概述

了解Quartz体系结构

Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心的概念,并在org.quartz通过接口和类对重要的这些核心概念进行描述:

●Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在 JobDataMap实例中;

●JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

通过该类的构造函数可以更具体地了解它的功用:JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass),该构造函数要求指定Job的实现类,以及任务在Scheduler中的组名和Job名称;

●Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和 CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则 可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;

●Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历 特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合—— java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个 Trigger可以和多个Calendar关联,以便排除或包含某些时间点。

假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触 发机制的基础上使用Calendar进行定点排除。针对不同时间段类型,Quartz在org.quartz.impl.calendar包下提供了若干 个Calendar的实现类,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分别针对每年、每月和每周 进行定义;

●Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到 Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组 及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多 个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可 以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于 ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。 SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个 put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例;

●ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

Job有一个StatefulJob子接口,代表有状态的任务,该接口是一个没有方法的标签接口,其目的是让Quartz 知道任务的类型,以便采用不同的执行方案。无状态任务在执行时拥有自己的JobDataMap拷贝,对JobDataMap的更改不会影响下次的执行。而 有状态任务共享共享同一个JobDataMap实例,每次任务执行对JobDataMap所做的更改会保存下来,后面的执行可以看到这个更改,也即每次执 行任务后都会对后面的执行发生影响。

正因为这个原因,无状态的Job可以并发执行,而有状态的StatefulJob不能并发执行,这意味着如果前次的 StatefulJob还没有执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务需要考虑更多的因素,程序往往拥有更高的 复杂度,因此除非必要,应该尽量使用无状态的Job。

如果Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。

Trigger自身也可以拥有一个JobDataMap,其关联的Job可以通过 JobExecutionContext#getTrigger().getJobDataMap()获取Trigger中的JobDataMap。不管 是有状态还是无状态的任务,在任务执行期间对Trigger的JobDataMap所做的更改都不会进行持久,也即不会对下次的执行产生影响。

Quartz拥有完善的事件和监听体系,大部分组件都拥有事件,如任务执行前事件、任务执行后事件、触发器触发前事件、触发后事件、调度器开始事件、关闭事件等等,可以注册相应的监听器处理感兴趣的事件。

图1描述了Scheduler的内部组件结构,SchedulerContext提供Scheduler全局可见的上下文信息,每一个任务都对应一个JobDataMap,虚线表达的JobDataMap表示对应有状态的任务:

Scheduler结构图

一个Scheduler可以拥有多个Triger组和多个JobDetail组,注册Trigger和JobDetail 时,如果不显式指定所属的组,Scheduler将放入到默认组中,默认组的组名为Scheduler.DEFAULT_GROUP。组名和名称组成了对 象的全名,同一类型对象的全名不能相同。

Scheduler本身就是一个容器,它维护着Quartz的各种组件并实施调度的规则。Scheduler还拥有一个线 程池,线程池为任务提供执行线程——这比执行任务时简单地创建一个新线程要拥有更高的效率,同时通过共享节约资源的占用。通过线程池组件的支持,对于繁忙 度高、压力大的任务调度,Quartz将可以提供良好的伸缩性。

提示: Quartz完整下载包examples目录下拥有10多个实例,它们是快速掌握Quartz应用很好的实例。

使用SimpleTrigger

SimpleTrigger拥有多个重载的构造函数,用以在不同场合下构造出对应的实例:

●SimpleTrigger(String name, String group):通过该构造函数指定Trigger所属组和名称;

●SimpleTrigger(String name, String group, Date startTime):除指定Trigger所属组和名称外,还可以指定触发的开发时间;

●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval):除指定以上信息外,还可以指定结束时间、重复执行次数、时间间隔等参数;

●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval):这是最复杂的一个构造函数,在指定触发参数的同时,还通过jobGroup和jobName,让该Trigger和 Scheduler中的某个任务关联起来。

通过实现 org.quartz..Job 接口,可以使 Java 类化身为可调度的任务。代码清单1提供了 Quartz 任务的一个示例:

SimpleJob:简单的Job实现类

package com.baobaotao.basic.quartz;

import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class SimpleJob implements Job {

①实例Job接口方法

public void execute(JobExecutionContext jobCtx)throws JobExecutionException {
System.out.println(jobCtx.getTrigger().getName()+ " triggered. time is:" + (new Date()));
}
}

这个类用一条非常简单的输出语句实现了Job接口的execute(JobExecutionContext context) 方法,这个方法可以包含想要执行的任何代码。下面,我们通过SimpleTrigger对SimpleJob进行调度:

SimpleTriggerRunner:使用SimpleTrigger进行调度

package com.baobaotao.basic.quartz;

import java.util.Date;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;

public class SimpleTriggerRunner {

public static void main(String args[]) {
try {
①创建一个JobDetail实例,指定SimpleJob
JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);

②通过SimpleTrigger定义调度规则:马上启动,每2秒运行一次,共运行100次
SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");
simpleTrigger.setStartTime(new Date());
simpleTrigger.setRepeatInterval(2000);
simpleTrigger.setRepeatCount(100);

③通过SchedulerFactory获取一个调度器实例
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, simpleTrigger);④ 注册并进行调度
scheduler.start();⑤调度启动
} catch (Exception e) {
e.printStackTrace();
}
}
}

首先在①处通过JobDetail封装SimpleJob,同时指定Job在Scheduler中所属组及名称,这里,组名为jGroup1,而名称为job1_1。

在②处创建一个SimpleTrigger实例,指定该Trigger在Scheduler中所属组及名称。接着设置调度的时间规则。

最后,需要创建Scheduler实例,并将JobDetail和Trigger实例注册到Scheduler中。这里, 我们通过StdSchedulerFactory获取一个Scheduler实例,并通过scheduleJob(JobDetail jobDetail, Trigger trigger)完成两件事:

1)将JobDetail和Trigger注册到Scheduler中;

2)将Trigger指派给JobDetail,将两者关联起来。

当Scheduler启动后,Trigger将定期触发并执行SimpleJob的execute(JobExecutionContext jobCtx)方法,然后每 10 秒重复一次,直到任务被执行 100 次后停止。

还可以通过SimpleTrigger的setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定运行的时间范围,当运行次数和时间范围冲突时,超过时间范围的任务运行不被执行。如可以通过 simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + 60000L))指定60秒钟以后开始。

除了通过scheduleJob(jobDetail, simpleTrigger)建立Trigger和JobDetail的关联,还有另外一种关联Trigger和JobDetail的方式:

JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);
SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");

simpleTrigger.setJobGroup("jGroup1");①-1:指定关联的Job组名
simpleTrigger.setJobName("job1_1");①-2:指定关联的Job名称
scheduler.addJob(jobDetail, true);② 注册JobDetail
scheduler.scheduleJob(simpleTrigger);③ 注册指定了关联JobDetail的Trigger

 

在这种方式中,Trigger通过指定Job所属组及Job名称,然后使用Scheduler的scheduleJob(Trigger trigger)方法注册Trigger。有两个值得注意的地方:

通过这种方式注册的Trigger实例必须已经指定Job组和Job名称,否则调用注册Trigger的方法将抛出异常;

引用的JobDetail对象必须已经存在于Scheduler中。也即,代码中①、②和③的先后顺序不能互换。

在构造Trigger实例时,可以考虑使用org.quartz.TriggerUtils工具类,该工具类不但提供了众 多获取特定时间的方法,还拥有众多获取常见Trigger的方法,如makeSecondlyTrigger(String trigName)方法将创建一个每秒执行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute)将创建一个每星期某一特定时间点执行一次的Trigger。而getEvenMinuteDate(Date date)方法将返回某一时间点一分钟以后的时间。

使用CronTrigger

CronTrigger 能够提供比 SimpleTrigger 更有具体实际意义的调度方案,调度规则基于 Cron 表达式,CronTrigger 支持日历相关的重复时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。因此,相对于SimpleTrigger而 言,CronTrigger在使用上也要复杂一些。

Cron表达式

Quartz使用类似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示:

Cron表达式时间字段

位置

时间域名

允许值

允许的特殊字符

1

0-59

, - * /

2

分钟

0-59

, - * /

3

小时

0-23

, - * /

4

日期

1-31

, - * ? / L W C

5

月份

1-12

, - * /

6

星期

1-7

, - * ? / L C #

7

年(可选)

空值1970-2099

, - * /

Cron表达式的时间字段除允许设置数值外,还可使用一些特殊的字符,提供列表、范围、通配符等功能,细说如下:

●星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如,*在分钟字段时,表示“每分钟”;

●问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;

●减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;

●逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;

●斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;

●L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月 份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;

●W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工 作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意 关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期 范围;

●LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;

●井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;

● C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。

Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。

表2下面给出一些完整的Cron表示式的实例:

Cron表示式示例

表示式

说明

"0 0 12 * * ? "

每天12点运行

"0 15 10 ? * *"

每天10:15运行

"0 15 10 * * ?"

每天10:15运行

"0 15 10 * * ? *"

每天10:15运行

"0 15 10 * * ? 2008"

在2008年的每天10:15运行

"0 * 14 * * ?"

每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59。

"0 0/5 14 * * ?"

每天14点到15点每5分钟运行一次,开始于14:00,结束于14:55。

"0 0/5 14,18 * * ?"

每天14点到15点每5分钟运行一次,此外每天18点到19点每5钟也运行一次。

"0 0-5 14 * * ?"

每天14:00点到14:05,每分钟运行一次。

"0 10,44 14 ? 3 WED"

3月每周三的14:10分到14:44,每分钟运行一次。

"0 15 10 ? * MON-FRI"

每周一,二,三,四,五的10:15分运行。

"0 15 10 15 * ?"

每月15日10:15分运行。

"0 15 10 L * ?"

每月最后一天10:15分运行。

"0 15 10 ? * 6L"

每月最后一个星期五10:15分运行。

"0 15 10 ? * 6L 2007-2009"

在2007,2008,2009年每个月的最后一个星期五的10:15分运行。

"0 15 10 ? * 6#3"

每月第三个星期五的10:15分运行。

CronTrigger实例

下面,我们使用CronTrigger对SimpleJob进行调度,通过Cron表达式制定调度规则,让它每5秒钟运行一次:

CronTriggerRunner:使用CronTrigger进行调度

package com.baobaotao.basic.quartz;

import org.quartz.CronExpression;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;

public class CronTriggerRunner {

public static void main(String args[]) {
try {
JobDetail jobDetail = new JobDetail("job1_2", "jGroup1",SimpleJob.class);
①-1:创建CronTrigger,指定组及名称
CronTrigger cronTrigger = new CronTrigger("trigger1_2", "tgroup1");
CronExpression cexp = new CronExpression("0/5 * * * * ?");①-2:定义Cron表达式
cronTrigger.setCronExpression(cexp);①-3:设置Cron表达式
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
//②
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行CronTriggerRunner,每5秒钟将触发运行SimpleJob一次。默认情况下Cron表达式对应当前的时区,可以通过 CronTriggerRunner的setTimeZone(java.util.TimeZone timeZone)方法显式指定时区。你还也可以通过setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定开始和结束的时间。

在代码清单3的②处需要通过Thread.currentThread.sleep()的方式让主线程睡眠,以便调度器可 以继续工作执行任务调度。否则在调度器启动后,因为主线程马上退出,也将同时引起调度器关闭,调度器中的任务都将相应销毁,这将导致看不到实际的运行效 果。在单元测试的时候,让主线程睡眠经常使用的办法。对于某些长周期任务调度的测试,你可以简单地调整操作系统时间进行模拟。

使用Calendar

在实际任务调度中,我们不可能一成不变地按照某个周期性的调度规则运行任务,必须考虑到实现生活中日历上特定日期,就象习惯了大男人作风的人在2月14号也会有不同表现一样。

下面,我们安排一个任务,每小时运行一次,并将五一节和国际节排除在外,其代码如代码清单4所示:

CalendarExample:使用Calendar

package com.baobaotao.basic.quartz;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.quartz.impl.calendar.AnnualCalendar;
import org.quartz.TriggerUtils;

public class CalendarExample {

public static void main(String[] args) throws Exception {

SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
①法定节日是以每年为周期的,所以使用AnnualCalendar
AnnualCalendar holidays = new AnnualCalendar();

②五一劳动节
Calendar laborDay = new GregorianCalendar();
laborDay.add(Calendar.MONTH,5);
laborDay.add(Calendar.DATE,1);
holidays.setDayExcluded(laborDay, true); ②-1:排除的日期,如果设置为false则为包含
③国庆节
Calendar nationalDay = new GregorianCalendar();
nationalDay.add(Calendar.MONTH,10);
nationalDay.add(Calendar.DATE,1);
holidays.setDayExcluded(nationalDay, true);③-1:排除该日期
scheduler.addCalendar("holidays", holidays, false, false);④向Scheduler注册日历
Date runDate = TriggerUtils.getDateOf(0,0, 10, 1, 4);⑤4月1号 上午10点
JobDetail job = new JobDetail("job1", "group1", SimpleJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1",
runDate,
null,
SimpleTrigger.REPEAT_INDEFINITELY,
60L * 60L * 1000L);
trigger.setCalendarName("holidays");⑥让Trigger应用指定的日历规则
scheduler.scheduleJob(job, trigger);
scheduler.start();
//实际应用中主线程不能停止,否则Scheduler得不到执行,此处从略
}
}

由于节日是每年重复的,所以使用org.quartz.Calendar的AnnualCalendar实现类,通过②、③的代码,指定五一和国庆 两个节日并通过AnnualCalendar#setDayExcluded(Calendar day, boolean exclude)方法添加这两个日期。exclude为true时表示排除指定的日期,如果为false时表示包含指定的日期。

在定制好org.quartz.Calendar后,还需要通过 Scheduler#addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)进行注册,如果updateTriggers为true,Scheduler中已引用Calendar的Trigger将 得到更新,如④所示。

在⑥处,我们让一个Trigger指定使用Scheduler中代表节日的Calendar,这样Trigger就会避开五一和国庆这两个特殊日子了。

任务调度信息存储

在默认情况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。

比如我们希望安排一个执行100次的任务,如果执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。在大多数实际的应用中,我们往往并不需要保存任务调度的现场数据,因为很少需要规划一个指定执行次数的任务。

对于仅执行一次的任务来说,其执行条件信息本身应该是已经持久化的业务数据(如锁定到期解锁任务,解锁的时间应该是业务数据),当执行完成后,条件信息也会相应改变。当然调度现场信息不仅仅是记录运行次数,还包括调度规则、JobDataMap中的数据等等。

如果确实需要持久化任务调度信息,Quartz允许你通过调整其属性文件,将这些信息保存到数据库中。使用数据库保存任务 调度信息后,即使系统崩溃后重新启动,任务的调度信息将得到恢复。如前面所说的例子,执行50次崩溃后重新运行,计数器将从51开始计数。使用了数据库保 存信息的任务称为持久化任务。

通过配置文件调整任务调度信息的保存策略

其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件并提供了默认设置。如果需要调整默认配置,可以在类路 径下建立一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。

先来了解一下Quartz的默认属性配置文件:

代码清单5 quartz.properties:默认配置

①集群的配置,这里不使用集群

org.quartz.scheduler.instanceName = DefaultQuartzScheduler

org.quartz.scheduler.rmi.export = false

org.quartz.scheduler.rmi.proxy = false

org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

②配置调度器的线程池

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

org.quartz.threadPool.threadCount = 10

org.quartz.threadPool.threadPriority = 5

org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

③配置任务调度现场数据保存机制

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

Quartz的属性配置文件主要包括三方面的信息:

1)集群信息;

2)调度器线程池;

3)任务调度现场数据的保存。

如果任务数目很大时,可以通过增大线程池的大小得到更好的性能。默认情况下,Quartz采用org.quartz.simpl.RAMJobStore保存任务的现场数据,顾名思义,信息保存在RAM内存中,我们可以通过以下设置将任务调度现场数据保存到数据库中:

quartz.properties:使用数据库保存任务调度现场数据

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

org.quartz.jobStore.tablePrefix = QRTZ_①数据表前缀

org.quartz.jobStore.dataSource = qzDS②数据源名称

③定义数据源的具体属性

org.quartz.dataSource.qzDS.driver = oracle.jdbc.driver.OracleDriver

org.quartz.dataSource.qzDS.URL = jdbc:oracle:thin:@localhost:1521:ora9i

org.quartz.dataSource.qzDS.user = stamen

org.quartz.dataSource.qzDS.password = abc

org.quartz.dataSource.qzDS.maxConnections = 10

要将任务调度数据保存到数据库中,就必须使用 org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore 并提供相应的数据库配置信息。首先①处指定了Quartz数据库表的前缀,在②处定义了一个数据源,在③处具体定义这个数据源的连接信息。

你必须事先在相应的数据库中创建Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不同数据库的SQL脚本。

查询数据库中的运行信息

任务的现场保存对于上层的Quartz程序来说是完全透明的,我们在src目录下编写一个如代码清单6所示的 quartz.properties文件后,重新运行代码清单2或代码清单3的程序,在数据库表中将可以看到对应的持久化信息。当调度程序运行过程中途停 止后,任务调度的现场数据将记录在数据表中,在系统重启时就可以在此基础上继续进行任务的调度。

JDBCJobStoreRunner:从数据库中恢复任务的调度

package com.baobaotao.basic.quartz;

import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

public class JDBCJobStoreRunner {

public static void main(String args[]) {
try {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
①获取调度器中所有的触发器组
String[] triggerGroups = scheduler.getTriggerGroupNames();
②重新恢复在tgroup1组中,名为trigger1_1触发器的运行
for (int i = 0; i < triggerGroups.length; i++) {
String[] triggers = scheduler.getTriggerNames(triggerGroups[i]);
for (int j = 0; j < triggers.length; j++) {
Trigger tg = scheduler.getTrigger(triggers[j],triggerGroups[i]);
if (tg instanceof SimpleTrigger
&& tg.getFullName().equals("tgroup1.trigger1_1")) {②-1:根据名称判断
②-1:恢复运行
scheduler.rescheduleJob(triggers[j], triggerGroups[i],tg);
}
}
}
scheduler.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

当代码清单2中的SimpleTriggerRunner执行到一段时间后非正常退出,我们就可以通过这个 JDBCJobStoreRunner根 据记录在数据库中的现场数据恢复任务的调度。Scheduler中的所有Trigger以及JobDetail的运行信息都会保存在数据库中,这里我们仅 恢复tgroup1组中名称为trigger1_1的触发器,这可以通过如②-1所示的代码进行过滤,触发器的采用GROUP.TRIGGER_NAME 的全名格式。通过Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)即可重新调度关联某个Trigger的任务。

下面我们来观察一下不同时期qrtz_simple_triggers表的数据:

1.运行代码清单2的SimpleTriggerRunner一小段时间后退出:

REPEAT_COUNT表示需要运行的总次数,而TIMES_TRIGGER表示已经运行的次数。

2.运行代码清单7的JDBCJobStoreRunner恢复trigger1_1的触发器,运行一段时间后退出,这时qrtz_simple_triggers中的数据如下:

首先Quartz会将原REPEAT_COUNT-TIMES_TRIGGER得到新的REPEAT_COUNT值,并记录已经运行的次数(重新从0开始计算)。

3.重新启动JDBCJobStoreRunner运行后,数据又将发生相应的变化:

4.继续运行直至完成所有剩余的次数,再次查询qrtz_simple_triggers表:

这时,该表中的记录已经变空。

值得注意的是,如果你使用JDBC保存任务调度数据时,当你运行代码清单2的SimpleTriggerRunner然后退出,当再次希望运行SimpleTriggerRunner时,系统将抛出JobDetail重名的异常:

Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.

因为每次调用Scheduler#scheduleJob()时,Quartz都会将JobDetail和Trigger的信息保存到数据库中,如果数据表中已经同名的JobDetail或Trigger,异常就产生了。

本 文使用quartz 1.6版本,我们发现当后台数据库使用MySql时,数据保存不成功,该错误是Quartz的一个Bug,相信会在高版本中得到修复。因为HSQLDB不 支持SELECT * FROM TABLE_NAME FOR UPDATE的语法,所以不能使用HSQLDB数据库。

小结

Quartz 提供了最为丰富的任务调度功能,不但可以制定周期性运行的任务调度方案,还可以让你按照日历相关的方式进行任 务调度。Quartz框架的重要组件包括Job、JobDetail、Trigger、Scheduler以及辅助性的JobDataMap和 SchedulerContext。

Quartz拥有一个线程池,通过线程池为任务提供执行线程,你可以通过配置文件对线程池进行参数定 制。Quartz的另 一个重要功能是可将任务调度信息持久化到数据库中,以便系统重启时能够恢复已经安排的任务。此外,Quartz还拥有完善的事件体系,允许你注册各种事件 的监听器。

posted @ 2011-05-24 10:55 昊天 阅读(5816) | 评论 (0)编辑 收藏

导航

<2011年5月>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

统计

留言簿(1)

随笔分类

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜