网站开发动静分离实践

1. 动静分离的实现思路

动静分离是将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问。

动静分离的一种做法是将静态资源部署在nginx上,后台项目部署到应用服务器上,根据一定规则静态资源的请求全部请求nginx服务器,达到动静分离的目标。

1.1 静态资源部署至CDN上

我们的方案是直接将静态资源全部存放在CDN服务器上。因为之前项目中的JavaScript,CSS以及img文件都是存放在CDN服务器上,将HTML文件一起存放到CDN上之后,可以将静态资源统一放置在一种服务器上,便于前端进行维护;而且用户在访问静态资源时,可以很好利用CDN的优点——CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

1.2 后端API提供数据

后端应用提供API,根据前端的请求进行处理,并将处理结果通过JSON格式返回至前端。目前应用主要采用Java平台开发,因此应用服务器主要是Tomcat服务器,现在也开始有部分应用采用 node进行开发,应用服务器也开始使用node服务器。

1.3 前后端域名

动静分离因为静态资源和应用服务分别部署在不同的服务器上,因此会面临域名策略的选择。

  • 相同域名
    采用相同域名下,用户请求api时可以避免跨域所带来的问题,相对开发更为快速,工作量也相对小一些。

  • 不同域名
    前后端采用不同域名时,需要前后端开发时兼容跨域请求的情况,开发量相对上一种会稍多一些。解决跨域方式最常用的方式就是采用JSONP,还有一种解决方式使用CORS(HTTP访问控制)允许某些域名下的跨域请求。
    目前在我们的项目中JSONP方式更多,CORS因为需要浏览器支持,因此只会在APP内嵌HTML5,且需要POST方式时中使用。

采用不同域名的方式优点也是非常明显的,不同域名采用两个域名服务器,不同的域名服务器根据请求的不同采用不同的负载均衡策略;而且不同域名也可以邮箱方式前端携带过多的Cookie。

2. 动静分离的实现优缺点

2.1 优点

  • api接口服务化:动静分离之后,后端应用更为服务化,只需要通过提供api接口即可,可以为多个功能模块甚至是多个平台的功能使用,可以有效的节省后端人力,更便于功能维护。
  • 前后端开发并行:前后端只需要关心接口协议即可,各自的开发相互不干扰,并行开发,并行自测,可以有效的提高开发时间,也可以有些的减少联调时间
  • 减轻后端服务器压力,提高静态资源访问速度:后端不用再将模板渲染为html返回给用户端,且静态服务器可以采用更为专业的技术提高静态资源的访问速度。

    2.2 缺点

  • 不利于网站SEO(搜索引擎优化):搜索引擎的网络爬虫一般是根据url访问页面,获取页面的内容后去掉没用的信息例如:CSS,JavaScript,然后分析剩下的文本内容;动静分离架构模式前端数据即在是由JavaScript来完成,这就会导致网络爬虫得到的信息部分丢失。在开发中可以采用前端缓存不经常变化数据的方式来解决,只有哪些经常发生变化的数据才每次向后端请求。

  • 开发量变大,前后端交流成本升高:后端api返回的数据,往往是有自身逻辑在内的,比如返回数据中的包含status(1-处理中,2-处理成功,3-处理失败),前端需要理解status的不同含义,对应的前端操作需要理解(如,status =1 or status = 2,不可提交)。

  • 在业务高速发展时需要慎重考虑:因为开发量变大,如果在业务开始阶段,缺乏前端又要求开发速度很快,就需要慎重考虑这种方式的实现成本对业务发展的影响。

3. 实现案例

这个是在公司做的内嵌到app里的HTML5页面

References

CDN wiki
CORS
关于大型网站技术演进的思考(十三)–网站静态化处理—CSI(5)

RPC是什么

1. RPC

RPC (Remote Procedure Call)是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。RPC是一个分布式计算的CS模式,总是由Client向Server发出一个执行若干过程请求,Server接受请求,使用客户端提供的参数,计算完成之后将结果返回给客户端。RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。

1.1 RPC的调用流程

图片来自你应该知道的RPC原理

  • 服务消费方(client)调用以本地调用方式调用服务;
  • client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  • client stub找到服务地址,并将消息发送到服务端;
  • server stub收到消息后进行解码;
  • server stub根据解码结果调用本地的服务;
  • 本地服务执行并将结果返回给server stub;
  • server stub将返回结果打包成消息并发送至消费方;
  • client stub接收到消息,并进行解码;
  • 服务消费方得到最终结果。

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

1.2 RPC需要解决的问题

  • 通讯的问题
  • 服务寻址的问题
  • 参数的序列化和反序列化
  • 负载均衡的问题

2. 服务通信协议

服务通信主要是Client和Server之间建立网络连接,所有交换的数据都在这个连接里传输,连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。不同的RPC框架可能使用不同的网络协议,常用的有直接使用TCP,基于HTTP/HTTP2.0 等,目前pigeon使用的有TCP和HTTP协议。在java领域一些工具和框架已经封装对底层协议的使用进行了封装例如比较著名的Netty(这也是pigeon在使用的通信框架),Apache MINA

3. 服务寻址

每次Client向Server端发起请求之前,需要知道该向哪个Server发请求,也就涉及到服务寻址的问题。最简单的方式是直接指定Server的ip,访问特定机器上的服务。

但是在分布式环境中,通过在代码中指定ip的方式是不合适的,所以现在的RPC框架基本都是使用配置的方式来实现服务的寻址。配置方式下涉及Server端服务的注册和Client端的服务寻址,这里就成为了Zookeeper应用的绝佳场景。

