Java(1-3年)面试宝典

Java

Java中有几种基本数据类型

有8种基本数据类型,分别是:

  1. 布尔型:boolean(1bit)
  2. 整数型:byte(1)、short(2)、int(4)、long(8)
  3. 字符型:char(2)
  4. 浮点型:float(4)、double(8)

Java中的IO流有几种

按流划分:有输入流和输出流。

按单位划分:有字节流和字符流。

字节流:inputstream,outputstream。

子父类:reader、writer。

什么是反射

反射是Java的一种机制,可以让我们在运行时获取类的信息

通过反射我们可以获取到类的所有信息,比如它的属性、构造器、方法、注解等

反射作用是什么

  1. 获取已装载类的成员变量信息。
  2. 获取已装载类的方法。
  3. 获取已装载类的构造方法信息。

什么是序列化

序列化就是一种用来处理对象流的机制,将对象内容流化,将流化后的对象传输于网络之间。

序列化是将对象转换为容易传输的格式的过程。

例如,可以序列化一个对象,然后通过HTTP通过Internet在客户端和服务器之间传输该对象。在另一端,反序列化将从流中构造成对象。

接口和抽象类的区别

  1. 接口
    1. 使用interface修饰。
    2. 不能实例化。
    3. 类可以实现多个接口。
  2. 抽象类
    1. 使用abstract修饰。
    2. 不能实例化。
    3. 抽象类只能单继承。
    4. 可以包含抽象方法和非抽象方法,非抽象方法需有方法体。

String能被继承吗,为什么

不能,因为String类有final修饰符,是最终类,所以是不能被继承的。

String 和 new String

stirng a = ‘123’ 会将其分配到常量池中,常量池中没有重复的元素,如果常量池中有相同的元素,就将其地址赋值给变量,如果没有就创建一个再赋值个变量。

string a = new string(‘123’),会将对象分配到堆中,即内存中一样,还是会创建一个新的对象。

String、StringBuffer、StringBuilder区别

string是字符串常量,是不可变的字符序列。string创建的字符在常量池中,对于string类的任何改变都会返回一个新的string对象。

stringbuffer、stringbuilder是字符串变量,是可变的字符序列。在堆中创建对象,对这两种中的内容修改都是返回当前对象。

stringbuilder是非线程安全的,stringbuffer是线程安全的,因为stringbuffer对于方法加了同步锁,而stringbuilder没有。

操作少量数据:适用String。

单线程操作字符串缓冲区下大量数据:适用stringbuilder。

多线程操作字符串缓冲区下大量数据:适用stringbuffer。

ArrayList、LinkedList区别

数据结构不同:ArrayList是动态数组、LinkedList是链表。

效率不同:

  1. 当随机访问(get或set)时,ArrayList比LinkedList的效率高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。而ArrayList可直接根据索引获取到对应元素。
  2. 当新增或删除(add或remove)时,LinkedList比ArrayList的效率高,因为ArrayList是数组,新增或者删除后要重置元素下标(索引)、移动数据,而LinkList只需修改指针即可。

自由性不同:

ArrayList需要手动指定固定大小容量,会预留一定空间,使用方便,只需要创建然后添加数据、通过下标进行使用。而LinkedList能够动态的随着数据量变化而变化,只需要存储节点信息和指针。

HashMap的特性

  1. 键值存、取快速、允许为null,key值不可重复、若key值重复则覆盖。
  2. 非同步,线程不安全。
  3. 底层是hash表,不保证有序。

HashMap JDK7和JDK8的底层结构

  1. JDK7采用的是数组+链表。
  2. JDB8采用的是数据+链表+红黑树。

HashMap JDK8为什么引入了红黑树

红黑树主要是为了解决链表过长导致的查询速度慢的问题,当链表长度大于等于8时,就会转变为红黑树,当链表长度小于等于6时,由红黑树转变回链表,因为链表过短时引入红黑树反而会降低查询速度。

HashMap链表的作用是什么

链表主要是为了解决数组中key发生hash碰撞,将发生碰撞的key存到链表中。

HashMap哈希碰撞

当两个不同的输入值,通过计算后得到相同散列值的现象,称为哈希碰撞。

HashMap扩容,扩容多大

hashMap中有个参数叫负荷因子,其实就是一个小数值0.75,也可以理解成75%。

hashMap默认大小是16,当填满了75%的空间大小时就该扩容了。

16乘以0.75,得到12,那就说明当集合中有12个元素的时候就进行扩容,扩容成原来的2倍。

HashMap Put方法执行过程

判断键值对数组是否为空,或者length是否为0,如果是则调用 resize 方法进行扩容。

根据键值对key计算hash值得到插入的数组索引。

判断索引是否为null,为null就直接新建节点进行添加,不为null则判断key是否一样,一样就直接覆盖。

判断索引是否为treenode,即判断是否为红黑树,如果是红黑树,直接在树中插入键值对。

如果不是treenode,则遍历链表,判断链表长度是否大于8,如果大于8就转为红黑树,在树中执行插入,如果不大于8就在链表中执行插入,在遍历的过程中判断key是否存在,存在就直接覆盖value值。

插入成功后,判断实际存储的键值对数量size是否超过了最大容量,如果超过了,就执行resize扩容。

== 和 equals 的区别

  1. 对于基本类型:== 比较的是值。
  2. 对于引用类型:== 比较的是地址值。
  3. equals 不能用于基本类型的比较。
  4. 如果没有重写 equals,equals就相当于 ==。
  5. 如果重写了 equals,equals比较的是对象的内容。

创建对象有几种方式

New、反射、反序列化、克隆

Spring

Spring是什么

spring是一个轻量级的IOC和AOP容器框架,是为JavaWeb程序提供基础服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需关心业务需求。

Spring的优点

  1. spring的DI机制将对象之间的依赖关系交由框架处理,降低组件的耦合性。
  2. spring提供了AOP技术,支持一些通用的任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。
  3. spring对主流的应用框架提供了集成支持。

Spring IOC的理解

控制反转,将对象的控制权转移给spring框架,由spring来负责控制对象的生命周期和对象之间的依赖关系。

直观来说就是:以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外一个对象,就必须通过new指令去创建依赖对象,使用完后还需要销毁,对象就会和其他接口或类耦合起来,而IOC则是由专门的容器来帮忙创建对象,将所有的类都在spring容器中登记,当需要某个对象时,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用的对象生命周期,而在IOC容器中,所有的对象都被spring控制,控制对象生命周期的不再是引用它的对象,而是spring容器,创建对象、注入依赖对象、都交由spring容器。而引用对象只是被动性的接受依赖对象,这就叫控制反转。

Spring AOP的理解

面向切面编程,使用AOP可以抽取一些公共模块出来,减少公共模块和业务代码的耦合。利用AOP可以对业务逻辑的各个部分进行隔离。

例如常用的日志功能,我们在调用每一个接口的时候说会去做记录日志的动作,那么使用AOP技术就可将记录日志的这个操作抽取出来做一个公共的模块,从而减少跟业务代码的耦合。

Spring AOP名词概念

  1. 切面
    类中可以被增强的方法,这个方法就被称为连接点
  2. 切入点
    类中有很多方法可以被增强,但实际中只有 add 和 update被增了,那么 add 和 update 方法就被称为切入点(实际实现的连接点)
  3. 通知
    通知是指一个切面在特定的连接点要做的事情(增强的功能)。通知分为方法执行前通知,方法执行后通知,环绕通知、异常通知和返回通知等.
  4. 切面
    把通知添加到切入点的过程叫切面.
  5. 目标
    代理的目标对象(要增强的类)
  6. 代理
    向目标对象应用通知之后创建的代理对象

Spring AOP代理

spring aop 是通过代理来实现面向切面编程的,它使用的代理有两种,JDK代理和CGlib代理。当我们的bean对象实现某一接口后,spring默认采用JDK动态代理,当我们bean对象没有实现接口时,默认采用CGlib代理。

Spring容器的启动流程

  1. 初始化spring容器,注册内置的BeanPostProcessor的BeanDefintion到容器中。
  2. 将配置类的BeanDefinition注册到容器中。
  3. 调用refresh方法刷新容器。

Spring Bean的生命周期

实例化 --> 属性赋值 --> 初始化 --> 销毁

Spring Bean的作用域

  1. singleton:默认作用域,单例bean,每个容器只有一个bean实例。
  2. prototype:为每一个bean请求创建一个实例。
  3. request:为每一个bean请求创建一个实例,在请求完成后,bean会失效并被回收。
  4. 另外几个说记不清。

Spring如何解决循环依赖

循环依赖其实就是循环引用,也就是两个或者两个以上的bean相互持有对方、最终形成闭环。

spring 解决循环依赖问题是通过三级缓存来实现的 ,3级缓存里边1级是存放已经实例化好并且进行依赖注入的bean对象,2级缓存里面是已经实例化好但是还没有进行依赖注入的bean对象,3级缓存主要用来存放bean工厂,主要用来生成原始bean对象,并且放到2级缓存里,3级缓存核心思想就是把bean的实例化和依赖注入分离,通过2级缓存存放的不完整的bean实例作为突破口解决循环依赖问题。

Spring自动装配

在spring中,使用AutoWtire来配置自动装载模式,对象无需自己查找或者创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象。

使用 @Autowired、@Resource 注解来自动装配指定的bean。

在启动 spring ioc 时,当扫描到 @Autowired、@Resource 就会在ioc容器自动查找需要的bean,并装配给该对象属性。

在使用 @Autowired 时,首先会在容器中查询对应类型的bean,如果查询刚好为1个,就将该bean装配给 @Autowired 指定的数据;如果查询结果不止一个,那么就 @Autowired 会根据名称来查找。如果查找结果为空,就会抛出异常。可以使用 required = false 来解决。

@Autowired 和 @Resource

Autowired 默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(required = true)。

Resource 默认是按照名称来装配注入的,只有当找不到名称匹配的bean才会按照类型来装配注入。

