Java全栈知识点问题汇总(下)-(一)

58 阅读1小时+

Java 全栈知识点问题汇总(下)

提示

Java 全栈知识点问题汇总(下), Java 全栈知识点问题汇总(上)。@pdai

9 开发基础

9.1 常用类库

平时常用的开发工具库有哪些?

  • Apache Common

    • Apache Commons是对JDK的拓展,包含了很多开源的工具,用于解决平时编程经常会遇到的问题,减少重复劳动。
  • Google Guava

    • Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。
  • Hutool

    • 国产后起之秀,Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率
  • Spring常用工具类

    • Spring作为常用的开发框架,在Spring框架应用中,排在ApacheCommon,Guava, Huool等通用库后,第二优先级可以考虑使用Spring-core-xxx.jar中的util包

Java常用的JSON库有哪些?有啥注意点?

  • FastJSON(不推荐,漏洞太多)

  • Jackson

  • Gson

    • 序列化
    • 反序列化
    • 自定义序列化和反序列化

Lombok工具库用来解决什么问题?

我们通常需要编写大量代码才能使类变得有用。如以下内容:

  • toString()方法
  • hashCode() and equals()方法
  • Getter and Setter 方法
  • 构造函数

对于这种简单的类,这些方法通常是无聊的、重复的,而且是可以很容易地机械地生成的那种东西(ide通常提供这种功能)。

  • @Getter/@Setter示例
@Setter(AccessLevel.PUBLIC)
@Getter(AccessLevel.PROTECTED)
private int id;
private String shap;
  • @ToString示例
@ToString(exclude = "id", callSuper = true, includeFieldNames = true)
public class LombokDemo {
    private int id;
    private String name;
    private int age;
    public static void main(String[] args) {
        //输出LombokDemo(super=LombokDemo@48524010, name=null, age=0)
        System.out.println(new LombokDemo());
    }
}
  • @EqualsAndHashCode示例
@EqualsAndHashCode(exclude = {"id", "shape"}, callSuper = false)
public class LombokDemo {
    private int id;
    private String shap;
}

为什么很多公司禁止使用lombok?

可以使用而且有着广泛的使用,但是需要理解部分注解的底层和潜在问题,否则会有坑:

  • @Data: 如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放。

  • 代码可读性,可调试性低 在代码中使用了Lombok,确实可以帮忙减少很多代码,因为Lombok会帮忙自动生成很多代码。但是这些代码是要在编译阶段才会生成的,所以在开发的过程中,其实很多代码其实是缺失的。

  • Lombok有很强的侵入性

    • 强J队友,如果项目组中有一个人使用了Lombok,那么其他人就必须也要安装IDE插件。
    • 如果我们需要升级到某个新版本的JDK的时候,若其中的特性在Lombok中不支持的话就会受到影响
  • Lombok破坏了封装性

举个简单的例子,我们定义一个购物车类:

@Data
public class ShoppingCart { 
​
    //商品数目
    private int itemsCount; 
​
    //总价格
    private double totalPrice; 
​
    //商品明细
    private List items = new ArrayList<>();
​
}
​
//例子来源于《极客时间-设计模式之美》

我们知道,购物车中商品数目、商品明细以及总价格三者之前其实是有关联关系的,如果需要修改的话是要一起修改的。

但是,我们使用了Lombok的@Data注解,对于itemsCount 和 totalPrice这两个属性。虽然我们将它们定义成 private 类型,但是提供了 publicgettersetter 方法。

外部可以通过 setter 方法随意地修改这两个属性的值。我们可以随意调用 setter 方法,来重新设置 itemsCount、totalPrice 属性的值,这也会导致其跟 items 属性的值不一致。

而面向对象封装的定义是:通过访问权限控制,隐藏内部数据,外部仅能通过类提供的有限的接口访问、修改内部数据。所以,暴露不应该暴露的 setter 方法,明显违反了面向对象的封装特性。

好的做法应该是不提供getter/setter,而是只提供一个public的addItem方法,同时去修改itemsCount、totalPrice以及items三个属性。(所以不能一股脑使用@Data注解)

  • 此外,Java14 提供的record语法糖,来解决类似问题
public record Range(int min, int max) {}

MapStruct工具库用来解决什么问题?

MapStruct是一款非常实用Java工具,主要用于解决对象之间的拷贝问题,比如PO/DTO/VO/QueryParam之间的转换问题。区别于BeanUtils这种通过反射,它通过编译器编译生成常规方法,将可以很大程度上提升效率。

举例:

@Mapper
public interface UserConverter {
    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);
​
    @Mapping(target = "gender", source = "sex")
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    UserVo do2vo(User var1);
​
    @Mapping(target = "sex", source = "gender")
    @Mapping(target = "password", ignore = true)
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    User vo2Do(UserVo var1);
​
    List<UserVo> do2voList(List<User> userList);
​
    default List<UserVo.UserConfig> strConfigToListUserConfig(String config) {
        return JSON.parseArray(config, UserVo.UserConfig.class);
    }
​
    default String listUserConfigToStrConfig(List<UserVo.UserConfig> list) {
        return JSON.toJSONString(list);
    }
}

Lombok和MapStruct工具库的原理?

会发现在Lombok使用的过程中,只需要添加相应的注解,无需再为此写任何代码。自动生成的代码到底是如何产生的呢?

核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。

  • 运行时解析

运行时能够解析的注解,必须将@Retention设置为RUNTIME, 比如@Retention(RetentionPolicy.RUNTIME),这样就可以通过反射拿到该注解。java.lang,reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。

  • 编译时解析

编译时解析有两种机制,分别简单描述下:

1)Annotation Processing Tool

apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:

  • api都在com.sun.mirror非标准包下
  • 没有集成到javac中,需要额外运行

2)Pluggable Annotation Processing API

JSR 269: Pluggable Annotation Processing API自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,这时javac执行的过程如下:

Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程中,它产生作用的具体流程如下:

  • javac对源代码进行分析,生成了一棵抽象语法树(AST)
  • 运行过程中调用实现了“JSR 269 API”的Lombok程序
  • 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
  • javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)

从上面的Lombok执行的流程图中可以看出,在Javac 解析成AST抽象语法树之后, Lombok 根据自己编写的注解处理器,动态地修改 AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而JDK的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些但是带来的性能损耗更加大。

9.2 网络协议和工具

什么是754层网络模型?

全局上理解 7层协议,4层,5层的对应关系。

OSI依层次结构来划分:应用层(Application)、表示层(Presentation)、会话层(Session)、传输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)

TCP建立连接过程的三次握手?

TCP有6种标识:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急); 然后我们来看三次握手

  • 什么是三次握手

为了保证数据能到达目标,TCP采用三次握手策略:

  1. 发送端首先发送一个带SYN(synchronize)标志的数据包给接收方【第一次的seq序列号是随机产生的,这样是为了网络安全,如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间的初始化序列号,并且伪造序列号进行攻击】
  2. 接收端收到后,回传一个带有SYN/ACK(acknowledgement)标志的数据包以示传达确认信息【SYN 是为了告诉发送端,发送方到接收方的通道没问题;ACK 用来验证接收方到发送方的通道没问题】
  3. 最后,发送端再回传一个带ACK标志的数据包,代表握手结束若在握手某个过程中某个阶段莫名中断,TCP协议会再次以相同的顺序发送相同的数据包
  • 为什么要三次握手

三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的

  1. 第一次握手,发送端:什么都确认不了;接收端:对方发送正常,自己接受正常
  2. 第二次握手,发送端:对方发送,接受正常,自己发送,接受正常 ;接收端:对方发送正常,自己接受正常
  3. 第三次握手,发送端:对方发送,接受正常,自己发送,接受正常;接收端:对方发送,接受正常,自己发送,接受正常
  • 两次握手不行吗?为什么TCP客户端最后还要发送一次确认呢

主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。经典场景:客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了。

  1. 由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。
  2. 此时此前滞留的那一次请求连接,网络通畅了到达服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
  3. 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
  • 为什么三次握手,返回时,ack 值是 seq 加 1(ack = x+1)
  1. 假设对方接收到数据,比如sequence number = 1000,TCP Payload = 1000,数据第一个字节编号为1000,最后一个为1999,回应一个确认报文,确认号为2000,意味着编号2000前的字节接收完成,准备接收编号为2000及更多的数据
  2. 确认收到的序列,并且告诉发送端下一次发送的序列号从哪里开始(便于接收方对数据排序,便于选择重传)
  • TCP三次握手中,最后一次回复丢失,会发生什么
  1. 如果最后一次ACK在网络中丢失,那么Server端(服务端)该TCP连接的状态仍为SYN_RECV,并且根据 TCP的超时重传机制依次等待3秒、6秒、12秒后重新发送 SYN+ACK 包,以便 Client(客户端)重新发送ACK包
  2. 如果重发指定次数后,仍然未收到ACK应答,那么一段时间后,Server(服务端)自动关闭这个连接
  3. 但是Client(客户端)认为这个连接已经建立,如果Client(客户端)端向Server(服务端)发送数据,Server端(服务端)将以RST包(Reset,标示复位,用于异常的关闭连接)响应,此时,客户端知道第三次握手失败

SYN洪泛攻击(SYN Flood,半开放攻击),怎么解决?

  • 什么是SYN洪范泛攻击

SYN Flood利用TCP协议缺陷,发送大量伪造的TCP连接请求,常用假冒的IP或IP号段发来海量的请求连接的第一个握手包(SYN包),被攻击服务器回应第二个握手包(SYN+ACK包),因为对方是假冒IP,对方永远收不到包且不会回应第三个握手包。导致被攻击服务器保持大量SYN_RECV状态的“半连接”,并且会重试默认5次回应第二个握手包,大量随机的恶意syn占满了未完成连接队列,导致正常合法的syn排不上队列,让正常的业务请求连接不进来。【服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击】

  • 如何检测 SYN 攻击?

当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击【在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击】

  • 怎么解决? SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害,
  1. 缩短超时(SYN Timeout)时间

  2. 增加最大半连接数

  3. 过滤网关防护

  4. SYN cookies技术:

    1. 当服务器接受到 SYN 报文段时,不直接为该 TCP 分配资源,而只是打开一个半开的套接字。接着会使用 SYN 报文段的源 Id,目的 Id,端口号以及只有服务器自己知道的一个秘密函数生成一个 cookie,并把 cookie 作为序列号响应给客户端。
    2. 如果客户端是正常建立连接,将会返回一个确认字段为 cookie + 1 的报文段。接下来服务器会根据确认报文的源 Id,目的 Id,端口号以及秘密函数计算出一个结果,如果结果的值 + 1 等于确认字段的值,则证明是刚刚请求连接的客户端,这时候才为该 TCP 分配资源

TCP断开连接过程的四次挥手?

  • 什么是四次挥手

  1. 主动断开方(客户端/服务端)-发送一个 FIN,用来关闭主动断开方(客户端/服务端)到被动断开方(客户端/服务端)的数据传送
  2. 被动断开方(客户端/服务端)-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号
  3. 被动断开方(客户端/服务端)-关闭与主动断开方(客户端/服务端)的连接,发送一个FIN给主动断开方(客户端/服务端)
  4. 主动断开方(客户端/服务端)-发回 ACK 报文确认,并将确认序号设置为收到序号加1
  • 为什么连接的时候是三次握手,关闭的时候却是四次握手
  1. 建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
  2. 关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以服务器可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接。因此,服务器ACK和FIN一般都会分开发送,从而导致多了一次。
  • 为什么TCP挥手每两次中间有一个 FIN-WAIT2等待时间

主动关闭的一端调用完close以后(即发FIN给被动关闭的一端, 并且收到其对FIN的确认ACK)则进入FIN_WAIT_2状态。如果这个时候因为网络突然断掉、被动关闭的一段宕机等原因,导致主动关闭的一端不能收到被动关闭的一端发来的FIN(防止对端不发送关闭连接的FIN包给本端),这个时候就需要FIN_WAIT_2定时器, 如果在该定时器超时的时候,还是没收到被动关闭一端发来的FIN,那么直接释放这个链接,进入CLOSE状态

  • 为什么客户端最后还要等待2MSL?为什么还有个TIME-WAIT的时间等待
  1. 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,服务器已经发送了FIN+ACK报文,请求断开,客户端却没有回应,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
  2. 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,这样新的连接中不会出现旧连接的请求报文。
  3. 2MSL,最大报文生存时间,一个MSL 30 秒,2MSL = 60s
  • 客户端 TIME-WAIT 状态过多会产生什么后果?怎样处理
  1. 作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple /tApl/ ,严重消耗着服务器的资源,此时部分客户端就会显示连接不上

  2. 作为客户端,短时间内大量的短连接,会大量消耗的Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法在发起新的连接了

  3. 在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接。这个场景下会出现大量socket处于TIME_WAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上

    1. 高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围,并不是很多,刨除系统和其他服务要用的,剩下的就更少了
    2. 短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接
  4. 解决方法:

    1. 用负载均衡来抗这些高并发的短请求;
    2. 服务器可以设置 SO_REUSEADDR 套接字选项来避免 TIME_WAIT状态,TIME_WAIT 状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的
    3. 强制关闭,发送 RST 包越过TIMEWAIT状态,直接进入CLOSED状态
  • 服务器出现了大量 CLOSE_WAIT 状态如何解决