3.1 ZooKeeper 管理分布式服务配置

ZooKeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。ZooKeeper的架构通过冗余服务实现高可用性。因此,如果第一次无应答,客户端就可以询问另一台ZooKeeper主机。ZooKeeper节点将它们的数据存储于一个分层的命名空间,非常类似于一个文件系统或一个前缀树结构。客户端可以在节点读写,从而以这种方式拥有一个共享的配置服务。

图片来自分布式服务框架 Zookeeper – 管理分布式环境中的数据

3.1.1 服务注册
当每次Server启动时,想调用ZooKeeper的Client,将自身提供的服务注册到ZooKeeper中,形如:

1
http://service.test.com/demoservice/demo_1.0.0:127.0.0.1

当Server Shut down 时也会将自身节点在ZooKeeper配置中进行摘除。

3.1.2 服务寻址

Client每次请求Server之前,需要向ZooKeeper中查询该服务对应的地址,例如需要使用demoservice,查询key为http://service.test.com/demoservice/demo_1.0.0的对应ip,即可访问对应服务。

3.1.3 服务修改广播

当一个服务发生修改时,如服务的启动与关闭,都会将消息发送到感兴趣的Client。

ZooKeeper的原理及使用参见 ZooKeeper项目

4. 序列化及反序列化

Client找到对应Server之后就需要传递参数到Server端,Server在处理完数据之后也需要将结果返回给Client。

每种序列化协议都有不同的优点和确定,一个成熟的序列化协议需要通盘考虑通用性,强健性,可调试性/可读性,性能,可扩展性以及安全性等方面。目前常见的序列化协议主要有hessian,XML、JSON、ProtobufThriftAvro

例如,pigeon支持多种序列化方式,序列化方式只需要在客户端调用时通过serialize属性指定,一般情况推荐兼容性最好的hessian。
如果需要自行设计序列化方式,可以继承com.dianping.pigeon.remoting.common.codec.DefaultAbstractSerializer类来定义自己的序列化类,并通过SerializerFactory.registerSerializer(byte serializerType, Serializer serializer)接口将自定义的序列化类注册进来。

5. 负载均衡

负载平衡(Load balancing)是一种计算机网络技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最佳化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。负载均衡器有各种各样的工作排程算法(用于决定将前端用户请求发送到哪一个后台服务器),最简单的是随机选择和轮询。更为高级的负载均衡器会考虑其它更多的相关因素,如后台服务器的负载,响应时间,运行状态,活动连接数,地理位置,处理能力,或最近分配的流量。

例如,pigeon支持random、roundRobin、weightedAutoware这几种类型,默认weighted Autoware策略。在pigeon框架中在每一次发送请求时都会有在线程中计算请求的返回时间,weighted Autoware策略是根据各个线程统计的响应时间来判断该服务的负载情况,响应时间越长说明该机器的负载越重。

6. 容灾

这里我们主要以pigeon为例介绍其实现容灾的方式,以供参考。

6.1 健康检测

在pigeon中,当使用tcp协议连接是,Client会定期发送心跳消息和接收心跳消息,如果心跳未正常返回,则在Client端会摘除这个Server节点,在后续服务寻址时不再选择该Server节点。

6.2 超时及重试

Client端可以配置一个服务的超时时间,当发送请求是,pigeon线程会计算该请求所耗费的时间,如果超过限制时间尚未返回,则根据配置返回超时或者进行重试,避免客户端过长时间的等待。

6.3 客户端配置集群策略模式

pigeon 的客户端集群策略有failfast、failover、failsafe和forking 四种方式。

  • failfast-调用服务的一个节点失败后抛出异常返回
  • failover-调用服务的一个节点失败后会尝试调用另外的一个节点
  • failsafe-调用服务的一个节点失败后不会抛出异常,返回null,后续版本会考虑按配置默认值返回
  • forking-同时调用服务的所有可用节点,返回调用最快的节点结果数据

6.4 服务隔离与限流

  • 配置服务方法级别的最大并发数
  • 限制某个客户端应用的最大并发数
    详情参见Pigeon开发指南

7. 目前常见的RPC框架

除了hessian协议之外,还提供了通过servlet方式实现的RPC框架

Thrift是一个跨语言的服务部署框架,最初由Facebook于2007年开发,2008年进入Apache开源项目。Thrift通过一个中间语言(IDL, 接口定义语言)来定义RPC的接口和数据类型,然后通过一个编译器生成不同语言的代码(目前支持C++,Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk和OCaml),并由生成的代码负责RPC协议层和传输层的实现。

Finagle是Twitter基于Netty开发的支持容错的、协议无关的RPC框架,该框架支撑了Twitter的核心服务。

  • Dubbo
    Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。

gRPC是一个高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。gRPC提供了一种简单的方法来精确地定义服务和为iOS、Android和后台支持服务自动生成可靠性很强的客户端功能库。客户端充分利用高级流和链接功能,从而有助于节省带宽、降低的TCP链接次数、节省CPU使用、和电池寿命。

8. RPC 与 微服务(MicroService)

微服务是一种架构思想,一个微服务一般只完成某个特定的功能,比如下单服务,订单查询服务,是将应用分解为小的,相互连接的服务。

微服务在系统层面有多种多样的表现形式,例如暴露restful api,SOA服务或者http接口。RPC可以作为实现微服务系统的一种实现方式将各个应用都暴露出RPC的服务接口,从而实现微服务的架构。

References

pigeon框架
pigeon user guide
RPC调用框架比较分析
RPC框架实现 - 容灾篇 - bangerlee

RPC原理详解 - 永志
你应该知道的RPC原理