Spring 用到了哪些设计模式

  1. 工厂模式:spring 使用工厂模式,通过beanfactory 和 applicationcontext 来创建对象。
  2. 单例模式:容器中的bean默认都是单例模式。
  3. 代理模式:spring 的 aop 功能用到了 jdk 的动态代理和gclib 字节码生成技术。
  4. 模板方法:可以将相同部分的代码放到父类中,而将不同代码放入不同子类中,用来解决代码重复问题,比如 restTemplate。
  5. 观察者模式:spring 事件驱动模型就是观察者模式的一个经典的应用。
  6. 桥接模式:可以根据客户的需求能够动态切换不同的数据源,比如我们项目需要连接多个数据库,客户每次访问中根据需要去访问不同的数据库。

什么是 Spring MVC

Spring MVC 是一个基于 Java 的实现MVC设计模型的轻量级Web框架,属于Spring 框架的一个模块。

什么是 MVC 模型

MVC 就是 Model(模型)、View(视图)、controller(控制器)的缩写。

model 模型是指模型表示业务的规则(业务逻辑层)。

view 视图是指用户看到的、和用户交互的界面(视图层)。

controller 控制器是接收用户输入然后调用模型和视图完成用户的需求(控制器层)

Spring MVC 执行流程

img

什么是 Spring boot

用来简化 spring 应用的初始化搭建以及开发过程,使用特定方式来进行配置,比如 properties 或 yml 文件,创建独立的spring引用程序。

嵌入 tomcat 无需安装部署。

简化 maven 配置。

spring boot 来简化 spring 应用开发,约定大于配置,去繁从简。

Spring boot 优点

  1. 快速创建独立运行的spring项目与主流框架集成。
  2. 使用嵌入式的servlet容器,应用无需打成war包。
  3. 大量自动配置,简化开发,也可修改默认值。

Spring boot、Spring mvc、Spring 区别

spring 最重要的特征就是依赖注入。所有的 springModules 不是依赖注入就是 ioc 反转。

spring mvc 提供了一种分离式的方法来开发web应用,通过运用想 DispatcherServelet、MoudlAndView 等一些简单的概念,开发web 应用将会变得非常简单。

spring boot 就是 spring 和 spring 的结合体,因为 spring 和 spring 的问题在于需要大量的配置参数。spring boot 通过一个自动配置和启动项目来解决这个问题。

Spring boot stater 是什么

spring boot stater 可以放在自己的程序中,你可以一站式获取你所需要的 spring 和相关技术,而不需要依赖示例代码或者搜索和复制粘贴的方式的负载。

例如如果我们想使用 jpa 访问数据库,那么我们只需要项目包含 spring boot stater data jpa 依赖,就可以完美进行。

Spring boot 自动配置原理

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

Spring boot 需要独立的容器运行吗

可以不需要,内置了 tomcat 容器。

Spring boot 运行方式有几种

  1. 打包用命令放到容器中运行。
  2. 之间只需main。
  3. 用maven插件运行。

什么是微服务架构

微服务架构就是将单体应用程序分成多个应用程序,这多个应用程序就成为了微服务,每个微服务运行在自己的进程中,并使用轻量级的机制通信,这些服务围绕着业务来划分,并通过自动化部署机制来独立部署,这些服务可以使用不同的编程语言,不同的数据库,保证最低限度集中式管理。

Spring cloud 是什么

spring cloud 是一系列框架的集合,它利用 spring boot 的开发便利性巧妙简化了分布式系统基础设施开发,如服务发现注册、配置中心、路由、数据监控等等。

Spring cloud 优缺点

  1. 优点:
    1. 首先一个是耦合度低,不会影响其他模块的开发。
    2. 减轻团队的成本,可以并行开发,不用关注其他人怎么开发,先关注自己开发。
    3. 配置简单,基本用注解就能够实现,不用使用过多的配置文件。
    4. 每个服务都有自己的独立数据库,也可以有公共的数据库。
  2. 缺点:
    1. 部署比较麻烦,每个服务都需要单独部署。
    2. 针对数据的管理比较麻烦,因为每个服务都有独立的数据库。
    3. 系统集成测试比较麻烦。

Spring cloud 和 Spring boot区别

  1. spring boot 专注于快速开发方便的个个单体微服务。
  2. spring cloud 是关注全局微服务协调整理治理的框架,它将 spring boot 开发的一个个单体微服务整合并管理起来。
  3. 为每个微服务提供配置管理、服务发现、路由、等等这些。
  4. spring boot 可以离开 spring cloud 独立开发项目,但是 spring cloud 离不开 spring boot,属于依赖关系。

Spring cloud 由什么组成

  1. nacos、eureka:服务发现注册。
  2. gateway、zuul:服务网关。
  3. ribbon:负载均衡。
  4. feign:远程调用。
  5. config:分布式统一配置。

等等

什么是服务发现

当我们开发一个项目时,我们通常在属性文件中进行所有的配置,随着越来越多的服务开发和部署,添加和修改的这些属性变得更加复杂,有些服务可能会下降,某些位置可能会发生变化,手动更改这些属性可能会产生问题。这时候我们就需要用到一些服务发现组件,所有服务在服务发现组件上注册,因此我们无需处理服务任何更改和处理。

Eureka\Nacos 是什么

  • Eureka
    • Eureka 作为 spring cloud 服务注册功能服务器,他是服务注册中心,系统中的其他服务使用 Eureka 客户端将其连接到 Eureka Service 中,并保持心跳,这样我们就可以通过 Eureka Service 来监控所有服务是否正常运行
  • Nacos
    • Nacos 是阿里巴巴开源的一个产品,现在是 spring cloud 的一个组件,在国内受欢迎的程度较高,相比 Eureka 功能更加丰富,能够帮助我们更好的实现服务注册、发现配置等等。

Eureka\Nacos 的区别

eureka 和 nacos 都支持服务发现、拉取。

  1. nacos 支持服务端主动检测提供者状态,临时实例采用心跳模式,非临时实例采用主动检测模式。eureka采用心跳模式。
  2. nacos 临时实例心跳不正常时会被剔除,非临时实例不会被提出。eureka 在短期内没有发送心跳,并不会被剔除,相当于有个缓冲时间,Eureka有自我保护机制,短期内没有发送心跳,不会直接剔除的。
  3. nacos 支持服务列表变更消息推送,eureka 则是由服务主动拉取。

什么是网关

网关就相当于一个网络服务架构的入口,所有网络请求都必须经过网关转发到具体服务。

网关的作用是什么

  1. 统一管理微服务请求
  2. 权限控制
  3. 负载均衡
  4. 路由转发
  5. 安全控制
  6. 黑白名单

Zuul\Gateway 是什么

  • Zuul
    • 是 spring cloud 提供的成熟的路由方案,他会根据请求路径的不同,网关会定位到指定的微服务,并代理请求到不同的微服务接口。
  • Gateway
    • 是 spring 公司为了替代 Zuul 而开发的网关服务,它不仅提供了统一的路由方式,而且基于 Filter 过滤链的方式提供了网关的基本功能,如安全、监控、限流等。性能比 Zuul 更强大。更容易扩展。

负载均衡是什么

假设说现在由100件事情要做,如果都给一个人做,那么这个人就很累,但是如果均匀的分配给更多人做,那么每个人就会轻松很多。

Ribbon 是什么

ribbon 是负载均衡算法,当服务出现超时、重试等,简单来说就是在配置文件中列出来的所有服务,ribbon 会自动的帮助你基于某种规则(如简单的轮询)去连接这些服务,我们也可以很容易使用ribbon实现自定义负载均衡算法。

Ribbon 底层实现原理

ribbon 会从注册中心读取目标服务器信息,对于同一请求计数,使用取余算法获得目标服务集群索引,返回获取到的目标服务信息。

什么是断路器

当一个服务调用另一个服务时由于网络原因或自身出现的问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,放生雪崩效应。

断路器由三种状态:

  1. 打开状态:一段时间内,达到一定的次数无法调用,并且多次检测没有恢复的迹象,断路器会完全打开,下次请求就不会请求到这个服务。
  2. 半打开状态:短时间内有恢复迹象,断路器会将部分请求发送给该服务,正常调用时,断路器关闭。
  3. 关闭状态:当服务一直处于正常状态,能正常使用。

什么是 Hystrix

在分布式系统中,我们一定会依赖各种服务,那么这些服务一定会出现失败的情况,就会导致雪崩,Hystrix就是这样一个工具,防雪崩利器,它具有服务降级,服务熔断,服务隔离、监控等一些防止雪崩的技术。

Hystrix 防雪崩方式

  1. 服务降级:接口调用失败就调用本地的方法返回一个空
  2. 服务熔断:接口调用失败就会进入接口提前定义好的一个熔断方法,返回错误信息。
  3. 服务隔离:隔离服务之间相互影响。
  4. 服务监控:在服务发生调用时,会将每秒请求数,超过请求数等运行参数记录下来。

谈谈服务雪崩效应

当某个服务发生宕机时,调用这个服务的其他服务也会发生宕机,大型项目中的微服务之间是互通的,这样就会将服务的不可用扩大到各个其他服务中,从而使整个项目的服务宕机崩溃。

发生服务雪崩的原因

  1. 单个服务的代码中存在 bug。
  2. 请求访问量急速增加导致服务发生崩溃(如秒杀、抢红包)。
  3. 服务的硬件故障也会导致服务不可用

在微服务中如何保护服务

一般使用 Hystrix 框架,实现服务隔离来避免出现服务雪崩效应,从而达到保护服务的效果,当然在微服务中,高并发的数据库访问量导致访问线程阻塞,从而导致单个访问宕机,服务的不可用会蔓延到其他服务,引起整体服务雪崩,使用服务降级能有效为不同的服务分配资源,一旦服务不可用则返回友好提示,不占用其他服务资源,从而避免单个服务崩溃引发整体服务的不可用。

服务降级、熔断、隔离

服务降级:当客户端请求服务端的时候,防止客户端一直等待,不会处理业务逻辑代码,直接返回友好提示。

服务熔断是在服务降级的基础上,更直接的一种保护方式,当在一个统计时间范围内的请求失败数量到达设定值,或者当期的请求错误频率到达设定的错误阈值时,会开启熔断,之后的请求直接走 fallback 方法,在设定时间后尝试恢复。