大量 CLOSE_WAIT 表示程序出现了问题,对方的 socket 已经关闭连接,而我方忙于读或写没有及时关闭连接,需要检查代码,特别是释放资源的代码,或者是处理请求的线程配置。

  • 服务端会有一个TIME_WAIT状态吗?如果是服务端主动断开连接呢
  1. 发起链接的主动方基本都是客户端,但是断开连接的主动方服务器和客户端都可以充当,也就是说,只要是主动断开连接的,就会有 TIME_WAIT状态
  2. 四次挥手是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发
  3. 由于TCP连接时全双工的,因此,每个方向的数据传输通道都必须要单独进行关闭。

DNS 解析流程?

.com.fi国际金融域名DNS解析的步骤一共分为9步,如果每次解析都要走完9个步骤,大家浏览网站的速度也不会那么快,现在之所以能保持这么快的访问速度,其实一般的解析都是跑完第4步就可以了。除非一个地区完全是第一次访问(在都没有缓存的情况下)才会走完9个步骤,这个情况很少。

  • 1、本地客户机提出域名解析请求,查找本地HOST文件后将该请求发送给本地的域名服务器。
  • 2、将请求发送给本地的域名服务器。
  • 3、当本地的域名服务器收到请求后,就先查询本地的缓存。
  • 4、如果有该纪录项,则本地的域名服务器就直接把查询的结果返回浏览器。
  • 5、如果本地DNS缓存中没有该纪录,则本地域名服务器就直接把请求发给根域名服务器。
  • 6、然后根域名服务器再返回给本地域名服务器一个所查询域(根的子域)的主域名服务器的地址。
  • 7、本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的服务器查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址。
  • 8、重复第7步,直到找到正确的纪录。
  • 9、本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机。

注意事项:

递归查询:在该模式下DNS服务器接收到客户机请求,必须使用一个准确的查询结果回复客户机。如果DNS服务器本地没有存储查询DNS信息,那么该服务器会询问其他服务器,并将返回的查询结果提交给客户机。

迭代查询:DNS所在服务器若没有可以响应的结果,会向客户机提供其他能够解析查询请求的DNS服务器地址,当客户机发送查询请求时,DNS服务器并不直接回复查询结果,而是告诉客户机另一台DNS服务器地址,客户机再向这台DNS服务器提交请求,依次循环直到返回查询的结果为止。

为什么DNS通常基于UDP?

DNS通常是基于UDP的,但当数据长度大于512字节的时候,为了保证传输质量,就会使用基于TCP的实现方式

  • 从数据包的数量以及占有网络资源的层面

使用基于UDP的DNS协议只要一个请求、一个应答就好了; 而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手; 明显基于TCP协议的DNS更浪费网络资源!

  • 从数据一致性层面

DNS数据包不是那种大数据包,所以使用UDP不需要考虑分包,如果丢包那么就是全部丢包,如果收到了数据,那就是收到了全部数据!所以只需要考虑丢包的情况,那就算是丢包了,重新请求一次就好了。而且DNS的报文允许填入序号字段,对于请求报文和其对应的应答报文,这个字段是相同的,通过它可以区分DNS应答是对应的哪个请求

什么是DNS劫持?

DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。DNS劫持通过篡改DNS服务器上的数据返回给用户一个错误的查询结果来实现的。

  • DNS劫持症状

在某些地区的用户在成功连接宽带后,首次打开任何页面都指向ISP提供的“电信互联星空”、“网通黄页广告”等内容页面。还有就是曾经出现过用户访问Google域名的时候出现了百度的网站。这些都属于DNS劫持。

什么是DNS污染?

DNS污染是一种让一般用户由于得到虚假目标主机IP而不能与其通信的方法,是一种DNS缓存投毒攻击(DNS cache poisoning)。其工作方式是:由于通常的DNS查询没有任何认证机制,而且DNS查询通常基于的UDP是无连接不可靠的协议,因此DNS的查询非常容易被篡改,通过对UDP端口53上的DNS查询进行入侵检测,一经发现与关键词相匹配的请求则立即伪装成目标域名的解析服务器(NS,Name Server)给查询者返回虚假结果。

而DNS污染则是发生在用户请求的第一步上,直接从协议上对用户的DNS请求进行干扰。

DNS污染症状

目前一些被禁止访问的网站很多就是通过DNS污染来实现的,例如YouTube、Facebook等网站。

解决方法:

  1. 对于DNS劫持,可以采用使用国外免费公用的DNS服务器解决。例如OpenDNS(208.67.222.222)或GoogleDNS(8.8.8.8)。
  2. 对于DNS污染,可以说,个人用户很难单单靠设置解决,通常可以使用VPN或者域名远程解析的方法解决,但这大多需要购买付费的VPN或SSH等,也可以通过修改Hosts的方法,手动设置域名正确的IP地址。

为什么要DNS流量监控?

预示网络中正出现可疑或恶意代码的 DNS 组合查询或流量特征。例如:

  • 1.来自伪造源地址的 DNS 查询、或未授权使用且无出口过滤地址的 DNS 查询,若同时观察到异常大的 DNS 查询量或使用 TCP 而非 UDP 进行 DNS 查询,这可能表明网络内存在被感染的主机,受到了 DDoS 攻击。
  • 2.异常 DNS 查询可能是针对域名服务器或解析器(根据目标 IP 地址确定)的漏洞攻击的标志。与此同时,这些查询也可能表明网络中有不正常运行的设备。原因可能是恶意软件或未能成功清除恶意软件。
  • 3.在很多情况下,DNS 查询要求解析的域名如果是已知的恶意域名,或具有域名生成算法( DGA )(与非法僵尸网络有关)常见特征的域名,或者向未授权使用的解析器发送的查询,都是证明网络中存在被感染主机的有力证据。
  • 4.DNS 响应也能显露可疑或恶意数据在网络主机间传播的迹象。例如,DNS 响应的长度或组合特征可以暴露恶意或非法行为。例如,响应消息异常巨大(放大攻击),或响应消息的 Answer Section 或 Additional Section 非常可疑(缓存污染,隐蔽通道)。
  • 5.针对自身域名组合的 DNS 响应,如果解析至不同于你发布在授权区域中的 IP 地址,或来自未授权区域主机的域名服务器的响应,或解析为名称错误( NXDOMAIN )的对区域主机名的肯定响应,均表明域名或注册账号可能被劫持或 DNS 响应被篡改。
  • 6.来自可疑 IP 地址的 DNS 响应,例如来自分配给宽带接入网络 IP 段的地址、非标准端口上出现的 DNS 流量,异常大量的解析至短生存时间( TTL )域名的响应消息,或异常大量的包含“ name error ”( NXDOMAIN )的响应消息,往往是主机被僵尸网络控制、运行恶意软件或被感染的表现。

输入URL 到页面加载过程?

  1. 地址栏输入URL

  2. DNS 域名解析IP

  3. 请求和响应数据

    1. 建立TCP连接(3次握手)
    2. 发送HTTP请求
    3. 服务器处理请求
    4. 返回HTTP响应结果
    5. 关闭TCP连接(4次挥手)
  4. 浏览器加载,解析和渲染

下图是在数据传输过程中的工作方式,在发送端是应用层-->链路层这个方向的封包过程,每经过一层都会增加该层的头部。而接收端则是从链路层-->应用层解包的过程,每经过一层则会去掉相应的首部。

如何使用netstat查看服务及监听端口?

netstat -t/-u/-l/-r/-n【显示网络相关信息,-t:TCP协议,-u:UDP协议,-l:监听,-r:路由,-n:显示IP地址和端口号】

  • 查看本机监听的端口
[root@pdai-centos ~]# netstat -tlun
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN                          
udp        0      0 172.21.0.14:123         0.0.0.0:*                          
udp        0      0 127.0.0.1:123           0.0.0.0:*                          
udp6       0      0 fe80::5054:ff:fe2b::123 :::*                               
udp6       0      0 ::1:123                 :::* 

如何使用TCPDump抓包?

tcpdump 是一款强大的网络抓包工具,它使用 libpcap 库来抓取网络数据包,这个库在几乎在所有的 Linux/Unix 中都有。

tcpdump 的常用参数如下:

$ tcpdump -i eth0 -nn -s0 -v port 80
  • -i : 选择要捕获的接口,通常是以太网卡或无线网卡,也可以是 vlan 或其他特殊接口。如果该系统上只有一个网络接口,则无需指定。
  • -nn : 单个 n 表示不解析域名,直接显示 IP;两个 n 表示不解析域名和端口。这样不仅方便查看 IP 和端口号,而且在抓取大量数据时非常高效,因为域名解析会降低抓取速度。
  • -s0 : tcpdump 默认只会截取前 96 字节的内容,要想截取所有的报文内容,可以使用 -s number, number 就是你要截取的报文字节数,如果是 0 的话,表示截取报文全部内容。
  • -v : 使用 -v-vv-vvv 来显示更多的详细信息,通常会显示更多与特定协议相关的信息。
  • port 80 : 这是一个常见的端口过滤器,表示仅抓取 80 端口上的流量,通常是 HTTP。

如何使用Wireshark抓包分析?

Wireshark(前称Ethereal)是一个网络封包分析软件。网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料。Wireshark使用WinPCAP作为接口,直接与网卡进行数据报文交换。

首先看下TCP报文首部,和wireshark捕获到的TCP包中的每个字段如下图所示:

9.3 开发安全

开发中有哪些常见的Web安全漏洞?

通过OWASP Top 10来回答

2013版至2017版,应用程序的基础技术和结构发生了重大变化:

  • 使用node.js和Spring Boot构建的微服务正在取代传统的单任务应用,微服务本身具有自己的安全挑战,包括微服务间互信、容器 工具、保密管理等等。原来没人期望代码要实现基于互联网的房屋,而现在这些代码就在API或RESTful服务的后面,提供给移动 应用或单页应用(SPA)的大量使用。代码构建时的假设,如受信任的调用等等,再也不存在了。
  • 使用JavaScript框架(如:Angular和React)编写的单页应用程序,允许创建高度模块化的前端用户体验;原来交付服务器端处理 的功能现在变为由客户端处理,但也带来了安全挑战。
  • JavaScript成为网页上最基本的语言。Node.js运行在服务器端,采用现代网页框架的Bootstrap、Electron、Angular和React则运 行在客户端。

什么是注入攻击?举例说明?

  • 什么是注入攻击?从具体的SQL注入说

重点看这条SQL,密码输入: ' OR '1'='1时,等同于不需要密码

String sql = "SELECT * FROM t_user WHERE username='"+userName+"' AND pwd='"+password+"'"; 
  • 如何解决注入攻击,比如SQL注入
  1. 使用预编译处理输入参数:要防御 SQL 注入,用户的输入就不能直接嵌套在 SQL 语句当中。使用参数化的语句,用户的输入就被限制于一个参数当中, 比如用prepareStatement
  2. 输入验证:检查用户输入的合法性,以确保输入的内容为正常的数据。数据检查应当在客户端和服务器端都执行,之所以要执行服务器端验证,是因为客户端的校验往往只是减轻服务器的压力和提高对用户的友好度,攻击者完全有可能通过抓包修改参数或者是获得网页的源代码后,修改验证合法性的脚本(或者直接删除脚本),然后将非法内容通过修改后的表单提交给服务器等等手段绕过客户端的校验。因此,要保证验证操作确实已经执行,唯一的办法就是在服务器端也执行验证。但是这些方法很容易出现由于过滤不严导致恶意攻击者可能绕过这些过滤的现象,需要慎重使用。
  3. 错误消息处理:防范 SQL 注入,还要避免出现一些详细的错误消息,恶意攻击者往往会利用这些报错信息来判断后台 SQL 的拼接形式,甚至是直接利用这些报错注入将数据库中的数据通过报错信息显示出来。
  4. 加密处理:将用户登录名称、密码等数据加密保存。加密用户输入的数据,然后再将它与数据库中保存的数据比较,这相当于对用户输入的数据进行了“消毒”处理,用户输入的数据不再对数据库有任何特殊的意义,从而也就防止了攻击者注入 SQL 命令。
  • 还有哪些注入
  1. xPath注入,XPath 注入是指利用 XPath 解析器的松散输入和容错特性,能够在 URL、表单或其它信息上附带恶意的 XPath 查询代码,以获得权限信息的访问权并更改这些信息
  2. 命令注入,Java中System.Runtime.getRuntime().exec(cmd);可以在目标机器上执行命令,而构建参数的过程中可能会引发注入攻击
  3. LDAP注入
  4. CLRF注入
  5. email注入
  6. Host注入

