Java面试题整理【梅开二度】

Java语言有哪些特点?

  • 简单易学,有丰富的类库
  • 面向对象(让程序耦合度更低,内聚性更高)
  • 可移植性(JVM是Java跨平台使用的根本)
  • 可靠安全
  • 支持多线程

equals与==的区别

==:
== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指向同一个对象。比较的是真正意义上的指针操作

  • 比较的是操作符两端的操作数是否是同一个对象
  • 两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过
  • 比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为true,如:int a = 10 与 long b = 10L 与 double c = 10.0都是相同的(为true),因为他们都指向地址为10的堆

equals:
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断

总结:所有比较是否相等时,都是用equals并且在堆常量相比较时,把常量写在前面,因为使用object的equals,object可能为null,则会发生空指针异常

  • 在阿里的代码规范中只使用equals,阿里插件默认会识别,并可以快速修改,如需排查老代码中的“==”,推荐使用阿里插件

HashCode的作用

java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插入的时候,如何判断是否已经存在该元素呢,可以通过equals方法。当时如果元素太多,用这样的方法就会比较慢
于是有人发明了哈希算法来提高集合中查找元素的效率。这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其他的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次

Java创建对象有几种方式?

  • new创建新对象
  • 通过反射机制
  • 采用clone机制
  • 通过序列化机制

有没有可能两个不相等的对象有相同的hashCode

有可能。在产生hash冲突时,两个不相等的对象就会有相同的hashCode值。当hash冲突产生时,一般有以下几种方式来处理:

  • 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储
  • 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
  • 再哈希:又叫双哈希法,有多个不同的Hash函数。当发生冲突时,使用第二个、第三个哈希函数计算地址,直到无冲突

深拷贝和浅拷贝的区别是什么?

浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其它对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象
深拷贝:被复制对象的所有变量都含有与原来对象相同的值。而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍

简述线程、程序、进程的基本概念。以及它们之间关系是什么?

线程与线程类似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个执行接着一个指令地执行着,同时,每个进程还占有某些系统资源,如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要使用一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段

线程有哪些基本状态?

  • 初始状态(NEW):线程被创建,但是还没有调用start()方法
  • 运行状态(RUNNABLE):Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
  • 阻塞状态(BLOCKED):表示线程阻塞于锁
  • 等待状态(WAITING):表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断)
  • 超时等待状态(TIME_WAITING):该状态不同于WAITING,它是可以在指定的时间自行返回的
  • 终止状态(TERMINATED):表示当前线程已经执行完毕

如何停止一个正在运行的线程?

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend以及resume一样都是过期作废的方法
  • 使用interrupt方法中断线程

notify()和notifyAll()有什么区别?

  • notify可能会导致死锁,而notifyAll则不会
  • 任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized中的代码。使用notifyAll()可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify()只能唤醒一个
  • notify()是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用,不然可能导致死锁。正确的场景应该是WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中

sleep() 和wait()有什么区别?

sleep()方法是属于Thread类中的,而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态仍然保持着,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放锁对象
当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态

Thread类中的start() 和 run()方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程

什么是线程安全?

线程安全就是说多线程访问同一代码,不会产生不确定的结果。在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了
线程安全一般都涉及到synchronized,就是一段代码同时只能有一个线程来操作,不然中间过程可能会产生不可预知的结果。如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的

说说自己是怎么使用synchronized关键字,在项目中用到了吗?如果有的话,说下synchronized关键字最主要的三种使用方法:

修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获取当前对象实例的锁
修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象。**因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象锁
修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

  • synchronized 关键字加到static静态方法和synchronized(class)代码块上都是给Class类上锁。synchronized 关键字加到实例方法上是给对象实例上锁

简述一下你对线程池的理解