Twitter的RPC框架Finagle简介
gRPC:Google开源的基于HTTP/2和ProtoBuf的通用RPC框架

Apache ZooKeeper
Apache ZooKeeper doc

分布式服务框架 Zookeeper – 管理分布式环境中的数据

Google Protocol Buffer 的使用和原理
Protocol Buffers
Netty at Twitter with Finagle
Twitter的RPC框架Finagle简介

Goodbye Microservices, Hello Right-sized Services
微服务架构解析
微服务架构的设计模式
解析微服务架构(二)微服务架构综述
解析微服务架构(一)单块架构系统以及其面临的挑战
微服务实战(一):微服务架构的优势与不足
单体应用与微服务优缺点辨析
SOLID

多台服务器查询日志

互联网行业基本都是采用分布式部署,一个应用有很多台服务器,多的会有上百台。一旦线上出现case,很可能需要查询日志,但是一下子这么多机器查起来费时费事。
现在我们使用下面的脚本直接就可以拉到相关机器的日志。

trick

code

加入我们有sb1,sb2,sb3三台机器:

1
2
3
for server in sb1 sb2 sb3; do
ssh "$server" grep 'test' /data/applogs/sb/app.lot
done

这样一下就可以拉到所有机器的日志

问题

这段代码有个小问题,一般线上服务器都需要用户名和密码,用户名在ssh中可以很轻松的带上,但是每次输入密码是件很痛苦的事情,可以考虑kinit的方式保存帐号和密码

我们需要什么样的微服务

微服务到底应该是多大,一种简单的划分方法是项目代码在100-300行。这个划分方式肯定是不合理的,但也表现了微服务的一个核心思想——服务尽量的小。

1. 微服务架构的特点

  • 领域驱动设计
  • 单一职责原则:每个服务只负责自己单独的部分,这也是SOLID原则之一;
  • 明确发布接口:每个服务都会发布一个定义明确的接口,消费者只需要关心接口即可;
  • 独立部署、升级、扩展和替换:每个服务都可以单独部署及重新部署而不影响整个系统。这使得服务很容易升级;
  • 服务相互解耦;
  • 轻量级通信:服务通信使用轻量级的通信协议,例如,同步的REST,异步的AMQP、STOMP、MQTT等。

这些使得微服务具有以下优点:

  • 开发易于开发、理解和维护
  • 比单体应用启动快,
  • 局部修改很容易部署,
  • 故障隔离,一个服务出现问题不会影响整个应用,
  • 不会受限于任何技术栈

这些让微服务看起来像是银弹一样,但是微服务更多的是将开发的工作量推到了运维侧,采用微服务架构使得运维工作量巨大。

另外微服务使得接口升级的工作量也非常大,因为一旦接口的定义发生修改,则会影响到所以使用此接口的其他系统,所以升级中需要考虑到升级的各种影响,反而使得上线的成本进一步加大。

2. 微服务的设计模式1

2.1 聚合器微服务设计模式

聚合器调用多个服务实现应用程序所需的功能。它可以是一个简单的Web页面,将检索到的数据进行处理展示。它也可以是一个更高层次的组合微服务,对检索到的数据增加业务逻辑后进一步发布成一个新的微服务,这符合DRY原则。另外,每个服务都有自己的缓存和数据库。如果聚合器是一个组合服务,那么它也有自己的缓存和数据库。聚合器可以沿X轴和Z轴独立扩展。

2.2 代理微服务设计模式

在这种情况下,客户端并不聚合数据,但会根据业务需求的差别调用不同的微服务。代理可以仅仅委派请求,也可以进行数据转换工作。

2.3 链式微服务设计模式

在这种情况下,服务A接收到请求后会与服务B进行通信,类似地,服务B会同服务C进行通信。所有服务都使用同步消息传递。在整个链式调用完成之前,客户端会一直阻塞。因此,服务调用链不宜过长,以免客户端长时间等待。

2.4 分支微服务设计模式

这种模式是聚合器模式的扩展,允许同时调用两个微服务链。

2.5 数据共享微服务设计模式

自治是微服务的设计原则之一,就是说微服务是全栈式服务。但在重构现有的“单体应用(monolithic application)”时,SQL数据库反规范化可能会导致数据重复和不一致。在这种情况下,部分微服务可能会共享缓存和数据库存储。不过,这只有在两个服务之间存在强耦合关系时才可以。对于基于微服务的新建应用程序而言,这是一种反模式。

2.6 异步消息传递微服务设计模式


虽然REST设计模式非常流行,但它是同步的,会造成阻塞。因此部分基于微服务的架构可能会选择使用消息队列代替REST请求/响应。

3. 我们需要何种大小的服务

微服务的小该小到何种地步,按照代码行数来划分,在300行一下。这种划分方式有一定道理,但这种方式显然是不合理的,因为不同的语言在写起功能来所需的行数是不一样的。
划分微服务最好的方式是按照系统的边界(领域)来划分。比如a服务只负责创建订单,b服务只负责查询订单。系统的实现大小根据业务领域的需求而变化,如果业务领域慢慢变大,则可以将业务领域继续细分,如创建订单服务,又可以划分为创建酒店订单,创建电影订单,随着业务的需要而不断细分。

4. 服务分级

微服务虽小,也是有级别的。简单划分可以按照服务的依赖情况来划分:

  • 基础服务
    这种服务除了对数据库的依赖之外就不再依赖其他的微服务,只向外提供接口。
  • 集成服务
    集成服务是指主要依赖与其他服务(基础服务和集成服务)进行业务处理,更多的是作为流程管理的角色,一般不维护自身数据。例如用户提交订单,集成服务首先去调用验证服务,之后再调用创建订单服务。

    References