什么是CSRF?举例说明并给出开发中解决方案?

你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。

  • 黑客能拿到Cookie吗?

CSRF 攻击是黑客借助受害者的 cookie 骗取服务器的信任,但是黑客并不能拿到 cookie,也看不到 cookie 的内容。

对于服务器返回的结果,由于浏览器同源策略的限制,黑客也无法进行解析。因此,黑客无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。

  • 什么样的请求是要CSRF保护?

为什么有些框架(比如Spring Security)里防护CSRF的filter限定的Method是POST/PUT/DELETE等,而没有限定GET Method?

我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行 CSRF 的保护。通常而言GET请作为请求数据,不作为修改数据,所以这些框架没有拦截Get等方式请求。比如银行系统中转账的请求会直接改变账户的金额,会遭到 CSRF 攻击,需要保护。而查询余额是对金额的读取操作,不会改变数据,CSRF 攻击无法解析服务器返回的结果,无需保护。

  • 为什么对请求做了CSRF拦截,但还是会报CRSF漏洞?

为什么我在前端已经采用POST+CSRF Token请求,后端也对POST请求做了CSRF Filter,但是渗透测试中还有CSRF漏洞?

直接看下面代码。

// 这里没有限制POST Method,导致用户可以不通过POST请求提交数据。
@RequestMapping("/url")
public ReponseData saveSomething(XXParam param){
    // 数据保存操作...
}

PS:这一点是很容易被忽视的,在笔者经历过的几个项目的渗透测试中,多次出现。@pdai

  • 有哪些CSRF 防御常规思路
  1. 验证 HTTP Referer 字段, 根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。只需要验证referer
  2. 在请求地址中添加 token 并验证,可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。 这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。
  3. 在 HTTP 头中自定义属性并验证
  • 开发中如何防御CSRF

可以通过自定义xxxCsrfFilter去拦截实现, 这里建议你参考 Spring Security - org.springframework.security.web.csrf.CsrfFilter.java。

什么是XSS?举例说明?

通常XSS攻击分为:反射型xss攻击, 存储型xss攻击DOM型xss攻击。同时注意以下例子只是简单的向你解释这三种类型的攻击方式而已,实际情况比这个复杂,具体可以再结合最后一节深入理解。

  • 反射型xss攻击?

反射型的攻击需要用户主动的去访问带攻击的链接,攻击者可以通过邮件或者短信的形式,诱导受害者点开链接。如果攻击者配合短链接URL,攻击成功的概率会更高。

在一个反射型XSS攻击中,恶意文本属于受害者发送给网站的请求中的一部分。随后网站又把恶意文本包含进用于响应用户的返回页面中,发还给用户。

  • 存储型xss攻击

这种攻击方式恶意代码会被存储在数据库中,其他用户在正常访问的情况下,也有会被攻击,影响的范围比较大。

  • DOM型xss攻击

基于DOM的XSS攻击是反射型攻击的变种。服务器返回的页面是正常的,只是我们在页面执行js的过程中,会把攻击代码植入到页面中。

  • XSS 攻击的防御

XSS攻击其实就是代码的注入。用户的输入被编译成恶意的程序代码。所以,为了防范这一类代码的注入,需要确保用户输入的安全性。对于攻击验证,我们可以采用以下两种措施:

  1. 编码,就是转义用户的输入,把用户的输入解读为数据而不是代码
  2. 校验,对用户的输入及请求都进行过滤检查,如对特殊字符进行过滤,设置输入域的匹配规则等

具体比如:

  1. 对于验证输入,我们既可以在服务端验证,也可以在客户端验证
  2. 对于持久性和反射型攻击服务端验证是必须的,服务端支持的任何语言都能够做到
  3. 对于基于DOM的XSS攻击,验证输入在客户端必须执行,因为从服务端来说,所有发出的页面内容是正常的,只是在客户端js代码执行的过程中才发生可攻击
  4. 但是对于各种攻击方式,我们最好做到客户端和服务端都进行处理

其它还有一些辅助措施,比如:

  1. 入参长度限制: 通过以上的案例我们不难发现xss攻击要能达成往往需要较长的字符串,因此对于一些可以预期的输入可以通过限制长度强制截断来进行防御。
  2. 设置cookie httponly为true(具体请看下文的解释)

一般的渗透测试流程?

渗透测试就是利用我们所掌握的渗透知识,对网站进行一步一步的渗透,发现其中存在的漏洞和隐藏的风险,然后撰写一篇测试报告,提供给我们的客户。客户根据我们撰写的测试报告,对网站进行漏洞修补,以防止黑客的入侵!

  • 渗透测试流程举例

我们现在就模拟黑客对一个网站进行渗透测试,这属于黑盒测试,我们只知道该网站的URL,其他什么的信息都不知道。

  • 确定目标

    • 确定范围:测试目标的范围、ip、域名、内外网、测试账户。
    • 确定规则:能渗透到什么程度,所需要的时间、能否修改上传、能否提权、等等。
    • 确定需求:web应用的漏洞、业务逻辑漏洞、人员权限管理漏洞、等等。
  • 信息收集

    • 方式:主动扫描,开放搜索等。
    • 开放搜索:利用搜索引擎获得:后台、未授权页面、敏感url、等等。
    • 基础信息:IP、网段、域名、端口。
    • 应用信息:各端口的应用。例如web应用、邮件应用、等等。
    • 系统信息:操作系统版本
    • 版本信息:所有这些探测到的东西的版本。
    • 服务信息:中间件的各类信息,插件信息。
    • 人员信息:域名注册人员信息,web应用中发帖人的id,管理员姓名等。
    • 防护信息:试着看能否探测到防护设备。
  • 漏洞探测

  • 漏洞验证

  • 内网转发

  • 内网横向渗透

  • 权限维持

  • 痕迹清除

  • 撰写渗透测试保告

9.4 单元测试

谈谈你对单元测试的理解?

  • 什么是单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。

  • 为什么要写单元测试

使用单元测试可以有效地降低程序出错的机率,提供准确的文档,并帮助我们改进设计方案等等。

  • 什么时候写单元测试

比较推荐单元测试与具体实现代码同步进行这个方案的。只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性,而能写出一些功能代码则说明对需求有一定理解了。

  • 单元测试要写多细

单元测试不是越多越好,而是越有效越好!进一步解读就是哪些代码需要有单元测试覆盖:

  1. 逻辑复杂的
  2. 容易出错的
  3. 不易理解的,即使是自己过段时间也会遗忘的,看不懂自己的代码,单元测试代码有助于理解代码的功能和需求
  4. 公共代码。比如自定义的所有http请求都会经过的拦截器;工具类等。
  5. 核心业务代码。一个产品里最核心最有业务价值的代码应该要有较高的单元测试覆盖率

JUnit 5整体架构?

与以前版本的JUnit不同,JUnit 5由三个不同子项目中的几个不同模块组成。JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform是基于JVM的运行测试的基础框架在,它定义了开发运行在这个测试框架上的TestEngine API。此外该平台提供了一个控制台启动器,可以从命令行启动平台,可以为Gradle和 Maven构建插件,同时提供基于JUnit 4的Runner。
  • JUnit Jupiter是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合.Jupiter子项目提供了一个TestEngine在平台上运行基于Jupiter的测试。
  • JUnit Vintage提供了一个TestEngine在平台上运行基于JUnit 3和JUnit 4的测试。

架构图如下:

JUnit 5与Junit4的差别在哪里?

对比下Junit5和Junit4注解:

Junit4Junit5注释
@Test@Test表示该方法是一个测试方法
@BeforeClass@BeforeAll表示使用了该注解的方法应该在当前类中所有测试方法之前执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
@AfterClass@AfterAll表示使用了该注解的方法应该在当前类中所有测试方法之后执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
@Before@BeforeEach表示使用了该注解的方法应该在当前类中每一个测试方法之前执行
@After@AfterEach表示使用了该注解的方法应该在当前类中每一个测试方法之后执行
@Ignore@Disabled用于禁用(或者说忽略)一个测试类或测试方法
@Category@Tag用于声明过滤测试的tag标签,该注解可以用在方法或类上

你在开发中使用什么框架来做单元测试?

  • JUnit4/5
  • Mockito, mock测试
  • Powermock, 静态util的测试

9.5 代码质量

你们项目中是如何保证代码质量的?

  • checkstyle, 静态样式检查
  • sonarlint Sonar是一个用于代码质量管理的开源平台,用于管理源代码的质量 通过插件形式,可以支持包括java,C#,C/C++,PL/SQL,Cobol,JavaScrip,Groovy等等二十几种编程语言的代码质量管理与检测
  • spotbugs, SpotBugs是Findbugs的继任者(Findbugs已经于2016年后不再维护),用于对代码进行静态分析,查找相关的漏洞; 它是一款自由软件,按照GNU Lesser General Public License 的条款发布

你们项目中是如何做code review的?

Gerrit + 定期线下review

9.6 代码重构

如何去除多余的if else?

  • 出现if/else和switch/case的场景

通常业务代码会包含这样的逻辑:每种条件下会有不同的处理逻辑。比如两个数a和b之间可以通过不同的操作符(+,-,*,/)进行计算,初学者通常会这么写:

public int calculate(int a, int b, String operator) {
    int result = Integer.MIN_VALUE;
 
    if ("add".equals(operator)) {
        result = a + b;
    } else if ("multiply".equals(operator)) {
        result = a * b;
    } else if ("divide".equals(operator)) {
        result = a / b;
    } else if ("subtract".equals(operator)) {
        result = a - b;
    }
    return result;
}

这种最基础的代码如何重构呢?

  • 工厂类
public class OperatorFactory {
    static Map<String, Operation> operationMap = new HashMap<>();
    static {
        operationMap.put("add", new Addition());
        operationMap.put("divide", new Division());
        // more operators
    }
 
    public static Optional<Operation> getOperation(String operator) {
        return Optional.ofNullable(operationMap.get(operator));
    }
}
  • 枚举
public enum Operator {
    ADD {
        @Override
        public int apply(int a, int b) {
            return a + b;
        }
    },
    // other operators
    
    public abstract int apply(int a, int b);
​
}
  • Command模式
public class AddCommand implements Command {
    // Instance variables
 
    public AddCommand(int a, int b) {
        this.a = a;
        this.b = b;
    }
 
    @Override
    public Integer execute() {
        return a + b;
    }
}
  • 规则引擎
  1. 定义规则
public interface Rule {
    boolean evaluate(Expression expression);
    Result getResult();
}
  1. Add 规则
public class AddRule implements Rule {
    @Override
    public boolean evaluate(Expression expression) {
        boolean evalResult = false;
        if (expression.getOperator() == Operator.ADD) {
            this.result = expression.getX() + expression.getY();
            evalResult = true;
        }
        return evalResult;
    }    
}
  1. 表达式
public class Expression {
    private Integer x;
    private Integer y;
    private Operator operator;        
}
  1. 规则引擎
public class RuleEngine {
    private static List<Rule> rules = new ArrayList<>();
 
    static {
        rules.add(new AddRule());
    }
 
    public Result process(Expression expression) {
        Rule rule = rules
          .stream()
          .filter(r -> r.evaluate(expression))
          .findFirst()
          .orElseThrow(() -> new IllegalArgumentException("Expression does not matches any Rule"));
        return rule.getResult();
    }
}
  • 策略模式
  1. 操作
public interface Opt {
    int apply(int a, int b);
}
​
@Component(value = "addOpt")
public class AddOpt implements Opt {
    @Autowired
    xxxAddResource resource; // 这里通过Spring框架注入了资源
​
    @Override
    public int apply(int a, int b) {
       return resource.process(a, b);
    }
}
​
@Component(value = "devideOpt")
public class devideOpt implements Opt {
    @Autowired
    xxxDivResource resource; // 这里通过Spring框架注入了资源
​
    @Override
    public int apply(int a, int b) {
       return resource.process(a, b);
    }
}
  1. 策略