线程池提供了一种限制和管理资源的方式(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。合理利用线程池能够带来三个好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

常用的线程池有哪些?

  • newSingleTheadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行
  • newFixedTheadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程数达到线程池的最大值
  • newCachedTheadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小
  • newScheduledTheadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求

依赖注入的方式有几种,各是什么?

一、构造器注入
将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。

优点:对象初始化完成后便可获得可使用的对象
缺点:当需要注入的对象很多时,构造器参数列表将会很长;不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数
二、setter方式注入
IOC Service Provider通过调用成员变量提供的setter函数将被依赖对象注入给依赖类

优点:灵活。可以选择性地注入需要的对象
缺点:依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用
三、接口注入
依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入。该函数的参数就是要注入的对象

优点:接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可
缺点:侵入性太强,不建议使用

讲一下什么是Spirng

Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于XML的配置、基于注解的配置、基于Java的配置
Spring主要有以下几个模块组成:

  • Spring Core:核心类库,提供IOC服务
  • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等)
  • Spring AOP:AOP服务
  • Spring DAO:对JDBC的抽象,简化了数据访问异常的处理
  • Spring ORM:对现有的ORM框架的支持
  • Spring Web:提供了基本的面向Web的综合特性,例如多方文件上传
  • Spring MVC:提供面向Web应用的Model-View-Controller实现

说一下Spring MVC的流程

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器找到具体的处理器(可以根据XML配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器
  5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
  8. Dispatcher Servlet将ModelAndView传给ViewResolver视图解析器
  9. ViewResolver解析后返回具体View
  10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
  11. DispatcherServlet响应用户

SpringMVC常用的注解有哪些?

@RequestMapping:用于处理请求url映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径
@RequestBody:注解实现接收Http请求的json数据,将json转换为java对象
@ResponseBody:注解实现将Controller方法返回对象转换为json对象响应给客户

解释一下Spring Bean 的生命周期

以Servlet的生命周期为对照:实例化,初始化,接受请求,销毁
Spring上下文中的Bean生命周期也类似,如下:

  1. 实例化Bean:
    对于BeanFactory容器,当客户向容器请求一个尚未初始化的Bean时,或初始化Bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的Bean
  2. 设置对象属性(依赖注入):
    实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息以及通过BeanWrapper提供的设置属性的接口完成依赖注入
  3. 处理Aware接口:
    接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
  • 如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值
  • 如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身
  • 如果这个Bean已经实现了ApplicationContextAware接口,对调用setApplicationContext(ApplicationContext)方法,传入Spring上下文
  1. BeanPostProcessor:
    如果想对Bean进行一些自定义的处理,那么可以让Bean实现BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj,String s)方法
  2. InitializingBean 与 init-method:
    如果Bean在Spring配置文件中配置了init-method属性,则会自动调用其配置的初始化方法
  3. 如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj,String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了

  1. DisposableBean:
    当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法
  2. destroy-method:
    最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法

Spring基于XML注入Bean的几种方式

  • Set方法注入
  • 构造器注入:①通过index设置参数的位置;②通过type设置参数类型
  • 静态工厂注入
  • 实例工厂

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

  • 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例
  • 单例模式:Bean默认为单例模式
  • 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
  • 模板方法:用来解决代码重复的问题。如:RestTemplate,JpaTemplate
  • 观察者模式:定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被自动更新,如Spring中listener的实现->ApplicationListener

什么是Mybatis

  1. Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态SQL,可以严格控制SQL执行性能,灵活度高
  2. Mybatis可以使用XML或者注解来配置和映射原生信息,将POJO映射成数据库中的记录,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集
  3. 通过XML文件或注解的方式将要执行的各种statement配置起来,并通过Java对象和statement中SQL的动态参数进行映射生成最终执行的SQL语句,最后由Mybatis框架执行SQL并将结果映射为Java对象并返回。(从执行SQL到返回result的过程)

Mybatis的优点和缺点

优点

  • 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成影响,SQL写在XML里,解除SQL与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
  • 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
  • 很好的与各种数据库兼容(因为Mybatis使用JDBC来连接数据库,所以只要JDBC支持的数据库Mybatis都支持)
  • 能够与Spring很好的集成
  • 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护
    缺点
  • SQL语句的编写工作量较大,尤其当字段多】关联表多时,对开发人员编写SQL语句的功底有一定要求
  • SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库

#{}和${}的区别是什么?

#{}是预编译处理$ {}是字符串替换
Mybatis在处理#{}时,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值,使用#{}可以有效地防止SQL注入,提高系统安全性
Mybatis在处理$ {}时,就是把${}替换成变量的值

当实体类中的属性名和表中字段名不一样,怎么办?

  • 通过在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致
  • 通过映射字段名和实体类属性名的一一对应关系来解决

Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在SQL內直接书写带有物理分页的参数来完成物理分页的功能,也可以使用分页插件来完成物理分页
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的SQL,然后重写SQL,根据dialect方法,添加对应的物理分页语句和物理分页参数