Goodbye Microservices, Hello Right-sized Services
微服务架构解析
微服务架构的设计模式
解析微服务架构(二)微服务架构综述
解析微服务架构(一)单块架构系统以及其面临的挑战
微服务实战(一):微服务架构的优势与不足
单体应用与微服务优缺点辨析
SOLID

分布式系统接口幂等性

1.幂等性定义

1.1 数学定义

在数学里,幂等有两种主要的定义:

  • 在某二元运算下,幂等元素是指被自己重复运算(或对于函数是为复合)的结果等于它自己的元素。例如,乘法下唯一两个幂等实数为0和1。
    即 s *s = s
  • 某一元运算为幂等的时,其作用在任一元素两次后会和其作用一次的结果相同。例如,高斯符号便是幂等的,即f(f(x)) = f(x)。

1.2 HTTP规范的定义

在HTTP/1.1规范中幂等性的定义是:

A request method is considered “idempotent” if the intended effect onthe server of multiple identical requests with that method is the same as the effect for a single such request. Of the request methods defined by this specification, PUT, DELETE, and safe request methods are idempotent.

HTTP的幂等性指的是一次和多次请求某一个资源应该具有相同的副作用。如通过PUT接口将数据的Status置为1,无论是第一次执行还是多次执行,获取到的结果应该是相同的,即执行完成之后Status =1。

2. 何种接口提供幂等性

2.1 HTTP支持幂等性的接口

在HTTP规范中定义GET,PUT和DELETE方法应该具有幂等性。

  • GET方法

The GET method requests transfer of a current selected representatiofor the target resourceGET is the primary mechanism of information retrieval and the focus of almost all performance optimizations. Hence, when people speak of retrieving some identifiable information via HTTP, they are generally referring to making a GET request.

GET方法是向服务器查询,不会对系统产生副作用,具有幂等性(不代表每次请求都是相同的结果)

  • PUT方法

T he PUT method requests that the state of the target resource be created or replaced with the state defined by the representation enclosed in the request message payload.

也就是说PUT方法首先判断系统中是否有相关的记录,如果有记录则更新该记录,如果没有则新增记录。

  • DELETE 方法

The DELETE method requests that the origin server remove the association between the target resource and its current functionality. In effect, this method is similar to the rm command in UNIX: it expresses a deletion operation on the URI mapping of the origin server rather than an expectation that the previously associated information be deleted.

DELETE方法是删除服务器上的相关记录。

2.2 实际业务

现在简化为这样一个系统,用户购买商品的订单系统与支付系统;订单系统负责记录用户的购买记录已经订单的流转状态(orderStatus),支付系统用于付款,提供

1
boolean pay(int accountid,BigDecimal amount) //用于付款,扣除用户的

接口,订单系统与支付系统通过分布式网络交互。

这种情况下,支付系统已经扣款,但是订单系统因为网络原因,没有获取到确切的结果,因此订单系统需要重试。
由上图可见,支付系统并没有做到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,不符合幂等性原则(同一个订单,无论是调用了多少次,用户都只会扣款一次)。
如果需要支持幂等性,付款接口需要修改为以下接口:

1
boolean pay(int orderId,int accountId,BigDecimal amount)

通过orderId来标定订单的唯一性,付款系统只要检测到订单已经支付过,则第二次调用不会扣款而会直接返回结果:

在不同的业务中不同接口需要有不同的幂等性,特别是在分布式系统中,因为网络原因而未能得到确定的结果,往往需要支持接口幂等性。

3.分布式系统接口幂等性

随着分布式系统及微服务的普及,因为网络原因而导致调用系统未能获取到确切的结果从而导致重试,这就需要被调用系统具有幂等性。
例如上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态确定之后,以后的操作都会返回相同的结果,对用户的扣款也只会有一次。这种接口的幂等性,简化到数据层面的操作:

1
update userAmount set amount = amount - 'value' ,paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay'

其中value是用户要减少的订单,paystatus代表支付状态,paid代表已经支付,unpay代表未支付,orderid是订单号。
在上文中提到的订单系统,订单具有自己的状态(orderStatus),订单状态存在一定的流转。订单首先有提交(0),付款中(1),付款成功(2),付款失败(3),简化之后其流转路径如图:

当orderStatus = 1 时,其前置状态只能是0,也就是说将orderStatus由0->1 是需要幂等性的

1
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0

当orderStatus 处于0,1两种状态时,对订单执行0->1 的状态流转操作应该是具有幂等性的。
这时候需要在执行update操作之前检测orderStatus是否已经=1,如果已经=1则直接返回true即可。

但是如果此时orderStatus = 2,再进行订单状态0->1 时操作就无法成功,但是幂等性是针对同一个请求的,也就是针对同一个requestid保持幂等。

这时候再执行

1
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0

接口会返回失败,系统没有产生修改,如果再发一次,requestid是相同的,对系统同样没有产生修改。

References

幂等
HTTP幂等性概念和应用
What Is Idempotent in REST?
REST之中的幂等指的是什么?
Hypertext Transfer Protocol (HTTP/1.1)

MySQL索引及查询优化

1. MySQL索引的原理

1.1 索引目的

索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?

1.2 索引原理

除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的目录等。它们的原理都是一样的,通过不断的缩小想要获得数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是我们总是通过同一种查找方式来锁定数据。
每个数据表都有一个主键(如果没有主键,数据库会将该表中的唯一索引当作主键使用?),MySQL会以主键的方式构造一棵树,叶子节点存放该主键对应的整行数据。