@Component
public class OptStrategyContext{
 
​
    private Map<String, Opt> strategyMap = new ConcurrentHashMap<>();
 
    @Autowired
    public OptStrategyContext(Map<String, TalkService> strategyMap) {
        this.strategyMap.clear();
        this.strategyMap.putAll(strategyMap);
    }
 
    public int apply(Sting opt, int a, int b) {
        return strategyMap.get(opt).apply(a, b);
    }
}

如何去除不必要的!=判空?

  • 空对象模式
public class MyParser implements Parser {
  private static Action NO_ACTION = new Action() {
    public void doSomething() { /* do nothing */ }
  };
​
  public Action findAction(String userInput) {
    // ...
    if ( /* we can't find any actions */ ) {
      return NO_ACTION;
    }
  }
}

然后便可以始终可以这么调用

ParserFactory.getParser().findAction(someInput).doSomething();
  • Java8中使用Optional
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);
}

我们可以通过利用 Java 8 的 Optional 类型来摆脱所有这些 null 检查。map 方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个 Optional 对象。这使我们能够在一行中进行多个 map 操作。Null 检查是在底层自动处理的。

Optional.of(new Outer())
    .map(Outer::getNested)
    .map(Nested::getInner)
    .map(Inner::getFoo)
    .ifPresent(System.out::println);

还有一种实现相同作用的方式就是通过利用一个 supplier 函数来解决嵌套路径的问题:

Outer obj = new Outer();
resolve(() -> obj.getNested().getInner().getFoo())
    .ifPresent(System.out::println);
​
/**
 * 使用Supplier解决嵌套路径的问题
 * @param resolver 处理返回的结果
 * @return 目标对象
 * @param <T> 返回对象的类型
 */
public static <T> Optional<T> resolve(Supplier<T> resolver) {
    try {
        T result = resolver.get();
        return Optional.ofNullable(result);
    } catch (NullPointerException e) {
        return Optional.empty();
    }
}

10 开发框架和中间件

开发框架相关

10.1 Spring

什么是Spring框架

Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。

Spring官网列出的Spring的6个特征:

  • 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。
  • 数据访问:事务,DAO支持,JDBC,ORM,编组XML。
  • Web支持:Spring MVC和Spring WebFlux Web框架。
  • 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言:Kotlin,Groovy,动态语言

列举一些重要的Spring模块?

下图对应的是Spring 4.x的版本,目前最新的5.x版本中Web模块的Portlet组件已经被废弃掉,同时增加了用于异步响应式处理的WebFlux组件。

  • Spring Core:基础,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。
  • Spring Aspects:该模块为与AspectJ的集成提供支持。
  • Spring AOP:提供面向切面的编程实现。
  • Spring JDBC:Java数据库连接。
  • Spring JMS:Java消息服务。
  • Spring ORM:用于支持Hibernate等ORM工具。
  • Spring Web:为创建Web应用程序提供支持。
  • Spring Test:提供了对JUnit和TestNG测试的支持。

Spring Framework Runtime Architecture

什么是IOC?如何实现的?