服务隔离就是为隔离的服务开启一个独立的线程池,这样在高并发的情况下不会影响其他服务,服务隔离有线程池和信号量两种方式。一般使用线程池。

什么是 Feign

feign 是一个声明式 web 客户端,使用它我们编写 web 服务客户端更加容易,只需将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建http请求了,直接调用接口就可以。

Spring cloud 有几种调用接口方式

Feign

RestTemplate

Ribbon

Ribbon 和 Feign 区别

Ribbon 需要我们自己构建 http 请求,模拟 http 请求然后通过 restTemplate 发送给其服务,步骤相对繁琐。

而 feign 则是在 ribbon 的基础上进行了一次改进,采用接口的形式将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建http请求。直接调用接口就可以了。

MySQL

执行一条语句的过程

  1. 客户端先通过连接器得到 mysql 服务器。
  2. 校验权限通过后,先查询是否有缓存,如果有则直接返回缓存数据,如果没有则进入分析器。
  3. 分析器会对查询语句进行语法分析和词法分析,判断 sql 是否正确,如果查询语法错误则会返回给客户端错误信息,如果语法正确则进入优化器。
  4. 优化器是对查询语句进行优化处理,例如一个表里有多个索引,优化器会判断哪个索引性能比较好。
  5. 优化器执行完成进入执行器,执行器开始执行语句进行查询比对,直到查询满足条件的所有数据,然后返回给客户端。

查询缓存的优缺点

查询缓存功能是在连接器之后发生的,优点是效率高,如果已经有缓存则直接返回结果。

查询缓存的缺点是失效太繁琐,导致命中率较低,任何更新表操作都会清空查询缓存,因此导致查询缓存非常容易失效。

常用的引擎有哪些

InnoDB、MylSAM、Memory 等,mysql5 版本后,InnoDB成为了默认存储引擎。

InnoDB 和 MylSAM

InnoDB 和 MylSAM 最大的区别就是 InnoDB 支持事务,而 mylsam 不支持。

innodb 支持崩溃后安全恢复、mylsam 不支持。

innodb 支持行级锁、mylsam 不支持,只支持表锁。

innodb 支持外键、mylsam 不支持。

mylsam 比 innodb 性能高。

什么叫回表查询

普通索引查询到主键索引后,回到主键索引树搜索的过程,我们称为回表查询。

为什么索引选择 B+ 树

因为 B+ 树相对于 B 树而言 B+ 树的层级更少,搜索效率更高,因为 B 树无论是叶子节点还是非叶子节点都会存储数据,这样会导致一页中的存储键值减少,指针也跟着减少,要同样保存大量的数据,只能增加树的高度,从而导致性能降低。

为什么不使用 Hash 索引

因为 hash 索引不支持范围查询和排序,而 B+ 可以。

索引的使用原则

针对数据量较大、且查询比较频繁的表建立索引。

针对于常作为查询条件、排序、分组操作的字段建立索引。

尽量选择区分度高的字段作为索引。

尽量建立唯一索引。

尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表查询,提高效率。

要控制索引的数量,索引不是越多越好,索引越多,维护索引的结构代价就越大,会影响增删改操作的效率。

索引的分类

按照逻辑划分:

  1. 主键索引:主键索引是一种特殊的唯一索引,不允许有空值。
  2. 普通索引
  3. 复合索引:复合索引指在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用,使用复核索引时需遵循最左前缀原则。
  4. 唯一索引
  5. 空间索引:只有只能在存储引擎为Myslam的表中创建,所以不是很了解。

按照数据结构划分:

  1. B+树索引。
  2. hash索引。

什么是最左前缀原则

最左前缀原则是Mysql中一种重要的原则,指的是索引以最左边为起点任何连续的索引都能匹配上,当遇到范围查询(>,<,like)就会停止匹配。比如复复合索引(a,b,c)。

where a=1 只使用了索引 a;
where a=1 and b=2 只使用了索引 a,b;
where a=1 and b=2 and c=3 使用 a,b,c;
where b=1 or where c=1 不使用索引;
where a=1 and c=3 只使用了索引 a;
where a=3 and b like ‘xx%’ and c=3 只使用了索引 a,b。

唯一索引和普通索引哪个好

唯一索引和普通索引的性能对比分为两种情况:

  1. 对于查询来说两者都是从索引树进行查询,性能几乎没有任何区别。
  2. 对于更新操作来说,因为唯一索引需要先将数据读取到内存,如果需要判断是否有冲突,因此唯一索引要多了个判断操作,从而性能就比普通索引要低。

主键的设计原则

满足业务需求的情况下,尽量降低主键长度。

插入数据时,尽量选择顺序插入,选择使用自增主键。

尽量不要使用uuid做主键或者是其他自然主键、比如身份证。

业务操作时,避免对主键的修改。

不设置主键会有主键吗

会,不设置主键InnoDB默认会生成一个rowid作为主键。

左、右、内、外连接区别

left join(左连接、左外连接):返回左表中的所有记录和右表中连接字段相等的记录。

right join(右连接、右外连接):返回右表中所有记录和左表中连接字段相等的记录。

inner join(内连接、等值连接):只返回两个表中连接字段相等的记录。

full join(全外连接):返回两个表中所有的记录和左右表中连接字段相等的记录。

事务是什么,四大特性

事务是一系列的数据库操作,要么全部成功,要么全部失败。

  • 原子性:要么全部执行,要不全部不执行。
  • 一致性:事务执行后使得数据库,操作前和操作后是数据都是正确的。
  • 隔离性:在事务提交之前,不允许把该事务对数据的任何改变提供给其他事务,就是每个事务之间都是相互隔离的。
  • 持久性:事务提交之后,结果永久保存在数据库中。

事务有几种隔离级别

有四种隔离级别:

  1. 读未提交(red uncommited):读取到未提交的数据。
  2. 读已提交(red commited):读取到已提交的数据,也叫不可重复读,两次读取的数据不一致。
  3. 可重复读(repetable red)。
  4. 串行化(serializable)读取数据都会锁住整张表,数据操不会出错,但并发性能极低,基本不用。

如何处理死锁

两种常见的方式:

  1. 通过 innodb_lock_wait_timeout 来设置操作时间,一直等待,直到超时。
  2. 发起死锁检测,发现死锁后,主动回滚死锁中的事务,让其他事务执行。

锁的分类有哪些

全局锁:就是给整个数据库加锁,加锁后就处于只读的状态,后续的更新操作都会被阻塞。

表记锁:就是给整张表加锁,锁的粒度比较大。

行级锁:就是给整行数据加锁,锁的粒度小。

InnoDB 如何实现行锁

只有通过索引条件检索数据,InnoDB 才会使用行锁,否则 InnoDB 将使用表锁, 使用 for update 来实现行锁。

性能指标都有哪些

QPS:每秒查询次数,数据库每秒能够处理的查询次数。

TPS:每秒处理事务次数。

通过 show status 查询当前数据库的状态信息。

重要的日志有哪些

错误日志:记录运行过程中的错误信息,比如无法加载数据库文件,权限不正确都会记录在这里,默认情况下是开启的。

查询日志:记录运行过程中的所有命令(不仅仅是select)。默认情况下是关闭的。

慢查询日志:记录查询超过指定时间的语句,默认情况下是关闭的。

回滚日志(undo log):记录日志被修改前的值,从而保证如果修改出现异常,可以使用 undo log 来实现回滚。

二进制日志(big log):是一个二进制文件,记录所有数据库表结构变更以及表修改的操作。

慢查询日志的获取方式

使用 Mysql 自带功能,开启慢查询日志,在Mysql的安装目录下找到 my.cnf 文件配置 slow-quey-log-on 开启慢查询,慢查询默认时长为10s,默认存储文件名 host_name_slow.log

如何分析定位慢查询语句

使用 mysql 中的 explain 分析执行语句:

explain select * from where id = 5

主要关注的就是 type 字段:

  1. all:扫描全表
  2. index:遍历索引
  3. range:索引范围查找
  4. fulltext:使用全文索引
  5. ref:使用非唯一索引查找数据

等等,其他的不了解

常用的读写分离方案

两种:

  1. 使用 mysql 官方提供的数据库代理产品 mysql proxySQL 搭建自动分配的数据库读写分离环境。
  2. 在应用程序层面配置多数据源使用代码实现读写分离。

如何配置多实例

mysql 多实例就是在同一台机器上启动多个 mysql 程序,然后它们去监听不同的端口,运行多个服务进程,它们相互独立,互不影响的对外提供服务。

  • 可以配置一个实例对于一个配置文件,不同的端口
  • 可以一个配置文件下配置不同的实例,基于 Mysql 的 d_multi 工具。

数据库分片是什么

数据库分片是将一个大型的数据库分割成多个部分,分别存储在多个独立的服务器节点上,从而实现数据库存储的水平扩展,这种方式能够解决单一数据库的容量限制、处理能力限制、备份恢复限制等问题。

数据库分片方案有哪些

有两种,客户端代理方式、中间件代理方式:

  • 客户端代理方式:分片逻辑在应用端,封装在 jar 包里面,通过修改或者封装 jdbc 层来实现,比如 sharding-jdbc、阿里 TDDL 都是这种方式。
  • 中间件代理:在应用层和数据层中间加了一层代理,分片逻辑维护在中间件服务中,比如 Mycat,网易的 DDB 都是这种方式。

查询语句的优化方案

常见的优化方案:

  • 不做列运算,把计算都放入各个业务系统中实现。
  • 查询语句尽可能简单,大语句拆小语句,减少锁的时间。
  • 不使用 select * 查询。
  • 避免 %xx 查询。
  • 避免在 where 后面使用 != 或者 >< 操作符,查询引用会放弃索引,而进行全表扫描。
  • 列表数据使用分页查询,每页数据量尽量不要太大。

表的优化策略有哪些

读写分离:主库负责写,从库负责读。

垂直区分:根据数据的属性,单独拆分表,甚至拆分成库。

水平区分:保持表结构不变,根据策略存储数据分片,这样每一片数据被分散到不同的表或者库中,水平拆分只是为了解决单张表数据量过大的问题。