Mybatis是如何将SQL执行结果封装为目标对象并返回的?都有哪些映射形式?

映射形式

  • 第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系
  • 第二种是使用SQL列的别名功能,将列的别名书写为对象属性名
    有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的
    Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
    Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载
    lazyLoadingEnabled=true|false
    它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的SQL,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理

Mybatis的一级、二级缓存:

  • 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Sessionflush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
  • 二级缓存:二级缓存与一级缓存机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作用域为Mapper(Namespace),并且可自定义存储源,如Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializble序列化接口(可用来保存对象的状态),可在它的映射文件中配置
  • 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespace)进行了C/U/D操作后,默认该作用域下所有select中的缓存将被clear掉并重新更新,如果开启了二级缓存,则指根据配置判断是否刷新

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

SpringBoot的核心注解是启动类上的@SpringBootApplication,主要组合包含了以下3个注解:
@SpringBootConfiguration:组合了@Configuration注解,实现配置文件的功能
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
@ComponentScan:Spring组件扫描

运行SpringBoot有哪几种方式?

  • 打包用命令或者放到容器中执行
  • 用Maven/Gradle插件运行
  • 直接执行main方法运行

数据库引擎有哪些?

MySQL常用引擎包括:MYISAM、Innodb、Memory、MERGE

  • MYISAM:全表锁,拥有较高的执行速度,不支持事务,不支持外键,并发性能差,占用空间相对较小,对事务完整性没有要求,以select、insert为主的应用基本上可以使用这引擎
  • Innodb:行级锁,提供了具有提交、回滚和崩溃回复能力的事务安全,支持自动增长列,支持外键约束,并发能力强,占用空间是MYISAM的2.5倍,处理效率相对会差一些
  • Memory:全表锁,存储在内容中,速度快,但会占用和数据量成正比的内存空间且数据在MySQL重启时会丢失,默认使用Hash索引,检索效率非常高,但不适用于精确查找,主要用于那些内容变化不频繁的代码表
  • MERGE:是一组MYISAM表的组合

InnoDB与MYISAM的区别

  1. InnoDB支持事务,MYISAM不支持。对于InnoDB,每一条SQL语句都默认封装成事务,自动提交;这样会影响速度,所以最好把多条SQL语句放在begin和commit之间,组成一个事务
  2. InnoDB支持外键,而MYISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败
  3. InnoDB是聚集索引,数据文件和索引是绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MYISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的
  4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MYISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快
  5. InnoDB不支持全文索引,而MYISAM支持全文索引,查询效率上MYISAM要高
    如何选择引擎?
    如果没有特别的需求,使用默认的InnoDB即可
    MYISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站
    InnoDB:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统

数据库的事务

概念:多条SQL语句,要么全部成功,要么全部失败
特性

  • 原子性(Atomic):组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作都成功,整个事务才会提交。任何一个操作失败,已经执行的任何操作都必须撤销,让数据库返回初始状态
  • 一致性(Consistency):实务操作成功后,数据库所处的状态和它的业务规则是一致的。及数据不会被破坏。如A转账100给B,不管操作是否成功,A和B的账户总额是不变的
  • 隔离性(Isolation):在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对彼此产生干扰
  • 持久性(Durabiliy):一旦事务提交成功,事务中的所有操作都必须持久化到数据库中

SQL优化

  1. 查询语句中不要使用select *
  2. 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
  3. 减少使用IN 或者 NOT IN,使用exists,not exists或者关联查询语句替代
  4. or的查询尽量用union或者union all代替(在确认没有重复数据或者不用剔除重复数据时,union all会更好)
  5. 应尽量避免在where子句中使用!=或者<>操作符,否则引擎将放弃使用索引而进行全表扫描
  6. 应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描

简单说一说drop、delete与truncate的区别

SQL中的drop、delete、truncate都表示删除,但是三者有一些差别。delete和truncate只删除表的数据不删除表的结构。而在速度方面,一般来说:drop>truncate>delete
delete语句是DML,这个操作会放到rollback segment中,事务提交之后才生效,如果有相应的trigger,执行的时候将被触发。truncate,drop是DDL,操作立即生效,原数据不放到rollback segment中,不能回滚,操作不触发trigger