IOC(Inversion Of Control,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

Spring 中的 IoC 的实现原理就是工厂模式加反射机制。

示例:

interface Fruit {
     public abstract void eat();
}
​
class Apple implements Fruit {
    public void eat(){
        System.out.println("Apple");
    }
}
​
class Orange implements Fruit {
    public void eat(){
        System.out.println("Orange");
    }
}
​
class Factory {
    public static Fruit getInstance(String className) {
        Fruit f = null;
        try {
            f = (Fruit)Class.forName(className).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return f;
    }
}
​
class Client {
    public static void main(String[] a) {
        Fruit f = Factory.getInstance("io.github.dunwu.spring.Apple");
        if(f != null){
            f.eat();
        }
    }
}

什么是AOP?有哪些AOP的概念?

AOP(Aspect Oriented Programming, 面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。

Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。

Spring AOP Process


当然也可以使用AspectJ,Spring AOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。使用AOP之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样可以大大简化代码量。我们需要增加新功能也方便,提高了系统的扩展性。日志功能、事务管理和权限管理等场景都用到了AOP。

AOP包含的几个概念

  1. Join point(连接点):具体的切面点点抽象概念,可以是在字段、方法上,Spring中具体表现形式是PointCut(切入点),仅作用在方法上。
  2. Advice(通知): 在连接点进行的具体操作,如何进行增强处理的,分为前置、后置、异常、最终、环绕五种情况。
  3. 目标对象:被AOP框架进行增强处理的对象,也被称为被增强的对象。
  4. AOP代理:AOP框架创建的对象,简单的说,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。
  5. Weaving(织入):将增强处理添加到目标对象中,创建一个被增强的对象的过程

总结为一句话就是:在目标对象(target object)的某些方法(joinpoint)添加不同种类的操作(通知、增强操作处理),最后通过某些方法(weaving、织入操作)实现一个新的代理目标对象。

AOP有哪些应用场景?

举几个例子:

  • 记录日志(调用方法后记录日志)
  • 监控性能(统计方法运行时间)
  • 权限控制(调用方法前校检是否有权限)
  • 事务管理(调用方法前开启事务,调用方法后提交关闭事务)
  • 缓存优化(第一次调用查询数据库,将查询结果放入内存对象,第二次调用,直接从内存中返回对象,不需要查询数据库)

有哪些AOP Advice通知的类型?

特定 Join Point 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 Join Point "周围"维护一系列的拦截器。

  • 前置通知(Before advice) : 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。
  • 后置通知(After advice) :这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
  • 返回后通知(After return advice) :这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
  • 环绕通知(Around advice):这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。
  • 抛出异常后通知(After throwing advice) :仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。

AOP有哪些实现方式?

实现 AOP 的技术,主要分为两大类:

静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;

  • 编译时编织(特殊编译器实现)
  • 类加载时编织(特殊的类加载器实现)。

动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。

  • JDK 动态代理

    • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;
    • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新,Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
    • JDK Proxy 是通过拦截器加反射的方式实现的;
    • JDK Proxy 只能代理实现接口的类;
    • JDK Proxy 实现和调用起来比较简单;
  • CGLIB

    • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;
    • CGLib 无需通过接口来实现,它是针对类实现代理,主要是对指定的类生成一个子类,它是通过实现子类的方式来完成调用的。

谈谈你对CGLib的理解?

JDK 动态代理机制只能代理实现接口的类,一般没有实现接口的类不能进行代理。使用 CGLib 实现动态代理,完全不受代理类必须实现接口的限制。

CGLib 的原理是对指定目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对 final 修饰的类进行代理。

举例:

public class CGLibDemo {
​
    // 需要动态代理的实际对象
    static class Sister  {
        public void sing() {
            System.out.println("I am Jinsha, a little sister.");
        }
    }
​
    static class CGLibProxy implements MethodInterceptor {
​
        private Object target;
​
        public Object getInstance(Object target){
            this.target = target;
            Enhancer enhancer = new Enhancer();
            // 设置父类为实例类
            enhancer.setSuperclass(this.target.getClass());
            // 回调方法
            enhancer.setCallback(this);
            // 创建代理对象
            return enhancer.create();
        }
​
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("introduce yourself...");
            Object result = methodProxy.invokeSuper(o,objects);
            System.out.println("score...");
            return result;
        }
    }
​
    public static void main(String[] args) {
        CGLibProxy cgLibProxy = new CGLibProxy();
        //获取动态代理类实例
        Sister proxySister = (Sister) cgLibProxy.getInstance(new Sister());
        System.out.println("CGLib Dynamic object name: " + proxySister.getClass().getName());
        proxySister.sing();
    }
}

CGLib 的调用流程就是通过调用拦截器的 intercept 方法来实现对被代理类的调用。而拦截逻辑可以写在 intercept 方法的 invokeSuper(o, objects); 的前后实现拦截。

Spring AOP和Aspect AOP有什么区别?

Spring AOP是属于运行时增强,而AspectJ是编译时增强。Spring AOP基于代理(Proxying),而AspectJ基于字节码操作(Bytecode Manipulation)。

Spring AOP已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。AspectJ相比于Spring AOP功能更加强大,但是Spring AOP相对来说更简单。

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比SpringAOP快很多。

Spring中的bean的作用域有哪些?

  1. singleton:唯一bean实例,Spring中的bean默认都是单例的。
  2. prototype:每次请求都会创建一个新的bean实例。
  3. request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  4. session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
  5. global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。

Spring中单例bean的线程安全问题了解吗?

大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。

有两种常见的解决方案:

  1. 在 bean 对象中尽量避免定义可变的成员变量(不太现实)。
  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

Spring中的bean生命周期?

Bean的完整生命周期经历了几种方法调用,这些方法可以划分为以下几类:

  • Bean自身的方法: 这个包括了Bean本身调用的方法和通过配置文件中<bean>的init-method和destroy-method指定的方法
  • Bean级生命周期接口方法: 这个包括了BeanNameAware、BeanFactoryAware、ApplicationContextAware;当然也包括InitializingBean和DiposableBean这些接口的方法(可以被@PostConstruct和@PreDestroy注解替代)
  • 容器级生命周期接口方法: 这个包括了InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。
  • 工厂后处理器接口方法: 这个包括了AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。

Bean Life Cycle

具体而言,流程如下

  • 如果 BeanFactoryPostProcessor 和 Bean 关联, 则调用postProcessBeanFactory方法.(即首先尝试从Bean工厂中获取Bean)

  • 如果 InstantiationAwareBeanPostProcessor 和 Bean 关联,则调用postProcessBeforeInstantiation方法

  • 根据配置情况调用 Bean 构造方法实例化 Bean

  • 利用依赖注入完成 Bean 中所有属性值的配置注入

  • 如果 InstantiationAwareBeanPostProcessor 和 Bean 关联,则调用postProcessAfterInstantiation方法和postProcessProperties

  • 调用xxxAware接口

    (上图只是给了几个例子)

    • 第一类Aware接口

      • 如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。
      • 如果 Bean 实现了 BeanClassLoaderAware 接口,则 Spring 调用 setBeanClassLoader() 方法传入classLoader的引用。
      • 如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。
    • 第二类Aware接口

      • 如果 Bean 实现了 EnvironmentAware 接口,则 Spring 调用 setEnvironment() 方法传入当前 Environment 实例的引用。
      • 如果 Bean 实现了 EmbeddedValueResolverAware 接口,则 Spring 调用 setEmbeddedValueResolver() 方法传入当前 StringValueResolver 实例的引用。
      • 如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。
      • ...
  • 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。

  • 如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。(或者有执行@PostConstruct注解的方法)

  • 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。

  • 如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。

  • 如果在 <bean> 中指定了该 Bean 的作用范围为 scope="singleton",则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 <bean> 中指定了该 Bean 的作用范围为 scope="prototype",则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。

  • 如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;(或者有执行@PreDestroy注解的方法)

  • 如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

说说自己对于Spring MVC的了解?

MVC是一种设计模式,Spring MVC是一款很优秀的MVC框架。Spring MVC可以帮助我们进行更简洁的Web层的开发,并且它天生与Spring框架集成。Spring MVC下我们一般把后端项目分为Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台页面)。

Spring MVC的简单原理图如下:

SpringMVC process

Spring MVC 的工作原理了解嘛?

Spring MVC project principle

流程说明:

  1. 客户端(浏览器)发送请求,直接请求到DispatcherServlet。
  2. DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler。
  3. 解析到对应的Handler(也就是我们平常说的Controller控制器)。
  4. HandlerAdapter会根据Handler来调用真正的处理器来处理请求和执行相对应的业务逻辑。
  5. 处理器处理完业务后,会返回一个ModelAndView对象,Model是返回的数据对象,View是逻辑上的View。
  6. ViewResolver会根据逻辑View去查找实际的View。
  7. DispatcherServlet把返回的Model传给View(视图渲染)。
  8. 把View返回给请求者(浏览器)。

Spring框架中用到了哪些设计模式?

举几个例子

  1. 工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。
  2. 代理设计模式:Spring AOP功能的实现。
  3. 单例设计模式:Spring中的bean默认都是单例的。
  4. 模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。
  5. 包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  6. 观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。
  7. 适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。

@Component和@Bean的区别是什么?

  1. 作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。
  2. @Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。
  3. @Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。

@Bean注解的使用示例:

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

上面的代码相当于下面的XML配置:

<beans>
    <bean id="transferService" class="com.yanggb.TransferServiceImpl"/>
</beans>

下面这个例子是无法通过@Component注解实现的:

@Bean
public OneService getService(status) {
    case (status)  {
        when 1:
                return new serviceImpl1();
        when 2:
                return new serviceImpl2();
        when 3:
                return new serviceImpl3();
    }
}

将一个类声明为Spring的bean的注解有哪些?

我们一般使用@Autowired注解去自动装配bean。而想要把一个类标识为可以用@Autowired注解自动装配的bean,可以采用以下的注解实现:

  1. @Component注解。通用的注解,可标注任意类为Spring组件。如果一个Bean不知道属于哪一个层,可以使用@Component注解标注。
  2. @Repository注解。对应持久层,即Dao层,主要用于数据库相关操作。
  3. @Service注解。对应服务层,即Service层,主要涉及一些复杂的逻辑,需要用到Dao层(注入)。
  4. @Controller注解。对应Spring MVC的控制层,即Controller层,主要用于接受用户请求并调用Service层的方法返回数据给前端页面。

Spring事务管理的方式有几种?

  1. 编程式事务:在代码中硬编码(不推荐使用)
  2. 声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。

Spring事务中的隔离级别有哪几种?

在TransactionDefinition接口中定义了五个表示隔离级别的常量:

ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。

ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。

ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

Spring事务中有哪几种事务传播行为?

在TransactionDefinition接口中定义了7个表示事务传播行为的常量。

支持当前事务的情况:

PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。

不支持当前事务的情况:

PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。

PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。

PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:

PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。

Bean Factory和ApplicationContext有什么区别?

ApplicationContext提供了一种解析文本消息的方法,一种加载文件资源(如图像)的通用方法,它们可以将事件发布到注册为侦听器的bean。此外,可以在应用程序上下文中以声明方式处理容器中的容器或容器上的操作,这些操作必须以编程方式与Bean Factory一起处理。ApplicationContext实现MessageSource,一个用于获取本地化消息的接口,实际的实现是可插入的。

如何定义bean的范围?

在Spring中定义一个时,我们也可以为bean声明一个范围。它可以通过bean定义中的scope属性定义。例如,当Spring每次需要生成一个新的bean实例时,bean scope属性就是原型(prototype)。另一方面,当每次需要Spring都必须返回相同的bean实例时,bean scope属性必须设置为单例(singleton)。

可以通过多少种方式完成依赖注入?

通常,依赖注入可以通过三种方式完成,即:

  • 构造函数注入
  • setter注入
  • 接口注入

10.2 Spring Boot

什么是SpringBoot?

Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,节省了繁重的配置,提供了各种启动器,开发者能快速上手。

  • 用来简化Spring应用的初始搭建以及开发过程,使用特定的方式来进行配置
  • 创建独立的Spring引用程序main方法运行
  • 嵌入的tomcat无需部署war文件
  • 简化maven配置
  • 自动配置Spring添加对应的功能starter自动化配置
  • SpringBoot来简化Spring应用开发,约定大于配置,去繁化简

为什么使用SpringBoot?

  • 独立运行

Spring Boot 内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。

  • 简化配置

spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。

  • 自动配置

Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 spring-boot-starter-web 启动器就能拥有 web 的功能,无需其他配置。

  • 无代码生成和XML配置

Spring Boot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。

  • 应用监控

Spring Boot 提供一系列端点可以监控服务及应用,做健康检测。

Spring、Spring MVC和SpringBoot有什么区别?

  • Spring

Spring最重要的特征是依赖注入。所有Spring Modules不是依赖注入就是IOC控制反转。

当我们恰当的使用DI或者是IOC的时候,可以开发松耦合应用。

  • Spring MVC

Spring MVC提供了一种分离式的方法来开发Web应用。通过运用像DispatcherServelet,ModelAndView 和 ViewResolver 等一些简单的概念,开发 Web 应用将会变的非常简单。

  • SpringBoot

Spring和Spring MVC的问题在于需要配置大量的参数。

SpringBoot通过一个自动配置和启动项来解决这个问题。

SpringBoot自动配置的原理?

在Spring程序main方法中,添加@SpringBootApplication或者@EnableAutoConfiguration会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建的Spring容器中的bean

Spring Boot的核心注解是哪些?他主要由哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,他也是SpringBoot的核心注解,主要组合包含了以下3个注解:

  • @SpringBootConfiguration:组合了@Configuration注解,实现配置文件的功能;
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置的功能:
  • @SpringBootApplication(exclude={DataSourceAutoConfiguration.class});
  • @ComponentScan:Spring组件扫描。

SpringBoot的核心配置文件有哪几个?他们的区别是什么?

SpringBoot的核心配置文件是application和bootstrap配置文件。

application配置文件这个容易理解,主要用于Spring Boot项目的自动化配置。

bootstrap配置文件有以下几个应用场景:

  • 使用Spring Cloud Config配置中心时,这时需要在bootstrap配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
  • 一些固定的不能被覆盖的属性;
  • 一些加密/解密的场景;

什么是Spring Boot Starter?有哪些常用的?

和自动配置一样,Spring Boot Starter的目的也是简化配置,而Spring Boot Starter解决的是依赖管理配置复杂的问题,有了它,当我需要构建一个Web应用程序时,不必再遍历所有的依赖包,一个一个地添加到项目的依赖管理中,而是只需要一个配置spring-boot-starter-web, 同理,如果想引入持久化功能,可以配置spring-boot-starter-data-jpa:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Spring Boot 也提供了其它的启动器项目包括,包括用于开发特定类型应用程序的典型依赖项。

spring-boot-starter-web-services - SOAP Web Services

spring-boot-starter-web - Web 和 RESTful 应用程序

spring-boot-starter-test - 单元测试和集成测试

spring-boot-starter-jdbc - 传统的 JDBC

spring-boot-starter-hateoas - 为服务添加 HATEOAS 功能

spring-boot-starter-security - 使用 SpringSecurity 进行身份验证和授权

spring-boot-starter-data-jpa - 带有 Hibernate 的 Spring Data JPA

spring-boot-starter-data-rest - 使用 Spring Data REST 公布简单的 REST 服务

spring-boot-starter-parent有什么作用?

我们知道,新建一个SpringBoot项目,默认都是有parent的,这个parent就是spring-boot-starter-parent,spring-boot-starter-parent主要有如下作用:

  • 定义了Java编译版本
  • 使用UTF-8格式编码
  • 继承自spring-boor-dependencies,这里面定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号
  • 执行打包操作的配置
  • 自动化的资源过滤
  • 自动化的插件配置

如何自定义Spring Boot Starter?

  • 实现功能
  • 添加Properties
@Data
@ConfigurationProperties(prefix = "com.pdai")
public class DemoProperties {
    private String version;
    private String name;
}
  • 添加AutoConfiguration
@Configuration
@EnableConfigurationProperties(DemoProperties.class)
public class DemoAutoConfiguration {
​
    @Bean
    public com.pdai.demo.module.DemoModule demoModule(DemoProperties properties){
        com.pdai.demo.module.DemoModule demoModule = new com.pdai.demo.module.DemoModule();
        demoModule.setName(properties.getName());
        demoModule.setVersion(properties.getVersion());
        return demoModule;
    }
    
}
  • 添加spring.factory

在META-INF下创建spring.factory文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pdai.demospringbootstarter.DemoAutoConfiguration
  • install

spring-boot-starter-xxx

为什么需要spring-boot-maven-plugin?

spring-boot-maven-plugin提供了一些像jar一样打包或者运行应用程序的命令。

  1. spring-boot:run 运行SpringBoot应用程序;
  2. spring-boot:repackage 重新打包你的jar包或者是war包使其可执行
  3. spring-boot:start和spring-boot:stop管理Spring Boot应用程序的生命周期
  4. spring-boot:build-info生成执行器可以使用的构造信息

SpringBoot打成jar和普通的jar有什么区别?

Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过java -jar xxx.jar命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。

Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。

如何使用Sping Boot实现异常处理?

Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。通过实现一个ControlerAdvice类,来处理控制类抛出的所有异常。

SpringBoot实现热部署有哪几种方式?

主要有两种方式:

  • Spring Loaded
  • Spring-boot-devtools

Spring Boot中的监视器是什么?

Spring boot actuator是spring启动框架中的重要功能之一。Spring boot监视器可帮助您访问生产环境中正在运行的应用程序的当前状态。

有几个指标必须在生产环境中进行检查和监控。即使一些外部应用程序可能正在使用这些服务来向相关人员触发警报消息。监视器模块公开了一组可直接作为HTTP URL访问的REST端点来检查状态。

Spring Boot 可以兼容老 Spring 项目吗?

可以兼容,使用 @ImportResource 注解导入老 Spring项目配置文件。

10.3 Spring Security

什么是Spring Security?核心功能?

Spring Security是基于Spring的安全框架.它提供全面的安全性解决方案,同时在Web请求级别和调用级别确认和授权.在Spring Framework基础上,Spring Security充分利用了依赖注入(DI)和面向切面编程(AOP)功能,为应用系统提供声明式的安全访问控制功能,建起了为企业安全控制编写大量重复代码的工作,是一个轻量级的安全框架,并且很好集成Spring MVC

spring security 的核心功能主要包括:

  • 认证(Authentication):指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
  • 授权(Authorization):指的是验证某个用户是否有权限执行某个操作
  • 攻击防护:指的是防止伪造身份

Spring Security的原理?

  • 基于Filter技术实现?

首先SpringSecurity是基于Filter技术实现的。Spring通过DelegatingFilterProxy建立Web容器和Spring ApplicationContext的联系,而SpringSecurity使用FilterChainProxy 注册SecurityFilterChain。

spring-security-filter

  • 认证模块的实现

SecurityContextHolder(用于存储授权信息)

spring-security-context

手动授权的例子(SecurityContextHolder.getContext().setAuthentication(authentication)这种授权方式多线程不安全):

SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

除了手动授权外,SpringSecurity通过AuthenticationManager和ProviderManager进行授权。其中AuthenticationProvider代表不同的认证机制(最常用的账号/密码)。

ProviderManager

AuthenticationManager

  • 授权模块的实现

认证完成之后,SpringSecurity通过AccessDecisionManager 完成授权操作。除了全局的授权配置之外,也可以通过@PreAuthorize, @PreFilter, @PostAuthorize , @PostFilter注解实现方法级别的权限控制。

Spring Security基于用户名和密码的认证模式流程?

请求的用户名密码可以通过表单登录,基础认证,数字认证三种方式从HttpServletRequest中获得,用于认证的数据源策略有内存,数据库,ldap,自定义等。

拦截未授权的请求,重定向到登录页面

表单登录的过程,进行账号密码认证

10.4 MyBatis

10.5 JPA

10.6 日志框架

什么是日志系统和日志门面?分别有哪些框架?

日志系统是具体的日志框架,日志门面是不提供日志的具体实现,而是在运行时动态的绑定日志实现组件来工作,是一种外观模式。

日志系统

  • java.util.logging (JUL),JDK1.4 开始,通过 java.util.logging 提供日志功能。虽然是官方自带的log lib,JUL的使用确不广泛。
  • Log4j,Log4j 是 apache 的一个开源项目,创始人 Ceki Gulcu。Log4j 应该说是 Java 领域资格最老,应用最广的日志工具。Log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX 系统日志等。Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。
  • Logback,Logback 是由 log4j 创始人 Ceki Gulcu 设计的又一个开源日志组件,是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。
  • Log4j2,维护 Log4j 的人为了性能又搞出了 Log4j2。Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。

日志门面

  • common-logging,common-logging 是 apache 的一个开源项目。也称Jakarta Commons Logging,缩写 JCL。
  • slf4j, 全称为 Simple Logging Facade for Java,即 java 简单日志门面。作者又是 Ceki Gulcu!这位大神写了 Log4j、Logback 和 slf4j。类似于 Common-Logging,slf4j 是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。但是,slf4j 在编译时静态绑定真正的 Log 库。使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。

日志库中使用桥接模式解决什么问题?

  • 什么是桥接呢?

假如你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图:

从图中应该可以看出,无论你的老项目中使用的是 common-logging 或是直接使用 log4j、java.util.logging,都可以使用对应的桥接 jar 包来解决兼容问题。

  • slf4j 兼容 common-logging
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.7.12</version>
</dependency>
  • slf4j 兼容 log4j
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.12</version>
</dependency>

在日志配置时会考虑哪些点?

  • 支持日志路径,日志level等配置
  • 日志控制配置通过application.yml下发
  • 按天生成日志,当天的日志>50MB回滚
  • 最多保存10天日志
  • 生成的日志中Pattern自定义
  • Pattern中添加用户自定义的MDC字段,比如用户信息(当前日志是由哪个用户的请求产生),request信息。此种方式可以通过AOP切面控制,在MDC中添加requestID,在spring-logback.xml中配置Pattern。
  • 根据不同的运行环境设置Profile - dev,test,product
  • 对控制台,Err和全量日志分别配置
  • 对第三方包路径日志控制

对Java日志组件选型的建议?

slf4j已经成为了Java日志组件的明星选手,可以完美替代JCL,使用JCL桥接库也能完美兼容一切使用JCL作为日志门面的类库,现在的新系统已经没有不使用slf4j作为日志API的理由了。

日志记录服务方面,log4j在功能上输于logback和log4j2,在性能方面log4j2则全面超越log4j和logback。所以新系统应该在logback和log4j2中做出选择,对于性能有很高要求的系统,应优先考虑log4j2。

对日志架构使用比较好的实践?

说几个点:

  • 总是使用Log Facade,而不是具体Log Implementation
  • 只添加一个 Log Implementation依赖
  • 具体的日志实现依赖应该设置为optional和使用runtime scope
  • 如果有必要, 排除依赖的第三方库中的Log Impementation依赖
  • 避免为不会输出的log付出代价
  • 日志格式中最好不要使用行号,函数名等字段

对现有系统日志架构的改造建议?

如果现有系统使用JCL作为日志门面,又确实面临着JCL的ClassLoader机制带来的问题,完全可以引入slf4j并通过桥接库将JCL api输出的日志桥接至slf4j,再通过适配库适配至现有的日志输出服务(如log4j),如下图:

这样做不需要任何代码级的改造,就可以解决JCL的ClassLoader带来的问题,但没有办法享受日志模板等slf4j的api带来的优点。不过之后在现系统上开发的新功能就可以使用slf4j的api了,老代码也可以分批进行改造。

如果现有系统使用JCL作为日志门面,又头疼JCL不支持logback和log4j2等新的日志服务,也可以通过桥接库以slf4j替代JCL,但同样无法直接享受slf4j api的优点。

如果想要使用slf4j的api,那么就不得不进行代码改造了,当然改造也可以参考1中提到的方式逐步进行。

如果现系统面临着log4j的性能问题,可以使用Apache Logging提供的log4j到log4j2的桥接库log4j-1.2-api,把通过log4j api输出的日志桥接至log4j2。这样可以最快地使用上log4j2的先进性能,但组件中缺失了slf4j,对后续进行日志架构改造的灵活性有影响。另一种办法是先把log4j桥接至slf4j,再使用slf4j到log4j2的适配库。这样做稍微麻烦了一点,但可以逐步将系统中的日志输出标准化为使用slf4j的api,为后面的工作打好基础。

10.7 Tomcat

Tomcat 整体架构的设计?

从组件的角度看

Server: 表示服务器,它提供了一种优雅的方式来启动和停止整个系统,不必单独启停连接器和容器;它是Tomcat构成的顶级构成元素,所有一切均包含在Server中;

Service: 表示服务,Server可以运行多个服务。比如一个Tomcat里面可运行订单服务、支付服务、用户服务等等;Server的实现类StandardServer可以包含一个到多个Services, Service的实现类为StandardService调用了容器(Container)接口,其实是调用了Servlet Engine(引擎),而且StandardService类中也指明了该Service归属的Server;

Container: 表示容器,可以看做Servlet容器;引擎(Engine)、主机(Host)、上下文(Context)和Wraper均继承自Container接口,所以它们都是容器。

  • Engine -- 引擎
  • Host -- 主机
  • Context -- 上下文
  • Wrapper -- 包装器

Connector: 表示连接器, 它将Service和Container连接起来,首先它需要注册到一个Service,它的作用就是把来自客户端的请求转发到Container(容器),这就是它为什么称作连接器, 它支持的协议如下:

  • 支持AJP协议
  • 支持Http协议
  • 支持Https协议

Service内部还有各种支撑组件,下面简单罗列一下这些组件

  • Manager -- 管理器,用于管理会话Session
  • Logger -- 日志器,用于管理日志
  • Loader -- 加载器,和类加载有关,只会开放给Context所使用
  • Pipeline -- 管道组件,配合Valve实现过滤器功能
  • Valve -- 阀门组件,配合Pipeline实现过滤器功能
  • Realm -- 认证授权组件

Tomcat一个请求的处理流程?

假设来自客户的请求为:http://localhost:8080/test/index.jsp 请求被发送到本机端口8080,被在那里侦听的Coyote HTTP/1.1 Connector,然后

  • Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应
  • Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host
  • Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)
  • localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context
  • Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为""的Context去处理)
  • path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet
  • Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类,构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法
  • Context把执行完了之后的HttpServletResponse对象返回给Host
  • Host把HttpServletResponse对象返回给Engine
  • Engine把HttpServletResponse对象返回给Connector
  • Connector把HttpServletResponse对象返回给客户browser