自己建立的索引,一般叫做辅助索引,辅助索引的树,也自己节点存放了两个东西,一个是索引自身的值,另外一个是索引对应主键的值。

如果索引是联合索引,比如UserID和AddTime索引的方式,索引叶子节点会存储UserID和AddTime之间的配对+主键的配对数据。

1.3 索引的类型

1.3.1 B-树索引

B-树索引在生产环境更为广泛,这里我只针对B-树索引进行讨论
B-树索引是一个复杂的内容,可以参见B-tree

1.3.2 Hash索引

哈希索引(Hash Index)建立在哈希表的基础上,它只对使用了索引中的每一列的精确查找有用。对于每一行,存储引擎计算出了被索引的哈希码(Hash Code),它是一个较小的值,并且有可能和其他行的哈希码不同。它把哈希码保存在索引中,并且保存了一个指向哈希表中的每一行的指针。

在mysql中,只有memory存储引擎支持显式的哈希索引。

  • Hash 索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
  • Hash 索引无法被用来避免数据的排序操作。
  • Hash 索引不能利用部分索引键查询。
  • Hash 索引在任何时候都不能避免表扫描。
  • Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高
1.3.3 空间索引(R-树)索引

主要用于GIS中空间数据的存储,但是MySQL的空间索引支持并不好,现在多使用PostgreSQL。

1.3.4 全文索引(Full-text)索引

文本字段上的普通索引只能加快对出现在字段内容最前面的字符串(也就是字段内容开头的字符)进行检索操作。如果字段里存放的是由几个、甚至是多个单词构成 的较大段文字,普通索引就没什么作用了。这种检索往往以LIKE %word%的形式出现,这对MySQL来说很复杂,如果需要处理的数据量很大,响应时间就会很长。

这类场合正是全文索引(full-text index)可以大显身手的地方。在生成这种类型的索引时,MySQL将把在文本中出现的所有单词创建为一份清单,查询操作将根据这份清单去检索有关的数 据记录。全文索引即可以随数据表一同创建,也可以等日后有必要时再使用下面这条命令添加:

1
ALTER TABLE tablename ADD FULLTEXT(column1, column2)

2. 索引的合理使用

字段名 数据类型 NULL INDEX
ID int(10) NOT NULL pk
UserID int(10) NOT NULL
Mobile varchar(15) NOT NULL
ArriveDate DateTime NOT NULL
AddDate DateTime NOT NULL
UpdateTime timetamp NOT NULL

下面我们的分析都会这个数据表为例。

2.1 索引在查询中的使用

不使用索引

现在刚建立的表上没有任何索引,但是我们想通过UserID找出ArriveDate这个数据,SQL将会写成如下方式:

1
select ArriveDate from TestSQL where UserID = 10;

这个SQL没有走索引,数据库就会根据主键(ID)扫描全表,每拿到一条数据库记录就与where条件比对,如果符合条件则将这条记录返回,重复直到全表扫描完毕。
在大数据量的情况下,不使用索引进行查询几乎是不可行的。

  • 使用UserID作为索引


这时候看到where条件是使用了UserID索引的。这时候数据库引擎会根据UserID到索引上找到ID,然后根据ID去查询对应记录,从而取出ArriveDate数据。
现在我们将UserID的索引更换为UserID,ArriveDate的联合索引。
现在再来查询:

可以发现这个Extra里面也是使用了索引的,这就意味这个SQL是完全走了索引,数据库引擎根据UserID找到对应的索引, 因为Select的字段是索引的一部分,所以找到索引之后不需要再读取表记录了。

当一个查询语句中使用设计到多个索引时,MySQL数据库引擎会计算不同索引涉及到的行数大小,选取行数最小的索引作为实际执行时使用的索引,如:

一次查询同一张表,MySQL每次只会使用一个索引。

2.2 索引在范围查询的使用

范围查询主要是指查询字段值在某个范围内的记录,表现在where条件中为>,<,between等关键字。如,我们使用如下SQL进行查询:

1
select UserID,ArriveDate  from TestSQL where UserID >0 and UserID <100  and ArriveDate = '2015-09-23 00:00:00';

作为对比,我们使用另一种SQL查询相同记录:

1
select UserID,ArriveDate from TestSQL where UserID in(2,12) and ArriveDate = '2015-09-23 00:00:00';

联调SQL都会查询出相同的记录:

UserID ArriveDate
2 2015-09-23 00:00:00
12 2015-09-23 00:00:00

第一个SQL(使用范围查询)的explain结果为:


第二个SQL(未使用范围查询)的explain结果为:


对比可见,两种sql的索引长度是不一样的。在范围查询中,索引的使用是遵循最左(leftmost)原则,例如这个表的使用的索引是IX_UserID_ArriveDate,但是因为UserID使用了范围查询(Range query),就不再使用ArrvieDate的索引了。

2.3 排序使用索引

在排序中以下情况无法使用索引:

  • Order by 的字段并不是索引
  • 使用了两种排序方向,但是索引都是使用升序排列的
    在索引的原理里我们讲到过

    如果索引是联合索引,比如UserID和AddTime索引的方式,索引叶子节点会存储UserID和AddTime之间的配对+主键的配对数据。

    这种情况下,索引会以UserID进行排序,当UserID相同时再以AddTime进行排序(默认为升序),以我们现有的数据库为例:

    1
    select UserID,ArriveDate  from TestSQL where UserID in(2,3,4)   order by UserID asc,ArriveDate desc;

    这种情况将只使用UserID 作为索引,而ArriveDate 将不再作为索引。

  • 不符合最左(Left most)的条件
    1
    2
    3
    4
    5
    6
    7
         select * from TestSQL where  ArriveDate =  '2015-09-23 00:00:00' order by UserID desc
    ```
    这种情况将不再使用UserID_ArriveDate索引
    - 联合查询的情况下,如果第一个筛选条件是范围查询,MySQL不再使用剩下的索引

    ```sql

    select * from TestSQL where UserID >1 and UserID < 5 order by UserID desc,ArriveDate desc