并发事务带来哪些问题?

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题

  • 脏读(Dirty read):当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所作的操作可能是不正确的
  • 丢失修改(Lost to modify):指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此成为丢失修改
  • 不可重复读(Unrepeatableread):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事物的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读
  • 幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读
    不可重复读和幻读的区别
    不可重复读的重点是修改。比如多次读取一条记录发现其中某些列的值被修改;幻读的重点在于新增或者删除。比如多次读取一条记录发现记录增多或减少了

事务隔离级别有哪些?MySQL的默认隔离级别是?

SQL标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • READ-COMMOITTED(读取已提交):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
  • SERIALIZABLE(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读

MySQL InnoDB存储引擎的默认支持的隔离级别是REPEATABLE-READ(可重复读)。需要注意的是:与SQL标准不同的地方在于InnoDB存储引擎在REPEATABLE-READ(可重复读)事务隔离级别下使用的是Next-Key Lock锁算法,因此可以避免幻读的产生。所以说InnoDB存储引擎的默认支持的隔离级别是REPEATABLE-READ(可重复读)已经完全可以保证事物的隔离性要求,即达到了SQL标准的SERIALIZABLE(串行化)隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容),但是InnoDB存储引擎默认使用REPEATABLE-READ(可重复读)并不会有任何性能损失

  • InnoDB存储引擎在分布式事务的情况下一般会用到SERIALIZABLE(可串行化)隔离级别

大表如何优化?

1. 限定数据的范围
务必禁止不带任何限制数据范围条件的查询语句。比如:让用户在查询订单历史的时候,控制在一个月的范围内
2.读/写分离
经典的数据库拆分方案,主库负责写,从库负责读
3.垂直分区
根据数据库里面数据表的相关性进行拆分。例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表
垂直拆分的优点:可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护
垂直拆分的缺点:主键会出现冗余,需要管理冗余列,并会引起join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂
4.水平分区
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。水平拆分可以支撑非常大的数据量
水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张表的数据拆成多张表来存放。举例:可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响
水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以水平拆分最好分库
水平拆分能够支持非常大的数据量存储,应用端改造也少,但分片事务难以解决,跨节点Join性能较差,逻辑复杂
数据库分片的两种常见方案:

  • 客户端代理:分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。当当网的Sharding-JDBC、阿里的TDDL是两种比较常用的实现
  • 中间件代理:在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。Mycat、Atlas、DDB等都是这种架构的实现

分库分表之后,id主键如何处理?

因为要是分成多个表之后,每个表都是从1开始累加,这样是不对的,我们需要一个全局唯一的id来支持
生成全局id有下面这几种方式:

  • UUID:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字
  • 数据库自增id:两台数据库分别设置不同步长,生成不重复ID的策略来实现高应用。这种方式生成的id有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈
  • 利用redis生成id:性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本
  • Twitter的snowflake算法
  • 美团的Leaf分布式ID生成系统

Redis持久化机制

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的
实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,在用这个临时文件替换上次的快照文件,然后子进程退出,内存释放
RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。(快照可以使其所表示的数据的一个副本,也可以是数据的一个复制品)
AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复

缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题

一、缓存雪崩
可以简单地理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃
解决办法
大多数系统设计者考虑用加锁(最多的解决方案)或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是将缓存失效时间分散开

二、缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题
解决办法
最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但他的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库

  • 布隆过滤器:就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决”冲突“。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值,我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在

三、缓存预热
缓存预热是一个比较常见的概念,就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据
解决思路:

  • 直接写个缓存刷新页面,上线时手动操作下
  • 数据量不大,可以在项目启动的时候自动进行加载
  • 定时刷新缓存

四、缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6种策略可供选择),还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  • 定时去清理过期的缓存
  • 当有用户请求过来时,判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存是否失效,逻辑相对比较复杂。具体使用哪种方案可以根据应用场景来权衡

五、缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)
以参考日志级别设置预案:

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

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户

Redis的数据类型,以及每种数据类型的使用场景

  • String
    常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存
  • hash
    这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好地模拟出类似session的效果
  • list
    使用List的数据结构,可以做简单的消息队列的功能。另外就是可以利用Irange命令,做基于redis的分页功能,性能极佳,用户体验好
  • set
    因为set堆放的是一堆不重复的集合。所以可以做全局去重的功能。不用JVM自带的Set进行去重的原因是,我们的系统一般都是集群部署,使用JVM自带的Set比较麻烦,也不会去为了做全局去重而再起一个公共服务,过于麻烦
  • sorted set
    sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作