Tomcat中类加载机制?

在Bootstrap中我们可以看到有如下三个classloader

ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
  • 为什么要设计多个类加载器

如果所有的类都使用一个类加载器来加载,会出现什么问题呢?

假如我们自己编写一个类java.util.Object,它的实现可能有一定的危险性或者隐藏的bug。而我们知道Java自带的核心类里面也有java.util.Object,如果JVM启动的时候先行加载的是我们自己编写的java.util.Object,那么就有可能出现安全问题!

所以,Sun(后被Oracle收购)采用了另外一种方式来保证最基本的、也是最核心的功能不会被破坏。你猜的没错,那就是双亲委派模式!

  • 什么是双亲委派模型

双亲委派模型解决了类错乱加载的问题,也设计得非常精妙。

双亲委派模式对类加载器定义了层级,每个类加载器都有一个父类加载器。在一个类需要加载的时候,首先委派给父类加载器来加载,而父类加载器又委派给祖父类加载器来加载,以此类推。如果父类及上面的类加载器都加载不了,那么由当前类加载器来加载,并将被加载的类缓存起来。

所以上述类是这么加载的

  • Java自带的核心类 -- 由启动类加载器加载
  • Java支持的可扩展类 -- 由扩展类加载器加载
  • 我们自己编写的类 -- 默认由应用程序类加载器或其子类加载
  • 为什么Tomcat的类加载器也不是双亲委派模型

Java默认的类加载机制是通过双亲委派模型来实现的,而Tomcat实现的方式又和双亲委派模型有所区别。

原因在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的。因此,如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。

举个例子,假如我们有两个Web程序,一个依赖A库的1.0版本,另一个依赖A库的2.0版本,他们都使用了类xxx.xx.Clazz,其实现的逻辑因类库版本的不同而结构完全不同。那么这两个Web程序的其中一个必然因为加载的Clazz不是所使用的Clazz而出现问题!而这对于开发来说是非常致命的!

Tomcat Container设计?

我们看下几个Container之间的关系:

从上图上,我们也可以看出Container顶层也是基于Lifecycle的组件设计的。

  • 在设计Container组件层次组件时,上述4个组件分别做什么的呢?为什么要四种组件呢?

Engine - 表示整个catalina的servlet引擎,多数情况下包含一个或多个子容器,这些子容器要么是Host,要么是Context实现,或者是其他自定义组。

Host - 表示包含多个Context的虚拟主机的。

Context — 表示一个ServletContext,表示一个webapp,它通常包含一个或多个wrapper。

Wrapper - 表示一个servlet定义的(如果servlet本身实现了SingleThreadModel,则可能支持多个servlet实例)。

  • 结合整体的框架图中上述组件部分,我们看下包含了什么

很明显,除了四个组件的嵌套关系,Container中还包含了Realm,Cluster,Listeners, Pipleline等支持组件。

这一点,还可以通过相关注释可以看出:

**Loader** - Class loader to use for integrating new Java classes for this Container into the JVM in which Catalina is running.
​
**Logger** - Implementation of the log() method signatures of the ServletContext interface.
​
**Manager** - Manager for the pool of Sessions associated with this Container.
​
**Realm** - Read-only interface to a security domain, for authenticating user identities and their corresponding roles.
​
**Resources** - JNDI directory context enabling access to static resources, enabling custom linkages to existing server components when Catalina is embedded in a larger server.

Tomcat LifeCycle机制?

  • Server及其它组件

  • Server后续组件生命周期及初始化

  • Server的依赖结构

public interface Lifecycle {
    /** 第1类:针对监听器 **/
    // 添加监听器
    public void addLifecycleListener(LifecycleListener listener);
    // 获取所有监听器
    public LifecycleListener[] findLifecycleListeners();
    // 移除某个监听器
    public void removeLifecycleListener(LifecycleListener listener);
    
    /** 第2类:针对控制流程 **/
    // 初始化方法
    public void init() throws LifecycleException;
    // 启动方法
    public void start() throws LifecycleException;
    // 停止方法,和start对应
    public void stop() throws LifecycleException;
    // 销毁方法,和init对应
    public void destroy() throws LifecycleException;
    
    /** 第3类:针对状态 **/
    // 获取生命周期状态
    public LifecycleState getState();
    // 获取字符串类型的生命周期状态
    public String getStateName();
}

Tomcat中Executor?

  1. Tomcat希望将Executor也纳入Lifecycle生命周期管理,所以让它实现了Lifecycle接口
  2. 引入超时机制:也就是说当work queue满时,会等待指定的时间,如果超时将抛出RejectedExecutionException,所以这里增加了一个void execute(Runnable command, long timeout, TimeUnit unit)方法; 其实本质上,它构造了JUC中ThreadPoolExecutor,通过它调用ThreadPoolExecutor的void execute(Runnable command, long timeout, TimeUnit unit)方法。

Tomcat中的设计模式?

  • 责任链模式:管道机制

在软件开发的常接触的责任链模式是FilterChain,它体现在很多软件设计中:

  1. 比如Spring Security框架中

  2. 比如HttpServletRequest处理的过滤器中

当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以使每个加工组件化,减少耦合。也可以使用在当一个request过来的时候,需要找到合适的加工方式。当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工。

网上找了图,这里我们后文将通过Tomcat请求处理向你阐述。

  • 外观模式:request请求
  • 观察者模式:事件监听

java中的事件机制的参与者有3种角色

  1. Event Eource:事件源,发起事件的主体。
  2. Event Object:事件状态对象,传递的信息载体,就好比Watcher的update方法的参数,可以是事件源本身,一般作为参数存在于listerner 的方法之中。
  3. Event Listener:事件监听器,当它监听到event object产生的时候,它就调用相应的方法,进行处理。

其实还有个东西比较重要:事件环境,在这个环境中,可以添加事件监听器,可以产生事件,可以触发事件监听器。

  • 模板方式: Lifecycle

LifecycleBase是使用了状态机+模板模式来实现的。模板方法有下面这几个:

// 初始化方法
protected abstract void initInternal() throws LifecycleException;
// 启动方法
protected abstract void startInternal() throws LifecycleException;
// 停止方法
protected abstract void stopInternal() throws LifecycleException;
// 销毁方法
protected abstract void destroyInternal() throws LifecycleException;

Tomcat JMX拓展机制?

11 开发工具

开发工具问题汇总。

11.1 Git

GIt中5个区,和具体操作?

  • 代码提交和同步代码

  • 代码撤销和撤销同步

平时是怎么提交代码的?

  • 第零步: 工作区与仓库保持一致
  • 第一步: 文件增删改,变为已修改状态
  • 第二步: git add ,变为已暂存状态
$ git status
$ git add --all # 当前项目下的所有更改
$ git add .  # 当前目录下的所有更改
$ git add xx/xx.py xx/xx2.py  # 添加某几个文件
  • 第三步: git commit,变为已提交状态
$ git commit -m "<这里写commit的描述>"
  • 第四步: git push,变为已推送状态
$ git push -u origin master # 第一次需要关联上
$ git push # 之后再推送就不用指明应该推送的远程分支了
$ git branch # 可以查看本地仓库的分支
$ git branch -a # 可以查看本地仓库和本地远程仓库(远程仓库的本地镜像)的所有分支

在某个分支下,我最常用的操作如下

$ git status
$ git add -a
$ git status
$ git commit -m 'xxx'
$ git pull --rebase
$ git push origin xxbranch

11.2 Maven

Maven中包的依赖原则?如何解决冲突?

  • 依赖原则

1.依赖路径最短优先原则

A -> B -> C -> X(1.0)
A -> D -> X(2.0)

由于 X(2.0) 路径最短,所以使用 X(2.0)。

2.声明顺序优先原则

A -> B -> X(1.0)
A -> C -> X(2.0)

在 POM 中最先声明的优先,上面的两个依赖如果先声明 B,那么最后使用 X(1.0)。

3.覆写优先原则

