使用API网关构建微服务
让我们想象一下,你要为一个购物应用程序开发一个原生移动客户端。你很可能需要实现一个产品详情页面,上面展示任何指定产品的信息。
例如,下图展示了在Amazon Android移动应用中滚动产品详情时看到的内容。
虽然这是个智能手机应用,产品详情页面也显示了大量的信息。例如,该页面不仅包含基本的产品信息(如名称、描述、价格),而且还显示了如下内容:
- 购物车中的件数
- 订单历史
- 客户评论
- 低库存预警
- 送货选项
- 各种推荐,包括经常与该产品一起购买的其它产品,购买该产品的客户购买的其它产品,购买该产品的客户看过的其它产品。
- 可选的购买选项。
当使用单体应用程序架构时,移动客户端将通过向应用程序发起一次REST调用(GET api.company.com/productdetails/<productId>)来获取这些数据。负载均衡器将请求路由给N个相同的应用程序实例中的一个。然后,应用程序会查询各种数据库表,并将响应返回给客户端。
相比之下,当使用微服务架构时,产品详情页面显示的数据归多个微服务所有。下面是部分可能的微服务,它们拥有要显示在示例中产品详情页面上的数据:
- 购物车服务——购物车中的件数
- 订单服务——订单历史
- 目录服务——产品基本信息,如名称、图片和价格
- 评论服务——客户的评论
- 库存服务——低库存预警
- 送货服务——送货选项、期限和费用,这些单独从送货方的API获取
- 推荐服务——建议的产品
我们需要决定移动客户端如何访问这些服务。让我们看看都有哪些选项。
客户端与微服务直接通信
从理论上讲,客户端可以直接向每个微服务发送请求。每个微服务都有一个公开的端点(https ://<serviceName>.api.company.name)。该URL将映射到微服务的负载均衡器,由它负责在可用实例之间分发请求。为了获取产品详情,移动客户端将逐一向上面列出的N个服务发送请求。
遗憾的是,这种方法存在挑战和局限。一个问题是客户端需求和每个微服务暴露的细粒度API不匹配。在这个例子中,客户端需要发送7个独立请求。在更复杂的应用程序中,可能要发送更多的请求。例如,按照Amazon的说法,他们在显示他们的产品页面时就调用了数百个服务。然而,客户端通过LAN发送许多请求,这在公网上可能会很低效,而在移动网络上就根本不可行。这种方法还使得客户端代码非常复杂。
客户端直接调用微服务的另一个问题是,部分服务使用的协议不是Web友好协议。一个服务可能使用Thrift二进制RPC,而另一个服务可能使用AMQP消息传递协议。不管哪种协议都不是浏览器友好或防火墙友好的,最好是内部使用。在防火墙之外,应用程序应该使用诸如HTTP和WebSocket之类的协议。
这种方法的另一个缺点是,它会使得微服务难以重构。随着时间推移,我们可能想要更改系统划分成服务的方式。例如,我们可能合并两个服务,或者将一个服务拆分成两个或更多服务。然而,如果客户端与微服务直接通信,那么执行这类重构就非常困难了。
由于这些问题的存在,客户端与微服务直接通信很少是合理的。
使用API网关
通常,一个更好的方法是使用所谓的API网关。API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、“请求整形(request shaping)”与管理、静态响应处理。
下图展示了API网关通常如何融入架构:
API网关负责服务请求路由、组合及协议转换。客户端的所有请求都首先经过API网关,然后由它将请求路由到合适的微服务。API网管经常会通过调用多个微服务并合并结果来处理一个请求。它可以在Web协议(如HTTP与WebSocket)与内部使用的非Web友好协议之间转换。
API网关还能为每个客户端提供一个定制的API。通常,它会向移动客户端暴露一个粗粒度的API。例如,考虑下产品详情的场景。API网关可以提供一个端点(/productdetails?productid=xxx),使移动客户端可以通过一个请求获取所有的产品详情。API网关通过调用各个服务(产品信息、推荐、评论等等)并合并结果来处理请求。
Netflix API网关是一个很好的API网关实例。Netflix流服务提供给数以百计的不同类型的设备使用,包括电视、机顶盒、智能手机、游戏系统、平板电脑等等。最初,Netflix试图为他们的流服务提供一个通用的API。然而他们发现,由于各种各样的设备都有自己独特的需求,这种方式并不能很好地工作。如今,他们使用一个API网关,通过运行特定于设备的适配器代码来为每个设备提供一个定制的API。通常,一个适配器通过调用平均6到7个后端服务来处理每个请求。Netflix API网关每天处理数十亿请求。
API网关的优点和不足
如你所料,使用API网关有优点也有不足。使用API网关的最大优点是,它封装了应用程序的内部结构。客户端只需要同网关交互,而不必调用特定的服务。API网关为每一类客户端提供了特定的API。这减少了客户端与应用程序间的交互次数,还简化了客户端代码。
API网关也有一些不足。它增加了一个我们必须开发、部署和维护的高可用组件。还有一个风险是,API网关变成了开发瓶颈。为了暴露每个微服务的端点,开发人员必须更新API网关。API网关的更新过程要尽可能地简单,这很重要。否则,为了更新网关,开发人员将不得不排队等待。不过,虽然有这些不足,但对于大多数现实世界的应用程序而言,使用API网关是合理的。
实现API网关
到目前为止,我们已经探讨了使用API网关的动机及其优缺点。下面让我们看一下需要考虑的各种设计问题。
性能和可扩展性
只有少数公司有Netflix的规模,每天需要处理数十亿请求。不管怎样,对于大多数应用程序而言,API网关的性能和可扩展性通常都非常重要。因此,将API网关构建在一个支持异步、I/O非阻塞的平台上是合理的。有多种不同的技术可以用于实现一个可扩展的API网关。在JVM上,可以使用一种基于NIO的框架,比如Netty、Vertx、Spring Reactor或JBoss Undertow中的一种。一个非常流行的非JVM选项是Node.js,它是一个以Chrome JavaScript引擎为基础构建的平台。另一个选项是使用NGINX Plus。NGINX Plus提供了一个成熟的、可扩展的、高性能Web服务器和一个易于部署的、可配置可编程的反向代理。NGINX Plus可以管理身份验证、访问控制、负载均衡请求、缓存响应,并提供应用程序可感知的健康检查和监控。
使用响应式编程模型
API网关通过简单地将请求路由给合适的后端服务来处理部分请求,而通过调用多个后端服务并合并结果来处理其它请求。对于部分请求,比如产品详情相关的多个请求,它们对后端服务的请求是独立于其它请求的。为了最小化响应时间,API网关应该并发执行独立请求。然而,有时候,请求之间存在依赖。在将请求路由到后端服务之前,API网关可能首先需要调用身份验证服务验证请求的合法性。类似地,为了获取客户意愿清单中的产品信息,API网关必须首先获取包含那些信息的客户资料,然后再获取每个产品的信息。关于API组合,另一个有趣的例子是Netflix Video Grid。
使用传统的异步回调方法编写API组合代码会让你迅速坠入回调地狱。代码会变得混乱、难以理解且容易出错。一个更好的方法是使用响应式方法以一种声明式样式编写API网关代码。响应式抽象概念的例子有Scala中的Future、Java 8中的CompletableFuture和JavaScript中的Promise,还有最初是微软为.NET平台开发的Reactive Extensions(RX)。Netflix创建了RxJava for JVM,专门用于他们的API网关。此外,还有RxJS for JavaScript,它既可以在浏览器中运行,也可以在Node.js中运行。使用响应式方法将使你可以编写简单但高效的API网关代码。
服务调用
基于微服务的应用程序是一个分布式系统,必须使用一种进程间通信机制。有两种类型的进程间通信机制可供选择。一种是使用异步的、基于消息传递的机制。有些实现使用诸如JMS或AMQP那样的消息代理,而其它的实现(如Zeromq)则没有代理,服务间直接通信。另一种进程间通信类型是诸如HTTP或Thrift那样的同步机制。通常,一个系统会同时使用异步和同步两种类型。它甚至还可能使用同一类型的多种实现。总之,API网关需要支持多种通信机制。
服务发现
API网关需要知道它与之通信的每个微服务的位置(IP地址和端口)。在传统的应用程序中,或许可以硬连线这个位置,但在现代的、基于云的微服务应用程序中,这并不是一个容易解决的问题。基础设施服务(如消息代理)通常会有一个静态位置,可以通过OS环境变量指定。但是,确定一个应用程序服务的位置没有这么简单。应用程序服务的位置是动态分配的。而且,单个服务的一组实例也会随着自动扩展或升级而动态变化。总之,像系统中的其它服务客户端一样,API网关需要使用系统的服务发现机制,可以是服务器端发现,也可以是客户端发现。下一篇文章将更详细地描述服务发现。现在,需要注意的是,如果系统使用客户端发现,那么API网关必须能够查询服务注册中心,这是一个包含所有微服务实例及其位置的数据库。
处理局部失败
在实现API网关时,还有一个问题需要处理,就是局部失败的问题。该问题在所有的分布式系统中都会出现,无论什么时候,当一个服务调用另一个响应慢或不可用的服务,就会出现这个问题。API网关永远不能因为无限期地等待下游服务而阻塞。不过,如何处理失败取决于特定的场景以及哪个服务失败。例如,在产品详情场景下,如果推荐服务无响应,那么API网关应该向客户端返回产品详情的其它内容,因为它们对用户依然有用。推荐内容可以为空,也可以,比如说,用一个固定的TOP 10列表取代。不过,如果产品信息服务无响应,那么API网关应该向客户端返回一个错误信息。
如果缓存数据可用,那么API网关还可以返回缓存数据。例如,由于产品价格不经常变化,所以如果价格服务不可用,API网关可以返回缓存的价格数据。数据可以由API网关自己缓存,也可以存储在像Redis或Memcached那样的外部缓存中。通过返回默认数据或者缓存数据,API网关可以确保系统故障不影响用户的体验。
在编写代码调用远程服务方面,Netflix Hystrix是一个异常有用的库。Hystrix会将超出设定阀值的调用超时。它实现了一个“断路器(circuit breaker)”模式,可以防止客户端对无响应的服务进行不必要的等待。如果服务的错误率超出了设定的阀值,那么Hystrix会切断断路器,在一个指定的时间范围内,所有请求都会立即失败。Hystrix允许用户定义一个请求失败后的后援操作,比如从缓存读取数据,或者返回一个默认值。如果你正在使用JVM,那么你绝对应该考虑使用Hystrix。而如果你正在使用一个非JVM环境,那么你应该使用一个等效的库。
小结
对于大多数基于微服务的应用程序而言,实现一个API网关是有意义的,它可以作为系统的唯一入口。API网关负责服务请求路由、组合及协议转换。它为每个应用程序客户端提供一个定制的API。API网关还可以通过返回缓存数据或默认数据屏蔽后端服务失败。在本系列的下一篇文章中,我们将探讨服务间通信。
Oracle中B-TREE索引的深入理解(原创) - CzmMiao的博客生活 - ITeye技术网站
索引概述
索引与表一样,也属于段(segment)的一种。里面存放了用户的数据,跟表一样需要占用磁盘空间。只不过,在索引里的数据存放形式与表里的数据存放形式非常的不一样。在理解索引时,可以想象一本书,其中书的内容就相当于表里的数据,而书前面的目录就相当于该表的索引。同时,通常情况下,索引所占用的磁盘空间要比表要小的多,其主要作用是为了加快对数据的搜索速度,也可以用来保证数据的唯一性。
但是,索引作为一种可选的数据结构,你可以选择为某个表里的创建索引,也可以不创建。这是因为一旦创建了索引,就意味着oracle对表进行DML(包括INSERT、UPDATE、DELETE)时,必须处理额外的工作量(也就是对索引结构的维护)以及存储方面的开销。所以创建索引时,需要考虑创建索引所带来的查询性能方面的提高,与引起的额外的开销相比,是否值得。
从物理上说,索引通常可以分为:分区和非分区索引、常规B树索引、位图(bitmap)索引、翻转(reverse)索引等。其中,B树索引属于最常见的索引,由于我们的这篇文章主要就是对B树索引所做的探讨,因此下面只要说到索引,都是指B树索引。
B树索引内部结构
B树索引是一个典型的树结构,其包含的组件主要是:
1) 叶子节点(Leaf node):数据行的键值(key value)、键值对应数据行的 ROWID。
2) 分支节点(Branch node):最小的键值前缀(minimum key prefix),用于在(本块的)两个键值之间做出分支选择,指向包含所查找键值的子块(child block)的指针()所有的 键值-ROWID 对(key and ROWID pair)都与其左右的兄弟节点(sibling)向链接(link),并按照(key,ROWID)的顺序排序
3) 根节点(Root node):一个B树索引只有一个根节点,它实际就是位于树的最顶端的分支节点。
可以用下图一来描述B树索引的结构。其中,B表示分支节点,而L表示叶子节点。
对于分支节点块(包括根节点块)来说,其所包含的索引条目都是按照顺序排列的(缺省是升序排列,也可以在创建索引时指定为降序排列)。每个索引条目(也可以叫做每条记录)都具有两个字段。第一个字段表示当前该分支节点块下面所链接的索引块中所包含的最小键值;第二个字段为四个字节,表示所链接的索引块的地址,该地址指向下面一个索引块。在一个分支节点块中所能容纳的记录行数由数据块大小以及索引键值的长度决定。比如从上图一可以看到,对于根节点块来说,包含三条记录,分别为(0 B1)、(500 B2)、(1000 B3),它们指向三个分支节点块。其中的0、500和1000分别表示这三个分支节点块所链接的键值的最小值。而B1、B2和B3则表示所指向的三个分支节点块的地址。
对于叶子节点块来说,其所包含的索引条目与分支节点一样,都是按照顺序排列的(缺省是升序排列,也可以在创建索引时指定为降序排列)。每个索引条目(也可以叫做每条记录)也具有两个字段。第一个字段表示索引的键值,对于单列索引来说是一个值;而对于多列索引来说则是多个值组合在一起的。第二个字段表示键值所对应的记录行的ROWID,该ROWID是记录行在表里的物理地址。如果索引是创建在非分区表上或者索引是分区表上的本地索引的话,则该ROWID占用6个字节;如果索引是创建在分区表上的全局索引的话,则该ROWID占用10个字节。
知道这些信息以后,我们可以举个例子来说明如何估算每个索引能够包含多少条目,以及对于表来说,所产生的索引大约多大。对于每个索引块来说,缺省的PCTFREE为10%,也就是说最多只能使用其中的90%。同时9i以后,这90%中也不可能用尽,只能使用其中的87%左右。也就是说,8KB的数据块中能够实际用来存放索引数据的空间大约为6488(8192×90%×88%)个字节。
假设我们有一个非分区表,表名为warecountd,其数据行数为130万行。该表中有一个列,列名为goodid,其类型为char(8),那么也就是说该goodid的长度为固定值:8。同时在该列上创建了一个B树索引。
在叶子节点中,每个索引条目都会在数据块中占一行空间。每一行用2到3个字节作为行头,行头用来存放标记以及锁定类型等信息。同时,在第一个表示索引的键值的字段中,每一个索引列都有1个字节表示数据长度,后面则是该列具体的值。那么对于本例来说,在叶子节点中的一行所包含的数据大致如下图二所示:
从上图可以看到,在本例的叶子节点中,一个索引条目占 18 个字节。同时我们知道 8KB 的数据块中真正可以用来存放索引条目的空间为 6488 字节,那么在本例中,一个数据块中大约可以放 360 ( 6488/18 )个索引条目。而对于我们表中的130 万条记录来说,则需要大约 3611 ( 1300000/360 )个叶子节点块。
而对于分支节点里的一个条目(一行)来说,由于它只需保存所链接的其他索引块的地址即可,而不需要保存具体的数据行在哪里,因此它所占用的空间要比叶子节点要少。分支节点的一行中所存放的所链接的最小键值所需空间与上面所描述的叶子节点相同;而存放的索引块的地址只需要 4 个字节,比叶子节点中所存放的 ROWID 少了 2 个字节,少的这 2 个字节也就是 ROWID 中用来描述在数据块中的行号所需的空间。因此,本例中在分支节点中的一行所包含的数据大致如下图三所示:
从上图可以看到,在本例的分支节点中,一个索引条目占 16 个字节。根据上面叶子节点相同的方式,我们可以知道一个分支索引块可以存放大约 405 ( 6488/16 )个索引条目。而对于我们所需要的 3611 个叶子节点来说,则总共需要大约 9 个分支索引块。
这样,我们就知道了我们的这个索引有 2 层,第一层为 1 个根节点,第二层为 9 个分支节点,而叶子节点数为 3611 个,所指向的表的行数为 1300000 行。但是要注意,在 oracle 的索引中,层级号是倒过来的,也就是说假设某个索引有 N层,则根节点的层级号为 N ,而根节点下一层的分支节点的层级号为 N-1 ,依此类推。对本例来说, 9 个分支节点所在的层级号为 1 ,而根节点所在的层级号为 2 。
注意:在Oracle中null被定义为无限大,且null不等于null,故在索引不会存有与null值对应的条目。如果不加其他限制条件的对表进行is null扫描,将会是全表扫描,如果是is not null扫描将会是全索引扫描
这里仅仅是作为研究来讨论如何估算,学习一样东西,我们当然要知其然,也知其所以然,实际环境中可以利用explain plan for 查看创建索引的执行计划,从而对索引大小,创建时间进行预判,具体可参见
http://czmmiao.iteye.com/blog/1471756
B树索引的访问
当oracle进程需要访问数据文件里的数据块时,oracle会有两种类型的I/O操作方式:
1) 随机访问,每次读取一个数据块(通过等待事件“db file sequential read”体现出来)。
2) 顺序访问,每次读取多个数据块(通过等待事件“db file scattered read”体现出来)。
第一种方式则是访问索引里的数据块,而第二种方式的I/O操作属于全表扫描。这里顺带有一个问题,为
何随机访问会对应到db file sequential read等待事件,而顺序访问则会对应到db file scattered read等待事件呢?这似乎反过来了,随机访问才应该是分散(scattered)的,而顺序访问才应该是顺序(sequential)的。其实,等待事件主要根据实际获取物理I/O块的方式来命名的,而不是根据其在I/O子系统的逻辑方式来命名的。下面对于如何获取索引数据块的方式中会对此进行说明。
事实上在B树索引虽然为一个树状的立体结构,但其对应到数据文件里的排列当然还是一个平面的形式,也就是像下面这样。
/根/分支/分支/叶子/…/叶子/分支/叶子/叶子/…/叶子/分支/叶子/叶子/…/叶子/分支/.....
因此,当oracle需要访问某个索引块的时候,势必会在这个结构上跳跃的移动。
当oracle需要获得一个索引块时,首先从根节点开始,根据所要查找的键值,从而知道其所在的下一层的分支节点,然后访问下一层的分支节点,再次同样根据键值访问再下一层的分支节点,如此这般,最终访问到最底层的叶子节点。可以看出,其获得物理I/O块时,是一个接着一个,按照顺序,串行进行的。在获得最终物理块的过程中,我们不能同时读取多个块,因为我们在没有获得当前块的时候是不知道接下来应该访问哪个块的。因此,在索引上访问数据块时,会对应到db file sequential read等待事件,其根源在于我们是按照顺序从一个索引块跳到另一个索引块,从而找到最终的索引块的。
那么对于全表扫描来说,则不存在访问下一个块之前需要先访问上一个块的情况。全表扫描时,oracle知道要访问所有的数据块,因此唯一的问题就是尽可能高效的访问这些数据块。因此,这时oracle可以采用同步的方式,分几批,同时获取多个数据块。这几批的数据块在物理上可能是分散在表里的,因此其对应到db file scattered read等待事件。
DML对B树索引的影响
INSERT
在每个INSERT操作过程中,关键字必须被插入在正确叶节点的位置。如果叶节点已满,不能容纳更多的关键字,就必须将叶节点拆分。拆分的方法有两种:
1)如果新关键字值在所有旧叶节点块的所有关键字中是最大的,那么所有的关键字将按照99:1的比例进行拆分,使得在新的叶节点块中只存放有新关键字,而其他的所有关键字(包括所有删除的关键字)仍然保存在旧叶节点块中。
2)如果新关键字值不是最大的,那么所有的关键字将按照50:50的比例进行拆分,这时每个叶节点块(旧与新)中将各包含原始叶节点中的一半关键字。
这个拆分必须通过一个指向新叶节点的新入口向上传送到父节点。如果父节点已满,那么这个父节点也必须进行拆分,并且需要将这种拆分向上传送到父节点的父节点。这时,如果这个父节点也已满,将继续进行这个过程。这样,某个拆分可能最终被一直传送到根节点。如果根节点满了,根结点也将进行分裂。根结点在进行分裂的时候,就是树的高度增加的时候。根节点进行分裂的方式跟其他的的节点分裂的方式相比较,在物理位置上的处理也是不同的。根节点分裂时,将原来的根结点分裂为分支节点或叶节点,保存到新的块中,而将新的根节点信息保存到原来的根结点块中,这样做的是为因为避免修改数据字典所带来的相对较大的开销。
注意:现在Oracle都是采用了平衡算法,正常情况下即使索引关键字不断增大,也不会产生不平衡树。当索引关键字不断增大,导致树级别单方向增长时,Oracle会自动进行索引翻转以维持索引的平衡,当然这种操作非常消耗资源
在索引的每一个层次之间,每一个层最左边的节点的block头部都有一个指向下层最左边的块的指针,这样有利于fast full scan 的快速定位最左边的叶子节点。
每个拆分过程都是要花费一定的开销的,特别是要进行物理硬盘I/O动作。此外,在进行拆分之前,Oracle必须查找到一个空块,用来保存这个拆分。可以用以下步骤来进行查找空块的动作:
1) 在索引的自由列表(free-list, 又称为空闲列表) 中查到一个空闲块,可以通过CREATE/ALTER INDEX命令为一个索引定义多个空闲列表。索引空闲列表并不能帮助Oracle查找一个可用来存放将要被插入的新关键字的块。这是因为关键字值不能随机地存放在索引中可用的第一个“空闲”叶节点块中,这个值必须经过适当的排序之后,放置在某个特定的叶节点块中。只有在块拆分过程中才需要使用索引的空闲列表,每个空闲列表都包含有一个关于“空”块的链接列表。当为某个索引定义了多个空闲列表时,首先将从分配给进程的空间列表中扫描一个空闲块。如果没有找到所需要的空闲块,将从主空闲列表中进行扫描空闲块的动作。
2) 如果没有找到任何空闲块,Oracle将试图分配另一个扩展段。如果在表空间中没有更多的自由空间,Oracle将产生错误ORA-01654。
3) 如果通过上述步骤,找到了所需的空闲块,那么这个索引的高水位标(HWM)将加大。
4) 所找到的空闲块将用来执行拆分动作。
在创建B*树索引时,一个需要注意的问题就是要避免在运行时进行拆分,或者,要在索引创建过程中进行拆分(“预拆分”),从而使得在进行拆分时能够快速命中,以便避免运行时插入动作。当然,这些拆分也不仅仅局限于插入动作,在进行更新的过程中也有可能会发生拆分动作。
UPDATE
索引更新完全不同于表更新,在表更新中,数据是在数据块内部改变的(假设数据块中有足够的空间来允许进行这种改变);但在索引更新中,如果有关键字发生改变,那么它在树中的位置也需要发生改变。请记住,一个关键字在B*树中有且只有一个位置。因此,当某个关键字发生改变时,关键字的旧表项必须被删除,并且需要在一个新的叶节点上创建一个新的关键字。旧的表项有可能永远不会被重新使用,这是因为只有在非常特殊的情况下, Oracle才会重用关键字表项槽,例如,新插入的关键字正好是被删除的那个关键字(包括数据类型、长度等等)。(这里重用的是块,但完全插入相同的值的时候,也不一定插入在原来的被删除的位置,只是插入在原来的块中,可能是该块中的一个新位置。也正因为如此,在索引块中保存的的记录可能并不是根据关键字顺序排列的,随着update等的操作,会发生变化。)那么,这种情况发生的可能性有多大呢?许多应用程序使用一个数列来产生NUMBER关键字(特别是主关键字)。除非它们使用了RECYCLE选项,否则这个数列将不会两次产生完全相同的数。这样,索引中被删除的空间一直没有被使用。这就是在大规模删除与更新过程中,表大小不断减小或至少保持不变但索引不断加大的原因。
DELETE
当删除表里的一条记录时,其对应于索引里的索引条目并不会被物理的删除,只是做了一个删除标记。当一个新的索引条目进入一个索引叶子节点的时候,oracle会检查该叶子节点里是否存在被标记为删除的索引条目,如果存在,则会将所有具有删除标记的索引条目从该叶子节点里物理的删除。
当一个新的索引条目进入索引时,oracle会将当前所有被清空的叶子节点(该叶子节点中所有的索引条目都被设置为删除标记)收回,从而再次成为可用索引块。
尽管被删除的索引条目所占用的空间大部分情况下都能够被重用,但仍然存在一些情况可能导致索引空间被浪费,并造成索引数据块很多但是索引条目很少的后果,这时该索引可以认为出现碎片。而导致索引出现碎片的情况主要包括:
1、不合理的、较高的PCTFREE。很明显,这将导致索引块的可用空间减少。
2、索引键值持续增加(比如采用sequence生成序列号的键值),同时对索引键值按照顺序连续删除,这时可能导致索引碎片的发生。因为前面我们知道,某个索引块中删除了部分的索引条目,只有当有键值进入该索引块时才能将空间收回。而持续增加的索引键值永远只会向插入排在前面的索引块中,因此这种索引里的空间几乎不能收回,而只有其所含的索引条目全部删除时,该索引块才能被重新利用。
3、经常被删除或更新的键值,以后几乎不再会被插入时,这种情况与上面的情况类似。
总结
通过上面对B树的分析,可以得出以下的应用准则:
1、避免对那些可能会产生很高的更新动作的列进行索引。
2、避免对那些经常会被删除的表中的多个列进行索引。若有可能,只对那些在这样的表上会进行删除的主关键字与/或列进行索引。如果对多个列进行索引是不可避免的,那么就应该考虑根据这些列对表进行划分,然后在每个这样的划分上执行TRUNCATE动作(而不是DELETE动作)。TRUNCATE在与DROP STORAGE短语一同使用时,通过重新设置高水位标来模拟删除表与索引以及重新创建表与索引的过程。
3、避免为那些唯一度不高的列创建B*树索引。这样的低选择性将会导致树节点块的稠密性,从而导致由于索引“平铺( flat)”而出现的大规模索引扫描。唯一性的程度越高,性能就越好,因为这样能够减少范围扫描,甚至可能用唯一扫描来取代范围扫描。
4)空值不存储在单列索引中。对于复合索引的方式,只有当某个列不空时,才需要进行值的存储。在为DML语句创建IS NULL或IS NOT NULL短语时,应该切记这个问题。
5)IS NULL不会导致索引扫描,而一个没有带任何限制的IS NOT NULL则可能会导致完全索引扫描。
本文未 进行 索引内部结构的转储实验,全部为理论研究,感兴趣的朋友可以自行搜索网上的相关文档进行研究学习
参考至:http://btxigua.itpub.net/post/34419/406433
http://space.itpub.net/?uid-9842-action-viewspace-itemid-324139
http://space.itpub.net/?uid-9842-action-viewspace-itemid-312607
http://space.itpub.net/?uid-9842-action-viewspace-itemid-324586
How to make searching faster ImproveSearchingSpeed - Lucene-java Wiki
How to make searching faster
Here are some things to try to speed up the seaching speed of your Lucene application. Please see ImproveIndexingSpeed for how to speed up indexing.
-
Be sure you really need to speed things up. Many of the ideas here are simple to try, but others will necessarily add some complexity to your application. So be sure your searching speed is indeed too slow and the slowness is indeed within Lucene.
-
Make sure you are using the latest version of Lucene.
-
Use a local filesystem. Remote filesystems are typically quite a bit slower for searching. If the index must be remote, try to mount the remote filesystem as a "readonly" mount. In some cases this could improve performance.
-
Get faster hardware, especially a faster IO system. Flash-based Solid State Drives works very well for Lucene searches. As seek-times for SSD's are about 100 times faster than traditional platter-based harddrives, the usual penalty for seeking is virtually eliminated. This means that SSD-equipped machines need less RAM for file caching and that searchers require less warm-up time before they respond quickly.
-
Tune the OS
One tunable that stands out on Linux is swappiness (http://kerneltrap.org/node/3000), which controls how aggressively the OS will swap out RAM used by processes in favor of the IO Cache. Most Linux distros default this to a highish number (meaning, aggressive) but this can easily cause horrible search latency, especially if you are searching a large index with a low query rate. Experiment by turning swappiness down or off entirely (by setting it to 0). Windows also has a checkbox, under My Computer -> Properties -> Advanced -> Performance Settings -> Advanced -> Memory Usage, that lets you favor Programs or System Cache, that's likely doing something similar.
-
Open the IndexReader with readOnly=true. This makes a big difference when multiple threads are sharing the same reader, as it removes certain sources of thread contention.
-
On non-Windows platform, using NIOFSDirectory instead of FSDirectory.
This also removes sources of contention when accessing the underlying files. Unfortunately, due to a longstanding bug on Windows in Sun's JRE (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6265734 -- feel particularly free to go vote for it), NIOFSDirectory gets poor performance on Windows.
-
Add RAM to your hardware and/or increase the heap size for the JVM. For a large index, searching can use alot of RAM. If you don't have enough RAM or your JVM is not running with a large enough HEAP size then the JVM can hit swapping and thrashing at which point everything will run slowly.
-
Use one instance of IndexSearcher.
Share a single IndexSearcher across queries and across threads in your application.
-
When measuring performance, disregard the first query.
The first query to a searcher pays the price of initializing caches (especially when sorting by fields) and thus will skew your results (assuming you re-use the searcher for many queries). On the other hand, if you re-run the same query again and again, results won't be realistic either, because the operating system will use its cache to speed up IO operations. On Linux (kernel 2.6.16 and later) you can clean the disk cache usingsync ; echo 3 > /proc/sys/vm/drop_caches. See http://linux-mm.org/Drop_Caches for details.
-
Re-open the IndexSearcher only when necessary.
You must re-open the IndexSearcher in order to make newly committed changes visible to searching. However, re-opening the searcher has a certain overhead (noticeable mostly with large indexes and with sorting turned on) and should thus be minimized. Consider using a so called warming technique which allows the searcher to warm up its caches before the first query hits.
-
Decrease mergeFactor. Smaller mergeFactors mean fewer segments and searching will be faster. However, this will slow down indexing speed, so you should test values to strike an appropriate balance for your application.
-
Limit usage of stored fields and term vectors. Retrieving these from the index is quite costly. Typically you should only retrieve these for the current "page" the user will see, not for all documents in the full result set. For each document retrieved, Lucene must seek to a different location in various files. Try sorting the documents you need to retrieve by docID order first.
-
Use FieldSelector to carefully pick which fields are loaded, and how they are loaded, when you retrieve a document.
-
Don't iterate over more hits than needed.
Iterating over all hits is slow for two reasons. Firstly, the search() method that returns a Hits object re-executes the search internally when you need more than 100 hits. Solution: use the search method that takes a HitCollector instead. Secondly, the hits will probably be spread over the disk so accessing them all requires much I/O activity. This cannot easily be avoided unless the index is small enough to be loaded into RAM. If you don't need the complete documents but only one (small) field you could also use the FieldCache class to cache that one field and have fast access to it.
-
When using fuzzy queries use a minimum prefix length.
Fuzzy queries perform CPU-intensive string comparisons - avoid comparing all unique terms with the user input by only examining terms starting with the first "N" characters. This prefix length is a property on both QueryParser and FuzzyQuery - default is zero so ALL terms are compared.
-
Consider using filters. It can be much more efficient to restrict results to a part of the index using a cached bit set filter rather than using a query clause. This is especially true for restrictions that match a great number of documents of a large index. Filters are typically used to restrict the results to a category but could in many cases be used to replace any query clause. One difference between using a Query and a Filter is that the Query has an impact on the score while a Filter does not.
-
Find the bottleneck.
Complex query analysis or heavy post-processing of results are examples of hidden bottlenecks for searches. Profiling with at tool such as VisualVM helps locating the problem.