这种情况下,因为UserID 为范围查询,所以就不会再使用ArriveDate 索引了。

2.4 join 中使用索引

我们来新建一个TestSQL_join表,其结构与TestSQL 相同,但只有Mobile索引。首先来看一个简单的join操作

1
select * from TestSQL join TestSQL_join on TestSQL.id = TestSQL.id

MySQL首先比较TestSQL和 TestSQL_join表那个行数少,如TestSQL中的记录较少,TestSQL就是一个小表,而TestSQL_join则是大表,MySQL引擎先把TestSQL中的ID全部去出来,然后根据id到TestSQL_join中查询相关的记录。

在这里,TestSQL中行数决定了循环的次数,但是TestSQL_join则决定了每次循环查询所需要查询的时间;这时如果TestSQL_join中的ID是索引则会大大减少查询时间如下SQL:

1
select * from TestSQL a join TestSQL_join b on a.ID = b.ID

由于b.ID 是TestSQL_join的主键,查询使用了TestSQL_join主键索引。

如果此时对TestSQL增加条件筛选:

1
2
 select * from TestSQL a join TestSQL_join b on a.ID = b.ID
where a.UserID = 1;

因为a.UserID 是TestSQL的索引,所以在过滤TestSQL表的行数时,采用次索引查询对应ID,然后根据ID查询TestSQL_join的记录。


如果此时针对TestSQL_join 增加where条件过滤:

1
2
 select * from TestSQL a join TestSQL_join b on a.ID = b.ID
where a.UserID >1 and b.Mobile = "2147483647"

这种时候,因为TestSQL 和 TestSQL_join 根据where条件所筛选出来的行数大小可能会有变化,也就是说TestSQL_join 有可能会变成小表,这时候将会优先从TestSQL_join 查询出相关ID,然后根据ID去查询TestSQL。

join操作时,大表小表的概念,主要是按照两张表分别执行对应查询条件,哪个开销更小,哪个就是小表。

join操作虽然在SQL层面很方便,而且在线上大流量的情况下,一旦SQL的join操作导致查询缓慢,较难即使优化。另外在服务化的系统中,容易导致业务领域不清晰,所以在互联网大流量的应用中是不推荐使用join操作的。

3. 索引建立的原则

  • 使用区分度高的列作为索引
    区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,区分度越高,索引树的分叉也就越多,一次性找到的概率也就越高。
  • 尽量使用字段长度小的列作为索引
  • 使用数据类型简单的列(int 型,固定长度)
  • 选用NOT NULL的列
    在MySQL中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值。

  • 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。这样也可避免索引重复。

    4. 索引使用的原则

  • 最左前缀匹配原则(leftmost),mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,=和in可以乱序,一个联合索引中,如UserID,ArriveDate的联合索引,使用ArriveDate in ()and UserID = 的任意顺序,MySQL的查询优化器会帮你优化成索引可以识别的形式
  • 索引列不能参与计算

    1
    select *  from TestSQL where UserID + 1 >1 and UserID < 5

    这种方式UserID 的索引就不会再被使用,因为在进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。
    另外当使用<>,like通配符放置在最前面 如:like’%ddd’ ,not, in, !=等运算符都不会使用索引。

  • 查询数据库记录时,查询到的条目数尽量小,当通过索引获取到的数据库记录> 数据库总揭露的1/3时,SQL将有可能直接全表扫描,索引就失去了应有的作用。

5. explain的使用

explain是MySQL查询优化过程的神器,详情可以查看explain的使用

References

High Performance MySQL-THIRD EDITION
explain的使用
MySQL索引学习漫画
MySQL索引原理及慢查询优化
理解MySQL——索引与优化
mysql索引的类型和优缺点

date: 2015-08-23 20:16:09
author: cjgnju
email: cjgnju@126.com
site:
ip: 45.116.12.222

ddddl


Clojure实现规则引擎

规则引擎

规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了将业务决策从应用程序代码中分离出来,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。

1
2
3
4
when
<conditions>
then
<actions>;

最为著名的规则引擎是Drools,现由JBoss维护,分为Guvnor,Expert,Jbpm5,Jbpm5和Planner五个模块,应用在jBPM工作流中。
目前Java中开源规则引擎也非常多,Open Source Rule Engines in Java,Java也已经指定了rule engine规范

Clojure实现的规则引擎

现在比较著名的规则引擎有:
mimir,这个library不牵涉到复杂的数据结构,使用起来也比较简单。
clara,与Drools比较相似,其目标是解绑业务逻辑,同时利用的Clojure和Java生态系统的优势表达其作为组合的规则,以控制业务复杂性;使用起来比较简单,可以很好的在分布式系统上运行。
core.logic,严格意义上不能算是规则引擎Forward chaining范畴,实现了类似Prolog的逻辑编程(Backward chaining)。

mimir 使用

mimir是一个实验性的规则引擎,相对简单,截至现在已经2年多没有更新了,在这里也就不多介绍了。

1
2
3
4
5
6
7
8
9
(rule xyz
X < Y
Z > 5
Z = (+ X Y)
Z != Y
=>
(str X '+ Y '= Z))