子 POM 内声明的依赖优先于父 POM 中声明的依赖。

  • 如何解决冲突
  1. 找到 Maven 加载的 Jar 包版本,使用 mvn dependency:tree 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。
  2. 发现了冲突的包之后,剩下的就是选择一个合适版本的包留下,如果是传递依赖的包正确,那么把显示依赖的包exclude掉。如果是某一个传递依赖的包有问题,那么我们需要手动把这个传递依赖execlude掉

Maven项目生命周期与构建原理?

Maven从项目的三个不同的角度,定义了单套生命周期,三套生命周期是相互独立的,它们之间不会相互影响。

  • 默认构建生命周期(Default Lifeclyle): 该生命周期表示这项目的构建过程,定义了一个项目的构建要经过的不同的阶段。
  • 清理生命周期(Clean Lifecycle): 该生命周期负责清理项目中的多余信息,保持项目资源和代码的整洁性。一般拿来清空directory(即一般的target)目录下的文件。
  • 站点管理生命周期(Site Lifecycle) :向我们创建一个项目时,我们有时候需要提供一个站点,来介绍这个项目的信息,如项目介绍,项目进度状态、项目组成成员,版本控制信息,项目javadoc索引信息等等。站点管理生命周期定义了站点管理过程的各个阶段。

12 架构

架构相关。

12.1 架构基础

如何理解架构的演进?

  • 初始阶段的网站架构
  • 应用服务和数据服务分离
  • 使用缓存改善网站性能
  • 使用应用服务器集群改善网站的并发处理能力
  • 数据库读写分离
  • 使用反向代理和CDN加上网站相应
  • 使用分布式文件系统和分布式数据库系统
  • 使用NoSQL和搜索引擎

  • 业务拆分 : 拆成A, B服务,以及MQ服务

  • 分布式服务

如何理解架构的服务化趋势?

  • 方向一:架构服务化

    • 单体分层架构
    • 面向服务架构 -SOA
    • 微服务架构 - Microservices
    • 云原生架构 - Cloud Native
  • 方向二:部署容器编排化

    • 虚拟机
    • 容器
    • Kubernetes 与编排

架构中有哪些技术点?

所谓网站架构模式即为了解决大型网站面临的高并发访问、海量数据、高可靠运行等一系列问题与挑战。为此,在实践中提出了许多解决方案,以实现网站高性能、高可靠性、易伸缩、可扩展、安全等各种技术架构目标。

  • 分层

分层是企业应用系统中最常见的一种架构模式,将系统在横向维度上切分成几个部分,每个部分负责一部分相对简单并比较单一的职责,然后通过上层对下层的依赖和调度组成一个完整的系统。

在网站的分层架构中,常见的为3层,即应用层服务层数据层:

  1. 应用层具体负责业务和视图的展示;
  2. 服务层为应用层提供服务支持;
  3. 数据库提供数据存储访问服务,如数据库、缓存、文件、搜索引擎等。

分层架构是逻辑上的,在物理部署上,三层架构可以部署在同一个物理机器上,但是随着网站业务的发展,必然需要对已经分层的模块分离部署,即三层结构分别部署在不同的服务器上,是网站拥有更多的计算资源以应对越来越多的用户访问。

所以虽然分层架构模式最初的目的是规划软件清晰的逻辑结构以便于开发维护,但在网站的发展过程中,分层结构对网站支持高并发向分布式方向的发展至关重要。

  • 分隔

如果说分层是将软件在横向方面进行切分,那么分隔就是在纵向方面对软件进行切分。

网站越大,功能越复杂,服务和数据处理的种类也越多,将这些不同的功能和服务分隔开来,包装成高内聚低耦合的模块单元,不仅有助于软件的开发维护也便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。

大型网站分隔的粒度可能会很小。比如在应用层,将不同业务进行分隔,例如将购物、论坛、搜索、广告分隔成不同的应用,有对立的团队负责,部署在不同的服务器上。

  • 分布式

对于大型网站,分层和分隔的一个主要目的是为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,通过远程调用协同工作。分布式意味着可以使用更多的计算机完同样的工作,计算机越多,CPU、内存、存储资源就越多,能过处理的并发访问和数据量就越大,进而能够为更多的用户提供服务。

在网站应用中,常用的分布式方案有一下几种.

  1. 分布式应用和服务:将分层和分隔后的应用和服务模块分布式部署,可以改善网站性能和并发性、加快开发和发布速度、减少数据库连接资源消耗。
  2. 分布式静态资源:网站的静态资源如JS、CSS、Logo图片等资源对立分布式部署,并采用独立的域名,即人们常说的动静分离。静态资源分布式部署可以减轻应用服务器的负载压力;通过使用独立域名加快浏览器并发加载的速度。
  3. 分布式数据和存储:大型网站需要处理以P为单位的海量数据,单台计算机无法提供如此大的存储空间,这些数据库需要分布式存储。
  4. 分布式计算:目前网站普遍使用Hadoop和MapReduce分布式计算框架进行此类批处理计算,其特点是移动计算而不是移动数据,将计算程序分发到数据所在的位置以加速计算和分布式计算。
  • 集群

对于用户访问集中的模块需要将独立部署的服务器集群化,即多台服务器部署相同的应用构成一个集群,通过负载均衡设备共同对外提供服务。

服务器集群能够为相同的服务提供更多的并发支持,因此当有更多的用户访问时,只需要向集群中加入新的机器即可;另外可以实现当其中的某台服务器发生故障时,可以通过负载均衡的失效转移机制将请求转移至集群中其他的服务器上,因此可以提高系统的可用性。

  • 缓存

缓存目的就是减轻服务器的计算,使数据直接返回给用户。在现在的软件设计中,缓存已经无处不在。具体实现有CDN、反向代理、本地缓存、分布式缓存等。

使用缓存有两个条件:访问数据热点不均衡,即某些频繁访问的数据需要放在缓存中;数据在某个时间段内有效,不过很快过期,否则会因为数据过期而脏读,影响数据的正确性。

  • 异步

使用异步,业务之间的消息传递不是同步调用,而是将一个业务操作分成多个阶段,每个阶段之间通过共享数据的方法异步执行进行协作。

具体实现则在单一服务器内部可用通过多线程共享内存对了的方式处理;在分布式系统中可用通过分布式消息队列来实现异步。

异步架构的典型就是生产者消费者方式,两者不存在直接调用。

  • 冗余

网站需要7×24小时连续运行,那么就得有相应的冗余机制,以防某台机器宕掉时无法访问,而冗余则可以通过部署至少两台服务器构成一个集群实现服务高可用。数据库除了定期备份还需要实现冷热备份。甚至可以在全球范围内部署灾备数据中心。

  • 自动化

具体有自动化发布过程,自动化代码管理、自动化测试、自动化安全检测、自动化部署、自动化监控、自动化报警、自动化失效转移、自动化失效恢复等。

  • 安全

网站在安全架构方面有许多模式:通过密码和手机校验码进行身份认证;登录、交易需要对网络通信进行加密;为了防止机器人程序滥用资源,需要使用验证码进行识别;对常见的XSS攻击、SQL注入需要编码转换;垃圾信息需要过滤等。

  • 敏捷性

积极接受需求变更,快速响应业务发展需求。

12.2 缓存

谈谈架构中的缓存应用?

缓存有各类特征,而且有不同介质的区别,那么实际工程中我们怎么去对缓存分类呢?在目前的应用服务框架中,比较常见的是,根据缓存应用的耦合度,分为local cache(本地缓存)和remote cache(分布式缓存)。

  • 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
  • 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。

目前各种类型的缓存都活跃在成千上万的应用服务中,还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。缓存的使用是程序员、架构师的必备技能,好的程序员能根据数据类型、业务场景来准确判断使用何种类型的缓存,如何使用这种缓存,以最小的成本最快的效率达到最优的目的。

在开发中缓存具体如何实现?

  • 本地缓存

    • 成员变量或局部变量实现, 比如map
    • 静态变量实现
    • Ehcache
    • Guava Cache
  • 分布式缓存

    • Redis集群+ Spring Cache注解方式

缓存会有哪些问题?如何解决?

参见redis缓存问题

使用缓存的经验?

不合理使用缓存非但不能提高系统的性能,还会成为系统的累赘,甚至风险。

  • 频繁修改的数据

如果缓存中保存的是频繁修改的数据,就会出现数据写入缓存后,应用还来不及读取缓存,数据就已经失效,徒增系统负担。一般来说,数据的读写比在2:1(写入一次缓存,在数据更新前至少读取两次)以上,缓存才有意义。

  • 没有热点的访问

如果应用系统访问数据没有热点,不遵循二八定律,那么缓存就没有意义。

  • 数据不一致与脏读

一般会对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中重新加载。因此要容忍一定时间的数据不一致,如卖家已经编辑了商品属性,但是需要过一段时间才能被买家看到。还有一种策略是数据更新立即更新缓存,不过这也会带来更多系统开销和事务一致性问题。

  • 缓存可用性

缓存会承担大部分数据库访问压力,数据库已经习惯了有缓存的日子,所以当缓存服务崩溃时,数据库会因为完全不能承受如此大压力而宕机,导致网站不可用。这种情况被称作缓存雪崩,发生这种故障,甚至不能简单地重启缓存服务器和数据库服务器来恢复。

实践中,有的网站通过缓存热备份等手段提高缓存可用性:当某台缓存服务器宕机时,将缓存访问切换到热备服务器上。但这种设计有违缓存的初衷,缓存根本就不应该当做一个可靠的数据源来使用。

通过分布式缓存服务器集群,将缓存数据分布到集群多台服务器上可在一定程度上改善缓存的可用性。当一台缓存服务器宕机时,只有部分缓存数据丢失,重新从数据库加载这部分数据不会产生很大的影响。

  • 缓存预热warm up

缓存中存放的是热点数据,热点数据又是缓存系统利用LRU(最近最久未用算法)对不断访问的数据筛选淘汰出来,这个过程需要花费较长的时间。新系统的缓存系统如果没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,那么最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫缓存预热。对于一些元数据如城市地名列表、类目信息,可以在启动时加载数据库中全部数据到缓存进行预热。

  • 避免缓存穿透

如果因为不恰当的业务、或者恶意攻击持续高并发地请求某个不存在的数据,由于缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成压力,甚至崩溃。一个简单的对策是将不存在的数据也缓存起来(其value为null)。

12.3 限流

什么是限流?三种限流的算法?

每个系统都有服务的上限,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。

令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法:

  • 令牌桶方式(Token Bucket)

    • Guava RateLimiter

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。先有一个木桶,系统按照固定速度,往桶里加入Token,如果桶已经满了就不再添加。当有请求到来时,会各自拿走一个Token,取到Token 才能继续进行请求处理,没有Token 就拒绝服务。

这里如果一段时间没有请求时,桶内就会积累一些Token,下次一旦有突发流量,只要Token足够,也能一次处理,所以令牌桶算法的特点是允许突发流量

  • 漏桶

水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。

因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.

  • 计数器

    计数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。也是最简单粗暴的算法。

    • 采用AtomicInteger

      • 使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。
      • 弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。
    • 采用令牌Semaphore:

      • 使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
      • 相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。
    • 采用ThreadPoolExecutor java线程池:

      • 固定线程池大小,超出固定先线程池和最大的线程数,拒绝线程请求;

限流令牌桶和漏桶对比?

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
  • 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
  • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

在单机情况下如何实现限流?

应用级限流方式只是单应用内的请求限流,不能进行全局限流。

  1. 限流总资源数
  2. 限流总并发/连接/请求数
  3. 限流某个接口的总并发/请求数
  4. 限流某个接口的时间窗请求数
  5. 平滑限流某个接口的请求数
  6. Guava RateLimiter

在分布式环境下如何实现限流?

我们需要分布式限流和接入层限流来进行全局限流。

  1. redis+lua实现中的lua脚本
  2. 使用Nginx+Lua实现的Lua脚本

12.4 降级和熔断

为什么会有容错?一般有哪些方式解决容错相关问题?

服务之间的依赖关系,如果被依赖的服务挂了以后,造成其他服务也会出现请求堆积、资源占用,慢慢扩散到所有服务,引发雪崩效应。

而容错就是要解决这类问题,常见的方式:

  • 主动超时:Http请求主动设置一个超时时间,超时就直接返回,不会造成服务堆积
  • 限流:限制最大并发数
  • 熔断:当错误数超过阈值时快速失败,不调用后端服务,同时隔一定时间放几个请求去重试后端服务是否能正常调用,如果成功则关闭熔断状态,失败则继续快速失败,直接返回。(此处有个重试,重试就是弹性恢复的能力)
  • 隔离:把每个依赖或调用的服务都隔离开来,防止级联失败引起整体服务不可用
  • 降级:服务失败或异常后,返回指定的默认信息