Redis的过期策略以及内存淘汰机制

redis采用的是定期删除+惰性删除策略
为什么不用定时删除策略?
定时删除策略,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每隔100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redsi不是每隔100ms将所有的key检查一次,而是随机抽取进行检查。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除
采用定期删除+惰性删除就没有其它问题了么?
并不是,如果定期删除没删除key,然后你也没即时去请求key,也就是说惰性删除也没生效。这样redis的内存会越来越高。那么就应该采用内存淘汰机制
在redis.conf中有一行配置
maxmemory-policy volatile-lru
该配置就是用来配内存淘汰策略的
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错

如果没有设置expire的key,不满足先决条件(prerequisites),那么volatile-lru,volatile-random和volatile-ttl策略的行为,和no-eviction(不删除)基本上一致

为什么Redis的操作是原子性的,怎么保证原子性的?

对于Redis而言,命令的原子性指的是:一个操作的不可再分,操作要么执行,要么不执行。Redis的操作之所以是原子性的,是因为Redis是单线程的。Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性

Redis事务

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH四个原语实现的,Redis会将一个事务中的所有命令序列化,然后按顺序执行

  1. Redis不支持回滚“Redis在事务失败时不进行回滚,而是继续执行余下的命令”,所以Redis的内部可以保持简单且快速
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行
  • MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行
  • EXEC用于执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值nil
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出
  • WATCH命令可以为Redis事务提供check-and-set(CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令

Redis为什么是单线程的?

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。Redis利用队列技术将并发访问变为串行访问
回答要点:

  1. 绝大部分请求是纯粹的内存操作(非常快速)
  2. 采用单线程,避免了不必要的上下文切换和竞争条件
  3. 非阻塞IO优点:
  • 速度快,因为数据存储在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
  • 支持丰富数据类型,支持String,list,set,sorted set,hash
  • 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
  • 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题

同时有多个子系统去set一个key,这个时候要注意什么呢?

不推荐使用redis事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,在这种情况下就显得十分鸡肋了

  • 如果对这个key操作,不要求顺序:准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可
  • 如果对这个key操作,要求顺序:分布式锁+时间戳。假设这个时候系统B先抢到锁,将key1设置为{valueB 3:05},接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了
  • 利用队列,将set方法变成串行访问也可以

Redis遇到高并发,如何保证读写key的一致性?
对Redis的操作都是具有原子性的,是线程安全的操作,所以不用考虑并发问题,redis内部已经处理好并发的问题;

Redis常见性能问题和解决方案

  • Master最好不要做任何持久化工作,如RDB内存快照和AOP日志文件
  • 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
  • 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
  • 尽量避免在压力很大的主库上增加从库
  • 主从复制不要用图状结构,用单向链表结构更为稳定

什么是微服务?

微服务架构是一种架构模式或者或是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储

什么是服务熔断?什么是服务降级?

熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用达到一定的阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制

服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做虽然水平下降,但好歹可用,比直接挂掉强
Hystrix相关注解
@EnableHystrix:开启服务熔断
@HystrixCommand(fallbackMethod+“XXX”):声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示

Eureka和zookeeper都可以供服务注册与发现的功能,请说说两个的区别

Zookeeper保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用)

  • 当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接挂掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其它节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30~120s,且选取期间zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的
  • Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:
  1. Eureka会从注册列表中移除因为长时间没有收到心跳而应该过期的服务
  2. Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)
  3. 当网络稳定时,当前实例新的注册信息会被同步到其他节点

因此,Eureka可以很好地应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪

负载平衡的意义是什么?

在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件,可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程

说说RPC的实现原理

首先需要有处理网络连接通讯的模块,负责建立连接、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回


简述一下什么是Nginx,它有什么优势和功能?