Redis

什么是 Redis

redis 是一种 noSQL 也就是 key-value 类型的内存数据库,整个数据库全部加载在内存中进行操作。

Redis 优缺点有哪些

优点:

  1. 高性能:redis 是一种基于内存的数据库,数据存储在内存当中,因此具有非常快的读写速度。
  2. 简单易用:redis 提供了简洁、值观的命令行和接口,使开发人员可以轻松的进行数据存储和检索。
  3. 数据持久化:redis 支持数据持久化,可以将内存中的数据保存到磁盘上,以防数据丢失。
  4. 高可用:redis 支持自从复制,可以实现数据的自动备份和故障转移。

缺点:

  1. 内存限制:redis 的数据存储在内存中,因此收到可用内存大小的限制。当数据量超过内存容量时,性能就会受到影响。
  2. 单线程模型:redis 采用单线程模型来处理客户端请求。这意味着在高并发的场景下,性能可能会受到限制。
  3. 缺乏复杂查询支持:redis 主要存储键值对和简单的数据结构,并不支持复杂的查询操作,相比之下,关系型数据库具有更强大的查询语言能力。

Redis 支持几种数据类型

String:字符串,一个 key 对应一个 value 字符串。

list:列表,一个key 对应一个 value 列表。

set:集合,一个key 对应一个没有重复元素的 set 集合。

zset:有序集合,一个key 对应一个没有重复元素的有序 set 集合。

hash:哈希,一个 key 对应多个键值对。

Redis 为什么这么快

  • 基于内存:redis 是使用内存存储,没有 io 上的开销,数据在内存中,读写速度快。
  • 高效的数据结构:redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。

为什么不用Redis做主数据库

虽然 redis 很块,但是它也有一些局限性,不能完全替代主数据库。

  1. 事务处理:redis 只支持简单的事务处理,对于复杂的事务无能为力,比如跨多个键的事务处理。
  2. 数据持久化:redis 是内存数据库,数据存在内存中,如果服务器宕机了,数据可能丢失,虽然 redis 提供了数据持久化机制,但有一些限制。
  3. 数据处理:redis 只支持简单的数据结构,比如字符串,列表,哈希等,如果需要处理复杂的数据结构,比如关系型数据库中的表,那么redis 可能不是一个好的选择。

虽然 redis 非常快,但是它还有一些限制,不能完全替代主数据库,所以,使用 redis 作为缓存是一种很好方式,可用提高应用程序的性能。

Redis 的应用场景

缓存热点数据,缓解数据库压力。

利用 redis 原子性的自增操作,可用实现计数的功能,比如统计用户点赞、用户访问计数等。

分布式锁,在分布式的场景下,无法实现单机环境下的锁来对多个节点上的进程同步。可用使用 redis 自带的 setnx 命令来实现分布式锁,除此之外,还可以使用官方提供的 redlock 分布式锁实现。

简单的消息队列,可用使用 redis 自身的发布/订阅模式来实现简单的消息队列。

什么是缓存穿透

缓存穿透就是查询一个不存在的数据,然后数据库查不到也不会直接写入缓存,就会导致每次请求都查数据库。

缓存穿透解决方案

缓存空数据,按照某一个查询条件查询到的数据为空时,将这个结果存到redis中,这样下次查询进到redis就能查到该数据了。

但是会出现数据不一致的情况,假如数据库中这个条件的数据更新了,redis中的数据可能没有进行更新,那么就需要加一步更新到redis的操作。

什么是缓存雪崩

缓存雪崩就是在同一时间段内,redis中的大量的key同时失效了,或者服务器宕机了,导致大量请求打到数据库,给数据库带来巨大压力。

缓存雪崩解决方案

给不同的key设置不同的过期时间,预防同时失效。

利用 redis 集群提高服务的高可用性,预防服务器宕机。

给缓存业务添加降级限流策略。

什么是缓存击穿

缓存击穿也叫热点key,就是一个高并发服务并且缓存重建业务较复杂的key,突然失效了,无数的请求访问会在瞬间给数据库带来冲击。

缓存击穿解决方案

  • 互斥锁
    • 当查询缓存未命中时,先去获取互斥锁,然后执行查询数据库操作,查询完成后缓存到redis中,最后释放锁,在高并发的场景下,当有一个线程获取到互斥锁后,其他线程进入休眠状态,等待设定好的时间后,进行重试,直到成功获取并返回。
  • 逻辑过期
    • 当查询缓存未命中时,先去获取互斥锁,然后开启一个新的线程去执行查询数据库和写入redis的操作,先返回过期的数据。在高并发的场景下,其他线程获取不到锁也是先返回过期的数据。直到持有互斥锁的线程执行完成后,后续的线程就可以命中缓存。
      在这里插入图片描述
      在这里插入图片描述

如何保证双写一致性

双写一致性就是当修改了数据库的数据后,也要更新到缓存,缓存中的数据要和数据库中的保持一致。

确保双写一致性有两种场景,一种是强一致性,一种是延时一致性,具体看业务需求来定。

  • 强一致性
    • 强一致性需要让数据库与redis的高度保持一致,要求实时性比较高,可以采用读写锁的方式来保证,用redisson 实现读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥,当我们更新数据的时候,添加排它锁(读读、读写都互斥),这样就能保证在写数据的同时是不会让其他线程读取数据的。
      • 那这个排它锁是如何保证读读、读写互斥的呢?
    • 其实排它锁底层使用的是 setnx,保证了同时只能有一个线程锁住的方法。
      • 有听过延迟双删吗,为什么不用它呢
    • 因为延迟双删没法保证强一致性,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延迟删除缓存中的数据,其实这个延迟多久不太好确定,在延迟的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
  • 延迟一致性
    • 延迟一致性可以容许数据库中的数据和缓存中的数据存在一定的延迟,只要最终一致即可。可以采用阿里的 canal 组件实现数据同步,部署一个 canal 服务。伪装成一个mysql的从节点,当mysql数据更新后,canal会读取 binlog 数据,然后通过客户端获取到数据,更新缓存即可。

Redis 数据持久化方案

  • RDB
    • RDB 持久化就是把当前内存里面的数据生成快照保存到磁盘里,是对 redis 数据执行周期性的持久化。
    • 优点:RDB 会产生多个数据文件,每个是一个二进制文件,代表 redis 再某个时间点上的数据快照。非常适于备份,全量复制的场景。
    • 缺点:RDB 默认是五分钟甚至更久才会生成一次,这意味着你这次同步到下次同步这五分钟的数据很有可能全部丢失掉。因此RDB 方式数据无法做到实时持久化。
  • AOF
    • AOF 持久化以日志的方式记录每次写命令,以追加的方式写入日志文件,重启时重新执行 AOF 中的命令达到恢复数据的目的。AOF 的主要作用是为了解决数据持久化的实时性,目前是 Redis 持久化的主流方式。两种机制全部开启的时候,Redis 重启的时候回默认使用 AOF 去重新构造数据,因为 AOF 的数据是比 RDB 完整的。
    • 优点:RDB 五分钟生成一次快照,但是 AOF 是一秒一次去通过一个后台的线程(fsync)操作,那么最多丢失这一秒的数据。
    • 缺点:一样的数据,AOF 比 RDB 还要大。
  • 如何选择
    • 全部要,你单独用 RDB 可能回丢失很多数据,因为五分钟执行一次,你单独用 AOF 你恢复没有 RDB 来的快,真出什么问题的时候第一时间用 RDB 恢复,然后用 AOF 补全。

Redis 内存淘汰策略

当 redis 中的内存不够用时,此时再向 redis 中添加新的数据,那么 redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被成为内存淘汰策略。

redis 支持 8 种 不同的策略来选择要删除的 key。

  1. noevication:不淘汰任何 key,但是内存满时不允许写入新数据(默认)。
  2. volatile-ttl:对设置了过期时间的 key,会比较 key 的剩余时间,剩余时间越小,越先被淘汰。
  3. allkeys-random:对全部 key 进行随机淘汰。
  4. volatile-random:对设置了过期时间的全部 key 进行随机淘汰。
  5. allkeys-lru:对全部 key 进行 LRU 算法淘汰。
  6. volatile-lru:对设置了过期时间的全部 key 进行 LRU 算法淘汰。
  7. allkeys-lfu:对全部 key 进行 LFU 算法淘汰。
  8. volatile-lfu:对设置了过期时间的全部 key 进行 LFU 算法淘汰。
  • LRU 和 LFU 算法你能说一下是什么吗
    • LRU:最少最近使用。当用时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
    • LFU:最少频率使用,会统计每个key的访问频率,值越小淘汰优先级越高。
  • 数据库有 100 万条数据,Redis 只能存 20 W 数据,如何保证 Redis 中的数据都是热点数据。
    • 使用 allkeys-lru(挑选出最近最少使用的数据淘汰)淘汰策略,这样留下来的都是经常访问的热点数据了。

Redis 线程模型

redis 的线程模型是:单线程、多路复用、异步非阻塞。采用这个模型的原因是希望通过单线程来避免切换,锁竞争等带来的性能问题。

Redis 是单线程的吗

redis 单线程指的是:接收客户端请求、解析请求、进行数据读写操作、发送数据给客户端的这个过程是由一个线程来完成的。

但是 Redis 程序并不是单线程的,Redis 在启动的时候,是会启动有后台线程的。

关闭文件、AOF刷盘、释放内存,这些任务需要创建独立的线程来处理,因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 redis 主线程就很容易发生阻塞,这样就无法处理后面的请求了。

后台线程就相当于一个消费者,生产者把耗时的任务丢到队列中去,消费者不停的轮询这个队列,拿出任务去执行对应的方法即可。

Redis 采用单线程为什么还那么快

redis 的大部分操作都是在内存中完成的,并且采用了高效的数据结构,因此 redis 瓶颈可能是机器内存或者网络带宽,并非 cpu、既然 cpu 不是瓶颈,那么自然就采用单线程的解决方案了。

redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程来回切换带来的时间和性能上的开销,而且也不会出现死锁的问题。