谈谈你对服务降级的理解?

由于爆炸性的流量冲击,对一些服务进行有策略的放弃,以此缓解系统压力,保证目前主要业务的正常运行。它主要是针对非正常情况下的应急服务措施:当此时一些业务服务无法执行时,给出一个统一的返回结果。

降级服务的特征

  • 原因:整体负荷超出整体负载承受能力。
  • 目的:保证重要或基本服务正常运行,非重要服务延迟使用或暂停使用
  • 大小:降低服务粒度,要考虑整体模块粒度的大小,将粒度控制在合适的范围内
  • 可控性:在服务粒度大小的基础上增加服务的可控性,后台服务开关的功能是一项必要配置(单机可配置文件,其他可领用数据库和缓存),可分为手动控制和自动控制。
  • 次序:一般从外围延伸服务开始降级,需要有一定的配置项,重要性低的优先降级,比如可以分组设置等级1-10,当服务需要降级到某一个级别时,进行相关配置

降级方式

  • 延迟服务:比如发表了评论,重要服务,比如在文章中显示正常,但是延迟给用户增加积分,只是放到一个缓存中,等服务平稳之后再执行。
  • 在粒度范围内关闭服务(片段降级或服务功能降级):比如关闭相关文章的推荐,直接关闭推荐区
  • 页面异步请求降级:比如商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进行降级;
  • 页面跳转(页面降级):比如可以有相关文章推荐,但是更多的页面则直接跳转到某一个地址
  • 写降级:比如秒杀抢购,我们可以只进行Cache的更新,然后异步同步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
  • 读降级:比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。

降级预案 在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级分类

  • 降级按照是否自动化可分为:自动开关降级(超时、失败次数、故障、限流)和人工开关降级(秒杀、电商大促等)。
  • 降级按照功能可分为:读服务降级、写服务降级。
  • 降级按照处于的系统层次可分为:多级降级。

自动降级分类

  • 超时降级:主要配置好超时时间和超时重试次数和机制,并使用异步机制探测回复情况
  • 失败次数降级:主要是一些不稳定的api,当失败调用次数达到一定阀值自动降级,同样要使用异步机制探测回复情况
  • 故障降级:比如要调用的远程服务挂掉了(网络故障、DNS故障、http服务返回错误的状态码、rpc服务抛出异常),则可以直接降级。降级后的处理方案有:默认值(比如库存服务挂了,返回默认现货)、兜底数据(比如广告挂了,返回提前准备好的一些静态页面)、缓存(之前暂存的一些缓存数据)
  • 限流降级: 当我们去秒杀或者抢购一些限购商品时,此时可能会因为访问量太大而导致系统崩溃,此时开发者会使用限流来进行限制访问量,当达到限流阀值,后续请求会被降级;降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)

什么是服务熔断?和服务降级有什么区别?

熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。

和服务降级有什么区别

服务熔断对服务提供了proxy,防止服务不可能时,出现串联故障(cascading failure),导致雪崩效应。

服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑。

  • 共性:

    • 目的 -> 都是从可用性、可靠性出发,提高系统的容错能力。
    • 最终表现->使某一些应用不可达或不可用,来保证整体系统稳定。
    • 粒度 -> 一般都是服务级别,但也有细粒度的层面:如做到数据持久层、只许查询不许增删改等。
    • 自治 -> 对其自治性要求很高。都要求具有较高的自动处理机制。
  • 区别:

    • 触发原因 -> 服务熔断通常是下级服务故障引起;服务降级通常为整体系统而考虑。
    • 管理目标 -> 熔断是每个微服务都需要的,是一个框架级的处理;而服务降级一般是关注业务,对业务进行考虑,抓住业务的层级,从而决定在哪一层上进行处理:比如在IO层,业务逻辑层,还是在外围进行处理。
    • 实现方式 -> 代码实现中的差异。

如何设计服务的熔断?

异常处理:调用受熔断器保护的服务的时候,我们必须要处理当服务不可用时的异常情况。这些异常处理通常需要视具体的业务情况而定。比如,如果应用程序只是暂时的功能降级,可能需要切换到其它的可替换的服务上来执行相同的任务或者获取相同的数据,或者给用户报告错误然后提示他们稍后重试。

异常的类型:请求失败的原因可能有很多种。一些原因可能会比其它原因更严重。比如,请求会失败可能是由于远程的服务崩溃,这可能需要花费数分钟来恢复;也可能是由于服务器暂时负载过重导致超时。熔断器应该能够检查错误的类型,从而根据具体的错误情况来调整策略。比如,可能需要很多次超时异常才可以断定需要切换到断开状态,而只需要几次错误提示就可以判断服务不可用而快速切换到断开状态。

日志:熔断器应该能够记录所有失败的请求,以及一些可能会尝试成功的请求,使得的管理员能够监控使用熔断器保护的服务的执行情况。 测试服务是否可用:在断开状态下,熔断器可以采用定期的ping远程的服务或者资源,来判断是否服务是否恢复,而不是使用计时器来自动切换到半断开状态。这种ping操作可以模拟之前那些失败的请求,或者可以使用通过调用远程服务提供的检查服务是否可用的方法来判断。

手动重置:在系统中对于失败操作的恢复时间是很难确定的,提供一个手动重置功能能够使得管理员可以手动的强制将熔断器切换到闭合状态。同样的,如果受熔断器保护的服务暂时不可用的话,管理员能够强制的将熔断器设置为断开状态。 并发问题:相同的熔断器有可能被大量并发请求同时访问。熔断器的实现不应该阻塞并发的请求或者增加每次请求调用的负担。 资源的差异性:使用单个熔断器时,一个资源如果有分布在多个地方就需要小心。比如,一个数据可能存储在多个磁盘分区上(shard),某个分区可以正常访问,而另一个可能存在暂时性的问题。在这种情况下,不同的错误响应如果混为一谈,那么应用程序访问的这些存在问题的分区的失败的可能性就会高,而那些被认为是正常的分区,就有可能被阻塞。

加快熔断器的熔断操作:有时候,服务返回的错误信息足够让熔断器立即执行熔断操作并且保持一段时间。比如,如果从一个分布式资源返回的响应提示负载超重,那么应该等待几分钟后再重试。(HTTP协议定义了”HTTP 503 Service Unavailable”来表示请求的服务当前不可用,他可以包含其他信息比如,超时等)

重复失败请求:当熔断器在断开状态的时候,熔断器可以记录每一次请求的细节,而不是仅仅返回失败信息,这样当远程服务恢复的时候,可以将这些失败的请求再重新请求一次。

服务熔断有哪些实现方案?

  • Hystrix

Spring Cloud Netflix Hystrix就是隔离措施的一种实现,可以设置在某种超时或者失败情形下断开依赖调用或者返回指定逻辑,从而提高分布式系统的稳定性. 流程图如下:

  • Sentinel

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。分为两个部分:

  1. 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  2. 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

主要特性:

12.5 负载均衡

什么是负载均衡?原理是什么?

负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。

  • 负载均衡原理

采用横向扩展的方式,通过添加机器来满足大型网站服务的处理能力。比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力。这就是典型的集群和负载均衡架构:如下图:

  1. 应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。
  2. 负载均衡设备:将用户访问的请求,根据负载均衡算法,分发到集群中的一台处理服务器。(一种把网络请求分散到一个服务器集群中的可用服务器上去的设备)
  • 负载均衡的作用(解决的问题):

1.解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);

2.提供故障转移,实现高可用;

3.通过添加或减少服务器数量,提供网站伸缩性(扩展性);

4.安全防护;(负载均衡设备上做一些过滤,黑白名单等处理)

负载均衡有哪些分类?

根据实现技术不同,可分为DNS负载均衡,HTTP负载均衡,IP负载均衡,链路层负载均衡等。

  • DNS负载均衡

最早的负载均衡技术,利用域名解析实现负载均衡,在DNS服务器,配置多个A记录,这些A记录对应的服务器构成集群。大型网站总是部分使用DNS解析,作为第一级负载均衡。如下图:

实践建议

将DNS作为第一级负载均衡,A记录对应着内部负载均衡的IP地址,通过内部负载均衡将请求分发到真实的Web服务器上。一般用于互联网公司,复杂的业务系统不合适使用。如下图:

  • IP负载均衡

在网络层通过修改请求目标地址进行负载均衡。

用户请求数据包,到达负载均衡服务器后,负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到一台真实服务器地址,然后将请求目的地址修改为,获得的真实ip地址,不需要经过用户进程处理。

真实服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器,再将数据包源地址修改为自身的ip地址,发送给用户浏览器。如下图:

IP负载均衡,真实物理服务器返回给负载均衡服务器,存在两种方式:(1)负载均衡服务器在修改目的ip地址的同时修改源地址。将数据包源地址设为自身盘,即源地址转换(snat)。(2)将负载均衡服务器同时作为真实物理服务器集群的网关服务器。

  • 链路层负载均衡

在通信协议的数据链路层修改mac地址,进行负载均衡。

数据分发时,不修改ip地址,指修改目标mac地址,配置真实物理服务器集群所有机器虚拟ip和负载均衡服务器ip地址一致,达到不修改数据包的源地址和目标地址,进行数据分发的目的。

实际处理服务器ip和数据请求目的ip一致,不需要经过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。也称为直接路由模式(DR模式)。如下图:

实践建议:DR模式是目前使用最广泛的一种负载均衡方式。

  • 混合型负载均衡

由于多个服务器群内硬件设备、各自的规模、提供的服务等的差异,可以考虑给每个服务器群采用最合适的负载均衡方式,然后又在这多个服务器群间再一次负载均衡或群集起来以一个整体向外界提供服务(即把这多个服务器群当做一个新的服务器群),从而达到最佳的性能。将这种方式称之为混合型负载均衡。

此种方式有时也用于单台均衡设备的性能不能满足大量连接请求的情况下。是目前大型互联网公司,普遍使用的方式。

方式一,如下图:

以上模式适合有动静分离的场景,反向代理服务器(集群)可以起到缓存和动态请求分发的作用,当时静态资源缓存在代理服务器时,则直接返回到浏览器。如果动态页面则请求后面的应用负载均衡(应用集群)。

方式二,如下图:

以上模式,适合动态请求场景。

因混合模式,可以根据具体场景,灵活搭配各种方式,以上两种方式仅供参考。

常见的负载均衡服务器有哪些?

平时我们常用的有四层负载均衡和七层负载均衡,四层的负载均衡是基于IP和端口实现的,七层的负载均衡是在四层的基础上,基于URL等信息实现。

  • 四层负载均衡

LVS:重量级软件,本身不支持正则表达式,部署起来比较麻烦,但是性能高,应用范围广,一般的大型互联网公司都有用到。

HAProxy:轻量级软件,支持的负载均衡策略非常多,较灵活。

Nginx:轻量级软件,支持的协议少(HTTP、HTTPS和Email协议),对于Session支持不友好。

  • 七层负载均衡

HAProxy:全面支持七层代理,灵活性高,支持Session会话保持。

Nginx:可以针对HTTP应用进行分流,正则规则灵活,支持高并发,部署简单。

Apache:性能较差,一般不考虑。

MySQL Proxy:官方的数据库中间件,可以实现读写分离,负载均衡等功能,但是对分表分库支持不完善(可选替代品:Atlas,Cobar,TDDL)。

常见的负载均衡的算法?

常见的负载均衡算法包含:

第一类,轮询法

  • 轮询法(Round Robin)

    • 将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
  • 加权轮询法(Weight Round Robin)

    • 不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
  • 平滑加权轮询法(Smooth Weight Round Robin)

第二类,随机法

  • 随机法(Random)

    • 通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多, 其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
  • 加权随机法(Weight Random)

    • 与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

第三类,哈希

  • 源地址哈希法(Hash)

    • 源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

第四类,连接数法

  • 最小连接数法(Least Connections)

    • 最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

12.6 灾备和故障转移

什么是容灾?一般基于什么实现?

容灾是指为了保证关键业务和应用在经历各种灾难后,仍然能够最大限度的提供正常服务的所进行的一系列系统计划及建设和管理行为。

容灾能力基于数据复制故障转移

一般怎么实现灾备?

备份是对数据进行保护,容灾是在备份的基础上,保障企业的业务连续性,从这个层面,一般将容灾划分为数据容灾和应用容灾。

  • 数据容灾是指建立一个异地的数据系统,该系统是本地关键应用数据的一个实时复制。
  • 应用容灾是指在数据容灾的基础上,在异地建立一套完整的与本地生产系统相当的备份应用系统,在灾难发生时,备端系统迅速接管业务继续运行。