背景

今年的第二本书,1、2月把《微服务架构设计模式》读完了,对微服务整体有了完整的了解。第二本打算看看具体的微服务框架,也是目前在用的SpringCloud。希望能有所长进,慢就是快,每天进步一点点。

【看了两天,这本书好像有点水,没读的朋友可以先不用读了......

第1章 Spring Cloud + Nginx高并发核心编程的学习准备

1.1 Spring Cloud + Nginx架构的主要组件

1.2 Spring Cloud和SpringBoot的版本选择

1.3 Spring Cloud微服务开发所涉及的中间件

  • ZooKeeper
  • Redis
  • Eureka
  • Spring Cloud Config
  • Zuul
  • Nginx/OpenResty

1.4 Spring Cloud 微服务开发和自验证环境

1.4.1 开发和自验证环境的系统选项和环境变量配置

  • Linux环境
  • 使用环境变量配置中间件相关信息,如ip、端口、账号等

1.4.2 使用Fiddler工具抓包和查看报文

1.5 carzy-springcloud微服务开发脚手架

1.6 以秒杀作为Spring Cloud + Nginx的实战案例

第2章 Spring Cloud入门实战

2.1 Eureka服务注册与发现

2.1.1 什么是服务注册与发现

  • 注册中心的主要功能如下

(1)服务注册表维护:此功能是注册中心的核心,用来记录各个 服务提供者实例的状态信息。注册中心提供Provider实例清单的查询和 管理API,用于查询可用的Provider实例列表,管理Provider实例的上 线和下线。

(2)服务健康检查:注册中心使用一定机制定时检测已注册的 Provider实例,如发现某实例长时间无法访问,就会从服务注册表中移 除该实例。

  • 服务提供者的主要功能如下

(1)服务注册:是指Provider微服务实例在启动时(或者定期) 将自己的信息注册到注册中心的过程。

(2)心跳续约:Provider实例会定时向注册中心提供“心跳”, 以表明自己还处于可用的状态。当一个Provider实例停止心跳一段时间 后,注册中心会认为该服务实例不可用了,就会将该服务实例从服务注 册表中剔除。如果被剔除掉的Provider实例过了一段时间后又继续向注 册中心提供心跳,那么注册中心会把该Provider实例重新加入服务注册 表中。

(3)健康状况查询:Provider实例能提供健康状况查看的API,注 册中心或者其他的微服务Provider能够获取其健康状况。

  • 注册中心客户端组件还有如下功能:

(1)服务发现:从注册中心查询可用Provider实例清单。

(2)实例缓存:将从注册中心查询的Provider实例清单缓存到本 地,不需要在每次使用时都去注册中心临时获取。

2.1.2 Eureka Server注册中心

Eureka Clien

Eureka所治理的每一个微服务实例被称为Provider Instance(提供者实 例)。每一个Provider Instance包含一个Eureka Client组件(相当于注册中 心客户端组件),它的主要工作如下:

(1)向Eureka Server完成Provider Instance的注册、续约和下线等操 作,主要的注册信息包括服务名、机器IP、端口号、域名等。

(2)向Eureka Server获取Provider Instance清单,并且缓存在本地。

Eureka Server

实际上一个Eureka Server实例身兼三个角色:注册中心、服务提供者、 注册中心客户端组件。主要原因如下:

(1)对于所有Provider Instance而言,Eureka Server的角色是注册中 心。

(2)对于Eureka Server集群中其他的Eureka Server而言,Eureka Server的角色是注册中心客户端组件。

(3)Eureka Server对外提供REST接口的服务,当然也是服务提供者。

2.1.3 服务提供者的创建和配置

2.1.4 服务提供者的续约(心跳)

2.1.5 服务提供者的健康状态

  • Eureka Server并不记录Provider的所有健康状况信息,仅仅维护了一个Provider 清单。Eureka Client组件查询的Provider注册清单中,包含每一个Provider的健康状 况的检查地址。通过该健康状况的地址可以查询Provider的健康状况。

2.1.6 Eureka自我保护模式与失效Provider的快速剔除

自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微 服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式 可以让Eureka集群更加健壮和稳定。

2.2 Config配置中心

2.2.1 config-server服务端组件

2.2.2 config-client客户端组件

2.3 微服务的RPC远程调用

微服务的调用涉及远程接口访问的RPC框架,包括序列化、反序列 化、网络框架、连接池、收发线程、超时处理、状态机等重要的基础 技术。

2.3.1 RESTful风格简介

2.3.2 RestTemplate远程调用

2.3.3 Feign远程调用

Feign是在RestTemplate基础上封装的,使用注解 的方式来声明一组与服务提供者Rest接口所对应的本地Java API接口 方法。Feign将远程Rest接口抽象成一个声明式的FeignClient(Java API)客户端,并且负责完成FeignClient客户端和服务提供方的Rest 接口绑定。

2.4 Feign+Ribbon实现客户端负载均衡

  • 服务端负载均衡:在消费者和服务提供者中间使用独立的反向代理服务进行负载均衡
  • 客户端负载均衡:客户端自己维护一份从注册中心获取的Provider列表清单,根据 自己配置的Provider负载均衡选择算法在客户端进行请求的分发

2.4.1 Spring Cloud Ribbon基础

Spring Cloud Ribbon是Spring Cloud集成Ribbon开源组件的一个 模块,微服务间的RPC调用以及API网关的代理请求的 RPC转发调用,实际上都需要通过Ribbon来实现负载均衡。

2.4.2 Spring Cloud Ribbon的负载均衡策略

1.随机策略(RandomRule)

2.线性轮询策略(RoundRobinRule)

3.响应时间权重策略(WeightedResponseTimeRule)

4.最少连接策略(BestAvailableRule)

5.重试策略(RetryRule)

6.可用过滤策略(AvailabilityFilteringRule)

7.区域过滤策略(ZoneAvoidanceRule)

2.4.3 Spring Cloud Ribbon的常用配置

1.手工配置Provider实例清单

2.RPC请求超时配置

3.重试机制配置

4.代码配置Ribbon

2.5 Feign+Hystrix实现RPC调用保护

  • Hystrix开源框架是 Netflix开源的一个延迟和容错的组件,主要用于在远程Provider服务 异常时对消费端的RPC进行保护。
  • 在启动类上添加@EnableHystrix或者@EnableCircuitBreaker。注 意,@EnableHystrix中包含了@EnableCircuitBreaker。

2.5.1 Spring Cloud Hystrix失败回退

  • 如何设置RPC调用的回退逻辑呢?有两种方式:

(1)定义和使用一个Fallback回退处理类。