Nginx是一个web服务器和反向代理服务器。用于HTTP、HTTPS、SMTP、POP3和IMAP协议。因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名
优点:

  • 更快
    这表现在两个方面:一方面,在正常情况下,单次请求会得到更快的响应;另一方面,在高峰期(如有数以万计的并发请求),Nginx可以比其他Web服务器更快地响应请求
  • 高扩展性,跨平台
    Nginx的设计极具扩展性,它完全是由多个不同功能、不同层次、不同类型且耦合度极低的模块组成。因此,当对某一个模块修复Bug或进行升级时,可以专注于模块自身,无需在意其他。而且在HTTP模块中,还设计了HTTP过滤器模块:一个正常的HTTP模块在处理完请求后,会有一串HTTP过滤器模块对请求的结果进行再处理。这样,当我们开发一个新的HTTP模块时,不但可以使用诸如HTTP核心模块、events模块、log模块等不同层次或者不同类型的模块,还可以原封不动地复用大量已有的HTTP过滤器模块。这种低耦合度的优秀设计,造就了Nginx庞大的第三方模块,当然,公开的第三方模块也如官方发布的模块一样容易使用
  • 高可靠性:用于反向代理,宕机的概率微乎其微
    高可靠性是我们选择Nginx的最基本条件,因为Nginx的可靠性是大家有目共睹的,很多家高流量网站都在核心服务器上大规模使用Nginx。Nginx的高可靠性来自于其核心框架代码的优秀设计、模块设计的简单性;另外,官方提供的常用模块都非常稳定,每个worker进程相对独立,master进程在一个worker进程出错时可以快速“拉起”新的worker子进程提供服务
  • 低内存消耗
    一般情况下,10000个非活跃的HTTP Keep-Alive连接在Nginx中仅消耗2.5MB的内存,这是Nginx支持高并发连接的基础
  • 单机支持10万以上的并发连接
    这是一个非常重要的特性。随着互联网的迅猛发展和互联网用户数量的成倍增长,各大公司、网站都需要应付海量并发请求,一个能够在峰值期顶住10万以上并发请求的Server,无疑会得到大家的青睐。理论上,Nginx支持的并发连接上限取决于内存,10万远未封顶。当然,能够及时地处理更多的并发请求,是与业务特点紧密相关的
  • 热部署
    master管理进程与worker工作进程的分离设计,使得Nginx能够提供热部署功能,既可以在7×24小时不间断服务的前提下,升级Nginx的可执行文件。当然,它也支持不停止服务就更新配置项、更换日志文件等功能
  • 最自由的BSD许可协议
    这是Nginx可以快速发展的强大动力。BSD许可协议不只是允许用户免费使用Nginx,它还允许用户在自己的项目中直接使用或修改Nginx源码,然后发布

选择Nginx的核心理由还是它能在支持高并发请求的同时保持高效的服务

Nginx是如何处理一个HTTP请求的呢?

Nginx是一个高性能的Web服务器,能够同时处理大量的并发请求。它结合多进程机制和异步机制,异步机制使用的是异步非阻塞方式

  • 多进程机制
    服务器每当收到一个客户端请求时,就有服务器主进程(master process)生成一个子进程(worker process)出来和客户端建立连接进行交互,直到连接断开,该子进程就结束了。使用进程的好处是各个进程之间相互独立,不需要加锁,减少了使用锁对性能造成影响,同时降低编程的复杂度,降低开发成本。其次,采用独立的进程,可以让进程之间不会互相影响,如果一个进程发生异常退出时,其他进程正常工作,master进程则很快启动新的worker进程,确保服务不会中断,从而将风险降到最低。缺点是操作系统生成一个子进程需要进行内存复制等操作,在资源和时间上会产生一定的开销。当有大量请求时,会导致系统性能下降
  • 异步非阻塞机制
    每个工作进程使用异步非阻塞方式,可以处理多个客户端请求。当某个工作进程接收到客户端的请求以后,调用IO进行处理,如果不能立即得到结果,就去处理其他请求(即为非阻塞);而客户端在此期间也无需等待响应,可以去处理其他事情(即为异步)。当IO返回时,就会通知此工作进程;该进程得到通知,暂时挂起当前处理的事务去响应客户端请求

列举一些Nginx的特性

  • 反向代理/L7负载均衡器
  • 嵌入式Perl解释器
  • 动态二进制升级
  • 可用于重新编写URL,具有非常好的PCRE支持

请解释Nginx服务器上的Master和Worker进程分别是什么

  • 主程序Master process启动后,通过一个for循环来接收和处理外部信号
  • 主进程通过fork()函数产生worker子进程,每个子进程执行一个for循环来实现Nginx服务器对事件的接收和处理

请解释代理中的正向代理和反向代理