redis 采用了 IO 多路复用机制处理大量客户端 socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,简单来说,在 redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 socket 和已连接 socket。内核会一直监听这些 socket 上的连接请求或者数据请求。一旦有请求到达,就会交给 redis 线程处理,这就实现了一个 redis 线程处理多个 IO 流的效果。

Redis 如何实现分布式锁

可以使用 redisson 实现分布式锁,底层是 setnx 和 lua 脚本,setnx 是 set if not exists 如果不存在则set 的意思,lua 脚本的作用是能够调用 redis 命令,保证多条命令执行的原子性。

  • redisson 的锁是可重入的吗
    • 是可重入的,多个锁重入需要判断是否是当前线程,在redis 中进行存储的时候用 hash 结构,来存储线程信息和可重入次数。
  • redisson 如何控制有效时长
    • 在 redisson 中引入一个看门狗机制,就是说每隔一段时间就检查当前业务是否持有锁,如果持有就增加锁的持有时间,当业务执行完成之后释放锁就可以了。
  • redisson 实现分布式锁能解决主从一致性的问题吗
    • 这个是不能的,比如,当线程1加锁成功后,master 节点会异步复制到 slave 节点,此时当持有 redis 锁的 master 节点宕机后,slave 会被提升为新的 master 节点,假如现在来了一个线程2,再次加锁,会在新的 master 节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
    • 我们可以利用 redisson 提供的红锁来解决这个问题,它的主要作用就是,不能只在一个 redis 实例上创建锁,应该是在多个 redis 实例上创建锁,并且要求在大多数 redis 节点上都能成功创建锁,红锁中要求的是 redis 节点数量要过半,这样就能保证避免线程1加锁成功后,master 节点宕机导致线程2成功加锁到新的master节点上的问题了,
    • 但是,如果使用了红锁,因为需要在多个节点上加锁,性能就变得很低了,并且维护的成功非常高,所以我们一般项目中也不会直接使用红锁,并且官方也暂时弃用了这个红锁。

Redis 主从同步

单节点的 redis 并发能力是有上限的,要进一步提高 redis 的并发能力,就需要搭建主从集群,实现读写分离,一般是一主多从,主节点负责写,从节点负责读。

Redis 主从同步流程

  • 全量同步:从节点第一次与主节点建立连接的时候使用全量同步
    • 从节点请求主节点同步数据,其中从节点会携带自己的 replication id 和 offset 偏移量。
    • 主节点判断是否是第一次请求,主要判断依据就是,主节点与从节点是否是同一个 replication id 如果不是,就说明是第一次同步,那么主节点就会把自己的 replication id 和 offset 发给从节点, 让从节点和主节点的信息保持一致。
    • 主节点会执行bgsave,生成 rdb 文件后,发送给从节点执行,从节点先把自己的数据清空,然后执行主节点发送过来的 rdb 文件,这样就保持一致了。
    • 当然如果 rdb 生成期间,依然有请求到了主节点后,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后会把这个日志文件发送给从节点,这样就能保证主节点和从节点完全一致了,后期再同步的时候,都是依赖于这个日志文件,这个就是全量同步。
  • 增量同步
    • 当从节点服务器重启之后,数据就不一致了,所以这个时候,从节点就会请求主节点同步数据,主节点还是判断是不是第一次请求,不是第一次就获取从节点的 offset 值,然后主节点从命令行中获取 offset 值之后的数据,发送给从节点进行数据同步,这就是增量同步。

Redis 如何保证高并发高可用

首先可以搭建主从集群,再加上 redis 中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面包含了对主从服务的监控、自动故障恢复、通知;如果 master 故障,sentinel 会将一个 slave 提升为 master。当故障实例恢复后也以新的 master 为主,同时 sentinel 也充当 redis 客户端的服务发现来源,当集群发生故障转移时,会将最新的信息推送给 redis 客户端,所以一般项目都会采用哨兵模式来保证redis的高并发高可用。

Redis 集群脑裂

有时候由于网络等原因可能会出现脑裂的情况,就是说,由于 redis 主节点和从节点和 sentinel 处于不同的网络分区,使得 sentinel 没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主节点,这样就出现了两个主节点,就像大脑分裂了一样,这样就会导致客户端在旧的主节点写入数据,新的节点就无法同步数据,当网络恢复后,sentinel 会将旧的主节点降为从节点,这时再从主节点中同步数据,就会导致旧的主节点中的大量数据丢失。

解决的话,我记得 redis 中可以设置:我们可以设置最少的从节点的个数,比如设置至少有一个从节点才能同步数据,还有就是可以设置主从数据同步的延迟时间,达不到要求就拒绝,就可以避免大量的数据丢失。

MQ

什么是RabbitMQ

rabbitMQ 是一个开源的 AMQP 高级消息队列协议的实现软件,服务端用 Erlang 语言编写,用于在分布式系统中存储和转发消息、有易用性、扩展性、高可用性。

主要特征就是面向消息、队列、路由(包括对点的发布订阅)、可靠性、安全。

消息中间件主要用于组件之间的解耦,消息的生产者无需知道消息的消费者的存在,反过来消费者也无需知道生产者的存在。

为什么需要用 RabbitMQ

  1. 可以实现消费者和生产者之间的解耦。
  2. 拥有持久化机制,进程消息,队列中的消息也可以保存下来。
  3. 可以使用消息队列达到异步的效果, 比如下单,后代进行逻辑下单。

RabbitMQ 使用场景

  1. 服务之间异步通信
  2. 顺序消费
  3. 定时任务

消息基于什么传输

由于 TCP 连接的创建和销毁开销很大,且并发数受系统资源限制,会造成性能瓶颈。

rabbtiMQ 使用通信的方式来传输数据,信道是建立在真实的TCP连接内的虚拟连接,且每条TCP连接上的信道数量没有限制。

消息如何分发

如果队列中至少有一个消费者订阅,消息以循环的方式发送给消费者,每条消息只会发给一个订阅的消费者。通过路由可以实现多消费的功能。

消息怎么路由

消息发布到交换器时,消息将拥有一个路由键,在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。

消息到达交换器后,rabbitMQ 会将消息的路由键与队列的路由键进行匹配。

  • 常用的交换器主要有几种?
    • fanout:如果交换器收到消息,将会广播到所以绑定队列上。
    • direct:如果路由键完全匹配,消息就会被投递到相应的队列。
    • topic:可以使用来自不同源头的消息能够到达同一个队列。使用 topic 交换器时,可以使用通配符。

如何保证消息不丢失

消息持久化,当然前提是队列必须持久化,rabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将他们写入磁盘上的一个持久化日志文件,当发布一条持久化消息到交换器上时,rabbitMQ 会在消息提交到日志文件后才发送响应。一旦消息消费者从持久化队列中消费了一条持久化消息,rabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。

如果持久化消息在被消费之前,rabbitMQ 重启了,那么 rabbitMQ 会自动创建交换器和队列,并重新发布持久化日志文件中的消息到合适的队列。

如何保证消息正确发送到MQ

可以使用发送方确认模式:将信道设置成发送方确认模式,则所有在信道上发布的消息都会被指派一个唯一的 ID。一旦消息被投放到目的队列后,或者消息被写入磁盘后,信道会发送一个确认给生产者。

如果 rabbitMQ 发生内部错误从而导致消息丢失,会发送一条未确认消息。

发送者确认模式是异步的,生产者在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调方法会被触发来处理确认消息。

如何保证接收方消费了消息

可以使用接收方确认机制:消费者接受每一条消息后都必须确认,消息接收和消息确认是两个不同的操作。只有消费者确认了消息,rabbitMQ 才能安全的把消息从队列中删除。

如何避免消息重复消费

在消息产生时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id 作为去重的依据,避免重复消费的消息进入队列,在消费时,要求消息体中必须有一个 bizid(对于同一业务全局唯一,如订单id,支付id)作为去重依据,避免同一条消息被重复消费。

RabbitMQ 的集群模式

镜像集群模式:创建的队列,无论是元数据还是队列里面的消息都会存于多个 实例上,然后每次你写消息到队列的时候,都会自动把消息发送到多个实例的队列里面进行消息同步。

好处在于,任何一个集群宕机了,别的机器都可以用,坏处就是:新能开销大,消息同步到所有集群,导致网络带宽压力和消耗很重。

RabbitMQ 的缺点

  • 系统可用性能低
    • 系统引入的外部依赖越多,越容易挂掉,本来只需要A系统调用B系统的接口就好了,AB两个系统没什么问题,还加个 MQ 进来,万一 MQ 挂了,整个系统就崩了。
  • 系统复杂性高
    • 加入了MQ后,需要考虑消息重复消费、消息丢失、消息传递顺序。
  • 一致性问题
    • A系统处理完直接返回成功了,本来都以为这个请求成功了,但是B系统写入数据库失败了,数据就不一致了。

rabbitMQ 消息中间件主要用于组件之间的解耦,消息的生产者无需知道消息的消费者的存在,反过来消费者也无需知道生产者的存在。

JVM

JDK、JRE、JVM

JDK 是整个 Java 的核心,包括了 Java 运行环境 JRE,Java 工具和Java 基础类库。

JRE 是运行 Java 程序必须的环境,它包含了 JVM 标准实现及 Java 核心类库。

JVM 是整个 Java 实现跨平台的最核心的部分,能够运行 Java 语言编写的程序。

JVM 重要组成部分

  • 类加载器
    • 加载类到内存。
  • 执行引擎
    • 负责解释命令,交由操作系统执行。
  • 本地接口
    • 本地接口的作用是融合不同语言为java 所用。
  • 运行时数据区
    • 运行时数据区是 jvm 的重点,我们所有写的程序都被加载到这里之后才能执行。
  • 虚拟机栈
    • 线程创建的时候创建,它的生命周期随着线程的生命周期,线程结束栈内存释放,对于栈来说不存在垃圾回收问题。
    • 一个jvm 实例只存在一个堆内存,堆内存的大小是可以调节的,类的加载器读取了类文件之后,需要把类、方法、常量、放到堆内存中,以便执行器执行。
  • 方法区
    • 方法区是被所有线程共享的,该区域保存所有字段和字节方法码以及一些特殊方法,如构造函数,接口代码。
  • 程序计数器
    • 每个线程都有一个程序计数器,就是一个指针,执行方法区的方法字节码,由执行引擎读取下一条指令。