package com.crazymaker.springcloud.user.info.remote.client;
//省略import/*** Feign客户端接口 *@description:获取用户信息的RPC接口类*/
@FeignClient(value = "uaa-provider",configuration = FeignConfiguration.class,fallback = UserClientFallback.class, #配置回退处理类path="/uaa-provider/api/user")
public interface UserClient {@RequestMapping(value = "/detail/v1", method = RequestMethod.GET)RestOut<UserDTO> detail(@RequestParam(value = "userId") Long userId);
}

(2)定义和使用一个FallbackFactory回退处理工厂类。

package com.crazymaker.springcloud.user.info.remote.fallback;
//省略import/*** Feign客户端接口的回退处理工厂类*/
@Slf4j
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {/*** 创建UserClient客户端的回退处理实例*/@Overridepublic UserClient create(final Throwable cause) {log.error("RPC异常了,回退!", cause); /***创建一个UserClient客户端接口的匿名回退实例*/return new UserClient() {/***方法: 获取用户信息RPC失败后的回退方法 */@Overridepublic RestOut<UserDTO> detail(Long userId) {return RestOut.error("FallbackFactory fallback:user detail rest服务调用失败");}};}
}

2.5.2 分布式系统面临的雪崩难题

引发雪崩效应的原因比较多,下面是常见的几种:

(1)硬件故障:如服务器宕机、机房断电、光纤被挖断等。

(2)流量激增:如流量异常、巨量请求瞬时涌入(如秒杀)等。

(3)缓存穿透:一般发生在系统重启所有缓存失效时,或者发生 在短时间内大量缓存失效时,前端过来的大量请求没有命中缓存,直击 后端服务和数据库,造成服务提供者和数据库超负荷运行,引起整体瘫 痪。

(4)程序BUG:如程序逻辑BUG导致内存泄漏等原因引发的整体瘫 痪。

(5)JVM卡顿:JVM的FullGC时间较长,极端的情况长达数十秒, 这段时间内JVM不能提供任何服务。

2.5.3 Spring Cloud Hystrix熔断器

第3章 Spring Cloud RPC远程调用核心原理

3.1 代理模式与RPC客户端实现类

3.1.1 客户端RPC远程调用实现类的职责

3.1.2 简单的RPC客户端实现类

  • 简单的RPC客户端实现类的主要工作如下:

(1)组装REST接口URL。

(2)通过HttpClient组件调用REST接口并获得响应结果。

(3)解析REST接口的响应结果,封装成JSON对象,并且返回给调用者。

3.1.3 从基础原理讲起:代理模式与RPC客户端实现类

  • 代理模式的定义:

为委托对象提供一种代理,以控制对委 托对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个目标对象,而代理对象可 以作为目标对象的委托,在客户端和目标对象之间起到中介的作用。

  • 代理模式的角色划分

  • 代理模式分类

(1)静态代理:在代码编写阶段由工程师提供代理类的源码,再编译成代理类。所谓静态,就是在程序运行前就已经存在代理类的字节码文件,代理类和被委托类的关系在运行前就确定了。

(2)动态代理:在代码编写阶段不用关心具体的代理实现类,而是在运行阶段直接获取具体的代理对象,代理实现类由JDK负责生成。

  • 静态代理模式组成

(1)抽象接口类(Abstract Subject):该类的主要职责是声明目标类与代理类的共同接口方 法。该类既可以是一个抽象类,又可以是一个接口。

(2)真实目标类(Real Subject):该类也称为被委托类或被代理类,该类定义了代理所表示 的真实对象,由其执行具体业务逻辑方法,而客户端通过代理类间接地调用真实目标类中定义的方 法。

(3)代理类(Proxy Subject):该类也称为委托类或代理类,该类持有一个对真实目标类的 引用,在其抽象接口方法的实现中需要调用真实目标类中相应的接口实现方法,以此起到代理的作用。

//省略import
@AllArgsConstructor
@Slf4j
class DemoClientStaticProxy implements DemoClient {/*** 被代理的真正实例*/private MockDemoClient realClient;@Overridepublic RestOut<JSONObject> hello() {log.info("hello方法被调用");return realClient.hello();}@Overridepublic RestOut<JSONObject> echo(String word) {log.info("echo方法被调用");return realClient.echo(word);}
}

静态代理的RPC实现类看上去是一堆冗余代码,发挥不了什么作用。为什么在这里一定要先介绍 静态代理模式的RPC实现类呢?原因有以下两点:

(1)上面的RPC实现类是出于演示目的而做了简化,对委托类并没有做任何扩展。而实际的远程调用代理类会对委托类进行很多扩展,比如远程调用时的负载均衡、熔断、重试等。

(2)上面的RPC实现类是动态代理实现类的学习铺垫。Feign的RPC客户端实现类是一个JDK动态 代理类,是在运行过程中动态生成的。大家知道,动态代理的知识对于很多读者来说不是太好理 解,所以先介绍一下代理模式和静态代理的基础知识,作为下一步的学习铺垫。

3.1.4 使用动态代理模式实现RPC客户端类

  • 静态代理的缺点

(1)手工编写代理实现类会占用时间,如果需要实现代理的类很多,那么代理类一个一个地手工编 码根本写不过来。

(2)如果更改了抽象接口,那么还得去维护这些代理类,维护上容易出纰漏。

  • 获取动态代理实例大致需要如下3步

(1)需要明确代理类和被委托类共同的抽象接口,JDK生成的动态代理类会实现该接口。

(2)构造一个调用处理器对象,该调用处理器要实现InvocationHandler接口,实现其唯一的抽象方 法invoke(...)。而InvocationHandler接口由JDK定义,位于java.lang.reflect包中。

(3)通过java.lang.reflect.Proxy类的newProxyInstance(...)方法在运行阶段获取JDK生成的动 态代理类的实例。注意,这一步获取的是对象而不是类。该方法需要三个参数,其中的第一个参数为类装 载器,第二个参数为抽象接口的class对象,第三个参数为调用处理器对象。

  • 动态代理实现示例

//省略import/*** 动态代理的调用处理器*/
@Slf4j
public class DemoClientInocationHandler implements InvocationHandler {/*** 被代理的被委托类实例*/private MockDemoClient realClient;public DemoClientInocationHandler(MockDemoClient realClient) {this.realClient = realClient;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String name = method.getName();log.info("{} 方法被调用", method.getName());/** *直接调用被委托类的方法:调用其hello方法 */if ("hello".equals(name)) {return realClient.hello();}/** *通过Java反射调用被委托类的方法:调用其echo方法 */if ("echo".equals(name)) {return method.invoke(realClient, args);}/** *通过Java反射调用被委托类的方法 */Object result = method.invoke(realClient, args);return result;}
}

调用处理器DemoClientInocationHandler既实现了InvocationHandler接口,又拥有一个内部被委托 类成员,负责完成实际的RPC请求。调用处理器有点儿像静态代理模式中的代理角色,但是在这里却不 是,仅仅是JDK所生成的代理类的内部成员。

//省略import
@Slf4j
public class StaticProxyTester {/*** 动态代理测试*/@Testpublic void dynamicProxyTest() {DemoClient client = new DemoClientImpl();//参数1:类装载器ClassLoader classLoader = StaticProxyTester.class.getClassLoader(); //参数2:被代理的实例类型Class[] clazz = new Class[]{DemoClient.class};//参数3:调用处理器InvocationHandler invocationHandler = new DemoClientInocationHandler(client); //获取动态代理实例DemoClient proxy = (DemoClient) Proxy.newProxyInstance(classLoader, clazz, invocationHandler);//执行RPC远程调用方法Result<JSONObject> result1 = proxy.hello();log.info("result1={}", result1.toString());Result<JSONObject> result2 = proxy.echo("回显内容");log.info("result2={}", result2.toString());}
}

3.1.5 JDK动态代理机制的原理

动态代理的实质是通过java.lang.reflect.Proxy的newProxyInstance(...)方法生 成一个动态代理类的实例。

    /*** 生成动态代理实例* @param loader 类加载器: 和被委托类的类加载器相同即可* @param interfaces 动态代理类需要实现的接口: 被委托类所实现的接口* @param handler 调用处理器: 将作为JDK生成的动态代理对象的内部成员,在对动态代理对象进行方法调用时,该处理器的invoke(...)方法会被执行* @return* @throws IllegalArgumentException*/public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler) throws IllegalArgumentException {}

【可能是我智商有限,没看懂,后面再找专门的书看吧.......

3.2 模拟Feign RPC动态代理的实现

3.2.1 模拟Feign的方法处理器MethodHandler

在模拟方法处理器实现类MockRpcMethodHandler的 invoke(Object[])完成了以下3个工作:
(1)组装URL,将来自RPC的请求上下文路径(一般来自RPC客户 端类级别注解)和远程调用的方法级别的URI路径拼接在一起,组成完 整的URL路径。
(2)通过HttpClient组件(也可以是其他组件)发起HTTP请求, 调用服务端的REST接口。
(3)解析REST接口的响应结果,解析成POJO对象(这里是JSON对 象)并且返回。

3.2.2 模拟Feign的调用处理器InvocationHandler

3.2.3 模拟Feign的动态代理RPC的执行流程

3.2.4 模拟动态代理RPC远程调用的测试

3.2.5 Feign弹性RPC客户端实现类

(1)失败回退:当RPC远程调用失败时将执行回退代码,尝试通过 其他方式来规避处理,而不是产生一个异常。
(2)熔断器熔断:当RPC远程服务被调用时,熔断器将监视这个调 用。如果调用的时间太长,那么熔断器将介入并中断调用。如果RPC调 用失败的次数达到某个阈值,那么将会采取快速失败策略终止持续的调 用失败。
(3)舱壁隔离:如果所有RPC调用都使用同一个线程池,那么很有 可能一个缓慢的远程服务将拖垮整个应用程序。弹性客户端应该能够隔 离每个远程资源,并分配各自的舱壁线程池,使之相互隔离,互不影 响。
(4)客户端负载均衡:RPC客户端可以在服务提供者的多个实例之 间实现多种方式的负载均衡,比如轮询、随机、权重等。
       弹性RPC客户端除了是对RPC调用的本地保护之外,也是对远程服务 的一种保护。当远程服务发生错误或者表现不佳时,弹性RPC客户端能 “快速失败”,不消耗诸如数据库连接、线程池之类的资源,能保护远程服务(微服务Provider实例或者数据库服务等)免于崩溃。
       总之,弹性RPC客户端可以避免某个Provider实例的单点问题或者 单点故障,在整个微服务节点之间传播,从而避免“雪崩”效应的发生。

3.3 Feign弹性RPC客户端的重要组件

在微服务启动时,Feign会进行包扫描,对加@FeignClient注解的 RPC接口创建远程接口的本地JDK动态代理实例。之后这些本地Proxy动 态代理实例会注入Spring IOC容器中。当远程接口的方法被调用时, 由Proxy动态代理实例负责完成真正的远程访问并返回结果。

3.3.1 演示用例说明

3.3.2 Feign的动态代理RPC客户端实例

3.3.3 Feign的调用处理器InvocationHandler

3.3.4 Feign的方法处理器MethodHandler

3.3.5 Feign的客户端组件

不同的feign.Client客户端实现类其内部提交HTTP请求的技术是不同的。常用的Feign客户端实现类如下:
(1)Client.Default类:默认的实现类,使用JDK的 HttpURLConnnection类提交HTTP请求。
(2)ApacheHttpClient类:该客户端类在内部使用Apache HttpClient开源组件提交HTTP请求。
(3)OkHttpClient类:该客户端类在内部使用OkHttp3开源组件提交HTTP请求。
(4)LoadBalancerFeignClient类:内部使用Ribbon负载均衡技术完成HTTP请求处理。

3.4 Feign的RPC动态代理实例的创建流程

3.4.1 Feign的整体运作流程

(1)通过应用启动类上的@EnableFeignClients注解开启Feign的装配和远程代理实例创建。
(2)通过对@FeignClient注解RPC接口扫描创建远程调用的动态代理实例。
(3)发生RPC调用时,通过动态代理实例类完成远程Provider的HTTP调用。
(4)在完成远程HTTP调用前需要进行客户端负载均衡的处理。

3.4.2 RPC动态代理容器实例的FactoryBean工厂类

3.4.3 Feign.Builder建造者容器实例

当从Spring容器获取RPC接口的动态代理实例时,对应 的FeignClientFactoryBean的getObject()方法会被调用到,然后通过 Feign.Builder建造者容器实例的target()方法创建RPC接口的动态代理 实例,并缓存到Spring IOC容器中。

3.4.4 默认的RPC动态代理实例的创建流程

(1)方法解析。
(2)创建方法反射实例和方法处理器的映射。
(3)创建一个JDK调用处理器。
(4)创建一个动态代理对象。

3.4.5 Contract远程调用协议规则类

3.5 Feign远程调用的执行流程

3.5.1 与FeignInvocationHandler相关的远程调用执行流程

3.5.2 与HystrixInvocationHandler相关的远程调用执行流程

3.5.3 Feign远程调用的完整流程及其特性

Spring Cloud Feign具有如下特性: 
(1)可插拔的注解支持,包括Feign注解和Spring MVC注解。 
(2)支持可插拔的HTTP编码器和解码器。 
(3)支持Hystrix和它的RPC保护机制。 
(4)支持Ribbon的负载均衡。 
(5)支持HTTP请求和响应的压缩。

3.6 HystrixFeign动态代理实例的创建流程

3.6.1 HystrixFeign.Builder建造者容器实例

3.6.2 配置HystrixFeign.Builder建造者容器实例

3.7 feign.Client客户端容器实例

3.7.1 装配LoadBalancerFeignClient负载均衡容器实例

3.7.2 装配ApacheHttpClient负载均衡容器实例

(1)必须满足ApacheHttpClient.class在当前的类路径中存在。

(2)必须满足工程配置文件中feign.httpclient.enabled配置项的值为 true。

3.7.3 装配OkHttpClient负载均衡容器实例

(1)必须满足OkHttpClient.class在当前类路径中存在。

(2)必须满足工程配置文件中feign.okhttp.enabled配置项的值为true。

3.7.4 装配Client.Default负载均衡容器实例

第4章 RxJava响应式编程框架

4.1 从基础原理讲起:观察者模式

4.1.1 观察者模式的基础原理

(1)Subject(抽象主题):Subject抽象主题的主要职责之一为维护Observer观察者对象的集合,集合里的所有观察者都订阅过该主题。Subject抽象主题负责提供一些接口,可以增加、删除和更新观察者对象。
(2)ConcreteSubject(具体主题):ConcreteSubject用于保持 主题的状态,并且在主题的状态发生变化时给所有注册过的观察者发出通知。具体来说,ConcreteSubject需要调用Subject(抽象主题)基类的通知方法给所有注册过的观察者发出通知。
(3)Observer(抽象观察者):观察者的抽象类定义更新接口,使得被观察者可以在收到主题通知的时候更新自己的状态。
(4)ConcreteObserver(具体观察者):实现抽象观察者Observer所定义的更新接口,以便在收到主题的通知时完成自己状态的真正更新。

4.1.2 观察者模式的经典实现

4.1.3 RxJava中的观察者模式

@Slf4j
public class RxJavaObserverDemo {/*** 演示RxJava中的Observer模式*/@Testpublic void rxJavaBaseUse() {//被观察者(主题)Observable observable = Observable.create(new Action1<Emitter<String>>() {@Overridepublic void call(Emitter<String> emitter) {emitter.onNext("apple");emitter.onNext("banana");emitter.onNext("pear");emitter.onCompleted();}}, Emitter.BackpressureMode.NONE);//订阅者(观察者)Subscriber<String> subscriber = new Subscriber<String>() {@Overridepublic void onNext(String s) {log.info("onNext: {}", s);}@Overridepublic void onCompleted() {log.info("onCompleted");}@Overridepublic void onError(Throwable e) {log.info("onError");}}; //订阅:Observable与Subscriber之间依然通过subscribe()进行关联 observable.subscribe(subscriber);}
}

4.1.4 RxJava的不完整回调

4.1.5 RxJava的函数式编程

public class RxJavaObserverDemo {/*** 演示RxJava中的Lamda表达式实现*/@Testpublic void rxJavaActionLamda() {Observable<String> observable = Observable.just("apple", "banana", "pear");log.info("第1次订阅:");//使用Action1 函数式接口来实现onNext回调 observable.subscribe(s -> log.info(s));log.info("第2次订阅:");//使用Action1 函数式接口来实现onNext回调 //使用Action1 函数式接口来实现onError回调 observable.subscribe(s -> log.info(s), e -> log.info("Error Info is:" + e.getMessage()));log.info("第3次订阅:");//使用Action1 函数式接口来实现onNext回调 //使用Action1 函数式接口来实现onError回调 //使用Action0 函数式接口来实现onCompleted回调 observable.subscribe(s -> log.info(s), e -> log.info("Error Info is:" + e.getMessage()), () -> log.info("onCompleted弹射结束"));}
}

4.1.6 RxJava的操作符

RxJava的操作符实质上是为了方便数据流的操作,是RxJava为Observable主题所定义的一系列函数。
RxJava的操作符按照其作用具体可以分为以下几类:
(1)创建型操作符:创建一个可观察对象Observable主题对象,并根据输入参数弹射数据。
(2)过滤型操作符:从Observable弹射的消息流中过滤出满足条件的消息。
(3)转换型操作符:对Observable弹射的消息执行转换操作。
(4)聚合型操作符:对Observable弹射的消息流进行聚合操作,比如统计数量等。

4.2 创建型操作符

4.2.1 just操作符

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import rx.Observable;@Slf4j
public class CreaterOperatorDemo {/*** 演示just的基本使用*/@Testpublic void justDemo() {//发送一个字符串"hello world" Observable.just("hello world").subscribe(s -> log.info("just string->" + s)); //逐一发送1,2,3,4四个整数Observable.just(1, 2, 3, 4).subscribe(i -> log.info("just int->" + i));}
}//        20:53:17.653[main]INFO c.c.d.r.b.CreaterOperatorDemo-just string->hello world
//        20:53:17.658[main]INFO c.c.d.r.b.CreaterOperatorDemo-just int->1
//        20:53:17.659[main]INFO c.c.d.r.b.CreaterOperatorDemo-just int->2
//        20:53:17.659[main]INFO c.c.d.r.b.CreaterOperatorDemo-just int->3
//        20:53:17.659[main]INFO c.c.d.r.b.CreaterOperatorDemo-just int->4

4.2.2 from操作符

@Slf4j
public class CreaterOperatorDemo {/***演示 from的基本使用 */@Testpublic void fromDemo() {//逐一发送一个数组中的每一个元素String[] items = {"a", "b", "c", "d", "e", "f"};Observable.from(items).subscribe(s -> log.info("just string->" + s));//逐一发送迭代器中的每一个元素Integer[] array = {1, 2, 3, 4};List<Integer> list = Arrays.asList(array);Observable.from(list).subscribe(i -> log.info("just int->" + i));}
}

4.2.3 range操作符

@Slf4j
public class CreaterOperatorDemo {/*** 演示 range的基本使用*/@Testpublic void rangeDemo() {//逐一发一组范围内的整数序列Observable.range(1, 10).subscribe(i -> log.info("just int->" + i));}
}

4.2.4 interval操作符

interval操作符创建一个Observable主题对象(消息流),该消 息流会按照固定时间间隔发射整数序列

@Slf4j
public class OtherOperatorDemo {/*** 演示interval转换*/@Testpublic void intervalDemo() throws InterruptedException {Observable.interval(100, TimeUnit.MILLISECONDS).subscribe(aLong -> log.info(aLong.toString()));Thread.sleep(Integer.MAX_VALUE);}
}

4.2.5 defer操作符

defer操作符在创建主题时并不弹 射数据,它会一直等待,直到有观察者订阅才会弹射数据。

@Slf4j
public class SimpleDeferDemo {/*** 演示defer延迟创建操作符*/@Testpublic void deferDemo() {AtomicInteger foo = new AtomicInteger(100);Observable observable = Observable.just(foo.get());/***有观察者订阅*/observable.subscribe(integer -> log.info("just emit {}", String.valueOf(integer)));/***延迟创建*/Observable dObservable = Observable.defer(() -> Observable.just(foo.get()));/***修改对象的值*/foo.set(200);/***有观察者订阅*/dObservable.subscribe(integer -> log.info("defer just emit {}", String.valueOf(integer)));}
}

实质上,通过defer创建的主题,在观察者订阅时会创建一个新的 Observable主题。因此,尽管每个订阅者都以为自己订阅的是同一个 Observable,事实上每个订阅者获取的是独立的消息序列。

4.3 过滤型操作符

4.3.1 filter操作符

@Slf4j
public class FilterOperatorDemo {/*** 演示filter的基本使用*/@Testpublic void filterDemo() {//通过filter筛选能被5整除的数Observable.range(1, 20).filter(new Func1<Integer, Boolean>() {@Overridepublic Boolean call(Integer integer) {return integer % 5 == 0;}}).subscribe(i -> log.info("filter int->" + i));}@Testpublic void filterLambdaDemo() {//通过filter筛选出能被5整除的数 Observable.range(1,20).filter(integer->integer%5==0).subscribe(i->log.info("filter int->"+i));}}

4.3.2 distinct操作符

@Slf4j
public class FilterOperatorDemo {/*** 演示distinct基本使用*/@Testpublic void distinctDemo() {//使用distinct过滤重复元素Observable.just("apple", "pair", "banana", "apple", "pair").distinct().subscribe(s -> log.info("distinct s->" + s));}
}

4.4 转换型操作符

4.4.1 map操作符

@Slf4j
public class TransformationDemo {/*** 演示map转换*/@Testpublic void mapDemo() {Observable.range(1, 4).map(i -> i * i).subscribe(i -> log.info(i.toString()));}
}

4.4.2 flatMap操作符

@Slf4j
public class TransformationDemo {/*** 演示flapMap转换*/@Testpublic void flapMapDemo() {/***注意 flatMap 中的just创建的是一个新流*/Observable.range(1, 4).flatMap(i -> Observable.just(i * i, i * i + 1)).subscribe(i -> log.info(i.toString()));}
}

@Slf4j
public class TransformationDemo {/*** 演示一个稍微复杂的flapMap转换*/@Testpublicvoid flapMapDemo2() {Observable.range(1, 4).flatMap(i -> Observable.range(1, i).toList()).subscribe(list -> log.info(list.toString()));}
}

4.4.3 scan操作符

scan操作符对一个Observable流序列的每一项数据应用一个累积函数,然 后将这个函数的累积结果弹射出去。除了第一项之外,scan操作符会将上一个 数据项的累积结果作为下一个数据项在应用累积函数时的输入,所以scan操作 符有点类似递归操作。

@Slf4j
public class TransformationDemo {/*** 演示scan操作符扫描*/@Testpublic void scanDemo() {/** 定义一个accumulator累积函数*/Func2<Integer, Integer, Integer> accumulator = new Func2<Integer, Integer, Integer>() {@Overridepublic Integer call(Integer input1, Integer input2) {log.info(" {} + {} = {} ", input1, input2, input1 + input2);return input1 + input2;}};/** 使用scan进行流扫描*/Observable.range(1, 5).scan(accumulator).subscribe(new Action1<Integer>() {@Overridepublic void call(Integer sum) {log.info(" 累加的结果: {} ", sum);}});}
}

4.5 聚合操作符

4.5.1 count操作符

@Slf4j
public class AggregateDemo {/*** 演示count计数操作符*/@Testpublic void countDemo() {String[] items = {"one", "two", "three", "four"};Integer count = Observable.from(items).count().toBlocking().single();log.info("计数的结果为 {}", count);}
}//    [main] INFO c.c.d.r.basic.AggregateDemo - 计数的结果为 4

Observable.toBlocking()操作返回了一个BlockingObservable阻塞型实例,该类型不是一种新的数据流,仅仅是对源Observable的包装,只 是该类型会阻塞当前线程,一直等待直到内部的源Observable弹射了自 己想要的数据。BlockingObservable.single()方法表示阻塞当前线 程,直到从封装的源Observable获取到唯一的弹射数据元素项,如果 Observable源流弹射出的数据元素不止一个,single()方法就会抛出异常。

4.5.2 reduce操作符

@Slf4j
public class AggregateDemo {/*** 演示reduce操作符*/@Testpublic void reduceDemo() {/** 定义一个accumulator归约函数*/Func2<Integer, Integer, Integer> accumulator = new Func2<Integer, Integer, Integer>() {@Overridepublic Integer call(Integer input1, Integer input2) {log.info(" {} + {} = {} ", input1, input2, input1 + input2);return input1 + input2;}};/** 使用reduce进行流归约 */Observable.range(1, 5).reduce(accumulator).subscribe(new Action1<Integer>() {@Overridepublic void call(Integer sum) {log.info(" 归约的结果: {} ", sum);}});}
}

reduce操作符与前面介绍的scan操作符很类似,只是scan会弹出每 次计算的中间结果,而reduce只会弹出最后的结果。

4.6 其他操作符

4.6.1 take操作符

take操作符用于根据索引在源流上进行元素的挑选操作,挑选源流 上的n个元素。如果源流序列中的项少于指定索引,就抛出错误。

@Slf4j
public class OtherOperatorDemo {/*** 演示take操作符 *这是一个10秒倒计时实例*/@Testpublic void takeDemo() throws InterruptedException {Observable.interval(1, TimeUnit.SECONDS) //设置执行间隔.take(10) //10秒倒计时.map(aLong -> 10 - aLong).subscribe(aLong -> log.info(aLong.toString()));Thread.sleep(Integer.MAX_VALUE);}
}

skip操作符与take操作符类似,也是用于根据索引在源流上进行元 素的挑选操作,只是take是取前n个元素,而skip是跳过前n个元素。⚠️注意,如果序列中的项少于指定索引,那么两个函数都抛出错误。

4.6.2 window操作符

RxJava的窗口可以理解为固定数量(或者固定时间间隔)的元素分 组。假定通过window操作符以固定数量n进行窗口划分,一旦流上弹射 的元素的数量足够一个窗口的数量n,那么输出流上将弹出一个新的元 素,输出元素是一个Observable主题对象,该主题包含源流窗口之内的 n个元素。


@Slf4j
public class WindowDemo {/*** 演示window创建操作符创建滚动窗口*/@Testpublic void simpleWindowObserverDemo() {List<Integer> srcList = Arrays.asList(10, 11, 20, 21, 30, 31);Observable.from(srcList).window(3) //以固定数量分组.flatMap(o -> o.toList()).subscribe(list -> log.info(list.toString()));}
}

在使用window进行分组时,不同窗口的元素还可以重叠,可以理解成滑动窗口。创建重叠窗口使用函数window(int count,int skip),其中第 一个参数为窗口的元素个数,第二个参数为下一个窗口跳过的元素个 数。

@Slf4j
public class WindowDemo {/*** 演示window创建操作符创建滑动窗口*/@Testpublic void windowObserverDemo() {List<Integer> srcList = Arrays.asList(10, 11, 20, 21, 30, 31);Observable.from(srcList).window(3, 1).flatMap(o -> o.toList()).subscribe(list -> log.info(list.toString()));}
}

@Slf4j
public class WindowDemo {/*** 演示window创建操作符创建时间窗口*/@Testpublic void timeWindowObserverDemo() throws InterruptedException {Observable eventStream = Observable.interval(100, TimeUnit.MILLISECONDS);eventStream.window(300, TimeUnit.MILLISECONDS).flatMap(o -> ((Observable<Integer>) o).toList()).subscribe(list -> log.info(list.toString()));Thread.sleep(Integer.MAX_VALUE);}
}

在此示例中,window操作符以300ms(毫秒)的固定间隔划分出非 重叠窗口,每个窗口保持300毫秒的时间,从而确保输入流eventStream 接收到3个值,直到停止。

4.7 RxJava的Scheduler调度器

@Slf4j
public class SchedulerDemo {/*** 演示Schedulers的基本使用*/@Testpublic void testScheduler() throws InterruptedException {//被观察者Observable observable = Observable.create(new Observable.OnSubscribe<String>() {@Overridepublic void call(Subscriber<? super String> subscriber) {for (int i = 0; i < 5; i++) {log.info("produce ->" + i);subscriber.onNext(String.valueOf(i));}subscriber.onCompleted();}});//订阅Observable与Subscriber之间依然通过subscribe()进行关联observable//使用具有线程缓存机制的可复用线程.subscribeOn(Schedulers.io())//每执行一个任务创建一个新的线程.observeOn(Schedulers.newThread()).subscribe(s -> log.info("consumer ->" + s));Thread.sleep(Integer.MAX_VALUE);}
}

通过上面的代码可以看出,RxJava提供了两个方法来改变流操作的调度器:
(1)subscribeOn():主要改变的是弹射的线程。
(2)observeOn():主要改变的是订阅的线程。
在RxJava中,创建操作符创建的Observable主题的弹射任务,将由其后最近的subscribeOn()所设置的调度器负责执行。
在RxJava中,Observable主题的下游消费型操作(如流转换等) 的线程调度,将由其前面最近的observeOn()所设置的调度器负责。observeOn()可以多次设置,每一次设置都对下一次observeOn()设置 之前的流操作产生作用。

4.8 背压

4.8.1 什么是背压问题

当上下游的流操作处于不同的线程时,如果上游弹射数据的速度快 于下游接收处理数据的速度,对于那些没来得及处理的数据就会造成积 压,这些数据既不会丢失,又不会被垃圾回收机制回收,而是存放在一 个异步缓存池中,如果缓存池中的数据一直得不到处理,越积越多,最 后就会造成内存溢出,这便是响应式编程中的背压问题。

package com.crazymaker.demo.rxJava.basic;@Slf4j
public class BackpressureDemo {/*** 演示不使用背压*/@Testpublic void testNoBackpressure() throws InterruptedException {//被观察者(主题)Observable observable = Observable.create(new Observable.OnSubscribe<String>() {@Overridepublic void call(Subscriber<? super String> subscriber) { //循环10次
//                        for (int i = 0; i < 10; i++) {for (int i = 0; ; i++) {log.info("produce ->" + i);subscriber.onNext(String.valueOf(i));}}});//观察者Action1<String> subscriber = new Action1<String>() {public void call(String s) {try {//每消费一次间隔50毫秒Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}log.info("consumer ->" + s);}};//订阅:observable与subscriber之间依然通过subscribe()进行关联observable.subscribeOn(Schedulers.io()).observeOn(Schedulers.newThread()).subscribe(subscriber);Thread.sleep(Integer.MAX_VALUE);}
}

异常原因:由于上游observable流弹射数据的速度远远大于下游通 过subscriber接收的速度,导致observable用于暂存弹射数据的队列空 间耗尽,造成上游数据积压。

4.8.2 背压问题的几种应对模式

(1)BackpressureMode.DROP:在这种模式下,Observable主题使用固定大小为128的 缓冲区。如果下游订阅者无法处理,流的第一个元素就会缓存下来,后续的会被丢弃。

(2)BackpressureMode.LATEST:这种模式与BackpressureMode.DROP类似,并且 Observable主题也使用固定大小为128的缓冲区。BackpressureMode.LATEST的缓存策略不同,使用最新的弹出元素替换缓冲区缓存的元素。当消费者可以处理下一个元素时,它收到 的是Observable最近一次弹出的元素。

(3)BackpressureMode.NONEBackpressureMode.ERROR:在这两种模式中发送的数据 不使用背压。如果上游observable主题弹射数据的速度大于下游通过subscriber接收的速 度,造成上游数据积压,就会抛出MissingBackpressureException异常。

(4)BackpressureMode.BUFFER:在这种模式下,有一个无限的缓冲区(初始化时是 128),下游消费不了的元素全部会放到缓冲区中。如果缓冲区中持续地积累,就会导致内 存耗尽,抛出OutOfMemoryException异常。

第5章 Hystrix RPC保护的原理

5.1 RPC保护的目标

(1)避免整个系统出现级联失败而雪崩

(2)RPC调用能够相互隔离

(3)能够快速地降级和恢复

(4)能够对RPC调用提供接近实时的监控和警报

5.2 HystrixCommand简介

Hystrix使用命令模式并结合RxJava的响应式编程和滑动窗口技术 实现了对外部服务RPC调用的保护。

Hystrix实现了HystrixCommand和HystrixObservableCommand两个 命令类,用于封装需要保护的RPC调用。HystrixObservableCommand命令不具备同步执行的能力,只具备异步执行能力,而HystrixCommand命令却都具备,且Spring Cloud中重 点使用HystrixCommand命令。

5.2.1 HystrixCommand的使用

5.2.2 HystrixCommand的配置内容和方式

  • 使用HystrixCommand.Setter 配置实例进行配置

    • CommandKey:该命令的名称
    • GroupKey:该命令属于哪一个组,以帮助我们更好地组织命令

    • ThreadPoolKey:该命令所属线程池的名称,相同的线程池名 称会共享同一线程池,若不进行配置,则默认使用GroupKey作为线程池 名称

    • CommandProperties:与命令执行相关的一些属性集,包括降 级设置、熔断器的配置、隔离策略以及一些监控指标配置项等

    • ThreadPoolProperties:与线程池相关的一些属性集,包括线 程池大小、排队队列的大小等

@Slf4j
public class SetterDemo {public static HystrixCommand.Setter buildSetter(String groupKey, String commandKey, String threadPoolKey) {/***与命令执行相关的一些属性集*/HystrixCommandProperties.Setter commandSetter = HystrixCommandProperties.Setter()//至少有3个请求,熔断器才达到熔断触发的次数阈值.withCircuitBreakerRequestVolumeThreshold(3)//熔断器中断请求5秒后会进入half-open状态,尝试放行.withCircuitBreakerSleepWindowInMilliseconds(5000)//错误率超过60%,快速失败.withCircuitBreakerErrorThresholdPercentage(60)//启用超时.withExecutionTimeoutEnabled(true)//执行的超时时间,默认为1000ms.withExecutionTimeoutInMilliseconds(5000)//可统计的滑动窗口内的buckets数量,用于熔断器和指标发布.withMetricsRollingStatisticalWindowBuckets(10)//可统计的滑动窗口的时间长度//这段时间内的执行数据用于熔断器和指标发布.withMetricsRollingStatisticalWindowInMilliseconds(10000);/*** 线程池配置*/HystrixThreadPoolProperties.Setter poolSetter = HystrixThreadPoolProperties.Setter()//这里我们设置了线程池大小为5.withCoreSize(5).withMaximumSize(5);/*** 与线程池相关的一些属性集*/HystrixCommandGroupKey hGroupKey = HystrixCommandGroupKey.Factory.asKey(groupKey);HystrixCommandKey hCommondKey = HystrixCommandKey.Factory.asKey(commandKey);HystrixThreadPoolKey hThreadPoolKey = HystrixThreadPoolKey.Factory.asKey(threadPoolKey);HystrixCommand.Setter outerSetter = HystrixCommand.Setter.withGroupKey(hGroupKey).andCommandKey(hCommondKey).andThreadPoolKey(hThreadPoolKey).andCommandPropertiesDefaults(commandSetter).andThreadPoolPropertiesDefaults(poolSetter);return outerSetter;}
}
  • 使用ConfigurationManager配置管理类的工厂实例进行配置
//熔断器的请求次数阈值:大于3次请求
ConfigurationManager.getConfigInstance().setProperty("hystrix.command.default.circuitBreaker. requestVolumeThreshold",3);

5.3 HystrixCommand命令的执行方法

独立使用HystrixCommand命令主要有以下两个步骤:

(1)继承HystrixCommand类,将正常的业务逻辑实现在继承的 run方法中,将回退的业务逻辑实现在继承的getFallback方法中。

(2)使用HystrixCommand类提供的执行启动方法启动命令的执 行。

HystrixCommand提供了4个执行启动的方法:execute()、 queue()、observe()和toObservable()。

5.3.1 execute()方法

HystrixCommand的execute()方法以同步堵塞方式执行run()。一旦开始执行该命令,当前线程 就会阻塞,直到该命令返回结果,然后才能继续执行下面的逻辑。

5.3.2 queue()方法

HystrixCommand的queue()方法以异步非阻塞方式执行run()方法,该方法直接返回一个Future 对象。可通过Future.get()拿到run()的返回结果,但Future.get()是阻塞执行的。

@Slf4j
public class HystryxCommandExcecuteDemo {@Testpublic void testQueue() throws Exception {/*** 使用统一配置*/HystrixCommand.Setter setter = getSetter("group-1","testCommand","testThreadPool");List<Future<String>> flist = new LinkedList<>();/*** 同时发起5个异步的请求*/for (int i = 0; i < COUNT; i++) {Future<String> future = new HttpGetterCommand(TEST_URL, setter).queue();flist.add(future);}/*** 统一获取异步请求的结果*/Iterator<Future<String>> it = flist.iterator();int count = 1;while (it.hasNext()) {Future<String> future = it.next();String result = future.get(10, TimeUnit.SECONDS);log.info("第{}次请求的结果:{}", count++, result);}Thread.sleep(Integer.MAX_VALUE);}
}

5.3.3 observe()方法

HystrixCommand的observe()方法会返回一个响应式编程Observable主题,可以为该主题对象注册上 Subscriber观察者回调实例,或者注册上Action1不完全回调实例来响应式处理命令的执行结果。

调用HystrixCommand的observe()方法会返回一个热主题(Hot Observable)。什么是热主题呢?就 是无论主题是否存在观察者订阅,都会自动触发执行它的run()方法。另外还有一点,observe()方法所返 回的主题可以重复订阅。

@Slf4j
public class HystryxCommandExcecuteDemo {@Testpublic void testObserve() throws Exception {/***使用统一配置类*/HystrixCommand.Setter setter = SetterDemo.buildSetter("group-1", "testCommand", "testThreadPool");Observable<String> observe = new HttpGetterCommand(HELLO_TEST_URL, setter).observe();Thread.sleep(1000);log.info("订阅尚未开始!");//订阅3次observe.subscribe(result -> log.info("onNext result={}", result), error -> log.error("onError error={}", error));observe.subscribe(result -> log.info("onNext result ={}", result), error -> log.error("onError error={}", error));observe.subscribe(result -> log.info("onNext result={}", result), error -> log.error("onError error ={}", error), () -> log.info("onCompleted called"));Thread.sleep(Integer.MAX_VALUE);}
}

5.3.4 toObservable()方法

ystrixCommand的toObservable()方法会返回一个响应式编程 Observable主题。同样可以为该主题对象注册上Subscriber观察者回调实 例,或者注册上Action1不完全回调实例,来响应式处理命令的执行结果。不 过,与observe()返回的主题不同,Observable主题返回的是冷主题,并且只 能被订阅一次。

什么是冷主题(Cold Observable)?就是在获取主题的时候不会立即触 发执行,只有在观察者订阅时才会执行内部的HystrixCommand命令的run()方 法。

@Slf4j
public class HystryxCommandExcecuteDemo {@Testpublic void testToObservable() throws Exception {/***使用统一配置类*/HystrixCommand.Setter setter = SetterDemo.buildSetter("group-1", "testCommand", "testThreadPool");for (int i = 0; i < COUNT; i++) {Thread.sleep(2);new HttpGetterCommand(HELLO_TEST_URL, setter).toObservable().subscribe(result -> log.info("result={}", result), error -> log.error("error={}", error));Thread.sleep(Integer.MAX_VALUE);}}
}

5.3.5 HystrixCommand的执行方法之间的关系

(1)toObservable()返回一个冷主题,订阅者可以订阅结果。

(2)observe()首先调用toObservable()获得一个冷主题,再创建 一个ReplaySubject重复主题去订阅该冷主题,然后将重复主题转化为 热主题。因此,调用observe()会自动触发执行run()/construct()方 法。

(3)queue()调用了toObservable().toBlocking().toFuture()。 详细来说,queue()首先通过toObservable()来获得一个冷主题,然后 通过toBlocking()将该冷主题转换成BlockingObservable阻塞主题,该 主题可以把数据以阻塞的方式发出来,最后通过toFuture方法把 BlockingObservable阻塞主题转换成一个Future异步回调实例,并且返 回该Future实例。但是,queue()自身并不会阻塞,消费者可以自己决 定如何处理Future的异步回调操作。

(4)execute()调用了queue().get(),阻塞消费者的线程,同步 获取Future异步回调实例的结果。

5.4 RPC保护之舱壁模式

5.4.1 什么是舱壁模式

 Hystrix提供了两种RPC隔离方式:线程池隔离和信号量隔离。但是信号量隔离不太适合使用在RPC调用的场景。

5.4.2 Hystrix线程池隔离

默认情况下,在Spring Cloud中,Hystrix会为每一个Command Group Key自动创建 一个同名的线程池。而在Hystrix客户端,每一个RPC目标Provider的Command Group Key 默认值为它的应用名称(Application Name)。

5.4.3 Hystrix线程池隔离配置

hystrix:threadpool:default: #默认配置coreSize: 10 #线程池核心线程数maximumSize: 20 #线程池最大线程数allowMaximumSizeToDivergeFromCoreSize: true #线程池maximumSize最大线程数是否生效keepAliveTimeMinutes:10 #设置可空闲时间,单位为分钟uaa-provider: # 单个服务个性化配置coreSize: 20 #线程池核心线程数maximumSize: 100 #线程池最大线程数allowMaximumSizeToDivergeFromCoreSize: true #线程池最大线程数是否有效command:default: #全局默认配置execution: #RPC隔离的相关配置isolation:strategy: THREAD  #配置请求隔离的方式,这里为线程池方式thread:timeoutInMilliseconds: 100000 #RPC执行的超时时间,默认为1000毫秒interruptOnTimeout: true #发生超时后是否中断方法的执行,默认值为true

(1)hystrix.threadpool.default.coreSize:设置线程池的核心 线程数。

(2)hystrix.threadpool.default.maximumSize:设置线程池的 最大线程数,起作用的前提是 allowMaximumSizeToDivergeFromCoreSize的属性值为true。 maximumSize属性值可以等于或者大于coreSize值,当线程池的线程不 够用时,Hystrix会创建新的线程,直到线程数达到maximumSize的值, 创建的线程为非核心线程。

(3) hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSi ze:该属性允许maximumSize起作用。

(4)hystrix.threadpool.default.keepAliveTimeMinutes:该属 性设置非核心线程的存活时间,如果某个非核心线程的空闲超过 keepAliveTimeMinutes设置的时间,非核心线程就会被释放。其单位为 分钟,默认值为1,默认情况下,表示非核心线程空闲1分钟后释放。

(5) hystrix.command.default.execution.isolation.strategy:该属性设 置RPC远程调用HystrixCommand命令的隔离策略。它有两个可选值: THREAD和SEMAPHORE,默认值为THREAD。THREAD表示使用线程池进行RPC 隔离,SEMAPHORE表示通过信号量来进行RPC隔离和限制并发量。

(6) hystrix.command.default.execution.isolation.thread.timeoutInMi lliseconds:设置调用者等待HystrixCommand命令执行的超时限制,超 过此时间,HystrixCommand被标记为TIMEOUT,并执行回退逻辑。超时 会作用在HystrixCommand.queue(),即使调用者没有调用get()去获得 Future对象。

5.4.4 Hystrix信号量隔离

  • 简介

信号量所起到的作用就像一个开关,而信号量的值就是每个命令的并发执行数量,当并 发数高于信号量的值时就不再执行命令。比如,如果Provider A的RPC信号量大小为10, 那么它同时只允许有10个RPC线程来访问服务Provider A,其他的请求都会被拒绝,从而 达到资源隔离和限流保护的作用。

Hystrix信号量机制不提供专用的线程池,也不提供额外的线程,在获取到信号量之 后,执行HystrixCommand命令逻辑的线程还是之前Web容器的IO线程。

信号量可以细分为run执行信号量和fallback回退信号量。

IO线程在执行HystrixCommand命令之前需要抢到run执行信号量,成功之后才允许执 行HystrixCommand.run()方法。如果争抢失败,就准备回退,但是在执行 HystrixCommand.getFallback()回退方法之前,还需要争抢fallback回退信号量,成功 之后才允许执行HystrixCommand.getFallback()回退方法。如果都获取失败,操作就会 直接终止。

  • 缺点

使用信号量进行RPC隔离是有自身弱点的。实际RPC远程调用最终是由Web容器的IO线 程来完成,这样就带来了一个问题,由于RPC远程调用是一种耗时的操作,如果IO线程被 长时间占用,就会导致Web容器请求处理能力下降,甚至会在一段时间内因为IO线程被占 满而造成Web容器无法对新的用户请求及时响应,最终导致Web容器崩溃。所以,信号量 隔离机制不适用于RPC隔离。但是,对于一些非网络的API调用或者耗时很小的API调用, 信号量隔离机制的效率比线程池隔离机制的效率更高。

  • 配置

(1)withExecutionIsolationSemaphoreMaxConcurrentRequests(int):此方法 设置执行信号量的大小,也就是HystrixCommand.run()方法允许的最大请求数。如果达 到最大请求数,后续的请求就会被拒绝。

在Web容器中,抢占信号量的线程应该是容器(比如Tomcat)IO线程池中的一小部 分,所以信号量的数量不能大于容器线程池的大小,否则就起不到保护作用。执行信号 量的大小默认值为10。

(2)withFallbackIsolationSemaphoreMaxConcurrentRequests(int):此方法设 置回退信号量的大小,也就是HystrixCommand.getFallback()方法允许的最大请求数。 如果达到最大请求数,后续的回退请求就会被拒绝。

  • 线程池隔离和信号量隔离对比

5.5 RPC保护之熔断器模式

熔断器的工作机制为:统计最近RPC调用发生错误的次数,然后根 据统计值中的失败比例等信息决定是否允许后面的RPC调用继续,或者 快速地失败回退。熔断器的3种状态如下:

(1)closed:熔断器关闭状态,这也是熔断器的初始状态,此状 态下RPC调用正常放行。

(2)open:失败比例到一定的阈值之后,熔断器进入开启状态, 此状态下RPC将会快速失败,执行失败回退逻辑。

(3)half-open:在打开一定时间之后(睡眠窗口结束),熔断器 进入半开启状态,小流量尝试进行RPC调用放行。如果尝试成功,熔断 器就变为closed状态,RPC调用正常;如果尝试失败,熔断器就变为 open状态,RPC调用快速失败。

5.5.1 熔断器状态变化的演示实例

5.5.2 熔断器和滑动窗口的配置属性

熔断器的配置包含滑动窗口的配置和熔断器自身的配置。Hystrix的健 康统计是通过滑动窗口来完成的,其熔断器的状态变化也依据滑动窗口的 统计数据。

  • 滑动窗口

可以这么来理解滑动窗口:一位乘客坐在正在行驶的列车的靠窗座位 上,列车行驶的公路两侧种着一排挺拔的白杨树,随着列车的前进,路边 的白杨树迅速从窗口滑过,我们用每棵树来代表一个请求,用列车的行驶 代表时间的流逝,列车上的这个窗口就是一个典型的滑动窗口,这个乘客 能通过窗口看到的白杨树的数量就是滑动窗口要统计的数据。

  • 时间桶

时间桶是统计滑动窗口数据时的最小单位。同样类比列车窗口,在列 车速度非常快时,如果每掠过一棵树就统计一次窗口内树的数据,显然开 销非常大,如果乘客将窗口分成N份,前进时列车每掠过窗口的N分之一就 统计一次数据,开销就大大地减小了。简单来说,时间桶就是滑动窗口的N 分之一。

  • 代码配置
/*** 命令参数配置*/
HystrixCommandProperties.Setter propertiesSetter=HystrixCommandProperties.Setter()//至少有3个请求,熔断器才达到熔断触发的次数阈值.withCircuitBreakerRequestVolumeThreshold(3)//熔断器中断请求5秒后会进入half-open状态,尝试放行.withCircuitBreakerSleepWindowInMilliseconds(5000)//错误率超过60%,快速失败.withCircuitBreakerErrorThresholdPercentage(60)//启用超时.withExecutionTimeoutEnabled(true)//执行的超时时间,默认为1000毫秒,这里设置为500毫秒.withExecutionTimeoutInMilliseconds(500)//可统计的滑动窗口内的时间桶数量,用于熔断器和指标发布.withMetricsRollingStatisticalWindowBuckets(10)//可统计的滑动窗口的时间长度//这段时间内的执行数据用于熔断器和指标发布.withMetricsRollingStatisticalWindowInMilliseconds(10000);
  • 滑动窗口-基础的健康统计配置属性说明

(1) hystrix.command.default.metrics.rollingStats.timeInMilliseconds: 设置健康统计滑动窗口的持续时间(以毫秒为单位),默认值为10 000毫 秒。熔断器的打开会根据一个滑动窗口的统计值来计算,若滑动窗口时间 内的错误率超过阈值,则熔断器将进入open状态。滑动窗口将被进一步细 分为时间桶,滑动窗口的统计值等于窗口内所有时间桶的统计信息的累 加,每个时间桶的统计信息包含请求成功(success)、失败 (failure)、超时(timeout)、被拒(rejection)的次数。

(2) hystrix.command.default.metrics.rollingStats.numBuckets:设置健康 统计滑动窗口被划分的时间桶的数量,默认值为10。若滑动窗口的持续时 间为默认的10000毫秒,在默认情况下,一个时间桶的时间即1秒。若要做 定制化的配置,则所设置的numBuckets(时间桶数量)的值和 timeInMilliseconds(滑动窗口时长)的值有关联关系,必须符合 timeInMilliseconds%numberBuckets==0的规则,否则会抛出异常。例如, 二者的关联关系为70 000(滑动窗口70秒)%700(桶数)==0是可以的,但 是70 000(滑动窗口70秒)%600(桶数)==400将抛出异常。

(3) hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds:设置健康统计滑动窗口拍摄运行状况统计指标的快照的时间间隔。 什么是拍摄运行状况统计指标的快照呢?就是计算成功和错误百分比这些 影响熔断器状态的统计数据。

拍摄快照的时间间隔的单位为毫秒,默认值为500毫秒。由于统计指标 的计算是一个消耗CPU的操作(即CPU密集型操作),也就是说,高频率地 计算错误百分比等健康统计数据会占用很多CPU资源,因此在高并发RPC流 量大的应用场景下可以适当调大拍摄快照的时间间隔。

  • 滑动窗口-百分比命令执行时间统计配置属性说明

(1) hystrix.command.default.metrics.rollingPercentile.enabled:该配置 项用于设置百分比命令执行时间统计滑动窗口是否生效,命令的执行时间是否被跟踪,并且计算各个百分比(如1%、10%、50%、90%、99.5%等)的 平均时间。该配置项默认为true。

(2) hystrix.command.default.metrics.rollingPercentile.timeInMilliseco nds:设置百分比命令执行时间统计滑动窗口的持续时间(以毫秒为单 位),默认值为60 000毫秒。当然,此滑动窗口进一步被细分为时间桶, 以便提高统计的效率。

(3) hystrix.command.default.metrics.rollingPercentile.numBuckets:设 置百分比命令执行时间统计滑动窗口被划分的时间桶的数量,默认值为6。 此滑动窗口的默认持续时间为60 000毫秒,在默认情况下,一个时间桶的 时间即10秒。若要做定制化的配置,则此窗口所设置的numBuckets(时间 桶数量)的值和timeInMilliseconds(滑动窗口时长)的值有关联关系, 必须符合timeInMilliseconds(滑动窗口时长)%numberBuckets==0的规 则,否则将抛出异常。

(4) hystrix.command.default.metrics.rollingPercentile.bucketSize:设 置百分比命令执行时间统计滑动窗口的时间桶内最大的统计次数,若 bucketSize为100,而桶的时长为1秒,这1秒里有500次执行,则只有最后100次执行的信息会被统计到桶里。增加此配置项的值会导致内存开销及其 他计算开销上升,该配置项的默认值为100。

  • 熔断器本身的配置

(1)hystrix.command.default.circuitBreaker.enabled:该配置用 来确定是否启用熔断器,默认值为true。

(2) hystrix.command.default.circuitBreaker.requestVolumeThreshold:该 配置用于设置熔断器触发熔断的最少请求次数。如果设置为20,那么当一 个滑动窗口时间内(比如10秒)收到19个请求时,即使19个请求都失败, 熔断器也不会打开变成open状态,默认值为20。

(3) hystrix.command.default.circuitBreaker.errorThresholdPercentage: 该配置用于设置错误率阈值,当健康统计滑动窗口的错误率超过此值时, 熔断器进入open状态,所有请求都会触发失败回退(fallback),错误率 阈值百分比的默认值为50。

(4) hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds :此配置项指定熔断器打开后经过多长时间允许一次请求尝试执行。熔断 器打开时,Hystrix会在经过一段时间后就放行一条请求,如果这条请求执 行成功,就说明此时服务很可能已经恢复正常,会将熔断器关闭,如果这 条请求执行失败,就认为目标服务依然不可用,熔断器继续保持打开状 态。

该配置用于设置熔断器的睡眠窗口,具体指定熔断器打开之后过多长 时间才允许一次请求尝试执行,默认值为5000毫秒,表示当熔断器开启 后,5000毫秒内会拒绝所有的请求,5000毫秒之后,熔断器才会进入half- open状态。

(5)hystrix.command.default.circuitBreaker.forceOpen:如果配 置为true,熔断器就会被强制打开,所有请求将被触发失败回退 (fallback)。此配置的默认值为false。

  • 配置示例
hystrix:command:default: #全局默认配置circuitBreaker: #熔断器相关配置enabled: true #是否启动熔断器,默认为truerequestVolumeThreshold: 20 #启用熔断器功能窗口时间内的最小请求数sleepWindowInMilliseconds: 5000 #指定熔断器打开后多长时间内允许一次请求尝试执行errorThresholdPercentage:50 #窗口时间内超过50%的请求失败后就会打开熔断器metrics:rollingStats:timeInMilliseconds: 6000numBuckets: 10UserClient#detail(Long): #独立接口配置,格式为: 类名#方法名(参数类型列表)circuitBreaker: #熔断器相关配置enabled: true #是否启动熔断器,默认为truerequestVolumeThreshold: 20 #窗口时间内的最小请求数sleepWindowInMilliseconds: 5000 #打开后允许一次尝试的睡眠时间,默认配置为5秒errorThresholdPercentage: 50 #窗口时间内超过50%的请求失败后就会打开熔断器metrics:rollingStats:timeInMilliseconds: 6000 #滑动窗口时间numBuckets: 10 #滑动窗口的时间桶数

5.5.3 Hystrix命令的执行流程

5.6 RPC监控之滑动窗口的实现原理

5.6.1 Hystrix健康统计滑动窗口的模拟实现

  • Hystrix健康统计滑动窗口的执行流程

首先,HystrixCommand命令器的执行结果(失败、成功)会以事件的 形式通过RxJava事件流弹射出去,形成命令完成事件流。

然后,桶计数流以事件流作为来源,将事件流中的事件按照固定时间 长度(桶时间间隔)划分成滚动窗口,并对时间桶滚动窗口内的事件按照 类型进行累积,完成之后将桶数据弹射出去,形成桶计数流。

最后,桶滑动统计流以桶计数流作为来源,按照步长为1、长度为设 定的桶数(配置的滑动窗口桶数)的规则划分滑动窗口,并对滑动窗口内 的所有桶数据按照各事件类型进行汇总,汇总成最终的窗口健康数据,并 将其弹射出去,形成最终的桶滑动统计流,作为Hystrix熔断器进行状态 转换的数据支撑。

5.6.2 Hystrix滑动窗口的核心实现原理

第6章 微服务网关与用户身份识别

微服务网关是微服务架 构中不可或缺的部分,它统一解决Provider路由、均衡负载、权限控制 等功能。微服务网关的实现框架有多种,Spring Cloud全家桶中比较常用的 有Zuul和Spring Cloud Gateway两大框架。

6.1 Zuul的基础使用

Zuul是Netflix公司的开源网关产品,可以和Eureka、Ribbon、 Hystrix等组件配合使用。Zuul的规则引擎和过滤器基本上可以用任何 JVM语言编写,内置支持Java和Groovy。

  • Zuul的功能

(1)路由:将不同REST请求转发至不同的微服务提供者,其作用 类似于Nginx的反向代理。同时,也起到了统一端口的作用,将很多微 服务提供者的不同端口统一到了Zuul的服务端口。

(2)认证:网关直接暴露在公网上时,终端要调用某个服务,通 常会把登录后的token(令牌)传过来,网关层对token进行有效性验 证。如果token无效(或没有token),就不允许访问REST服务。可以 结合Spring Security中的认证机制完成Zuul网关的安全认证。

(3)限流:高并发场景下瞬时流量不可预估,为了保证服务对外 的稳定性,限流成为每个应用必备的一道安全防火墙。如果没有这道 安全防火墙,那么请求的流量超过服务的负载能力时很容易造成整个 服务的瘫痪。

(4)负载均衡:在多个微服务提供者之间按照多种策略实现负载 均衡。

6.2 创建Zuul网关服务

Spring Cloud对Zuul进行了整合与增强。Zuul作为网关层,自身 也是一个微服务,跟其他服务提供者一样都注册在Eureka Server上, 可以相互发现。Zuul能感知到哪些Provider实例在线,同时通过配置 路由规则可以将REST请求自动转发到指定的后端微服务提供者。

  • 新建Zuul网关服务项目时需要在启动类中添加注解 @EnableZuulProxy,声明这是一个网关服务提供者
  • pom.xml文件中手动添加

<dependency><groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

6.2.1 Zuul路由规则配置

#服务网关配置
eureka:client:serviceUrl:defaultZone: http://${EUREKA_ZONE_HOST:localhost}:7777/eureka/instance:prefer-ip-address: true #访问路径可以显示IP地址instance-id: ${spring.cloud.client.ip-address}:${server.port}ip-address: ${spring.cloud.client.ip-address}zuul:sensitiveHeaders: # 全局不清空请求头ribbonIsolationStrategy: THREADhost:connect-timeout-millis: 600000socket-timeout-millis: 600000 #路由规则routes:seckill-provider:path: /seckill-provider/**serviceId: seckill-providerstrip-prefix: falseurlDemo:path: /blog/**url: https://www.cnblogs.comsensitiveHeaders: Cookie,Set-Cookie,token,backend,Authorization #外部请求清空请求头

urlDemo使用url属性来指定直接的上游URL的前缀;seckill-provider使用serviceId属性来指定上游服务提供者的名称,需要结合Eureka Client客户端来实现动态的路由转发功能。

6.2.2 过滤敏感请求头部

大家知道,Cookie经常用于在流量中缓存用户的会话、用户凭证 等信息,对于外部系统而言是需要保密的,所以应该设置为敏感标 题,不应该带往系统外部。

默认情况下,Zuul转发请求时会把header清空,如果在微服务集 群内部转发请求,上游Provider就会收不到任何头部。如果需要传递 原始的header信息到最终的上游,就需要添加如下敏感头部设置:

zuul:sensitiveHeaders:

6.2.3 路径前缀的处理

如果不进行任何配置(stripPrefix的值默认为true),默认情况下Zuul会去掉路由的路径前缀。

例:

请求 http://crazydemo.com:7799/demo-provider/api/demo/hello/v1

在Zuul进行路由处理时,会去掉在路由规则清单中配置的路径前 缀demo-provider。处理之后,转发到上游的服务提供者的URL将变成 下面的样子:

http://{provider-ip}:{provider-port}/api/demo/hello/v1

6.3 Zuul过滤器

Zuul可以通过定义过滤器来实现请求的拦截和 过滤,而它本身的大部分功能也是通过过滤器实现的。

6.3.1 Zuul网关的过滤器类型

1.pre类型的过滤器

此类型为请求路由之前调用的过滤器,可利用此类过滤器来实现身 份验证、记录调试信息等。

2.route类型的过滤器

此类型为发送请求到上游服务的过滤器,比如使用Apache HttpClient或Netflix Ribbon请求上游服务。

3.post类型的过滤器

此类型为上游服务返回之后调用的过滤器,可用来为响应添加HTTP 响应头、收集统计信息和指标、将响应回复给客户端。

4.error类型的过滤器

此类型为在其他阶段发生错误时执行的过滤器。

5.自定义

例如可以定制一种echo类型的过滤器,直接在Zuul中生成响应,而 不将请求转发到上游的服务。

Zuul提供了一个动态读取、编译和运行过滤器的框架。过滤器不直 接相互通信,而是通过RequestContext共享状态,RequestContext(请 求上下文)实例对每个请求都是唯一的。

6.3.2 实战:用户的黑名单过滤

Zuul提供了一个过滤器ZuulFilter抽象基类,可以作为自定义过 滤器的父类。定制一个过滤器需要实现的父类方法有4个,具体如下。

1.filterType方法

返回自定义过滤器的类型,以常量的形式定义在FilterConstants 类中

2.filterOrder方法

返回过滤器顺序,值越小优先级越高。

3.shouldFilter方法

返回过滤器是否生效的boolean值,返回true代表生效,返回 false代表不生效。比如,在请求处理过程中,需要根据请求中是否携 带某个参数来判断是否需要过滤时,可以用shouldFilter方法对请求 进行参数判断,并返回一个相应的boolean值。

如果直接返回true,那么该过滤器总是生效。

4.run方法

过滤器的处理逻辑。在该函数中,可以进行当前的请求拦截和参 数定制,也可以进行后续的路由定制,同时可以进行返回结果的定制,等等。

例:


/*** 演示过滤器:黑名单过滤*/
@Slf4j
@Component
public class DemoFilter extends ZuulFilter {/*** 示例所使用的黑名单:实际使用场景,需要从数据库或者其他来源获取*/static List<String> blackList = Arrays.asList("foo", "bar", "test");/*** 过滤的执行类型*/@Overridepublic String filterType() {//pre:路由之前 //routing:路由之时 //post:路由之后 //error:发送错误调用return "pre";}/*** 过滤的执行次序*/@Overridepublic int filterOrder() {return 0;}/*** 这里是判断逻辑—是否要执行过滤,true为跳过*/@Overridepublic boolean shouldFilter() {/*** 获取上下文*/RequestContext ctx = RequestContext.getCurrentContext();/*** 如果请求已经被其他的过滤器终止,本过滤器就不做处理*/if (!ctx.sendZuulResponse()) {return false;} /***获取请求*/HttpServletRequest request = ctx.getRequest();/*** 返回true表示需要执行过滤器的run方法*/if (request.getRequestURI().startsWith("/ZuulFilter/demo")) {return true;}/*** 返回false表示需要跳过此过滤器,不执行run方法*/return false;}/*** 过滤器的具体逻辑 * 通过请求中的用户名称参数判断是否在黑名单中*/@Overridepublic Object run() {RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();/*** 对用户名称进行判断 * 如果用户名称在黑名单中,就不再转发给后端的服务提供者*/String username = request.getParameter("username");if (username != null && blackList.contains(username)) {log.info(username + " is forbidden:" +request.getRequestURL().toString());/*** 终止后续的访问流程*/ctx.setSendZuulResponse(false);try {ctx.getResponse().setContentType("text/html;charset=utf-8");ctx.getResponse().getWriter().write("对不起,您已经进入黑名单");} catch (Exception e) {e.printStackTrace();}return null;}return null;}
}

6.4 Spring Security原理和实战

Spring Security是Spring应用项目中的一个安全模块,特别是在 Spring Boot项目中,Spring Security默认为自动开启。

6.4.1 Spring Security核心组件

Spring Security核心组件之Authentication(认证/身份验证)

  • 方法说明

(1)getPrincipal方法:Principal直译为“主要演员、主角”,用于获取用户 身份信息,可以是用户名,也可以是用户的ID等,具体的值需要依据具体的认证令牌 实现类确定。

(2)getAuthorities方法:用于获取用户权限集合,一般情况下获取到的是用 户的权限信息。

(3)getCredentials方法:直译为获取资格证书。用户名和密码认证时,通常 情况下获取到的是密码信息。

(4)getDetails方法:用于获取用户的详细信息。用户名和密码认证时,这部 分信息可以是用户的POJO实例。

(5)isAuthenticated方法:判断当前Authentication凭证是否已验证通过。

(6)setAuthenticated方法:设置当前Authentication凭证是否已验证通过 (true或false)。

  • 实现类

(1)UsernamePasswordAuthenticationToken:用于在用户名+密码认证的场景 中作为验证的凭证,该凭证(令牌)包含用户名+密码信息。

(2)RememberMeAuthenticationToken:用于“记住我”的身份认证场景。如果 用户名+密码成功认证之后,在一定时间内不需要再输入用户名和密码进行身份认 证,就可以使用RememberMeAuthenticationToken凭证。通常是通过服务端发送一个 Cookie给客户端浏览器,下次浏览器再访问服务端时,服务端能够自动检测客户端的 Cookie,根据Cookie值自动触发RememberMeAuthenticationToken凭证/令牌的认证操 作。

(3)AnonymousAuthenticationToken:对于匿名访问的用户,Spring Security 支持为其建立一个AnonymousAuthenticationToken匿名凭证实例存放在 SecurityContextHolder中。

(4)支持自定义

Spring Security核心组件之AuthenticationProvider(认证提供者)

  • 方法说明

(1)authenticate方法:表示认证的操作,对authentication参数对象进行身 份认证操作。如果认证通过,就返回一个认证通过的凭证/令牌。通过源码中的注释 可以知道,如果认证失败,就抛出异常。

(2)supports方法:判断实参authentication是否为当前认证提供者所能认证 的令牌。

  • 实现类

(1)AbstractUserDetailsAuthenticationProvider:这是一个对 UsernamePasswordAuthentication Token类型的凭证/令牌进行验证的认证提供者 类,用于“用户名+密码”验证的场景。

(2)RememberMeAuthenticationProvider:这是一个对 RememberMeAuthenticationToken类型的凭证/令牌进行验证的认证提供者类,用于 “记住我”的身份认证场景。

(3)AnonymousAuthenticationProvider:这是一个对 AnonymousAuthenticationToken类型的凭证/令牌进行验证的认证提供者类,用于匿 名身份认证场景。

(4)支持自定义

Spring Security核心组件之AuthenticationManager(认证管理者)

  • 方法说明

(1)authenticate方法:认证流程的入口,接收一个Authentication令牌对象作为参数

  • 实现类

(1)ProviderManager:该类有一个 providers成员变量,负责管理一个提供者清单列表

  • 验证逻辑

认证管理者ProviderManager在进行令牌验证时,会对提供者列表进行迭代,找 出支持令牌的认证提供者,并交给认证提供者去执行令牌验证。如果该认证提供者的 supports方法返回true,就会调用该提供者的authenticate方法。如果验证成功,那 么整个认证过程结束;如果不成功,那么继续处理列表中的下一个提供者。只要有一 个验证成功,就会认证成功。

6.4.2 Spring Security的请求认证处理流程

(1)定制一个凭证/令牌类。

(2)定制一个认证提供者类和凭证/令牌类进行配套,并完成对自制凭证/令牌 实例的验证。

(3)定制一个过滤器类,从请求中获取用户信息组装成定制凭证/令牌,交给 认证管理者。

(4)定制一个HTTP的安全认证配置类(AbstractHttpConfigurer子类),将上 一步定制的过滤器加入请求的过滤处理责任链。

(5)定义一个Spring Security安全配置类(WebSecurityConfigurerAdapter 子类),对Web容器的HTTP安全认证机制进行配置。

6.4.3 基于数据源的认证流程

在大多数生产场景中,用户信息都存储在某个数据源(如数据库)中,认证 过程中涉及从数据源加载用户信息的环节。Spring Security为这种场景内置了 一套解决方案,主要涉及几个内置类。

1.UsernamePasswordAuthenticationToken

此认证类实现了Authentication接口,主要封装用户输入的用户名和密码信 息,提供给支持的认证提供者进行认证。

2.AbstractUserDetailsAuthenticationProvider

此认证提供者类与UsernamePasswordAuthenticationToken凭证/令牌类配 套,但这是一个抽象类,具体的验证逻辑需要由子类完成。

此认证提供者类的常用子类为DaoAuthenticationProvider类,该类依赖一 个UserDetailsService用户服务数据源,用于获取UserDetails用户信息,其中 包括用户名、密码和所拥有的权限等。此认证提供者子类从数据源 UserDetailsService中加载用户信息后,将待认证的令牌中的“用户名+密码” 信息和所加载的数据源用户信息进行匹配和验证。

3.UserDetailsService

UserDetailsService有一个loadUserByUsername方法,其作用是根据用户名 从数据源中查询用户实体。一般情况下,可以实现一个定制的 UserDetailsService接口的实现类来从特定的数据源获取用户信息。

4.UserDetails

UserDetails是一个接口,主要封装用户名、密码、是否过期、是否可用等 信息。

5.PasswordEncoder

PasswordEncoder是一个负责明文加密、判断明文和密文匹配的接口

6.5 JWT+Spring Security进行网关安全认证

6.5.1 JWT安全令牌规范详解

JWT(JSON Web Token)是一种用户凭证的编码规范,是一种网络环境下编码用 户凭证的JSON格式的开放标准(RFC 7519)。JWT令牌的格式被设计为紧凑且安全 的,特别适用于分布式站点的单点登录(SSO)、用户身份认证等场景。

一个编码之后的JWT令牌字符串分为三部分:header+payload+signature。这三 部分通过点号“.”连接,第一部分常被称为头部(header),第二部分常被称为负 载(payload),第三部分常被称为签名(signature)。

1.JWT的header

编码之前的JWT的header部分采用JSON格式,一个完整的头部就像如下的JSON内 容:

{
"typ":"JWT","alg":"HS256"}

其中,"typ"是type(类型)的简写,值为"JWT"代表JWT类型;"alg"是加密算 法的简写,值为"HS256"代表加密方式为HS256。

采用JWT令牌编码时,header的JSON字符串将进行Base64编码,编码之后的字符 串构成了JWT令牌的第一部分。

2.JWT的playload

编码之前的JWT的playload部分也是采用JSON格式,playload是存放有效信息的部分,一个简单的playload就像如下的JSON内容:

{
"sub":"session id",
"exp":1579315717,
"iat":1578451717
}

采用JWT令牌编码时,playload的JSON字符串将进行Base64编码,编码之后的字 符串构成了JWT令牌的第二部分。

payload部分JSON中的属性被称为JWT的声明。JWT的声明分 为两类:

(1)公有的声明(如iat)。

(2)私有的声明(自定义的JSON属性)。

公有的声明也就是JWT标准中注册的声明,主要为以下JSON属性:

(1)iss:签发人。

(2)sub:主题。

(3)aud:用户。

(4)iat:JWT的签发时间。

(5)exp:JWT的过期时间,这个过期时间必须要大于签发时间。

(6)nbf:定义在什么时间之前该JWT是不可用的。

私有的声明是除了公有声明之外的自定义JSON字段,私有的声明可以添加任何 信息,一般添加用户的相关信息或其他业务需要的必要信息。由于JWT的payload声明(JSON属性)是可以解码的,属于明文信息,因此不建 议添加敏感信息。

3.JWT的signature

JWT的第三部分是一个签名字符串,这一部分是将header的Base64编码和 payload的Base64编码使用点号(.)连接起来之后,通过header声明的加密算法进 行加密所得到的密文。为了保证安全,加密时需要加入加密因子。

6.5.2 JWT+Spring Security认证处理流程

6.5.3 Zuul网关与UAA微服务的配合

过Zuul网关和UAA微服务相互结合来完成整个用户的登录与认证闭环流 程。二者的关系大致为:

(1)登录时,UAA微服务负责用户名称和密码的验证并且将用户信息(包括令牌加密盐)放在分布式 Session中,然后返回JWT令牌(含Session ID)给前台。

(2)认证时,前台请求带上JWT令牌,Zuul网关能根据令牌中的Session ID取出分布式Session中的 加密盐,对JWT令牌进行验证。在crazy-springcloud脚手架的会话架构中,Zuul网关必须能和UAA微服务 进行会话的共享,如图6-7所示。

6.5.4 使用Zuul过滤器添加代理请求的用户标识

为了使程序的可扩展 性和可移植性更好,建议使用第三种用户身份标识(非sessionId、UserId)的代理传递方案。

6.6 服务提供者之间的会话共享关系

  • 全局共享

  • 局部共享

6.6.1 分布式Session的起源和实现方案

HTTP本身是一种无状态的协议,这就意味着每一次请求都需要进 行用户的身份信息查询,并且需要用户提供用户名和密码来进行用户 认证。为什么呢?服务端并不知道是哪个用户发出的请求。所以,为 了能识别是哪个用户发出的请求,需要在服务端存储一份用户身份信 息,并且在登录成功后将用户身份信息的标识传递给客户端,客户端 保存好用户身份标识,在下次请求时带上该身份标识。然后,在服务 端维护一个用户的会话,用户的身份信息保存在会话中。通常,对于 传统的单体架构服务器,会话都是保存在内存中的,而随着认证用户 增多,服务端的开销会明显增大。

6.6.2 Spring Session的核心组件和存储细节

  • Session接口

Spring Session单独抽象出Session接口,该接口是Spring Session对会话的抽象,主要是为了鉴定用户,为HTTP请求和响应提供 上下文容器。

Spring Session之所以要单独抽象出Session接口,主要是为了应 对多种传输、存储场景下的会话管理,比如HTTP会话场景 (HttpSession)、WebSocket会话场景(WebSocket Session)、非 Web会话场景(如Netty传输会话)、Redis存储场景(RedisSession) 等。

  • RedisSession会话类

RedisSession用于使用Redis进行会话属性存储的场景。在 RedisSession中有两个非常重要的成员属性,分别说明如下:

(1)cached:实际上是一个MapSession实例,用于进行本地缓 存,每次在进行getAttribute操作时优先从本地缓存获取,没有取到 再从Redis中获取,以提升性能。而MapSession是由Spring Security Core定义的一个通过内部的HashMap缓存键-值对的本地缓存类。

(2)delta:用于跟踪变化数据,目的是保存变化的Session的属 性。

RedisSession提供了一个非常重要的saveDelta方法,用于持久化 Session至Redis中:当调用RedisSession中的saveDelta方法后,变化 的属性将被持久化到Redis中。

  • SessionRepository存储接口

根据Session的实现类不同,Session存储实现类分为很多种。 RedisSession会话的存储类为RedisOperationsSessionRepository, 由其负责Session数据到Redis数据库的读写。

6.6.3 Spring Session的使用和定制

  • SessionIdFilter

作用是根据请求头中的用 户身份标识User ID定位到分布式会话的Session ID。

  • CustomedSessionRepositoryFilter

这个类的 源码来自Spring Session,其主要的逻辑是将request(请求)和 response(响应)进行包装,将HttpSession替换成RedisSession。

  • SessionDataLoadFilter

判断RedisSession 中的用户数据是否存在,如果是首次创建的Session,就从数据库中将 常用的用户数据加载到Session,以便控制层的业务逻辑代码能够被高 速访问。

6.6.4 通过用户身份标识查找Session ID

6.6.5 查找或创建分布式Session

6.6.6 加载高速访问数据到分布式Session

第7章 Nginx/OpenResty详解

7.1 Nginx简介

Nginx是一个高性能的HTTP和反向代理Web服务器,有以下3个主要社区分支:

  • Nginx官方版本:

更新迭代比较快,并且提供免费版本和商 业版本。

  • Tengine:

Tengine是由淘宝网发起的Web服务器项目。它在 Nginx的基础上针对大访问量网站的需求添加了很多高级功能和特性。 Tengine的性能和稳定性已经在大型的网站(如淘宝网、天猫商城等) 得到了很好的检验。它的最终目标是打造一个高效、稳定、安全和易 用的Web平台。

  • OpenResty:

2011年,中国人章亦春老师把LuaJIT VM嵌入 Nginx中,实现了OpenResty这个高性能服务端解决方案。OpenResty是 一个基于Nginx与Lua的高性能Web平台,其内部集成了大量精良的Lua 库、第三方模块以及大多数的依赖项,用于方便地搭建能够处理超高 并发、扩展性极高的动态Web应用、Web服务和动态网关。

OpenResty的目标是让Web服务直接跑在Nginx服务内部,充分利用 Nginx的非阻塞I/O模型,不仅对HTTP客户端请求,甚至对远程后端 (诸如MySQL、PostgreSQL、Memcached以及Redis等)都进行一致的高 性能响应。

OpenResty通过汇聚各种设计精良的Nginx模块(主要由OpenResty 团队自主开发)从而将Nginx有效地变成一个强大的通用Web应用平 台,使得Web开发人员和系统工程师可以使用Lua脚本语言调动Nginx支 持的各种C以及Lua模块,快速构造出足以胜任10KB乃至1000KB以上单 机并发连接的高性能Web应用系统。

7.1.1 正向代理与反向代理

正向代理和反向 代理的用途都是代理服务进行客户端请求的转发。

  • 正向代理

客户端需要配置目标服务器信息,比如IP和端 口。一般来说,正向代理服务器是一台和客户端网络连通的局域网内部 的机器或者是可以打通两个隔离网络的双网卡机器。通过正向代理的方 式,客户端的HTTP请求可以转发到之前与客户端网络不通的其他不同的 目标服务器。

正向代理的主要场景是客户端。由于网络不通等物理原因, 需要通过正向代理服务器这种中间转发环节顺利访问目标服务器。当 然,也可以通过正向代理服务器对客户端某些详细信息进行一些伪装和 改变。

  • 反向代理

客户端向反向代理服务器直接发送请求,接着反向代理将请求转发 给目标服务器,并将目标服务器的响应结果按原路返回给客户端。

反向代理的主要场景是服务端。服务提供方可以通过反向代 理服务器轻松实现目标服务器的动态切换,实现多目标服务器的负载均 衡等。

7.1.2 Nginx的启动与停止

Windows平台OpenResty的安装和启动

Linux平台OpenResty的安装

OpenResty服务器下的Lua开发调试

7.1.3 Nginx的启动命令和参数详解

// -v:查看Nginx的版本
nginx -v// -c:指定一个新的Nginx配置文件来替换默认的Nginx配置文件
nginx -p ./ -c nginx-debug.conf// -t:表示测试Nginx的配置文件。如果不能确定Nginx配置文 件的语法是否正确,就可以通过Nginx命令的-t参数来测试。此参数代 表不运行配置文件,只是测试配置文件。
nginx -t -c nginx-debug.conf// -p:设置前缀路径【“-p./”表示将当前目录C: \dev\refer\LuaDemoProject\src作为前缀路径,也就是说,nginx- debug.conf配置文件中所用到的相对路径都加上这个前缀。】
nginx -p ./ -c nginx-debug.conf// -s:给Nginx进程发送信号,包含stop(停止)、 reload(重写加载)。
nginx -p ./ -c nginx-debug.conf -s reload
nginx -p ./ -c nginx-debug.conf -s stop

7.1.4 Linux下OpenResty的启动、停止脚本

  • 脚本:openresty-start.sh
#!/bin/bash
#设置OpenResty的安装目录
OPENRESTRY_PATH="/usr/local/openresty"
#设置Nginx项目的工作目录
PROJECT_PATH="/work/develop/LuaDemoProject/src/"
#设置项目的配置文件
#PROJECT_CONF="nginx-location-demo.conf"
PROJECT_CONF="nginx.conf"
echo "OPENRESTRY_PATH:$OPENRESTRY_PATH"
echo "PROJECT_PATH:$PROJECT_PATH"
#查找Nginx所有的进程id
pid=$(ps -ef | grep -v 'grep' | egrep nginx| awk '{printf $2 " "}')
#echo "$pid"
if [ "$pid" != "" ]; then #如果已经在执行,就提示echo "openrestry/nginx is started already, and pid is $pid, operating failed!"
else#如果没有执行,就启动$OPENRESTRY_PATH/nginx/sbin/nginx -p ${PROJECT_PATH} \-c ${PROJECT_PATH}/conf/${PROJECT_CONF} pid=$(ps -ef | grep -v 'grep' | egrep nginx| awk '{printf $2 " "}')echo "openrestry/nginx starting succeeded!"echo "pid is $pid "
fi
  • 说明

(1)echo显示命令:用于显示信息到终端屏幕。

(2)ps进程列表:用于显示在本地机器上当前运行的进程列表。

(3)grep查找命令:用于查找文件里符合条件的字符串。

7.1.5 Windows下OpenResty的启动、停止脚本

  • 脚本:openresty-start.bat
@echo off
rem启动标志flag=0表示之前已经启动,flag=1表示现在立即启动
set flag=0rem设置OpenResty/Nginx的安装目录
set installPath=E:/tool/openresty-1.13.6.2-win32rem设置Nginx项目的工作目录
set projectPath=C:/dev/refer/LuaDemoProject/srcrem设置项目的配置文件
set PROJECT_CONF=nginx-location-demo.conf
rem set PROJECT_CONF=nginx.confecho installPath: %installPath%
echo project prefix path: %projectPath%
echo config file: %projectPath%/conf/%PROJECT_CONF%
echo openresty starting...rem查找OpenResty/Nginx进程信息,然后设置flag标志位
tasklist|find /i "nginx.exe" > nul
if %errorlevel%==0 (
echo "OpenResty/Nginx already running ! "
rem exit /b
) else set flag=1rem如果需要,就启动OpenResty/Nginx
cd /d %installPath%
if %flag%==1 (
start nginx.exe -p "%projectPath%" -c "%projectPath%/conf/%PROJECT_CONF%"
ping localhost -n 2 > nul
)rem输出OpenResty/Nginx的进程信息
tasklist /fi "imagename eq nginx.exe" tasklist|find /i "nginx.exe" > nul
if %errorlevel%==0 (
echo "OpenResty/Nginx starting succeeded!"
)
  • 说明

(1)rem注释命令:一般用来给程序加上注释,该命令后的内容 不被执行。

(2)echo显示命令:用于显示信息到终端屏幕。

(3)cd目录切换:用于切换当前的目录。

(4)tasklist进程列表:用于显示在本地或远程机器上当前运行 的进程列表。

7.2 Nginx的核心原理

与Java底层通信框架Netty在原理上有很多相似的地方。

7.2.1 Reactor模型

Nginx对高并发IO的处理使用了Reactor事件驱动模型。Reactor模 型的基本组件包含事件收集器事件发送器事件处理器3个基本单 元,其核心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用 器上,同时主线程/进程阻塞在多路复用器上,一旦有I/O事件到来或者 准备就绪(文件描述符或Socket可读、写),多路复用器返回并将事先 注册的相应I/O事件分发到对应的处理器中。

(1)事件收集器:负责收集Worker进程的各种I/O请求。

(2)事件发送器:负责将I/O事件发送到事件处理器。

(3)事件处理器:负责各种事件的响应工作。

事件收集器将各个连接通道的IO事件放入一个待处理事件列,通过 事件发送器发送给对应的事件处理器来处理。而事件收集器之所以能够同时管理上百万连接通道的事件,是基于操作系统提供的“多路IO复用”技术,常见的包括select、epoll两种模型。

正是由于Nginx使用了高性能的Reactor模式,因此是目前并发能力 很高的Web服务器之一,成为迄今为止使用广泛的工业级Web服务器。当 然,Nginx也解决了著名的网络读写的C10K问题。什么是C10K问题呢? 网络服务在处理数以万计的客户端连接时,往往出现效率低下甚至完全 瘫痪,这类问题就被称为C10K问题。

7.2.2 Nginx的两类进程

  • Nginx启动方式有两种:

(1)单进程启动:此时系统中仅有一个进程,该进程既充当 Master管理进程角色,又充当Worker工作进程角色。

(2)多进程启动:此时系统有且仅有一个Master管理进程,至少有一个Worker工作进程。

  • Master管理进程的主要工作

(1)Master管理进程主要负责调度Worker工作进程,比如加载配 置、启动工作进程、接收来自外界的信号、向各Worker进程发送信号、 监控Worker进程的运行状态等。所以Nginx启动后,我们能够看到至少 有两个Nginx进程。

(2)Master负责创建监听套接口,交由Worker进程进行连接监听。

  • Worker工作进程的主要工作

Worker进程主要用来处理网络事 件,当一个Worker进程在接收一条连接通道之后,就开始读取请求、解 析请求、处理请求,处理完成产生数据后,再返回给客户端,最后断开 连接通道。

各个Worker进程之间是对等且相互独立的,它们同等竞争来自客户 端的请求,一个请求只可能在一个Worker进程中处理。这都是典型的 Reactor模型中Worker进程(或者线程)的职能。

7.2.3 Nginx的模块化设计

Nginx的主要模块说明:

(1)Core核心模块:核心模块是Nginx服务器正常运行必不可少的 模块,提供错误日志记录、配置文件解析、Reactor事件驱动机制、进 程管理等核心功能。非核心模块可以在编译时按需加入。

(2)标准HTTP模块:标准HTTP模块提供HTTP协议解析相关的功 能,比如端口配置、网页编码设置、HTTP响应头设置等。

(3)可选HTTP模块:可选HTTP模块主要用于扩展标准的HTTP功 能,让Nginx能处理一些特殊的服务,比如Flash多媒体传输、网络传输 压缩、安全协议SSL的支持等。

(4)邮件服务模块:邮件服务模块主要用于支持Nginx的邮件服 务,包括对POP3协议、IMAP协议和SMTP协议的支持。

(5)第三方模块:第三方模块是为了扩展Nginx服务器的功能,定 制开发者自定义功能,比如JSON支持、Lua支持等。

7.2.4 Nginx配置文件上下文结构

一个Nginx的功能模块包含一系列的命令(cmd)以及 与命令对应的处理函数(cmd→handler)。而Nginx根据配置文件中的 配置指令就知道对应到哪个模块的哪个命令,然后调用命令对应的处理 函数来处理。

一个Nginx配置文件包含若干配置项,每个配置项由配置指令和指 令参数两部分组成,可以简单认为配置项是一个键-值对。图7-6中有3 个简单的Nginx配置项。

 7.2.5 Nginx的请求处理流程

Nginx中HTTP请求的处理流程可以分为4步:

(1)读取解析请求行。

(2)读取解析请求头。

(3)多阶段处理,也就是执行handler处理器列表。

(4)将结果返回给客户端。

7.2.6 HTTP请求处理的11个阶段

1.post-read阶段:改写请求的来源地址

# 这里的配置是让Nginx把来自正向代理服务器192.168.0.100的所有 请求的IP来源地址都改写为请求头X-My-IP所指定的值,放在 $remote_addr内置标准变量中。
server {listen 8080;set_real_ip_from 192.168.0.100; real_ip_header X-My-IP; location /test {echo "from: $remote_addr "; }
}

2.server-rewrite阶段

3.find-config:根据请求URL地址去匹配location路由表达式

4.rewrite

5.post-rewrite:请求地址URI重写提交(Post)阶段

6.preaccess

访问权限检查准备阶段,控制访问频率的ngx_limit_req模块和限 制并发度的ngx_limit_zone模块的相关指令就注册在此阶段。

7.access

在访问权限检查阶段,配置指令多是执行访问控制类型的任务,比 如检查用户的访问权限、检查用户的来源IP地址是否合法等。

8.post-access

访问权限检查提交阶段。如果请求不被允许访问Nginx服务器,该 阶段负责就向用户返回错误响应。

9.try-files

如果HTTP请求访问静态文件资源,那么try-files配置项可以使这 个请求按顺序访问多个静态文件资源,直到某个静态文件资源符合选取 条件。

10.content

11.log

总结:

(1)Nginx将一个HTTP请求分为11个处理阶段,这样做让每个HTTP 模块可以只专注于完成一个独立、简单的功能。而一个请求的完整处理 过程由多个HTTP模块共同合作完成,可以极大地提高多个模块合作的协 同性、可测试性和可扩展性。

(2)Nginx请求处理的11个阶段中,有些阶段是必备的,有些阶段 是可选的,各个阶段可以允许多个模块的指令同时注册。但是,find- config、post-rewrite、post-access、try-files四个阶段是不允许其 他模块的处理指令注册的,它们仅注册了HTTP框架自身实现的几个固定 的方法。

(3)同一个阶段内的指令,Nginx会按照各个指令的上下文顺序执 行对应的handler处理器方法。

7.3 Nginx的基础配置

7.3.1 events事件驱动配置

示例

events {use epoll; #使用epoll类型IO多路复用模型worker_connections 204800; #最大连接数限制为20万accept_mutex on; #各个Worker通过锁来获取新连接
}

1.worker_connections指令

worker_connections指令用于配置每个Worker进程能够打开的最 大并发连接数量,指令参数为连接数的上限。

2.use指令

use指令用于配置IO多路复用模型,有多种模型可配置,常用的有 select、epoll两种。

Linux系统下,select类型IO多路复用模型有两个较大的缺陷:缺 陷之一,单服务进程并发数不够,默认最大的客户端连接数为 1024/2048,因为Linux系统一个进程所打开的FD文件描述符是有限制 的,由FD_SETSIZE设置,默认值是1024/2048,因此select模型的最大 并发数被相应限制了;缺陷之二,性能问题,每次IO事件查询都会线 性扫描全部的FD集合,连接数越大,性能越会线性下降。总之, select类型IO多路复用模型的性能是不高的。

使用Nginx的目标之一是为了高性能和高并发。所以,在Linux系 统下建议使用epoll类型的IO多路复用模型。epoll模型是在Linux 2.6 内核中实现的,是select系统调用的增强版本。epoll模型中有专门的 IO就绪队列,不再像select模型一样进行全体连接扫描,时间复杂度 从select模型的O(n)下降到了O(1)。在IO事件的查询效率上,无 论上百万连接还是数十个连接,对于epoll模型而言差距是不大的;而 对select模型而言,效率的差距就非常巨大了。

select、epoll都是常见的IO多路复用模型。本质上都是查询多个 FD描述符,一旦某个描述符的IO事件就绪(一般是读就绪或者写就 绪),就进行相应的读写操作,而且都是在读写事件就绪后,应用程 序自己负责进行读写。所以,select、epoll本质上都是同步I/O,因 为它们的读写过程是阻塞的。虽然不是异步I/O,但是通过合理的设 计,epoll类型的IO多路复用模型的性能还是非常高,足以应对目前的 高并发处理要求。

3.accept_mutex指令

accept_mutex指令用于配置各个Worker进程是否通过互斥锁有序 接收新的连接请求。on参数表示各个Worker通过互斥锁有序接收新请 求;off参数指每个新请求到达时会通知(唤醒)所有的Worker进程参 与争抢,但只有一个进程可获得连接。

配置off参数会造成“惊群”问题影响性能。accept_mutex指令的 参数默认为on。

7.3.2 虚拟主机配置

1.虚拟主机的监听套接字配置

server {listen 80; #使用listen指令直接配置监听端口listen 127.0.0.1:80; #使用listen指令配置监听的IP和端口
}

2.虚拟主机名称配置

#后台管理服务虚拟主机demo
server {listen 80;server_name admin.crazydemo.com; #后台管理服务的域名前缀为admin location / {default_type 'text/html'; charset utf-8;echo "this is admin server";}
}#文件服务虚拟主机demo
server {listen 80;server_name file.crazydemo.com; #文件服务的域名前缀为admin location / {default_type 'text/html'; charset utf-8;echo "this is file server";}
}#默认服务虚拟主机demo
server {listen 80 default;server_name crazydemo.com *.crazydemo.com; #如果没有前缀,这就是默认访问的虚拟主机 location / {default_type 'text/html'; charset utf-8;echo "this is default server";}
}
多个虚拟主机之间,匹配优先级从高到低大致如下:

(1)字符串精确匹配:如果请求的域名为admin.crazydemo.com, 那么首先会匹配到名称为admin.crazydemo.com的虚拟管理主机。

(2)左侧*通配符匹配:若浏览器请求的域名为 xxx.crazydemo.com,则会匹配到*.crazydemo.com虚拟主机。为什么 呢?因为配置文件中并没有server_name为xxx.crazydemo.com的主机, 所以退而求其次,名称为*.crazydemo.com的虚拟主机按照通配符规则 匹配成功。

(3)右侧*通配符匹配:右侧*通配符和左侧*通配符匹配类似,只 不过优先级低于左侧*通配符匹配。

(4)正则表达式匹配:与通配符匹配类似,不过优先级更低。

(5)default_server:在listen指令后面如果带有default的指令 参数,就代表这是默认的、最后兜底的虚拟主机,如果前面的匹配规则 都没有命中,就只能命中default_server指定的默认主机。

7.3.3 错误页面配置

#后台管理服务器demo
server {listen 80;server_name admin.crazydemo.com; root /var/www/;location / {default_type 'text/html'; charset utf-8;echo "this is admin server";}#设置错误页面 error_page code ... [=[response]] uri;error_page 404 /404.html;error_page 404 =200 /404.html #防止404页面被劫持#设置错误页面 error_page code ... [=[response]] uri;error_page 500 502 503 504 /50x.html;
}

7.3.4 长连接相关配置

# 配置长连接的有效时长:
# timeout参数用于设置保持连接超时时长,0表示禁止 长连接,默认为75秒
keepalive_timeout timeout [header_timeout];# 配置长连接的一条连接允许的最大请求数
# number参数用于设置在一条长连接上允许被请求的资 源的最大数量,默认为100
keepalive_requests  number;# 配置向客户端发送响应报文的超时限制
# time参数用于设置Nginx向客户端发送响应报文的超时限制,此处时长是指两次向客户端写操作之间的间隔时长,并非整个响应过程的传输时长
send_timeout time;

7.3.5 访问日志配置

# 访问记录配置指令的完整格式
# path表示日志文件的本地路径
# format表示日志输出的格式名称
access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];# 定义日志输出格式的配置指令为
# name参数用于指定格式名称
# string参数用于设置格式字符串,可以有多个,字符串中可以使 用Nginx核心模块及其他模块的内置变量
log_format name string ...;# 示例
# $request:记录用户的HTTP请求的起始行信息
# $status:记录HTTP状态码,即请求返回的状态,例如200、404、502等
# $remote_addr:记录访问网站的客户端地址
# $remote_user:记录远程客户端用户名称
# $time_local:记录访问时间与时区
# $body_bytes_sent:记录服务器发送给客户端的响应body字节数
# $http_referer:记录此次请求是从哪个链接访问过来的,可以根据其进行盗链的监测
# $http_user_agent:记录客户端访问信息,如浏览器、手机客户端等
# $http_x_forwarded_for:当前端有正向代理服务器时,此参数用于保持客户端真实的IP地址。 该参数生效的前提:前端的代理服务器上进行了相关的x_forwarded_for设置
http {#先定义日志格式,format_main是日志格式的名字log_format format_main '$remote_addr - $remote_user [$time_local] $request - ' ' $status - $body_bytes_sent [$http_referer#配置:日志文件、访问日志格式access_log logs/access_main.log format_main;
}

7.3.6 Nginx核心模块内置变量

(1)$arg_PARAMETER:请求URL中以PARAMETER为名称的参数值。 请求参数即URL的“?”号后面的name=value形式的参数对,变量 $arg_name得到的值为value。

另外,$arg_PARAMETER中的参数名称不区分字母大小写,例如通 过变量$arg_name不仅可以匹配name参数,也可以匹配NAME、Name请求 参数,Nginx会在匹配参数名之前自动把原始请求中的参数名调整为全 部小写的形式。

(2)$args:请求URL中的整个参数串,其作用与$query_string 相同。

(3)$binary_remote_addr:二进制形式的客户端地址。

(4)$body_bytes_sent:传输给客户端的字节数,响应头不计算 在内。

(5)$bytes_sent:传输给客户端的字节数,包括响应头和响应 体。

(6)$content_length:等同于$http_content_length,用于获 取请求体body的大小,指的是Nginx从客户端收到的请求头中Content- Length字段的值,不是发送给客户端响应中的Content-Length字段值,如果需要获取响应中的Content-Length字段值,就使用$sent_http_content_length变量。

(7)$request_length:请求的字节数(包括请求行、请求头和 请求体)。注意,由于$request_length是请求解析过程中不断累加 的,如果解析请求时出现异常,那么$request_length是已经累加部分 的长度,并不是Nginx从客户端收到的完整请求的总字节数(包括请求 行、请求头、请求体)。

(8)$connection:TCP连接的序列号。 (9)$connection_requests:TCP连接当前的请求数量。 (10)$content_type:请求中的Content-Type请求头字段值。 (11)$cookie_name:请求中名称name的cookie值。 (12)$document_root:当前请求的文档根目录或别名。

(13)$uri:当前请求中的URI(不带请求参数,参数位于$args 变量)。$uri变量值不包含主机名,如“/foo/bar.html”。此参数可 以修改,可以通过内部重定向。

(14)$request_uri:包含客户端请求参数的原始URI,不包含主 机名,此参数不可以修改,例如“/foo/bar.html?name=value”。

(15)$host:请求的主机名。优先级为:HTTP请求行的主机名 >HOST请求头字段>符合请求的服务器名。

(16)$http_name:名称为name的请求头的值。如果实际请求头 name中包含中画线“-”,那么需要将中画线“-”替换为下画线

“_”;如果实际请求头name中包含大写字母,那么可以替换为小写字 母。例如获取Accept-Language请求头的值,变量名称为 $http_accept_language。

(17)$msec:当前的UNIX时间戳。UNIX时间戳是从1970年1月1日 (UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。

(18)$nginx_version:获取Nginx版本。

(19)$pid:获取Worker工作进程的PID。

(20)$proxy_protocol_addr:代理访问服务器的客户端地址, 如果是直接访问,那么该值为空字符串。

(21)$realpath_root:当前请求的文档根目录或别名的真实路 径,会将所有符号连接转换为真实路径。

(22)$remote_addr:客户端请求地址。

(23)$remote_port:客户端请求端口。

(24)$request_body:客户端请求主体。此变量可在location中 使用,将请求主体通过proxy_pass、fastcgi_pass、uwsgi_pass和 scgi_pass传递给下一级的代理服务器。

(25)$request_completion:如果请求成功,那么值为OK;如果 请求未完成或者请求不是一个范围请求的最后一部分,那么值为空。

(26)$request_filename:当前请求的文件路径,由root或 alias指令与URI请求结合生成。

(27)$request_length:请求的长度,包括请求的地址、HTTP请 求头和请求主体。

(28)$request_method:HTTP请求方法,比如GET或POST等。

(29)$request_time:处理客户端请求使用的时间,从读取客户 端的第一个字节开始计时。

(30)$scheme:请求使用的Web协议,如HTTP或HTTPS。

(31)$sent_http_name:设置任意名称为name的HTTP响应头字 段。例如,如果需要设置响应头Content-Length,那么将“-”替换为 下画线,大写字母替换为小写字母,变量为 $sent_http_content_length。

(32)$server_addr:服务器端地址为了避免访问操作系统内 核,应将IP地址提前设置在配置文件中。

(33)$server_name:虚拟主机的服务器名,如crazydemo.com。

(34)$server_port:虚拟主机的服务器端口。

(35)$server_protocol:服务器的HTTP版本,通常为HTTP/1.0 或HTTP/1.1。

(36)$status:HTTP响应代码。

7.4 location路由规则配置详解

location路由匹配发生在HTTP请求处理的find-config配置查找阶 段,主要功能是:根据请求的URI地址匹配location路由表达式,如果 匹配成功,就执行location后面的上下文配置块。

7.4.1 location语法详解

  • 语法格式
location [=|~|~*|^~] 模式字符串 {}
  • 匹配规则
# 精准匹配:优先级最高
location = /lua {echo "hit location: =/Lua";
}# 普通匹配:符串前缀匹配,如果匹配到多个前缀,那么最长模式匹配优先(Nginx默认的匹配类型,^~可省略)
location ^~ /lua {echo "hit location: ^~ /lua";
}# 正则匹配
# ~:标准正则匹配,区分字母大小写,进行正则表达式测试,若测试成 功,则匹配成功
# ~*:标准正则匹配,不区分字母大小写,进行正则表达式测试,若测 试成功,则匹配成功
# !~:反向正则匹配,区分字母大小写,进行正则表达式测试,若测试 不成功,则匹配成功
# !~*:反向正则匹配,不区分字母大小写,进行正则表达式测试,若 测试不成功,则匹配成功
location ~*hello\.(asp|php)$ {echo "正则匹配: hello.(asp|php)$ ";
}# 默认根路径匹配:只能匹配到“/”根路径
location / {echo "默认根路径匹配: /";
}
  • 匹配次序

(1)类型之间的优先级:精准匹配>普通匹配>正则匹配>“/”默认根路径 匹配。

(2)普通匹配同类型location之间的优先级为最长前缀优先。普通匹配的 优先级与location在配置文件中所处的先后顺序无关,而与匹配到的前缀长度有 关。

(3)正则匹配同类型location之间的优先级为顺序优先。只要匹配到第一个正则规则的location,就停止后面的正则规则的测试。正则匹配与location规 则定义在配置文件中的先后顺序强相关。

7.4.2 常用的location路由配置

# “/”根路由规则
# 路由到一个静态首页
location  / {root   html;index index.html index.htm;
}
# 路由到一个访问很频繁的上游服务
location / {proxy_pass http://127.0.0.1:7799/ ;
}# 静态文件路由规则
# 目录匹配(前缀匹配)
root /www/resources/static/;
location ^~ /static/ {root  /www/resources/;
}
# 后缀匹配(正则匹配)
location ~*\.(gif|jpg|jpeg|png|css|js|ico)${root /www/resources/;
}

7.5 Nginx的rewrite模块指令

Nginx的rewrite模块即ngx_http_rewrite_module标准模块,主要 功能是重写请求URI,也是Nginx默认安装的模块。rewrite模块会根据 PCRE正则匹配重写URI,然后根据指令参数或者发起内部跳转再一次进 行location匹配,或者直接进行30x重定向返回客户端。

7.5.1 set指令

# 用于向变量存放值
# set $variable  value;
set $a "hello world";
set $a "foo";
set $b "$a, $a";

Nginx变量一旦创建,其变量名的可见范围就是整个Nginx配置, 甚至可以跨越不同虚拟主机的server配置块。但是,对于每个请求, 所有变量都有一份独立的副本,或者说都有各变量用来存放值的容器 的独立副本,彼此互不干扰。Nginx变量的生命期是不可能跨越请求边 界的。

7.5.2 rewrite指令

rewrite指令是由ngx_http_rewrite_module标准模块提供的,主要功 能是改写请求URI。rewrite指令的格式如下:

# 如果regrex匹配URI,URI就会被替换成replacement的计算结果
# flag参数的值有last、break、redirect、permanent
# 如果flag参数使用last值,并且匹配成功,那么停止处理任何 rewrite相关的指令,立即用计算后的新URI开始下一轮的location匹配和 跳转
# 如果flag参数使用break值,就如同break指令的字面意思一样,停止 处理任何rewrite的相关指令,但是不进行location跳转
# 如果rewrite指令使用的flag参数的值是permanent,就表示进行外部 重定向,也就是在客户端进行重定向。此时,服务器将新URI地址返回给 客户端浏览器,并且返回301(永久重定向的响应码)给客户端。客户端 将使用新的重定向地址再发起一次远程请求。
#
rewrite  regrex  replacement  [flag];# 例
location /download/ {rewrite ^/download/(.*)/video/(.*)$ /view/$1/mp3/$2.mp3 last;                 rewrite ^/download/(.*)/audio/(.*)*$ /view/$1/mp3/$2.rmvb last; return 404;
}
location /view {echo "uri: $uri ";
}

7.5.3 if条件指令

# if条件指令配置项的格式
# condition
# ==:相等
# !=:不相等
# ~:区分字母大小写模式匹配
# ~*:不区分字母大小写模式匹配
# 还有其他几个专用比较符号,比如判断文件及目录是否存在 的符号
if (condition) {...}# 例
#if指令的演示程序 location /if_demo {#匹配Firefox浏览器if ($http_user_agent ~*"Firefox") {return 403; }#匹配Chrome谷歌浏览器if ($http_user_agent ~*"Chrome") {return 301; }#匹配iPhone手机if ($http_user_agent ~*"iphone") {return 302; }#匹配安卓手机if ($http_user_agent ~*"android") {return 404; }#其他浏览器默认访问规则 return 405;
}# return
#格式一:返回响应的状态码和提示文字,提示文字可选
return code [text];#格式二:返回响应的重定向状态码(如301)和重定向URL
return code URL;#格式三:返回响应的重定向URL,默认的返回状态码是临时重定向302
return URL;

7.5.4 add_header指令

upstream zuul {#server 192.168.233.1:7799; server "192.168.233.128:7799"; keepalive 1000;
}server {listen 80;server_name nginx.server *.nginx.server; default_type 'text/html';charset utf-8;#转发到上游服务器,但是 'OPTIONS' 请求直接返回空 location / {if ($request_method = 'OPTIONS') {# 配置Nginx,加入Access-Control-Max-Age请求头,用来指定本次预检请 求的有效期,单位为秒。上面结果中的有效期是20天(1 728 000秒),即允 许缓存该条回应1 728 000秒,在此期间客户端不用发出另一条预检请求。add_header Access-Control-Max-Age 1728000;add_header Access-Control-Allow-Origin *;add_header Access-Control-Allow-Credentials true;add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';add_header Access-Control-Allow-Headers 'Keep-Alive,User-Agent,X-Requested-With,\
If-Modified-Since,Cache-Control,Content-Type,token';return 204; }proxy_pass http://zuul/ ;}
}

7.5.5 指令的执行顺序

7.6 反向代理与负载均衡配置

要做到高并发和高可用,肯定需要做Nginx集群的负载均衡,而 Nginx负载均衡的基础之一就是反向代理。

7.6.1 演示环境说明

7.6.2 proxy_pass反向代理指令

# 指令的格式
proxy_pass 目标URL前缀;# 不带location前缀的代理
# proxy_pass后面的目标URL前缀加“/根路径”
location /foo_no_prefix {proxy_pass http://127.0.0.1:8080/;
}
# -uri= /bar.html -host= 127.0.0.1 -remote_addr= 127.0.0.1 -proxy_add_x_forwarded_for= 127.0.0.1 -http_x_forwarded_for=# 带location前缀的代理
# proxy_pass后面的目标URL前缀不加“/根路径”
location /foo_prefix {proxy_pass http://127.0.0.1:8080;
}
# -uri= /foo_prefix/bar.html -host= 127.0.0.1 -remote_addr= 127.0.0.1 -proxy_add_x_forwarded_for= 127.0.0.1 -http_x_forwarde# 带部分URI路径的代理
# 如果proxy_pass的路径参数中不止有IP和端口,还有部分目标URI的路径,那么最终的代理URL由两部 分组成:第一部分为配置项中的目标URI前缀;第二部分为请求URI中去掉location中前缀的剩余部分。
#带部分URI路径的代理,实例1
location /foo_uri_1 {proxy_pass http://127.0.0.1:8080/contextA/;
}
# -uri= /contextA/bar.html -host= 127.0.0.1 -remote_addr= 127.0.0.1 -proxy_add_x_forwarded_for= 127.0.0.1 -http_x_forwarded_f
#带部分URI路径的代理,实例2
location /foo_uri_2 {proxy_pass http://127.0.0.1:8080/contextA-;
}
#-uri= /contextA-bar.html -host= 127.0.0.1 -remote_addr= 127.0.0.1 -proxy_add_x_forwarded_for= 127.0.0.1 -http_x_forwarded_f

7.6.3 proxy_set_header请求头设置指令

在反向代理之前,proxy_set_header指令能重新定义/添加字段传 递给代理服务器的请求头。请求头的值可以包含文本、变量和它们的 组合。

# head_field表示请求头,field_value表示值
proxy_pass_header head_field field_value;#不带location前缀的代理
location /foo_no_prefix/ {proxy_pass http://127.0.0.1:8080/;proxy_set_header X-real-ip $remote_addr;
}# 带location前缀的代理
# $proxy_add_x_forwarded_for内置变量,它的作用就 是记录转发历史,其值的第一个地址就是真实地址$remote_addr,然 后每经过一个代理服务器就在后面累加一次代理服务器的地址
location /foo_prefix {proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_pass http://127.0.0.1:8080;
}location /hello {proxy_pass http://127.0.0.1:8080;# 目标主机proxy_set_header Host $host;# 客户端IPproxy_set_header X-real-ip $remote_addr;# 转发记录proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_redirect指令的作用是修改从上游被代理服务器传来的应 答头中的Location和Refresh字段,尤其是当上游服务器返回的响应码 是重定向或刷新请求(如HTTP响应码是301或者302)时, proxy_redirect可以重设HTTP头部的location或refresh字段值。off 参数表示禁止所有的proxy_redirect指令                    proxy_redirect off;
}

7.6.4 upstream上游服务器组

Nginx的负载均衡配置主要用到upstream(上游服务器组)指令。

# 语法
# name参数是上游服务器组的名称
# upstream 块中将使用server指令定义组内的上游候选服务器。
upstream name { ... }
# 上下文: http配置块#upstream负载均衡虚拟节点
upstream balanceNode {server "192.168.1.2:8080"; #上游候选服务1 server "192.168.1.3:8080"; #上游候选服务2 server "192.168.1.4:8080"; #上游候选服务3 server "192.168.1.5:8080"; #上游候选服务4
}

实例中配置的balanceNode相当于一个主机节点,不过这是一个负 载均衡类型的特定功能虚拟主机。当请求过来时,balanceNode主机节 点的作用是按照默认负载均衡算法(带权重的轮询算法)在4个上游候 选服务中选取一个进行请求转发。

7.6.5 upstream的上游服务器配置

upstream块中将使用server指令定义组内的上游候选服务器。

# 语法:
# 选参数大致如下
# weight=number(设置上游服务器的权重):默认情况下, upstream使用加权轮询(Weighted Round Robin)负载均衡方法在上 游服务器之间分发请求。weight值默认为1,并且各上游服务器的 weight值相同,表示每个请求按先后顺序逐一分配到不同的上游服务器,如果某个上游服务器宕机,就自动剔除。权重越大的节点,将被分发到更多请求。
# max_conns=number(设置上游服务器的最大连接数): max_conns参数限制到上游节点的最大同时活动连接数。默认值为零, 表示没有限制。如果upstream服务器组没有通过zone指令设置共享内 存,那么在单个Worker工作进程范围内对上游服务的最大连接数进行 限制;如果upstream服务器组通过zone指令设置了共享内存,那么在 全体的Worker工作进程范围内对上游服务进行统一的最大连接数限制。
# backup(可选参数):backup参数标识该server是备份的上 游节点,当普通的上游服务(非backup)不可用时,请求将被转发到 备份的上游节点;当普通的上游服务(非backup)可用时,备份的上 游节点不接受处理请求。
# down(可选参数):down参数标识该上游server节点为不可 用或者永久下线的状态。
# max_fails=number(最大错误次数):如果上游服务不可访 问了,如何判断呢?max_fails参数是其中之一,该参数表示请求转发 最多失败number次就判定该server为不可用。max_fails参数的默认次 数为1,表示转发失败1次,该server即不可用。如果此参数设置为0, 就会禁用不可用的判断,一直不断地尝试连接后端server。
# fail_timeout=time(失败测试的时间长度):这是一个失 效监测参数,一般与上面的参数max_fails协同使用。fail_timeout的 意思是失败测试的时间长度,指的是在fail_timeout时间范围内最多 尝试max_fails次,就判定该server为不可用。fail_timeout参数的默 认值为10秒。
#
server address [parameters];
#上下文: upstream配置块# 配置共享内存区域
# 语法:
# name参数设置共享内存区的名称
# size可选参数用于设置 共享内存区域的大小
zone name [size];
# 如果配置了upstream的共享内存区域,那么其 运行时状态(包括最大连接数)在所有的Worker工作进程之间是共享 的。在name相同的情况下,不同的upstream组将共享同一个区,这种 情况下,size参数的大小值只需设置一次。
# 上下文: upstream配置块upstream zuul {# 名称为upstream_zuul,大小为64KB的共享内存区域zone upstream_zuul 64k; server "192.168.233.128:7799" weight=5 max_conns=500;# 默认权重为1server "192.168.233.129:7799" fail_timeout=20s max_fails=2;# 后备服务    server "192.168.233.130:7799" backup;
}

7.6.6 upstream的负载分配方式

  • 加权轮询

默认情况下,upstream使用加权轮询(Weighted Round Robin) 负载均衡方法在上游服务器之间分发请求,默认的权重weight值为1, 并且各上游服务器weight值相同,表示每个请求按到达的先后顺序逐 一分配到不同的上游服务器,如果某个上游服务器宕机,就自动剔 除。

指定权重weight值,weight和分配比率成正比,用于后端服务器 性能不均的情况。

  • hash指令

基于hash函数值进行负载均衡,hash函数的key可以包含文本、变 量或二者的组合

# 语法:
hash key [consistent];
# 上下文: upstream配置块upstream backend {hash $request_uri consistent; server 192.168.1.101 ;server 192.168.1.102 ;server 192.168.1.103 ;
}

如果upstream组中摘除掉一个server,就会导致hash值重 新计算,即原来的大多数key可能会寻址到不同的server上。若配置有 consistent参数,则hash一致性将选择Ketama算法。这个算法的优势 是,如果有server从upstream组里摘除掉,那么只有少数的key会重新 映射到其他的server上,即大多数key不受server摘除的影响,还走到 原来的server。这对提高缓存server命中率有很大帮助。

  • ip_hash指令

基于客户端IP的hash值进行负载平衡,这样每个客户端固定访问 同一个后端服务器,可以解决类似session不能跨服务器的问题。如果 上游server不可用,就需要手工摘除或者配置down参数。

 upstream backend {ip_hash;server 192.168.1.101:7777; server 192.168.1.102:8888; server 192.168.1.103:9999;
}

第8章 Nginx Lua编程

8.1 Nginx Lua编程的主要应用场景

API网关:实现数据校验前置、请求过滤、API请求聚合、AB 测试、灰度发布、降级、监控等功能,著名的开源网关Kong就是基于 Nginx Lua开发的。

高速缓存:可以对响应内容进行缓存,减少到后端的请求, 从而提升性能。比如,Nginx Lua可以和Java容器(如Tomcat)、 Redis整合,由Java容器进行业务处理和数据缓存,而Nginx负责读缓 存并进行响应,从而解决Java容器的性能瓶颈。

简单的动态Web应用:可以完成一些业务逻辑处理较少但是 耗费CPU的简单应用,比如模板页面的渲染。一般的Nginx Lua页面渲 染处理流程为:从Redis获取业务处理结果数据,从本地加载XML/HTML 页面模板,然后进行页面渲染。

网关限流:缓存、降级、限流是解决高并发的三大利器, Nginx内置了令牌限流的算法,但是对于分布式的限流场景,可以通过 Nginx Lua编程定制自己的限流机制。

8.2 Nginx Lua编程简介

8.2.1 ngx_lua简介

Lua是一种轻量级、可嵌入式的脚本语言,可以非常容易地嵌入其 他语言中使用。因为Lua的小巧轻量级,可以在Nginx中嵌入Lua VM(Lua虚拟机),请求时创建一个VM,请求结束时回收VM。

ngx_lua是Nginx的一个扩展模块,将Lua VM嵌入Nginx中,从而可 以在Nginx内部运行Lua脚本,使得Nginx变成一个Web容器;这样开发 人员就可以使用Lua语言开发高性能Web应用。ngx_lua提供了与Nginx 交互的很多API,对于开发人员来说只需要学习这些API就可以进行功 能开发,而对于开发Web应用来说,如果开发人员接触过Servlet,可 以发现ngx_lua开发和Servlet类似,无外乎就是知道API的接收请求、 参数解析、功能处理、返回响应这些内容。

8.2.2 Nginx Lua项目的创建

在IDEA创建Lua脚本的工程。在工程类型选择时选择Lua项目类型, 剩余的操作只要选择默认值,直到创建完成即可。

8.2.3 Lua项目的工程结构

8.2.4 Lua项目的启动

8.3 Lua开发基础

8.3.1 Lua模块的定义和使用

-- src/luaScript/module/common/basic.lua
--定义一个应用程序公有的Lua对象app_info
local app_info = { version = "0.10" }
-- 增加一个path属性,保存Nginx进程所保存的Lua模块路径,包括conf文件配置的部分路径 app_info.path = package.path;-- 局部函数,取得最大值
local function max(num1, num2)if (num1 > num2) thenresult = num1; elseresult = num2; endreturn result;
end-- 统一的模块对象
local _Module = {app_info = app_info;max = max;
}
return _Module

模块内的所有对象、数据、函数都定义成局部变量或者局部函 数。然后,对于需要暴露给外部的对象或者函数,作为成员属性保存 到一个统一的Lua局部对象(如_Module)中,通过返回这个统一的局 部对象将内部的成员对象或者方法暴露出去,从而实现Lua的模块化封 装。

8.3.2 Lua模块的使用

---src/luaScript/module/demo/helloworld.lua
---启动调试
local mobdebug = require("luaScript.initial.mobdebug");
mobdebug.start();
--导入自定义的模块
local basic = require("luaScript.module.common.basic ");--使用模块的成员属性
ngx.say("Lua path is: " .. basic.app_info.path);
ngx.say("<br>" );
--使用模块的成员方法
ngx.say("max 1 and 11 is: ".. basic.max(1,11) );

8.3.3 Lua的数据类型

Lua是弱类型语言,和JavaScript等脚本语言类似,变量没有固定 的数据类型,每个变量可以包含任意类型的值。使用内置的type(...) 方法可以获取该变量的数据类型。

8.3.4 Lua的字符串

  • 表示字符串的方式

(1)使用一对匹配的半角英文单引 号,例如'hello';

(2)使用一对匹配的半角英文双引号,例 如"hello";

(3)使用一种双方括号“[[]]”括起来的方式定义,例 如[["add\name",'hello']]

  • 主要的字符串操作
--(1)“..”:字符串拼接符号。
local function stringOperator(s)local here="这里是:" .. "高性能研习社群" .. "疯狂创客圈"; print(here);basic.log("字符串拼接演示",here);
end--(2)string.len(s):获取字符串的长度。
local function stringOperator(s)local here = "这里是:" .. "高性能研习社群" .. "疯狂创客圈"; basic.log("获取字符串的长度", string.len(here));basic.log("获取字符串的长度方式二", #here);
end--(3)string.format(formatString,...):格式化字符串。
--简单的圆周率格式化规则
string.format(" 保留两位小数的圆周率 %.4f", 3.1415926);
--格式化日期
string.format("%s %02d-%02d-%02d", "今天is:", 2020, 1, 1));--(4)string.find(s,pattern[,init[,plain]]):字符串匹配。
--在s字符串中查找第一个匹配正则表达式pattern的子字符串,返 回第一次在s中出现的满足条件的子串的开始位置和结束位置,若匹配 失败,则返回nil。第三个参数init默认为1,表示从起始位置1开始找 起。第四个参数的值默认为false,表示第二个参数pattern为正则表 达式,默认进行表达式匹配,当第四个参数为true时,只会把pattern 看成一个普通字符串。
local function stringOperator(s)local here="这里是:" .. "高性能研习社群" .. "疯狂创客圈"; local find = string.find; basic.log("字符串查找",find(here,"疯狂创客圈"));
end--(5)string.upper(s):字符串转成大写
--(6)string.lower(s):字符串转成小写
local function stringOperator(s)local src = "Hello world!"; basic.log("字符串转成大写",string.upper(src)); basic.log("字符串转成小写",string.lower(src));
end

8.3.5 Lua的数组容器

  • Lua数组要点

要点一:Lua数组内部实际采用哈希表保存键-值对,这一点和 Java的容器HashMap类似。不同的是,Lua在初始化一个普通数组时, 如果不显式地指定元素的key,就会默认用数字索引作为key。

要点二:定义一个数组使用花括号,中间加上初始化的元素序 列,元素之间以逗号隔开即可。

要点三:普通Lua数组的数字索引对应于Java的元素下标,是从1 开始计数的。

要点四:普通Lua数组的长度的计算方式和C语言有些类似。从第 一个元素开始,计算到最后一个非nil的元素为止,中间的元素数量就 是长度。

要点五:取得数组元素值使用[]符号,形式为array[key],其中 array代表数组变量名称,key代表元素的索引,这一点和Java语言类 似。对于普通的数组,key为元素的索引值;对于键-值对(Key-Value Pair)类型的数组容器,key就是键-值对中的key。

  • table模块主要操作

--定义一个数组
local array1 = { "这里是:", "高性能研习社群", "疯狂创客圈" }
--定义一个K-V元素类型的数组
local array2 = { k1 = "这里是:", k2 = "高性能研习社群", k3 = "疯狂创客圈" } --(1)table.getn(t):获取长度。
--取得数组长度
basic.log("使用table.getn (t)获取长度", table.getn (array1));
basic.log("使用 一元操作符#获取长度", #array1 );--(2)table.concat(array,[,sep,[,i,[,j]]]):连接数组元素。
--按照array[i]..sep..array[i+1]..sep..array[j]的方式将普通 数组中所有的元素连接成一个字符串并返回。分隔字符串sep默认为空白字符串。起始位置i默认为1,结束位置j默认是array的长度。如果i 大于j,就返回一个空字符串。
local testTab = { 1, 2, 3, 4, 5, 6, 7 }
basic.log("连接元素",table.concat(testTab)) --输出: 1234567
basic.log("带分隔符连接元素",table.concat(testTab, "*", 1, 3)) --输出: 1*2*3--(3)table.insert(array,[pos,],value):插入元素。
--在array的位置pos处插入元素value,后面的元素向后顺移。pos 的默认值为#list+1,因此调用table.insert(array,x)会将x插在 普通数组array的末尾。
local testTab = { 1, 2, 3, 4 }
--插入一个元素到末尾
table.insert(testTab, 5)
basic.printTable(testTab) --输出: 1 2 3 4 5
--插入一个元素到位置索引2
table.insert(testTab, 2, 10)
basic.printTable(testTab) --输出: 1 10 2 3 4 5
--在屏幕上输出table元素
function _printTable(tab)local output = ""for i, v in ipairs(tab) dongx.say(v .. " "); endngx.say("<br>");
end--(4)table.remove(array[,pos]):删除元素
--删除array中pos位置上的元素,并返回这个被删除的值。当pos是 1到#list之间的整数时,将后面的所有元素前移一位,并删除最后一个元素。
testTab = { 1, 2, 3, 4, 5, 6, 7 }
--删除最后一个元素
table.remove(testTab)
basic.printTable(testTab) --输出: 1 2 3 4 5 6
--删除第二个元素
table.remove(testTab, 2)
basic.printTable(testTab) --输出: 1 3 4 5 6

8.3.6 Lua的控制结构

  • if
-- 1.单分支结构:if
Local x = '疯狂创客圈'
if x == '疯狂创客圈' thenbasic.log("单分支演示:", "这个是一个高性能研习社群")
end-- 2.两分支结构:if-else
local x = '疯狂创客圈'
if x == '这个是一个高性能研习社群' thenbasic.log("两分支演示:", "这儿是疯狂创客圈")
elsebasic.log("两分支演示:", "这儿还是疯狂创客圈")
end-- 3.多分支结构:if-elseif-else
local x = '疯狂创客圈'
if x == '这个是一个高性能研习社群' thenbasic.log("多分支演示:", "这儿是疯狂创客圈")
elseif x == '疯狂创客圈' thenbasic.log("多分支演示:", "这个是一个高性能研习社群")
elsebasic.log("多分支演示:", "这儿不是疯狂创客圈")
end
  • for
-- (1)基础for循环
-- 步长表达 式step是可选的,如果没有设置,默认值就为1
-- 在循环过程中不要改变迭代变量var的值,否 则会带来不可预知的影响。
for var = begin, finish, step do--body
end-- (2)增强版的foreach循环
for key, value in pairs(table) do--body
end
--foreach循环,打印table t中所有的键(key)和值(value)
local days = {"Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"
}for key, value in pairs(days) dongx.say(key .. ":" .. value .. "; ")
end
-- 输出结果
-- 1:Sunday; 2:Monday; 3:Tuesday; 4:Wednesday; 5:Thursday; 6:Friday; 7:Saturday;local days2 = { Sunday = 1, Monday = 2, Tuesday = 3, Wednesday = 4, Thursday = 5, Friday = 6, Saturday = 7 }
for key, value in pairs(days2) do ngx.say(key .. ":" .. value .. "; ")
end
-- 输出结果
-- Tuesday:3; Monday:2; Sunday:1; Thursday:5; Friday:6; Wednesday:4; Saturday:7;

8.3.7 Lua的函数定义

  • 使用函数的好处

(1)降低程序的复杂性:模块化编程的好处是将复杂问题变成一个个小问 题,然后分而治之。把函数作为一个独立的模块或者当作一个黑盒,而不需要 考虑函数里面的细节。

(2)增强代码的复用度:当程序中有相同的代码部分时,可以把这部分写 成一个函数,通过调用函数来实现这部分代码的功能,可以节约空间、减少代 码长度。

(3)隐含局部变量:在函数中使用局部变量,变量的作用范围不会超出函 数,这样就不会给外界带来干扰。

  • 函数的定义
-- 函数定义说明
-- (1)optional_function_scope:该参数表示所定义的函数是全局函数还 是局部函数,该参数是可选参数,默认为全局函数,如果定义为局部函数,那 么设置为关键字local。
-- (2)function_name:该参数用于指定函数名称。
-- (3)argument1,argument2,argument3,...,argumentn:函数参数,多 个参数以逗号隔开,也可以不带参数。
-- (4)function_body:函数体,函数中需要执行的代码语句块。
-- (5)result_params_comma_separated:函数返回值,Lua语言中的函数可 以返回多个值,每个值以逗号隔开。
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn) function_bodyreturn result_params_comma_separated
end-- 示例:局部函数,取得最大值
local function max(num1, num2)local result = nil; if (num1 > num2) thenresult = num1; elseresult = num2; endreturn result;
end-- 可变参数:在屏幕上打印日志,可以输入多个打印的数据
local function log(...)local args = { ... } --这里的...和{}符号中间需要有空格号,否则会出错 for i, v in pairs(args) doprint("index:", i, " value:", v)ngx.say(v .. ","); endngx.say("<br>");
end-- 示例:返回多个值
local s, e = string.find("hello world", "lo") -- 返回值为:4 5
print(s, e) -- 输出:4 5
  • 函数参数值的传递方式

值传递:Lua中的函数的参数大部分是按值传递的。值传 递就是调用函数时,把实参的值通过赋值传递给形参,然后形参的改变和实参 就没有关系了。在这个过程中,实参和形参是通过在参数表中的位置匹配起来 的。

引用传递:table类型的传递方式是 引用传递。当函数参数是table类型时,传递进来的是实际参数的引用(内存地 址),此时在函数内部对该table所做的修改会直接对实际参数生效,而无须自 己返回结果和让调用者进行赋值。

  • 注意点

(1)利用名字来解释函数、变量的目的是使人通过名字就能看出来函数的 作用。让代码自己说话,不需要注释最好。

(2)由于全局变量一般会占用全局名字空间,同时也有性能损耗(查询全局环境表的开销),因此我们应当尽量使用“局部函数”,在开头加上local修 饰符。

(3)由于函数定义本质上就是变量赋值,而变量的定义总是要放置在变量 使用之前,因此函数的定义也需要放置在函数调用之前。

8.3.8 Lua的面向对象编程

--正方形类
_Square = { side = 0 }
_Square.__index = _Square
--类的方法getArea
function _Square.getArea(self)return self.side *self.side;
end
--类的方法new
function _Square.new(self, side)local cls = {} setmetatable(cls, self) cls.side = side or 0 return cls
end
--一个统一的模块对象
local _Module = {...Square = _Square;
}-- (1)metatable元表:简单来说,如果一个表(也叫对象)的属性找不到,就去它的元表中查找。通过setmetatable(table, metatable)方法设置一个表的元表。
-- (2)第一点不完全对。为什么呢?准确来说,不是直接查找元表的属性,是去元表中的一个特定的属性,名为__index的表(对象)中查找属性。__index也是一个table类型,Lua会在__index中查找相应的属性。
所以,在上面的代码中,_Square表设置了__index属性的值为自,当为新创建的new对象查找getArea方法时,需要在原表_Square表的__index属性中查找,找到的就是getArea方法的定义。这个调用的链条如果断了,新创建的new对象的getArea方法就会导航失败。-- 在调用Square类的方法时,建议将点号改为冒号。使用冒号进 成员方法调用时,Lua会隐性传递一个self参数,它将调用者对象本身 作为第一个参数传递进来。
ngx.say("<br><hr>下面是面向对象操作的演示:<br>");
local Square = dataType.Square;
local square = Square:new(20);
ngx.say("正方形的面积为", square:getArea());

8.4 Nginx Lua编程基础

8.4.1 Nginx Lua的执行原理

8.4.2 Nginx Lua的配置指令

8.4.3 Nginx Lua的内置常量和变量

8.5 Nginx Lua编程实例

8.5.1 Lua脚本获取URL中的参数

8.5.2 Nginx Lua的内置方法

8.5.3 通过ngx.header设置HTTP响应头

8.5.4 Lua访问Nginx变量

8.5.5 Lua访问请求上下文变量

8.6 重定向与内部子请求

8.6.1 Nginx Lua内部重定向

8.6.2 Nginx Lua外部重定向

8.6.3 ngx.location.capture子请求

8.6.4 ngx.location.capture_multi并发子请求

8.7 Nginx Lua操作Redis

8.7.1 Redis的CRUD基本操作

8.7.2 实战:封装一个操作Redis的基础类

8.7.3 在Lua中使用Redis连接池

性能优化的第一件事情就是把短连接改成长连接,可以减少大量创 建连接、拆除连接的时间。从性能上来说肯定要比短连接好很多,但还 是有比较大的浪费。

性能优化的第二件事情就是使用连接池。通过一个连接池pool将所 有长连接缓存和管理起来,谁需要使用,就从这里取走,干完活立马放 回来。

实际上,大家在开发过程中用到的连接池是非常多的,比如HTTP连 接池、数据库连接池、消息推送连接池等。实际上,几乎所有点到点之 间的连接资源复用都需要通过连接池完成。

8.8 Nginx Lua编程实战案例

8.8.1 Nginx+Redis进行分布式访问统计

8.8.2 Nginx+Redis+Java容器实现高并发访问

8.8.3 Nginx+Redis实现黑名单拦截

  • 静态IP黑名单拦截实现方式:

(1)在操作系统层面配置iptables防火墙规则,拒绝黑名单中IP 的网络请求。

(2)使用Nginx网关的deny配置指令拒绝黑名单中IP的网络请求。

(3)在Nginx网关的access处理阶段,通过Lua脚本检查客户端IP 是否在黑名单中。

(4)在Spring Cloud内部网关(如Zuul)的过滤器中检查客户端 IP是否在黑名单中。

  • 动态IP黑名单拦截

8.8.4 使用Nginx Lua共享内存

Nginx Lua共享内存就是在内存块中分配出一个内存空间,该共享 内存是一种字典结构,类似于Java Map的键-值(Key-Value)映射结 构。同一个Nginx下的Worker进程都能访问存储在这里面的变量数据。

第9章 限流原理与实战

举一个具体的例子,假设某个接口能够扛住的QPS为10 000,这时有20 000个请求进来,经过限流模块,会先放10 000个请求,其余的 请求会阻塞一段时间。不简单粗暴地返回404,让客户端重试,同时又能起到流量削峰的作用

每个API接口都是有访问上限的,当访问频率或者并发量超过其承 受范围时,就必须通过限流来保证接口的可用性或者降级可用性,给 接口安装上保险丝,以防止非预期的请求对系统压力过大而引起系统 瘫痪。

9.1 限流策略原理与参考实现

9.1.1 三种限流策略:计数器、漏桶和令牌桶

(1)计数器:在一段时间间隔内(时间窗),处理请求的最大数 量固定,超过部分不做处理。

(2)漏桶:漏桶大小固定,处理速度固定,但请求进入的速度不固定(在突发情况请求过多时,会丢弃过多的请求)。

(3)令牌桶:令牌桶的大小固定,令牌的产生速度固定,但是消耗令牌(请求)的速度不固定(可以应对某些时间请求过多的情 况)。每个请求都会从令牌桶中取出令牌,如果没有令牌,就丢弃这 次请求。

9.1.2 计数器限流原理和Java参考实现

9.1.3 漏桶限流原理和Java参考实现

9.1.4 令牌桶限流原理和Java参考实现

9.2 分布式计数器限流

9.2.1 实战:Nginx Lua分布式计数器限流

(1)数据一致性问题:计数器的读取和自增由两次Redis远程操作完 成,如果存在多个网关同时进行限流,就可能会出现数据一致性问题。

(2)性能问题:同一次限流操作需要多次访问Redis,存在多次网络传 输,大大降低了限流的性能。

9.2.2 实战:Redis Lua分布式计数器限流

优点:

(1)减少网络开销:不使用Lua的代码需要向Redis发送多次请求,而脚本只需 一次即可,减少网络传输。

(2)原子操作:Redis将整个脚本作为一个原子执行,无须担心并发,也就无须 事务。

(3)复用:只要Redis不重启,脚本加载之后会一直缓存在Redis中,其他客户 端可以通过sha1编码执行。

9.3 Nginx漏桶限流详解

使用Nginx可通过配置的方式完成接入层的限流,其 ngx_http_limit_req_module模块所提供的limit_req_zone和limit_req 两个指令使用漏桶算法进行限流。其中,limit_req_zone指令用于定义 一个限流的具体规则(或者计数内存区),limit_req指令应用前者定 义的规则完成限流动作。

9.4 实战:分布式令牌桶限流

9.4.1 分布式令牌桶限流Lua脚本

9.4.2 Java分布式令牌桶限流

9.4.3 Java分布式令牌桶限流的自验证

第10章 Spring Cloud + Nginx秒杀实战

10.1 秒杀系统的业务功能和技术难点

10.1.1 秒杀系统的业务功能

10.1.2 秒杀系统面临的技术难题

(1)限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部 分流量,只允许少部分流量进入服务后端。

(2)分布式缓存:秒杀系统最大的瓶颈一般都是数据库读写,由 于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻 辑转移到分布式缓存,效率就会有极大提升。

(3)可拓展:秒杀系统的服务节点一定是可以弹性拓展的。如果 流量来了,就可以按照流量预估进行服务节点的动态增加和摘除。比 如淘宝、京东等双十一活动时,会增加大量机器应对交易高峰。

(4)超卖或者少卖问题:比如10万次请求同时发起秒杀请求,正 常需要进行10万次库存扣减,但是由于某种原因,往往会造成多减库 存或者少减库存,就会出现超卖或少卖问题。

(5)削峰:秒杀系统是一个高并发系统,采用异步处理模式可以 极大地提高系统并发量,实际上削峰的典型实现方式就是通过消息队 列实现异步处理。限流完成之后,对于后端系统而言,秒杀系统仍然 会瞬时涌入大量请求,所以在抢购一开始会有很高的瞬间峰值。高峰 值流量是压垮后端服务和数据库很重要的原因,秒杀后端需要将瞬间 的高流量变成一段时间平稳的流量,常用的方法是利用消息中间件进 行请求的异步处理。

10.2 秒杀系统的系统架构

10.2.1 秒杀的分层架构

(1)客户端:负责内容提速和交互控制

客户端需要完成秒杀商品的静态化展示。无论是在桌面浏览器还是 在移动端App展示秒杀商品,秒杀商品的图片和文字元素都需要尽可能 静态化,尽量减少动态元素。这样就可以通过CDN来提速和抗峰值。

另外,在客户端这一层的用户交互上需要具备一定的控制用户行为 和禁止重复秒杀的能力。比如,当用户提交秒杀请求之后,可以将秒杀 按钮置灰,禁止重复提交。

(2)接入层:负责认证、负载均衡、限流

秒杀系统的特点是并发量极大,但实际的优惠商品有限,秒杀成功 的请求数量很少,如果不在接入层进行拦截,大量请求就会造成数据库 连接耗尽、服务端线程耗尽,导致整体雪崩。因此,必须在接入层进行 用户认证、负载均衡、接口限流。

(3)业务层:负责保障秒杀数据的一致性

秒杀的业务逻辑主要是下订单和减库存,都是数据库操作。大家都 知道,数据库层只能承担“能力范围内”的访问请求,既是非常脆弱的 一层,又是需要进行事务保护的一层。在业务层还需要防止超出库存的 秒杀(超卖和少卖),为了安全起见,可以使用分布式锁对秒杀的数据 库操作进行保护。

10.2.2 秒杀的限流架构

在接入层可以进行两个级别的限流策略:应用级别的限 流和接口级别的限流。

10.2.3 秒杀的分布式锁架构

解决超卖或者少卖问题有效的办法之一就是利用分布式锁将对同一 个商品的并行数据库操作予以串行化。秒杀场景的分布式锁应该具备如 下条件:

(1)一个方法在同一时间只能被一个机器的一个线程执行。

(2)高可用地获取锁与释放锁。

(3)高性能地获取锁与释放锁。

(4)具备可重入特性。

(5)具备锁失效机制,防止死锁。

(6)具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失 败。

ZooKeeper分布式锁虽然高可靠,但是性能不高,不能满足秒杀场 景分布式锁的第3个条件(高性能地获取锁与释放锁),所以在秒杀的 场景建议使用Redis分布式锁来保护秒杀的数据库操作。

10.2.4 秒杀的削峰架构

削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的 访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。通过 消息队列可以大大地缓冲瞬时流量,把同步的直接调用转换成异步的间 接推送,中间通过一个队列在入口承接瞬时的流量洪峰,在出口平滑地 将消息推送出去。消息队列就像“水库”一样,拦蓄上游的洪水,削减 进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

10.3 秒杀业务的参考实现

10.3.1 秒杀的功能模块和接口设计

10.3.2 数据表和PO实体类设计

10.3.3 使用分布式ID生成器

10.3.4 秒杀的控制层设计

10.3.5 service层逻辑:获取秒杀令牌

10.3.6 service层逻辑:执行秒杀下单

10.3.7 秒杀的Lua脚本设计

10.3.8 BusinessException定义

10.4 Zuul内部网关实现秒杀限流

10.5 Nginx高性能秒杀和限流

10.5.1 Lua脚本:获取秒杀令牌

10.5.2 Lua脚本:执行令牌桶限流

《Spring Cloud、Nginx高并发核心编程》读书笔记【END】相关推荐

  1. 高并发核心编程Spring Cloud+Nginx秒杀实战,秒杀业务的参考实现

    秒杀业务的参考实现 本节从功能入手重点介绍Spring Cloud秒杀实战业务处理的3层实现:dao层.service层.controller层. 秒杀的功能模块和接口设计 秒杀系统的实现有多种多样的 ...

  2. 可能要用心学高并发核心编程,限流原理与实战,分布式令牌桶限流

    实战:分布式令牌桶限流 本节介绍的分布式令牌桶限流通过Lua+Java结合完成,首先在Lua脚本中完成限流的计算,然后在Java代码中进行组织和调用. 分布式令牌桶限流Lua脚本 分布式令牌桶限流Lu ...

  3. 《Java高并发核心编程.卷2,多线程、锁、JMM、JUC、高并发设计模式》

    <Java高并发核心编程.卷2,多线程.锁.JMM.JUC.高并发设计模式> 目录 第1章 多线程原理与实战 1.2 无处不在的进程和线程 1.2.1 进程的基本原理 1.2.2 线程的基 ...

  4. 华为18级工程师耗时三年才总结出这份Java亿级高并发核心编程手册

    移动时代.5G时代.物联网时代的大幕已经开启,新时代提升了对Java应用的高性能.高并发的要求,也抬升了Java工程师的技术台阶和面试门槛. 很多公司的面试题从某个侧面反映了生产场景的技术要求.之前只 ...

  5. 《Spring Cloud 微服务架构进阶》读书笔记

    前页 随着 DevOps 和以 Docker 为主的容器技术的发展,云原生应用架构和微服 务变得流行起来. 云原生包含的内容很多,如 DevOps.持续交付.微服务.敏捷等 第一章,微服务架构介绍 架 ...

  6. C++Windows核心编程读书笔记(转)

    http://www.makaidong.com/(马开东博客) 这篇笔记是我在读<windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的 ...

  7. pthon核心编程-读书笔记:知识点摘录与总结(方便理解和快速记忆)

    Python 中的列表(大小可变的数组)和字典(哈希表)就是内建于语言本身的.在核心语言中提供这些重要的构建单元,可以鼓励人们使用它们, 缩短开发时间与代码量,产生出可读性更好的代码.C不提供, c+ ...

  8. Python核心编程读书笔记

    转载自 http://blog.csdn.net/hunter8777/article/category/786856 本次笔记针对原书1~2章节 第一章:欢迎来到Python的世界 1.在C语言中, ...

  9. windows核心编程读书笔记(一)

    第一章:错误处理 通过GetLastError函数获得更多的错误信息,或者在监视框中使用@err,hr(vs2005)获得错误信息,而不仅仅是错误编号. 第二章:字符和字符串处理 在应用程序中,应确保 ...

  10. 《Java高并发程序设计》读书笔记 第二章 并行程序基础

最新文章

  1. 临危不乱,.Net+IIS环境经常出现的问题及排障。
  2. zeptojs-跑马灯效果
  3. 基于倒谱法、自相关法、短时幅度差法的基音频率估计算法(MATLAB及验证)
  4. 中等职业计算机等级考试,中等职业学校计算机等级考试题库(含答案):EXCEL
  5. Input.GetAxis(Mouse ScrollWheel)控制摄像机视野缩放
  6. [数位dp] Jzoj P4239 光棍
  7. [转载] python win32api 使用小技巧
  8. C/C++ 创建两个链表,实现两个链表低位到高位相加,并输出链表
  9. 字符串,字典,元祖,列表
  10. IO流总结-知识体系
  11. python和c的语法区别_python和c语言语法有什么区别?
  12. 下载Chrome浏览器历史版本方法
  13. 【逗老师的无线电】宝峰神机刷OpenGD77摇身变为DMR大热点
  14. TNS-12555 permission denied
  15. CSS几种常见的页面布局方式介绍
  16. 教你辨别专利编号| 专利的专利号申请号公开号公告号
  17. 基于 FPGA 使用 Verilog 实现 DS18B20 温度采集以及数码管显示项目源码【免费——互相学习】
  18. springboot logback日志问题
  19. 专业恢复电脑数据软件Easyrecovery16
  20. 【python+SQLAlchemy】

热门文章

  1. 量子力学第十一弹——变分法
  2. 波浪能及波能流的推导
  3. vue-cli3的eslint配置问题
  4. 一代JS代码可以搞定机器自动刷票,投票页数据验证很重要
  5. WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED
  6. 项目管理知识体系指南(九)项目沟通管理
  7. 立方人物|吴胜男律师:一位温而不沸的90后执行主任
  8. phantomjs自动截图生成图片
  9. Unity发布windows程序,Fullscreen Mode设置为Windowed,可运行总是全屏
  10. 随机数种子(seed)