(matches? "2+4=6"))

这段代码是先指定了规则(rule),然后判定一个fact是否满足该条件。

clara 使用

clara的基本使用在clara-example里面有很多示例。clara可以在Clojure,ClojureScript和Javay语言中使用。

1
2
3
4
5
(defrule free-lunch-with-gizmo
"Anyone who purchases a gizmo gets a free lunch."
[Purchase (= item :gizmo)]
=>
(insert! (->Promotion :free-lunch-with-gizmo :lunch)))

clara还提供了规则查询的功能

1
2
3
4
(defquery get-promotions
"Query to find promotions for the purchase."
[]
[?promotion <- Promotion])

core.logic使用

core.logic 严格来说是逻辑变成的范畴,过程是

1
事实 + 规则 =结果

是通过目前已经符合条件的事实 和现有规则来推导符合该规则的条件,相比与规则引擎是一种反向过程。
例如:

1
2
3
(run* [q]
(membero q [1 2 3])
(membero q [2 3 4]))

q满足了是[1 2 3]集合元素,又是 [2 3 4]的元素,计算q应该是什么。
core.logic主要采用一下方式:

1
2
(run* [logic-variable]
&logic-expressions)

支持fresh unify, ==等操作,默认条件是and操作,也可以通过conde支持OR操作。详细参见 core.logic Features

ruleengine

这是我为了练习Clojure而开的项目,结合了去年自己做优惠基础平台时的经验和想法的项目,目前还处于private状态。先写在这里,等到完成之后再公开项目。

References

Quora:How can Clojure be used for rules engines
The Rule Engine

undertow core

###XNIO
undertow 是以XNIO为实现核心。XNIO有两个核心概念:

####Channel
Channel,是传输管道的抽象概念,在NIO的Channel上进行的扩展加强,使用ChannelListener API进行事件通知。在创建Channel时,就赋予IO线程,用于执行所有的ChannelListener回调方法。

####IOWorker
区分IO线程和工作线程,创建一个工作线程池可以用来执行阻塞任务。一般情况下,非阻塞的Handler由IO线程执行,而阻塞任务比如Servlet则被调度到工作线程池执行。这样就很好的区分了阻塞和非阻塞的两种情形。

  • WORKER_IO_THREADS, IO thread处理非阻塞任务,要保证不做阻塞操作,因为很多连接同时用到这类线程,类似于nodejs中的loop,这个线程只要有任务就去执行,实际配置时每个CPU一个线程比较好。
  • WORKER_TASK_CORE_THREADS,用于执行阻塞任务,从线程池中获得,任务完成后返回到线程池中。因为不同应用对应的服务器负载不同,所以不易给出具体数值,一般建议每个CPU core设置10个。

我们知道NIO的基本要求是不阻塞当前线程的执行,对于非阻塞请求的结果,可以用两种方式获得:一种是对于请求很快返回一个引用(如JDK中Future,XNIO中称为IoFuture,其中很多方法是类似的),过一段时间再查询结果;还有一种是当结果就绪时,调用事先注册的回调方法来通知(如NIO2的CompletionHandler,XNIO的ChannelListener)。显而易见后者效率更高一些,避免了数据未就绪情景下的无用处理过程。但JDK7之前无法将函数作为方法参数,所以只能用Java的匿名内部类来模拟函数式方法,造成代码嵌套层次过多,难以理解和维护,所以Netty和XNIO这样的框架通过调度方法调用过程,简化了编程工作。

XNIO和Netty都对ByteBuffer进行池化管理,简单来说就是开发者在程序开始时就计划好读写缓存区大小,统一分配好放到池中,Xnio中有Pool和Pooled接口用来管理池化缓存区。开发过高并发应用就知道,JVM GC经常出现并难以控制是很头疼的问题。我们通常在接收网络数据时,往往简单的new出一块数据区,填充,解析,使用,最后丢弃,这种方法随着大量的数据读入,必然造成GC反复出现。重用缓存区就可以在这个方面解决一部分问题。

和Netty的ChannelHandler不同,XNIO对应的ChannelListener只有一个方法handleEvent(),也就意味着所有的事件都要经由这个方法。在实际实行过程中,会进行若干状态机的转变,比如在服务器端,开始时accept状态就绪,当连接建立后转变为可读或者可写状态。请参见下面的例子。

除了ChannelIOWorker 两个重要的基础,XNIO还提供了SSL支持

undertow core

####Listeners
目前undertow中支持的Listner类型主要有

  • HTTP
  • HTTPS
  • AJP
  • HTTP2

####Handler
undertow原生提供了io.undertow.server.HttpHandler,接口定义比较简单:

1
2
3
public interface HttpHandler {
void handleRequest(HttpServerExchange exchange) throws Exception;
}

undertow中并没有pipeline的概念,但是可以在构建hanlder时指定next,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SetHeaderHandler implements HttpHandler {
private final HttpString header;
private final String value;
private final HttpHandler next;
public SetHeaderHandler(final HttpHandler next, final String header, final String value) {
this.next = next;
this.value = value;
this.header = new HttpString(header);
}
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(header, value);
next.handleRequest(exchange);
}
}