JVM 运行时数据区

不同的虚拟机的运行时数据区可能略微不同,但是都会遵从Java虚拟机规范,Java 虚拟机规范规定的区域有个部分:

  • 程序计数器
    • 它是当线程执行的字节码的行号指示器,字节码解析器的工具是通过改变这个计数器的值,来选取下一条执行的字节码指令,分支、循环、跳转、异常等基础功能,都需要依赖这个计数器来完成。
  • Java 虚拟机栈
    • 它是用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
  • 本地方法栈
    • 与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用本地方法服务的。
  • Java 堆
    • 它是Java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配存储。
  • 方法区
    • 它是用于存储虚拟机加载的类信息,常量,变量,即时编译后的代码等数据。

队列和栈有什么区别

队列和栈都是用来存储数据的

队列允许先进先出去检索元素

栈是先进后出去检索元素

JVM 体系结构及执行过程

在这里插入图片描述

首先 Java 源文件,也就是 xxx.java 会先经过编译器进行编译,生成 class 文件,在这个过程当中会涉及到一系列的词法、语法、语义分析、字节码生成器等操作。

字节码文件生成后,也就是 xxx.class 会通过类加载子系统加载进入内存当中,也就是运行时数据区、在这个过程当中会涉及到字节码校验、翻译字节码、解释和执行或者编译执行、也就是执行引擎那部分。

执行引擎是非常重要的一部分,因为操作系统本身是不识别字节码指令的,只能够识别机器指令,那么字节码指令翻译成机器指令的过程就是依靠执行引擎来完成的。

JVM 的生命周期

  • 虚拟机的启动
    • Java 虚拟机的启动是通过引导类(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
  • 虚拟机的运行
    • 一个运行中的 Java 虚拟机有着一个清晰的任务,就是执行 Java 程序。
    • 程序开始执行时他才运行,程序停止它就停止。
    • 执行一个所谓的 Java 程序的时候,真真执行的是一个叫作 java 虚拟机的进程。
  • 虚拟机的退出
    • 虚拟机正常执行结束
    • 程序在执行的过程中遇到了异常或者错误而异常终止。
    • 由于操作系统出现错误而导致 Java 虚拟机进程终止。
    • 某个线程调用 Runtime 类或者 System 类的 exit 方法。

类加载子系统有几个过程

  • loading
    • 获取类的二进制字节流。
    • 在内存当中生成一个代表这个类的对象,作为方法区这个类的各种数据的访问入口
  • linking
    • 确保 class 文件的字节流中包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全。
    • 为类的属性分配内存并且设置该类属性的默认初始值(即为0)。
  • lnitialization
    • 好像是执行类的构造方法吧,记不清楚

类加载器的分类

JVM 支持两种类型的类加载器,分别是引导类加载器(Bootstrap class loader)和扩展类加载器(extension class loader)。

  • 引导类加载器(启动类加载器)
    • 这个类加载器使用 c 语言实现的,嵌套在 JVM 内部。
    • 它用来加载 Java 的核心库(JAVA_HOME/jre/rt.jar、resource.jar、或sun.boot.class.path下的内容),用于提供 JVM 自身需要的类。
    • 并不继承 classLoader,没有父加载器。
    • 出于安全的考虑,引导类加载器只加载包名为 Java、javax、sun 开头的类。
  • 扩展类加载器
    • java 语言编写
    • 派生于 classLoader 类
    • 父类加载器为启动类加载器

双亲委派机制

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类去加载器去执行。

如果父类加载器还存在其父类加载器,则会进一步向上委托,依次递归,请求最终到达顶层的引导类加载器。

如果父类加载器可以完成类加载任务,就成功返回,无法完成类加载任务的情况下,子加载器才会尝试自己去加载,这就是双亲委派机制。

  • 优势
    • 避免类的重复加载
    • 保护程序的安全,防止核心 API 被随意篡改

沙箱安全机制

假设说我们自定义了一个 String 类并且执行该类的 main 方法,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 JDK 自带的文件,java.lang.String,就会报错说没有找到main,因为 java.lang.String 没有main 方法,这样就保证了对 Java核心的源代码的保护,这就是沙箱机制。

PC寄存器存储字节码指令有什么用

因为 CPU 需要不断的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。

PC寄存器为什么线程私有

为了能够保证准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然就是给每个线程都分配一个PC寄存器,这样以来各个线程便可以进行独立计算,从而不会出现相互干扰的情况。

虚拟机栈可能出现的异常

Java 虚拟机规范允许 Java 栈的大小是动态或者固定不变的。

如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过 Java 虚拟机允许的最大容量,Java 虚拟机将会抛出一个 stackOverFlowError 异常。

如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将会抛出一个 outOfMemoryError 异常。

虚拟机栈中存储的是什么

每个线程都有自己的栈,栈中的数据都是以栈帧的格式存储的。

在这个线程上执行的每个方法都有各自对应的一个栈帧。

栈帧的内部结构

  • 局部变量表
    • 底层是一个数字数组,主要用于存储方法参数和定义方法内部的局部变量。
  • 操作数栈
    • 就是在方法执行的过程当中,根据字节码指令,往栈中写入数据或提取数据。
    • 主要用于保存计算过程的中间结果,同时作为计算过程中临时的存储空间。
  • 动态链接
    • 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用,保存在 class 文件里面。比如:描述一个方法调用了另外一个方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态连接的作用就是为了将这些符号引用转换为调用方法的直接引用。
  • 方法返回地址
    • 存放该方法的 pc 寄存器的指。
    • 一个方法的返回通常有:
      • 正常执行完成
      • 出现异常,非正常退出
    • 无论哪张方式退出,在方法退出后都方法该方法被调用的位置。方法正常退出时,调用者的 pc 计数器的指作为返回地址。而非正常退出,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息。

JVM 有哪些垃圾回收算法

一共有四种垃圾回收算法:

  • 清除算法
    • 标记没有用的对象,然后进行清除回收。缺点是效率不高,无法清除垃圾碎片。
  • 整理算法
    • 标记没有用的对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  • 复制算法
    • 按照容量划分两个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉,缺点是内存使用效率不高,只有原来的一半。
  • 分代算法
    • 根据对象的存活周期不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

JVM 有哪些垃圾回收器

只了解 CMS

介绍一下 CMS 垃圾回收器

CMS 是以牺牲吞吐量代价来换取最短回收停顿时间的垃圾回收器。对于要求服务器响应速度上的应用,这种垃圾回收器非常适合。

分代垃圾回收器是怎么工作的

分代回收器有两个分区;老年代和新生代,新生代默认空间是总空间的1/3,老年代的默认占比是 2/3;

新生代使用的是复制算法,新生代里面有三个分区:eden、to survivor、from survivor,它们默认占比是:8:1:1。

执行流程就是:

先把 eden 和 from survivor 存活的对象放入 to survivor 区;

然后清空 eden 和 from survivor 分区;

最后 from survivor 和 to survivor 分区交换;

每次 from survivor 到 to survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置就是15)时,升级为老年代。

老年代占用空间到达某个值之后就会触发全局垃圾回收,一般使用标记整理执行算法,这就是整个分代垃圾回收器的执行过程。

JVM 调优工具

常用的就是 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控;
  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

常用的 JVM 调优参数

  • Xms
    • 调整堆内存的起始大小
  • Xmx
    • 调整堆内存的最大大小
  • Xmn
    • 新生代的大小

堆和栈有什么区别

JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局 部变量,而对象总是在堆上分配。

栈通常都比堆小,也不会在多个线程之间共享, 而堆被整个 JVM 的所有线程共享。

数据结构

设计模式

什么是设计模式

设计模式就是一套被反复使用,前辈的代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码的可靠性。

为什么要学习设计模式

看懂源代码,如果你不懂设计模式的话,然后去看一些jdk、spring 底层的源码,你会很迷茫,看不懂。

可以编写出自己理想中的好代码,我个人反正是这样,对于自己开发的项目我会很认真。

设计模式的分类

创建型模式:工厂方法、抽象工厂、单例、建造者。

结构型模式:适配器、装饰器、代理、外观、桥接、组合、享元。

行为型模式:策略、模板方法、观察者、命令。。。。

设计模式的六大原则

  • 开放封闭原则
    • 尽量通过扩展软件实体类来解决需求的变化,而不是通过修改已有的代码来完成变化。
  • 里氏代换原则
    • 使用的父类可以在任何地方使用继承的子类,完美的替代父类。就是子类可以去扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法。
  • 依赖倒转原则
    • 记不住
  • 接口隔离原则
    • 记不住
  • 迪米特法则
    • 记不住
  • 单一职责原则
    • 一个方法只负责一件事情。
    • 一个方法一个类一个职责,各个职责程序改动,不影响其他程序。

单例设计模式

单例模式是创建型模式,保证一个类只有一个实例,并且提供一个全局访问点。

哪里用了单例模式

Spring IOC 容器中的类默认是单实例的。

多线程的线程池的设计一般也是单例模式,因为单线程要方便对池中的线程进行控制。

单例模式的优缺点

  • 优点
    • 在单例模式中,活动的实例只有一个,对单例类的所有实例化后得到的都是相同的一个实例。这样就能防止其他对象是自己的实例化,确保所有的对象都访问同一个实例。
    • 系统内存中只会存在一个对象,因此可以节约系统资源。
  • 缺点
    • 不适用于变化的对象,如果同一个类的对象总是要在不同的场景下发生变化,单例出现数据错误,不能保存彼此的状态。

使用单例模式注意事项

使用时不能用反射模式创建单例,否则会实例化一个新的对象。

使用懒汉式单例模式时需要注意线程安全问题

饿汉式单例模式和懒汉式单例模式构造方法都是私有的,因此是不能被继承的。

单例模式的创建方式

主要使用的是懒汉式和饿汉式

饿汉式:类初始化时、会立刻加载该类对象,线程安全、调用效率高