首先,代理服务器一般指局域网内部的机器通过代理服务器发送请求到互联网上的服务器,代理服务器一般作用在客户端。例如:使用翻墙软件,我们的客户端在进行翻墙操作的时候,我们使用的正是正向代理,通过正向代理的方式,在我们的客户端运行一个软件,将我们的HTTP请求转发到其他不同的服务器端,实现请求的分发

正向代理中,对于Web客户端来说,代理扮演的是服务器的角色,接收Request,返回Response;对于Web服务器来说,代理扮演的是客户端的角色,发送Request,接收Response

反向代理服务器作用在服务端,它在服务器端接收客户端的请求,然后将请求分发给具体的服务器进行处理,然后再将服务器的响应结果反馈给客户端。Nginx就是一个反向代理服务器软件
反向代理正好与正向代理相反,对于客户端而言,代理服务器就像是原始服务器,并且客户端不需要进行特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端

说一下Nginx的用途

Nginx服务器的最佳用法是在网络上部署动态HTTP内容,使用SCGI、WSGI应用程序服务器、用于脚本的FastCGI处理程序。Nginx还可以作为负载均衡器


为什么使用MQ

  • 解耦
    A系统发送数据到BCD三个系统,通过接口调用发送。如果E系统也要这个数据呢?那如果C系统现在不需要了呢?A系统负责人几乎崩溃…A系统跟其他各种乱七八糟的系统严重耦合,A系统产生一条比较关键的数据,很多系统都需要A系统将这个数据发送过来,如果使用MQ,A系统产生一条数据,发送到MQ里面去,哪个系统需要数据自己去MQ里面消费。如果新系统需要数据,直接从MQ里消费即可;如果某个系统不需要这条数据了,就取消对MQ消息的消费即可。这样下来,A系统根本不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况
    抽象出来就是,一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,所以用MQ给它异步化解耦
  • 异步
    A系统接收一个请求,需要在自己本地写库,还需要在BCD三个系统写库,自己本地写库要3ms,BCD三个系统分别写库要300ms、450ms、200ms。最终请求总延时是3+300+450+200=953ms,接近1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。如果使用MQ,那么A系统连续发送3条消息到MQ队列中,假如耗时5ms,A系统从接受一个请求到返回相应给用户,总时长是3+5=8ms
  • 削峰
    减少高峰时期对服务器压力

MQ优缺点

优点:解耦、异步、削峰
缺点:

  • 系统可用性降低:
    系统引入的外部依赖越多,越容易挂掉。MQ挂了,整套系统崩溃,万事休矣
  • 系统复杂度提高:
    把MQ加进来,怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
  • 一致性问题:
    A系统处理完了直接返回成功,都以为这个请求就成功了;但问题是,要是BCD三个系统中,BD两个系统写库成功了,而C系统写库失败了,A返回成功后,其实数据就不一致了

Kafka、ActiveMQ、RabbitMQ、RocketMQ之间有什么区别

对于吞吐量来说Kafka和RocketMQ支持高吞吐,ActiveMQ和RabbitMQ比他们低一个数量级。对于延迟量来说RabbitMQ是最低的

  1. 持久化消息比较
    ActiveMQ和RabbitMQ都支持。持久化消息主要是指机器在不可抗力因素等情况下挂掉了,消息不会丢失的机制
  2. 高并发
    毋庸置疑,RabbitMQ最高,原因是它的实现语言是天生具备高并发高可用的的erlang语言
  3. RabbitMQ 和 Kafka 的比较
    RabbitMQ 比 Kafka 成熟,在可用性上,稳定性上,可靠性上,RabbitMQ胜于Kafka(理论上)。另外Kafka的定位主要在日志等方面,因为Kafka设计的初衷就是处理日志的,可以看作是一个日志(消息)系统的一个重要组件,针对性很强,所以业务方面还是建议选择RabbitMQ。还有就是Kafka的性能(吞吐量、TPS)比RabbitMQ要高出来很多

如何保证高可用?