或者类似下面的方式,只做自身逻辑的处理,不做传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class FilterHandler implements HttpHandler {

private final Map<DispatcherType, List<ManagedFilter>> filters;
private final Map<DispatcherType, Boolean> asyncSupported;
private final boolean allowNonStandardWrappers;

private final HttpHandler next;

public FilterHandler(final Map<DispatcherType, List<ManagedFilter>> filters, final boolean allowNonStandardWrappers, final HttpHandler next) {
this.allowNonStandardWrappers = allowNonStandardWrappers;
this.next = next;
this.filters = new EnumMap<>(filters);
Map<DispatcherType, Boolean> asyncSupported = new EnumMap<>(DispatcherType.class);
for(Map.Entry<DispatcherType, List<ManagedFilter>> entry : filters.entrySet()) {
boolean supported = true;
for(ManagedFilter i : entry.getValue()) {
if(!i.getFilterInfo().isAsyncSupported()) {
supported = false;
break;
}
}
asyncSupported.put(entry.getKey(), supported);
}
this.asyncSupported = asyncSupported;
}

@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletRequest request = servletRequestContext.getServletRequest();
ServletResponse response = servletRequestContext.getServletResponse();
DispatcherType dispatcher = servletRequestContext.getDispatcherType();
Boolean supported = asyncSupported.get(dispatcher);
if(supported != null && ! supported) {
exchange.putAttachment(AsyncContextImpl.ASYNC_SUPPORTED, false );
}

final List<ManagedFilter> filters = this.filters.get(dispatcher);
if(filters == null) {
next.handleRequest(exchange);
} else {
final FilterChainImpl filterChain = new FilterChainImpl(exchange, filters, next, allowNonStandardWrappers);
filterChain.doFilter(request, response);
}
}

####组装服务器
1.创建XNIO Workder
2.创建 XNIO SSL实例
3.Create an instance of the relevant Undertow listener class
4.Open a server socket using XNIO and set its accept listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Xnio xnio = Xnio.getInstance();
XnioWorker worker = xnio.createWorker(OptionMap.builder()
.set(Options.WORKER_IO_THREADS, ioThreads)
.set(Options.WORKER_TASK_CORE_THREADS, workerThreads)
.set(Options.WORKER_TASK_MAX_THREADS, workerThreads)
.set(Options.TCP_NODELAY, true)
.getMap());
OptionMap socketOptions = OptionMap.builder()
.set(Options.WORKER_IO_THREADS, ioThreads)
.set(Options.TCP_NODELAY, true)
.set(Options.REUSE_ADDRESSES, true)
.getMap();
Pool<ByteBuffer> buffers = new ByteBufferSlicePool(BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR,bufferSize, bufferSize * buffersPerRegion);
if (listener.type == ListenerType.AJP) {
AjpOpenListener openListener = new AjpOpenListener(buffers, serverOptions, bufferSize);
openListener.setRootHandler(rootHandler);
ChannelListener<AcceptingChannel<StreamConnection>> acceptListener = ChannelListeners.openListenerAdapter(openListener);
AcceptingChannel<? extends StreamConnection> server = worker.createStreamConnectionServer(new InetSocketAddress(Inet4Address.getByName(listener.host), listener.port), acceptListener, socketOptions);
server.resumeAccepts();
} else if (listener.type == ListenerType.HTTP) {
HttpOpenListener openListener = new HttpOpenListener(buffers, OptionMap.builder().set(UndertowOptions.BUFFER_PIPELINED_DATA, true).addAll(serverOptions).getMap(), bufferSize);
openListener.setRootHandler(rootHandler);
ChannelListener<AcceptingChannel<StreamConnection>> acceptListener = ChannelListeners.openListenerAdapter(openListener);
AcceptingChannel<? extends StreamConnection> server = worker.createStreamConnectionServer(new InetSocketAddress(Inet4Address.getByName(listener.host), listener.port), acceptListener, socketOptions);
server.resumeAccepts();
} else if (listener.type == ListenerType.HTTPS){
HttpOpenListener openListener = new HttpOpenListener(buffers, OptionMap.builder().set(UndertowOptions.BUFFER_PIPELINED_DATA, true).addAll(serverOptions).getMap(), bufferSize);
openListener.setRootHandler(rootHandler);
ChannelListener<AcceptingChannel<StreamConnection>> acceptListener = ChannelListeners.openListenerAdapter(openListener);
XnioSsl xnioSsl;
if(listener.sslContext != null) {
xnioSsl = new JsseXnioSsl(xnio, OptionMap.create(Options.USE_DIRECT_BUFFERS, true), listener.sslContext);
} else {
xnioSsl = xnio.getSslProvider(listener.keyManagers, listener.trustManagers, OptionMap.create(Options.USE_DIRECT_BUFFERS, true));
}
AcceptingChannel <SslConnection> sslServer = xnioSsl.createSslConnectionServer(worker, new InetSocketAddress(Inet4Address.getByName(listener.host), listener.port), (ChannelListener) acceptListener, socketOptions);
sslServer.resumeAccepts();
}

###undertow servlet
类似与jetty,undertow 也可以部署采用部署war包的方式启动项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DeploymentInfo servletBuilder = Servlets.deployment()
.setClassLoader(ServletServer.class.getClassLoader())
.setContextPath("/myapp")
.setDeploymentName("test.war")
.addServlets(
Servlets.servlet("MessageServlet", MessageServlet.class)
.addInitParam("message", "Hello World")
.addMapping("/*"),
Servlets.servlet("MyServlet", MessageServlet.class)
.addInitParam("message", "MyServlet")
.addMapping("/myservlet"));

DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
manager.deploy();
PathHandler path = Handlers.path(Handlers.redirect("/myapp"))
.addPrefixPath("/myapp", manager.start());

Undertow server = Undertow.builder()
.addHttpListener(8080, "localhost")
.setHandler(path)
.build();
server.start();

###Refernces

Undertow服务器基础分析 - 概述
Undertow服务器基础分析 - XNIO
Undertow服务器基础分析
undertow cookdoc