懒汉式:类初始化时、不会立即创建该类对象,真正需要使用的时候才会创建该类的对象,具备懒加载的功能。

静态内部类方式:结合了饿汉式和懒汉式的优点,真正需要对象的时候才会加载,加载类是线程安全的。

枚举方式:使用枚举实现单例模式、优点:实现简单、调用效率高、因为枚举本身就是单例,由JVM从根本上提供保障、避免通过反射和反序列化的漏洞,缺点是没有延迟加载。

策略模式

策略模式是行为型模式,抽取共同行为,根据抽象策略实现不同策略,交给不同的子类实现。策略模式主要解决多重条件判断的问题。

策略模式的优缺点

  • 优点
    • 避免多重条件判断(if-else)
    • 扩展性高(在不影响整体功能的前提下添加和删除算法)
  • 缺点
    • 策略类的增加,每实现一个策略都需要添加策略类,复用性低

策略模式的应用场景

算法需要灵活去切换

多个类都有公共的抽象行为,通过的行为实现不同的场景。

比如说支付场景(微信、支付宝、银联)

还有登录场景(微信登录、QQ登录、手机号码登录)

发短信这些。

观察者模式

观察者模式是行为型模式,当一个对象状态发生变化时,已经登记的其他对象能够观察到这一变化从而做出自己相应的改变,通过这种方式来达到减少依赖关系,解耦合的作用。

观察者模式的优缺点

  • 优点
    • 观察者模式支持类似于广播通讯的这种功能,被观察者会向所有的登记过的观察者发出通知。
  • 缺点
    • 如果一个被观察者对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。

观察者模式的应用场景

对一个对象的状态更新,需要其他对象同步更新,而且其他对象的数量是动态可变的。

对象仅仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节。

模板方法模式

模板方法模式是行为型模式,就是定义一个操作的算法骨架(父类),而将一些步骤延迟到子类。模板方法模式使得子类可以不改变一个算法的结构来重定义该算法。

模板方法模式的优缺点

  • 优点
    • 利用模板方法模式可以将相同的处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
    • 把不同的代码放到不同的子类中,通过子类去扩展新的行为,提高代码的复用性,符合开闭原则。
  • 缺点
    • 类数目的增加,每定义一个新的逻辑,就需要一个子类来实现,这样会导致类的个数增加,系统复杂度增加。
    • 如果父类添加新的抽象方法,所有的子类都要改一遍。

模板方法的应用场景

实现一些操作时,整体的步骤很固定,但是呢,就是其中一小部分需要动态改变,这时候就可以使用模板方法,把易变的部分抽象出来,供子类去实现。

多线程

进程和线程

进程就是运行在操作系统上的一个任务,进行是计算机调度的一个单位,操作系统在启动程序的时候,会为其创建一个进程,JVM 就是一个进程,进程与进程之间是相互隔离的,每个进程都有独立的存储空间。

线程就是进程的最小任务只需单元,具体来说就是,一个程序顺序执行的流程就是一个线程,我们常见的比如说执行一个 main 方法,就说一个线程。

线程有哪些状态

  • 初始化状态
    • 线程被创建出来,就是初始状态,这时候线程对象只是一个普通的对象,并不是一个线程。
  • 就绪状态
    • 执行 start 方法之后,进入就绪状态,等待被分配到时间片。
  • 运行状态
    • 拿到CPU的线程开始执行,处于运行时间的线程并不是永久持有CPU知道运行结束的,很可能没有执行完时间片就到期了,就会被收回CPU的使用权。然后进入等待状态
  • 等待状态
    • 等待状态分为有期限等待和无期限等待,有期限等待就是线程使用 sleep 方法主动进入睡眠,有一定的时间限制,时间到期了就会重新进入就绪状态,再次等待CPU选中。
    • 而无期限等待并不是指永远的等待下去,而是指没有时间限制。
  • 阻塞状态
    • 阻塞状态是一种比较特殊的等待状态,处于其他等待状态的线程是在等着别的线程执行结束,等着拿CPU的使用权;而处于阻塞状态的线程等待的不仅仅是cpu的使用权,主要是锁标记,没有拿到锁标记,即便是cpu有空也没法执行
  • 终止状态
    • 已经终止的线程处于该状态

wait 和 sleep 的区别

  • wait
    • wait 睡眠时,会释放锁对象
    • wait 和 notify 是成对出现的,必须在 synchronize 中被调用。
    • 常用于线程通信
  • sleep
    • sleep 睡眠时,保持锁对象,仍然占有该锁
    • 常用于暂停执行。

notify 和 notifyAll

notify() 和 notifyAll() 都是 Object 对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
notify 可能会导致死锁,而 notifyAll 则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码
使用 notifyall, 可以唤醒 所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个

synchronized 和 lock 区别

  • synchronized
    • 它是一个 Java 关键字,在 jvm 层面上
    • 假设 A线程获得锁,B线程等待,如果A线程阻塞,B线程会一直等待
    • 获取锁的线程执行完同步代码块,释放锁,线程执行发生异常jvm会释放
  • lock
    • 他是 jvm 的一个接口
    • 大致就是可以尝试获得锁,线程可以不用之一等待。
    • 在 finally 中必须释放锁,不然容易造成线程死。

synchronized 加在静态方法和实例方法的区别

加在静态方法,是对类进行加锁,假设类中有方法A和方法B都是被 synchronized 修饰的静态方法,此时有两个线程分别调用 方法A 和方法B,那么就会有一条线程会阻塞等待知道另一条线程执行完成才能执行。

修饰实例方法,是对实例进行加锁,锁的是实例对象的对象头,如果调用同一个对象的两个不同的被 synchronized 修饰的实例方法时,看到的效果和前面的一样,如果调用不同对象的两个不同的 synchronized 修饰的实例方法时,则不会阻塞。

Sychornized是公平锁?

不是公平锁

创建线程的方式

继承 Thread 类

实现 Runnable 接口

实现 Callable 接口,结合 FutureTask 使用

通过线程池创建线程

线程数量和CPU数量的关系

首先需要确认业务是CPU密集型还是 IO 密集型

如果是 CPU 密集型,那么就应该尽可能少的线程数量,一般为CPU 的核心数 + 1

如果是 IO 密集型,就可以分配多一点 CPU 核心数,好像是核心数 * 2吧

多线程之间如何通信

  1. 可以通过共享变量,变量需要用 volatile 修饰
  2. 使用 wait 或者 notifyAll 方法,但是由于需要使用同一把锁,所以必须通知线程释放锁,被通知的线程才能获取到锁,这样会导致通知不及时。
  3. 使用 countDownLatch 实现,通知线程到指定条件,调用 countDown,被通知的线程执行 await。

CountDownLatch 用法

可以让主线程 await,业务线程进行业务处理,处理完成时调用 countDown 方法。

也可以让业务线程 await,主线程处理完数据之后执行 countDown 方法,此时业务线程被唤醒,然后去主线程拿数据,或者执行自己的业务逻辑。

Executor 有几种线程池

目前只用过两种

  • newScheduledThreadPool
    • 这个是定时的线程池,我们可以延迟任务执行的时间,也可以设置一个周期性的时间让线程池去重复的执行。
    • 作用就是返回一个可以控制线程池内线程定时或者周期性执行某个任务的线程池。
  • newCachedThreadPool
    • 创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,也可以创建新的线程。
    • 作用就是返回一个可以根据实际情况调整线程池中线程数量的线程池,该线程池中的线程数不确定,是根据实际情况来动态调整的。

线程池的参数

corePoolSize:线程池核心线程大小

maxmumPoolSize:线程池最大线程数量

keepAliveTime:空闲线程存活时间

TimeUnit:空闲线程存活时间单位

拒绝策略

当工作队列中的任务已经到达最大限制,并且线程池中的线程数量也到达最大限制,这时如果有新的任务提交进来,就会执行拒绝策略。

只了解 3 种 JDK 拒绝策略

  1. 丢弃任务,抛出异常
  2. 直接丢弃任务
  3. 丢弃最早进入线程池的任务,尝试把这次拒绝的任务放入队列

线程池线程存在哪里

存在一个名为 workers 的 hashset 里面,(未使用的线程也是)

多线程状态以及切换

  • running 运行状态
    • 能够接受新提交的任务,并且也能处理阻塞队列中的任务
  • shutdown 关闭状态
    • 不在接受新提交的任务,但却可以继续处理阻塞队列中的任务
  • stop 停止状态
    • 不能接受新提交的任务,也不能处理队列中的任务,会中断正在处理的线程
  • 还有一种就是所有任务已经终止,wokercount = 0
  • 其他的不记得

如何在方法栈中进行数据传递

提供方法参数传递

提供共享变量

如果在同一个线程里面,还可以使用 ThreadLocal 进行传递。

ThreadLocal

是一个线程变量,在ThreadLocal中填充的变量只属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。

ThreadLocal 应用场景

存储用户的 session 或者 token

多线程有什么优势

1、发挥多核CPU 的优势

现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU 上就浪费了 50%, 在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充分利用CPU 的目的。

2、防止阻塞

从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行

如何保证线程按顺序执行

用 join 方法。

start 和 run 区别

只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run()方法里面的代码。

如何终止一个线程

stop 终止,不推荐。

网络编程

什么是网络编程

网络编程的本质就是多台计算机之间的数据交换,不就是把一个设备中数据发送给其他设备,然后接受另外一个设备反馈的数据,现在的网络编程基本上都是基于请求和响应的方式。

例如打电话,我给对方打过去就类似于一个客户端,对方接电话的人必须保持电话畅通就类似于一个服务器,然后打通了之后,就是客户端和服务端可以进行数据传递了,而且双方的身份是等价的,程序既有客户端功能也有服务端功能,最常见的软件就是QQ和微信这类了。

什么是网络协议

在计算机中要交换数据,就必须遵守一些约定好的规则,比如说数据交换的格式,是否需要发送一个应答的信息,这些规则就被称为网络协议。

TCP/IP 模型、协议