RabbitMQ是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以RabbitMQ为例,讲解第一种MQ的高可用性怎么实现。RabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式

  • 单机模式:
    就是Demo级别的,一般就是本地启动来玩玩,生产不用单机模式
  • 普通集群模式:
    意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。创建的queue,只会放在一个RabbitMQ实例上,但是每个实例都同步queue的元数据(元数据可以认为是queue的一些配置信息,通过元数据,可以找到queue所在实例)。消费的时候,实际上如果连接到了另外一个实例,那么那个实例就会从queue所在实例上拉取数据过来。这一方案主要是提高吞吐量,就是说让集群中多个节点来服务某个queue的读写操作
  • 镜像集群模式:
    这种模式,才是所谓的RabbitMQ高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,创建的queue,无论是元数据还是queue里的消息都会存在于多个实例上,就是说,每个RabbitMQ节点都有这个queue的一个完整镜像,包含queue的全部数据的意思。然后每次写消息到queue的时候,都会自动把消息同步到多个实例的queue上。RabbitMQ有很好的管理控制台。就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,任何一个机器宕机了,没事儿,其他机器(节点)还包含了这个queue的完整数据,别的consumer都可以到其他节点上去消费数据。坏处在于,性能开销太大了,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ一个queue的数据都是放在一个节点里的,镜像集群模式下,每个节点都放这个queue的完整数据

Kafka一个最基本的架构认识:由多个broker组成,每个broker是一个节点;创建一个topic,这个topic可以划分为多个partition,每个partition可以存在于不同的broker上,每个partition就放一部分数据。这就是天然的分布式消息队列,就是说一个topic的数据,是分散放在多个机器上的,每个机器就放一部分数据。Kafka 0.8以后,提供了HA机制,就是replica副本机制。每个partition的数据都会同步到其他机器上,形成自己的多个replica副本。所有的replica会选举一个leader出来,那么生产和消费都跟这个leader打交道,然后其他的replica副本就是follower。写的时候,leader会负责把数据同步到所有follower上去,读的时候就直接读leader上的数据即可。只能读写leader吗?很简单,如果你可以随意读写每个follower,那么就要担心数据一致性问题,系统复杂度太高,很容易出问题。Kafka会均匀地将一个partition的所有replica分布在不同的机器上,这样才可以提高容错性。因为如果某个broker宕机了,没有关系,那个broker上面的partition在其他机器上有副本,如果这上面有某个partition的leader,那么此时会从follower中重新选举一个新的leader出来,继续读写那个新的leader即可
这就有所谓的高可用性了。写数据的时候,生产者就写leader,然后leader将数据落地写本地磁盘,接着其他follower自己主动从leader来pull数据。一旦所有follower同步好数据了,就会发送ack给leader,leader收到所有follower的ack之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)消费的时候,只会从leader去读,但是只有当一个消息已经被所有follower都同步成功返回ack的时候,这个消息才会被消费者读到

如何保证消息的可靠传输?如果消息丢了怎么办?

数据的丢失问题,可能出现在生产者、MQ、消费者中

  • 生产者丢失:
    生产者将数据发送到RabbitMQ的时候,可能数据在半路就给搞丢了,因为网络问题啥的,都有可能。此时可以选择用RabbitMQ提供的事务功能,就是生产者发送数据之前开启RabbitMQ事务channel.txSelect,然后发送消息,如果消息没有成功被RabbitMQ接收到,那么生产者会受到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。吞吐量会下来,因为太耗性能
    所以一般来说,如果要确保写RabbitMQ的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的id,然后如果写入了RabbitMQ中,RabbitMQ会给你回传一个ack消息,告诉你说这个消息ok了。如果RabbitMQ没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。事务机制和confirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送这个消息之后就可以发送下一个消息,然后那个消息RabbitMQ接收了之后会异步回调你一个接口通知你这个消息接收到了。所以一般在生产者这块避免数据丢失,都是用confirm机制的
  • MQ中丢失:
    就是RabbitMQ自己弄丢了数据,这个你必须开启RabbitMQ的持久化,就是消息写入之后会持久化到磁盘,哪怕是RabbitMQ自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。设之持久化有两个步骤:一是创建queue的时候将其设置为持久化,这样就可以保证RabbitMQ持久化queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化,此时RabbitMQ就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,RabbitMQ哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个queue里面的数据。持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,RabbitMQ挂了,数据丢了,生产者收不到ack,你也是可以自己重发的
  • 消费端丢失:
    你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了。RabbitMQ认为你消费了,这数据就丢失了。这个时候得用RabbitMQ提供的ack机制。简单来说,就是关闭RabbitMQ的自动ack,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里ack一下。这样的话,如果你还没处理完,就没有ack,然后RabbitMQ就认为你还没处理完,这个时候RabbitMQ会把这个消费分配给别的consumer去处理,消息不会丢失
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值