TCP/IP 分有四层协议(数据链路层、网络层、传输层、应用层)

  • 应用层
    • 应用层是最靠近用户的一层,是为计算机用户提供应用的,也是为用户直接提供各种网络服务,常见应用层网络服务协议有 http、https、ftp、telnet。
  • 传输层
    • 建立主机端到端的连接,就是两个主机、两个传输实体之间的一个可靠的通路吧,我们常说的 TCP UDP 就是在这一层。
  • 网络层
    • 就是通过IP寻址来建立两个节点之间的连接,正确地按照地址传送给目标端的传输层,就是通常说的IP层,这层就是我们常说的IP协议层、IP协议的是网络的基础。
  • 数据链路层
    • 通过一些协议来控制这些数据的传输,以保证传输数据的正确性,这样就构成了数据链路。

TCP 和 UDP

TCP 就是一个传输的网络协议,发送数据前要先建立连接,发送方和接收方之间必须建立连接,也就是说,通过 TCP 连接传输的数据不会丢失、没有重复。

UDP 他是属于 TCP 协议的一种,是无连接协议,发送数据前不需要建立连接,是没有可靠性的,因为不需要建立连接,所以可以在网络上任何路径上传输。

TCP 和 UDP 区别

  1. TCP 是面向连接的协议,发送数据前要先建立连接,可靠性强,也就是说,通过 TCP 连接传输的数据不会丢失,没有重复。
  2. UDP 是无连接协议,发送数据前不需要建立连接,是没有可靠性的。
  3. TCP 通信类似于要打个电话,接通了,要先确认身份、才开始进行沟通。
  4. UDP 类型于一个广播,靠着广播、播报直接进行通信。
  5. TCP 支支持点对点通信,而UDP 支持一对一、一对多、多对多。

TCP 和 UDP 应用场景

对某些实时性要求比较高的情况下,使用UDP,比如游戏,媒体通信,实时直播这种,即使出现传输错误也可以容忍,其他大部分情况下,HTTP 都是用的 TCP,因为传输要求内容的可靠性,不出现丢失的情况。

什么是 HTTP

http 协议客户端和服务端之间,数据实现可靠性的传输,文字、图片、视频等超文本数据的规范,简称就是 超文本传输协议。

http 属于应用层。

HTTP 和 HTTPS 区别

http 协议是运行在 TCP 之上,明文传输,客户端和服务端都无法验证对方的身份,https 是带有那个 SSL 证书,是添加了加密和认证机制的 http。

端口不同,http 和 https 使用不同的连接方式,用的端口也不一样,http 是 80 , https 是 443。

资源消耗不同,https 通信会由于加密处理消耗更多的 cpu 资源。

开销不同, https 需要购买 SSL 证书。

HTTPS 工作原理

首先 https 请求,服务端生成证书,然后客户端验证证书的有效期、合法性、域名和请求域名是否一致等。

客户端验证通过后、就会根据证书的公钥,生产随机数,随机数使用公钥进行加密(RSA)。

消息体产生后,对他的摘要进行加密(MD5/SHA1),此时就得到了RSA 签名;

然后发送给服务端,服务端用私钥(RSA)解密。

三次握手

三次握手其实主要的一个目的就是为了建立可靠的通信通道,就是双方确认自己与对方是否能正常的进行发送和接收数据。

第一次握手:客户端什么都不用确认,服务端确认了对方发送是否正常。

第二次握手:客户端确认自己发送和接收正常、对方发送和接收正常。然后服务端确认自己接收正常、对方发送正常。

第三次握手:客户端确认自己发送和接收正常、对方发送和接收正常。然后服务端确认自己发送和接收正常、对方发送和接收正常。

所以三次握手后就能确认双方功能的正常了,缺一不可。

四次挥手

四次挥手就是,比如说 A 向 B 发送出FIN报文段时,只是表示 A 已经没有数据要发送了,而此时A 还能够接收到 B 发来的数据,B 要向 A 发出 ACK 报文段也只是告诉 A,它知道自己 A 没有数据要发了,但B还能够向A 发送数据。

所以想要愉快的结束这次对话就需要四次挥手。

为什么 TCP 连接要三次握手

目的是为了防止已失效的连接请求报文又传到了服务端,因此而产生错误。

假设不采用 三次握手,那么只要B发出确认,新的连接就建立了。由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据,但B却以为新的连接已经建立了,并一直等待A发来数据,这样,B的很多资源就浪费了。采用三次握手就可以避免这种情况的发生。

TCP 如何保证传输可靠性

  • 数据包校验
    • 目的是检测数据在传输的过程中的任何变化,若检测出包有错,则丢弃报文段并且不给出响应,这时TCP发送数据端超时后会重新发送。
  • 丢弃重复数据
    • 对于重复的数据,能够丢弃。
  • 超时重发
    • 当 TCP 发送一个报文段后,它启动一个定时器,等待目的段确认收到这个报文段,如果不能及时收到,将重发这个报文段。
  • 还有几个不清楚。

Socket

Scoket 我理解的就是 TCP 协议的一个外观模式吧,它把复杂的 TCP 协议隐藏在 Socket 后面,对于用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。

Socket 通讯过程

  • 基于 TCP
    • 服务端会先初始化 Socket,然后与端口进行绑定,然后等待客户端连接。在这个时候如果有一个客户端初始化了 Socket 然后连接了服务端,如果连接成功。这时客户端与服务端的连接就建立了,然后客户端发送数据请求,服务端接收请求并处理后,然后响应数据发送给客户端,客户端读取数据,最后关闭连接,就结束了。
  • 基于 UDP
    • UDP会表现出更大的优势,客户端只需要发送,服务端能不能接收到,我不需要管。

Socket 和 HTTP

Socket 连接是所谓的长连接,理论上是客户端和服务端一旦建立连接后将不会主动断掉。

Socket 适用场景是,游戏、直播等

http 是短连接,就是客户端向服务端发送一次请求,服务端响应后就会断开连接。

http 适用场景是,网站这些

HTTP 请求步骤

先是由域名解析为IP地址,然后寻址IP地址的过程的先经过浏览器、系统缓存、hosts 文件、路由器缓存,再到根域名服务器。

然后开始建立 TCP 连接。

由浏览器发送一个 HTTP 请求。

经过路由器的转发,该请求到达了 HTTP 服务器

服务器处理 HTTP 请求,返回一个HTML文件,或者 xml、json

浏览器解析返回的数据,并显示在页面上。

HTTP 无状态协议

就是 http 是一个没有状态的协议,也就是没有记忆力,就意味着每一次请求都是独立的,缺少状态意味着如果后续处理需要前面的信息,则必须重新传,这样可能导致每次连接传输的数据量增大。

  • 优点:解放了服务器,每一次请求都不会造成不必要的连接占用,
  • 缺点:每次请求会传输大量重复的内容信息。

Session、Cookie

cookie 实际上是一段小的文本信息,客户端请求服务器,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器发送一个 cookie,然后客户端浏览器就会把 cookie 保存起来。当浏览器再次请求该网站时,浏览器就会把请求地址和 cookie 一起提交给服务器,服务器检测 cookie,以此来辨认用户状态,还可以根据需求修改 cookie 的内容

session 就是保存在服务端的会话状态,客户端请求服务器,如果服务器需要记录该用户的状态,就获取 session 来保存状态,如果服务器已经为这个用户创建过 session,服务器就按照 sessionId 把这个 session 检索出来使用,如果没有创建过就为这个用户创建一个 session 并且生成一个 sessionId,并将这个 sessionId 返回给客户端保存。

  • 客户端怎么保存?
    • 采用 cookie 的机制保存,这样在交互的过程中浏览器可以自动的按照规则把这个标识发送给服务器,如果浏览器禁用了 cookie,可以通过 url 重写将 sessionId 发送给服务器。
  • 两者区别(对比)?
    • 实现机制不同:session 的实现依赖于 cookie 机制,通过 cookie 机制回传 sessionId。
    • 大小限制不同:cookie 有大小限制,并且通过浏览器对每个网站的 cookie 数量也有限制,而 session 没有没有大小限制,理论上只与服务器内存大小有关。
    • 安全隐患不同:cookie 存在安全隐患,通过拦截本地文件得到 cookie 后可以进行攻击,而 session 保存在服务器,所以相对安全。
    • 资源消耗:session 是保存在服务器上,会存在一段时间后才会消失,如果 session 过多会增加服务器的压力。

Application

application 是与一个 web 应用程序相对应,为应用程序提供一个全局的状态,所有客户都可以使用该状态。

常用的 HTTP 方法

GET:通过URL传参的方式访问资源

POST:通过Body 传参的方式访问资源

PUT:一般用于传输文件

DELETE:删除对于 URI 位置的文件

常见HTTP状态码

1、1xx(临时响应)
2、2xx(成功)
3、3xx(重定向):表示要完成请求需要进一步操作
4、4xx(错误):表示请求可能出错,妨碍了服务器的处理
5、5xx(服务器错误):表示服务器在尝试处理请求时发生内部错误

什么是沾包

沾包就是指发送方发送若干个数据包到接收方时沾成了一个包,后一个包是数据头紧接着前一个包的数据尾。只有 TCP 有沾包的现象,UDP 不会。

沾包产生的原因

当发送连续的数据时,会将较小的内容拼接成大的内容(有一个算法),一次性发送到服务器端,因此造成沾包。就是当发送内容较大时,由于服务器的 buffer_size 方法中 buffer_size 较小,不能一次性完全接受全部内容,因此下一次请求到达时,接收内容依然是上一次还没有完全接收的内容,这就是沾包现象。

其他

注解是什么

Java 注解就是代码中的一些特殊标记,就是在运行时进行解析和使用的,它的本质就是继承了 Annotation 的特殊接口,具体实现类就是 JDK 动态代理的代理类。

通过反射获取到注解时,返回的也是 Java 运行时生成的动态代理对象。通过代理对象调用自定义注解的方法。

如何自定义注解

① 创建一个自定义注解:与创建接口类似,但自定义注解需要使用 @interface

② 添加元注解信息,比如 @Target、@Retention、@Document、@Inherited 等

③ 创建注解方法,但注解方法不能带有参数

④ 注解方法返回值为基本类型、String、Enums、Annotation 或其数组

⑤ 注解可以有默认值

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值