序言

今年秋招我从提前批开始到目前拿到offer为止,面试了三四十加企业,一场场录音总结出来的,文末有资料大礼包,可开奖!

spring

Spring的一个核心功能是IOC,就是将Bean初始化加载到容器中,Bean是如何加载到容器的,可以使用Spring注解方式或者Spring XML配置方式。

控制反转,依赖注入及其原理

IoC叫控制反转,DI叫依赖注入。控制反转是把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。"控制反转"就是对组件对象控制权的转移,从程序代码本身转移到了外部容器,由容器来创建对象并管理对象之间的依赖关系。底层是对象工厂,先配置xml文件,配置创建的对象,再创建工厂类。

依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由容器负责,查找资源的逻辑应该从应用组件的代码中抽取出来,交给容器来完成。就是把底层类作为参数传入上层类,实现上层类对下层类的"控制"。

依赖注入可以通过接口,setter方法注入(设值注入)、构造器注入和接口注入三种方式来实现,Spring支持setter注入和构造器注入,通常使用构造器注入来注入必须的依赖关系,对于可选的依赖关系,则setter注入是更好的选择,setter注入需要类提供无参构造器或者无参的静态工厂方法来创建对象。

Spring中自动装配的方式有哪些?

  • no:不进行自动装配,手动设置Bean的依赖关系。
  • byName:根据Bean的名字进行自动装配。
  • byType:根据Bean的类型进行自动装配。
  • constructor:类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误。
  • autodetect:如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配。

spring aop

AOP能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,可拓展性和可维护性。

springboot的有点:

Spring Boot 的优点快速开发,特别适合构建微服务系统,另外给我们封装了各种经常使用的套件

Spring 为 Java 程序提供了全面的基础架构支持,包含了很多非常实用的功能,如 Spring JDBC、Spring AOP、Spring ORM、Spring Test 等,这些模块的出现,大大的缩短了应用程序的开发时间,同时提高了应用开发的效率。

Spring Boot 本质上是 Spring 框架的延伸和扩展,它的诞生是为了简化 Spring 框架初始搭建以及开发的过程,使用它可以不再依赖 Spring 应用程序中的 XML 配置,为更快、更高效的开发 Spring 提供更加有力的支持。

Spring aop是通过动态代理实现的,有这两种方法:JDK是基于反射机制,生成一个实现代理接口的匿名类,然后重写方法,实现方法的增强。它生成类的速度很快,但是运行时因为是基于反射,调用后续的类操作会很慢.

而且他是只能针对接口编程的.

CGLIB是基于继承机制,继承被代理类,所以方法不要声明为final,然后重写父类方法达到增强了类的作用。它底层是基于asm第三方框架,是对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。生成类的速度慢,但是后续执行类的操作时候很快。可以针对类和接口.

Spring的事务机制包括声明式事务和编程式事务。

编程式事务管理:通过Transaction Template手动管理事务

声明式事务管理:将我们从复杂的事务处理中解脱出来,获取连接,关闭连接、事务提交、回滚、异常处理等这些操作都不用我们处理了,Spring都会帮我们处理。

声明式事务管理使用了 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。

声明式事务优点:不需要在业务逻辑代码中编写事务相关代码,只需要在配置文件配置或使用注解(@Transaction),这种方式没有侵入性。

声明式事务缺点:声明式事务的最细粒度作用于方法上,如果像代码块也有事务需求,只能变通下,将代码块变为方法。

Bean的生命周期

实例化 Instantiation属性赋值 Populate初始化 Initialization销毁 Destruction

依赖循环

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。类似于死锁

Spring中循环依赖场景有:

(1)构造器的循环依赖

(2)field属性的循环依赖,也就是setter注入的循环依赖。(可以通过三级缓存来解决)

Spring循环依赖的理论依据其实是Java基于引用传递,当我们获取到对象的引用时,对象的field或者或属性是可以延后设置的。

为了避免循环依赖,在Spring中创建bean的原则是不等bean创建完成就会将创建bean的ObjectFactory提早曝光加入到缓存中,一旦下一个bean创建时候需要依赖上一个bean则直接使用ObjectFactory 。

计算机网络

应用层的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间的通信和交互的规则。比如http,dns,smtp。

表示层,信息的语法语义以及它们的关联,如加密解密、转换翻译、压缩解压缩。

会话层:不同机器上的用户之间建立及管理会话。

运输层:负责向两台主机进程之间的通信提供通用的数据传输服务。tcp,udp。端口号

网络层:在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。

数据链路层:通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异

tcp:面向连接,可靠,字节流。udp:无连接,不可靠,报文。

TCP 协议如何保证可靠传输

应用数据被分割成 TCP 认为最适合发送的数据块。

TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

TCP 的接收端会丢弃重复的数据。

流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)

拥塞控制: 当网络拥塞时,减少数据的发送。1.慢开始2.拥塞控制3.快重传4.快恢复

ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。

超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

http状态码

1** 信息,服务器收到请求,需要请求者继续执行操作。2** 成功,操作被成功接收并处理。3** 重定向,需要进一步的操作以完成请求。4** 客户端错误,请求包含语法错误或无法完成请求。5** 服务器错误,服务器在处理请求的过程中发生了错误。

TIME_WAIT与CLOSE_WAIT状态

TIME_WAIT是主动关闭连接的一方保持的状态,对于爬虫服务器来说他本身就是"客户端",在完成一个任务之后,他就会发起主动关闭连接,从而进入TIME_WAIT的状态,然后在保持这个状态2MSL(max segment lifetime)时间之后,彻底关闭回收资源。

防止上一次连接中的包,迷路后重新出现,影响新连接

CLOSE_WAIT是被动关闭连接是形成的。根据TCP状态机,服务器端收到客户端发送的FIN,则按照TCP实现发送ACK,因此进入CLOSE_WAIT状态。但如果服务器端不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK,则系统中会存在很多CLOSE_WAIT状态的连接。此时,可能是系统忙于处理读、写操作,而未将已收到FIN的连接,进行close。此时,recv/read已收到FIN的连接socket,会返回0。

数据结构

DFS深度优先搜索。以深度为准则,先一条路走到底,直到达到目标。否则既没有达到目标又无路可走了,那么则退回到上一步的状态,走其他路。

BFS广度优先搜索。而广度优先搜索旨在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路,并重复这样的操作。

操作系统

进程和线程

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

2) 线程的划分尺度小于进程,使得多线程程序的并发性高。

3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

进程是分配资源的基本单位;线程是系统调度和分派的基本单位。

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

①需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大) 
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

②需要进行大量计算的优先使用线程(CPU频繁切换) 
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。

③强相关的处理用线程,弱相关的处理用进程 
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。 
一般的Server需要完成如下任务:消息收发、消息处理。"消息收发"和"消息处理"就是弱相关的任务,而"消息处理"里面可能又分为"消息解码"、"业务处理",这两个任务相对来说相关性就要强多了。因此"消息收发"和"消息处理"可以分进程设计,"消息解码"、"业务处理"可以分线程设计。 
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

④可能要扩展到多机分布的用进程,多核分布的用线程

⑤都满足需求的情况下,用你最熟悉、最拿手的方式

进程间通信方式

管道,是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

有名管道,是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

消息队列,是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享存储,就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

信号量,是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

套接字,套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

信号 ,是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

上下文,进程切换,为什么损耗那么大

进程切换分两步:1.切换页目录以使用新的地址空间。2.切换内核栈和硬件上下文。

1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲等很多东西都会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

IO

BIO:同步阻塞,数据的读取写入必须阻塞在一个线程内等待其完成。

NIO:同步非阻塞,一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程处理。

(1)缓冲区 Buffer,在 NIO 中,所有数据都是用缓冲区处理的。

(2)通道 Channel,可以通过它读取和写入数据。通道与流的不同之处在于通道是双向的,而且通道可以用于读、写或者用于读写。同时Channel 是全双工的。

(3)多路复用器Selector,一个多用复用器 Selector 可以同时轮询多个 Channel。如果某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,进行后续的 I/O 操作。

AIO:AIO是一个有效请求一个线程

异步非阻塞,用户进程只需要发起一个IO操作便立即返回,等 IO 操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据处理就好了,不需要进行实际的 IO 读写操作,因为真正的 IO 操作已经由操作系统内核完成了。

什么情况下会发生死锁?解决死锁的策略有哪些?

银行家算法

要设法保证系统动态分配资源后不进入不安全状态,以避免可能产生的死锁。即:每当进程提出资源请求且系统的资源能够满足该请求时,系统将判断如果满足此次资源请求,系统状态是否安全,如果判断结果为安全,则给该进程分配资源,否则不分配资源,申请资源的进程将阻塞。

内存管理的页面淘汰 算法

为提高内存利用率,解决内存供不应求的问题,更加合理的使用内存,人们创造了分页式内存抽象。同时有一个虚拟内存的概念,是指将内存中暂时不需要的部分写入硬盘,看上去硬盘扩展了内存的容量,所以叫做"虚拟"内存。分页式内存管理将物理内存分为等大的小块,每块大小通常为1K、2K、4K等,称为页帧;逻辑内存(使用虚拟内存技术扩大的内存,可认为其位于硬盘上)也被分为等大的小块,称为页;且页和页帧的大小一定是一样的,它是写入真实内存和写回硬盘最小单位。

Lru最近最少使用,为获得对最优算法的模拟,提出了LRU算法。由于当前时间之后需要用到哪些页无法提前获知,于是记录当前时间之前页面的使用情况,认为之前使用过的页面以后还会被用到。在置换时,将最近使用最少的页面换出内存。此种方法的开销比较大。

先进先出算法

FIFO算法的思想很简单,就是置换出当前已经待在内存里时间最长的那个页。FIFO算法的运行速度很快,不需要考虑其他的因素,需要的开销很少。但是正是由于没有考虑页面的重要性的问题,FIFO算法很容易将重要的页换出内存。

redis

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。

虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

热点key

原因:用户消费的数据远大于生产的数据,请求分片集中,超过单 Server 的性能极限。

危害:1、流量集中,达到物理网卡上限。2、请求过多,缓存分片服务被打垮。3、DB 击穿,引起业务雪崩。

解决方案:客户端。在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录,维护成本较高。无法实现规模化运维统计。

代理层统计,提前预估,流量分析,redis自带的hotkey全局搜索方法

集群

主从复制

一类是主数据库(master),另一类是从数据库(slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。

Sentinel(哨兵)模式

第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预。哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个 Redis 实例。通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器。当哨兵监测到 master 宕机,会自动将 slave 切换成 master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;

Cluster 模式

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容。Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念。Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。

脑裂

因为网络问题,导致redis master节点跟redis slave节点处于不同的网络分区,此时因为sentinel集群无法感知到master的存在,所以将slave节点提升为master节点。同时有两个主节点,它们都能接收写请求。是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。进一步导致数据丢失。

解决:redis.conf 修改属性,通过活跃slave节点数和数据同步延迟时间来限制master节点的写入操作。原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。

缓存过期时间

因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。还有就是业务需求,比如验证码。

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性式删除 。

内存淘汰机制:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。。从已设置过期时间的数据集中挑选将要过期的数据淘汰。。从已设置过期时间的数据集中任意选择数据淘汰。。当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。。从数据集中任意选择数据淘汰。。禁止驱逐数据。。从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。。当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。

布隆过滤器

在java中,一个int类型占32个比特,现假如我们用int字节码的每一位表示一个数字的话,那么32个数字只需要一个int类型所占内存空间大小就够了,这样在大数据量的情况下会节省很多内存。

优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 bitmap

当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

Counting Bloom filter实际只是在标准Bloom filter的每一个位上都额外对应得增加了一个计数器,在插入元素时给对应的 k (k 为哈希函数个数)个 Counter 的值分别加 1,删除元素时给对应的 k 个 Counter 的值分别减 1。

1.读方: BF.EXISTS KEY element

如果想一次查询多个元素,可以使用bf.mexists命令。

BF.MADD批量添加

2.写方: BF.ADD KEY element

bf.reserve

错误率{error_rate}越小,所需的存储空间越大; 初始化设置的元素数量{capacity}越大,所需的存储空间越大,当然如果实际远多于预设时,准确率就会降低。RedisBloom官方默认的error_rate是 0.01,默认的capacity是 100。

Zset

当zset满足以下两个条件的时候,使用ziplist:

保存的元素少于128个

保存的所有元素大小都小于64字节

不满足这两个条件则使用skiplist。

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。

通过指针来共享相同元素的成员和分值,所以不会产生重复,造成内存的浪费。

并发

为什么要用到多线程

避免阻塞浪费大量时间。单线程模型中事件的执行是顺序执行的,下一个操作必须等待前一个操作完成后才能执行,而如果前一个操作非常耗时,就会影响下一个操作的效率。此时可以使用多线程进行异步调用。同时当CPU性能是处理任务的瓶颈时等特殊情况下,可以提高性能。

线程安全问题

在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。

中断线程:

interrupt(): 用于线程中断,该方法并不能直接中断线程,只会将线程的中断标志位改为true。它只会给线程发送一个中断状态,线程是否中断取决于线程内部对该中断信号做什么响应,若不处理该中断信号,线程就不会中断。

多线程的实现方式

1.继承Thread类,重写run方法

2.实现Runnable接口,重写run方法

3.通过Callable和Future,三个接口实际上都是属于Executor框架,1.5,实现有返回结果的线程

4.通过线程池创建线程

Runnable的实现方式是实现其接口即可

Thread的实现方式是继承其类

Runnable接口支持多继承,但基本上用不到

Thread实现了Runnable接口并进行了扩展,而Thread和Runnable的实质是实现的关系,不是同类东西,所以Runnable或Thread本身没有可比性。

中断线程

interrupt中断线程。该方法并不能直接中断线程,只会将线程的中断标志位改为true。它只会给线程发送一个中断状态,线程是否中断取决于线程内部对该中断信号做什么响应,若不处理该中断信号,线程就不会中断。

需要在try,catch语句中。sleep中断或者抛出InterruptedException异常

多线程通信方式

通过synchronized,volatile关键字这种锁方式来实现线程间的通信。

while轮询,会浪费cpu资源

Object类的 wait() 和 notify() 方法。进入阻塞状态

管道通信,使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

线程的几种状态

1)初始状态 2)可运行状态3)运行状态4)阻塞状态 5)死亡状态

等待和阻塞的区别?

最主要的区别就是释放锁所有权与否

Sleep,令阻塞,是Thread类的方法,在指定的时间内阻塞线程的执行。并不会失去对任何监视器(monitors)的所有权,不会释放锁,仅仅会让出cpu的执行权。

Wait方法,令线程等待。是Object的方法,他前提是当前线程已经获取了对象监视器monitor的所有权。会让出cpu的执行权,还会释放锁,并且进入wait set中

线程抢到了锁进了同步代码块,(由于某种业务需求)某些条件下Object.wait()了,就处于了等待状态。线程和其他线程抢锁没抢到,就处于阻塞状态了。

线程池

ThreadPoolExecutor

corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。

maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;

unit : keepAliveTime 参数的时间单位。

threadFactory : 用来实现创建线程的工厂接口

handler :饱和策略。

抛出异常来拒绝新任务的处理。调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。不处理新任务,直接丢弃掉。此策略将丢弃最早的未处理的任务请求。

创建线程是有代价的,不能每次要执行一个任务时就创建一个线程,但是也不能在任务非常多的时候,只有少量的线程在执行,这样任务是来不及处理的,而是应该创建合适的足够多的线程来及时的处理任务。随着任务数量的变化,当任务数明显很小时,原本创建的多余的线程就没有必要再存活着了,因为这时使用少量的线程就能够处理的过来了,所以说真正工作的线程的数量,是随着任务的变化而变化的。

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

对象头主要包括两部分数据:Mark Word、Klass Pointer。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

无锁,偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

syn:1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁。2.修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。3.修饰代码块 :指定加锁对象,对给定对象/类加锁。

volatile通过在变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。是线程同步的轻量级实现, 性能更好。但只能用于变量而 syn可修饰方法以及代码块。

volatile能保证数据的可见性,但不能保证数据的原子性。syn两者都能。

volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

没有流水线技术前,如果同时两个指令过来执行 一个需要5秒,那么两个就需要10秒;有了流水线技术之后,可能就只要6秒。多指令同时执行时性能显著提升。

线程复用会产生脏数据。由于线程池会重用 Thread 对象 ,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。

内存泄漏:绝大多数的静态threadLocal对象都不会被置为null。这样子的话,通过 stale entry 这种机制来清除Value 对象实例这条路是走不通的。

每次用完ThreadLocal,都要及时调用 remove() 方法去清理。

Renntranlock和AQS

Lock是接口, ReentrantLock是具体实现

1、synchronize 系java 内置关键字;而Lock 是一个类

2、synchronize 可以作用于变量、方法、代码块;而Lock 是显式地指定开始和结束位置

3、synchronize 不需要手动解锁,当线程抛出异常的时候,会自动释放锁;而Lock则需要手动释放,所以lock.unlock()需要放在finally 中去执行

4、性能方面,如果竞争不激烈的时候,synchronize 和Lock 的性能差不多,如果竞争激烈的时候,Lock 的效率会比synchronize 高

5、Lock 可以知道是否已经获得锁,synchronize 不能知道。Lock 扩展了一些其他功能如让等待的锁中断、知道是否获得锁等功能;Lock 可以提高效率。

6、synchronize 是悲观锁的实现,而Lock 则是乐观锁的实现,采用的CAS 的尝试机制

1、ReenTrantLock 可以中断锁的等待,提供了一些高级功能

2、多个线程在等待的时候,可以提供公平的锁;默认的是非公平锁,性能会比公平锁好一些;

3、ReenTrantLock 可以绑定多个锁条件

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

内存溢出

jps:查看所有 Java 进程。jstat: 监视虚拟机各种运行状态信息。jinfo: 实时地查看和调整虚拟机各项参数。jmap:生成堆转储快照。jhat: 分析 heapdump 文件。jstack :生成虚拟机当前时刻的线程快照。JConsole:Java 监视与管理控制台

动态化线程池

简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求。

参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。

增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。

线程阻塞的情况:

1、当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;

2、当线程碰到一条wait()语句时,它会一直阻塞到接到通知(notify())、被中断或经过了指定毫秒时间为止(若制定了超时值的话)

3、线程阻塞与不同I/O的方式有多种。常见的一种方式是InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间;

4、线程也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得synchronized语句必须的锁时阻塞)。

reactor和proactor

Netty提供了多个解码器,可以进行分包的操作,分别是,(回车换行分包,FrameDecoder(特殊分隔符分包)(固定长度报文来分包)自定义长度来分包)

reactor模式用于同步I/O,而Proactor运用于异步I/O操作。

同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知)。

Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。


而在Proactor模式中,处理器--或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。

Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.

jvm

类加载过程

加载,验证,准备()解析,初始化

(1) 装载:

通过全类名获取定义此类的二进制字节流

将字节流所代表的静态存储结构转换为方法区的运行时数据结构

在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

(a)校验:检查载入Class文件数据的正确性;

(b)准备:

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

(c)解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

(3) 初始化:初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,

(1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

(2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

(3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

不想用:自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法

Java 对象的创建过程

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的 符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必 须先执行相应的类加载过程。

2.4.2.2. Step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成 后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配 方式有 "指针碰撞和 "空闲列表两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种方式:

指针碰撞:堆内存规整,用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可

空闲列表:不规则,虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很

频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保

证线程安全:

CAS+失败重试CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设 没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存 时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用 上述的 CAS 进行内存分配

2.4.2.3. Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操 作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的 数据类型所对应的零值。

2.4.2.4. Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才 能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头 中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方 式。

2.4.2.5. Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视⻆来看,一个新的对象已经产生了,但从 Java 程序的视 ⻆来看,对象创建才刚开始, <init> 方法还没有执行,所有的字段都还为零。所以一般来说, 执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真 正可用的对象才算完全产生出来。

对象的访问定位有哪两种方式?

建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有1使用句柄和2直接指针两种:

1. 句柄如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中 存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信 息;

2. 直接指针如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类 型数据的相关信息,而reference 中存储的直接就是对象的地址。

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄 地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直 接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

新生代和老生代的收集算法,比例,gc

标记清除,整理,复制

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用"复制算法",只需要付出少量存活对象的复制成本就可以完成收集。

在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记-清理"或"标记-整理"算法来进行回收。

Eden区、From Survivor区(S0)、To Survivor区(S1)。默认8:1:1。

老年代和新生代是2:1

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC

设置两个Survivor区最大的好处就是解决了碎片化,碎片化带来的风险是极大的,严重影响JAVA程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存

刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

MinorGC:是指清理新生代

MajorGC:是指清理老年代(很多MajorGC是由MinorGC触发的)

FullGC:是指清理整个堆空间包括年轻代和永久代。调用System.gc()时,是建议JVM进行Full GC,只是建议,不是一定会发生。老年代空间不够时。方法区空间不够时。

G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

jvm调优,gc优化

GC优化。确定目标、优化参数、验收结果。

请求高峰期发生GC,导致服务可用性下降。在执行垃圾回收时,Java应用程序中除了垃圾回收器线程之外其他所有线程都被挂起,意味着在此期间,用户正常工作的线程全部被暂停下来。1.39s

CMS的四个阶段:1.初始标记,该阶段进行可达性分析,标记GC ROOT能直接关联到的对象,所以很快。 2.并发标记,由前阶段标记过的绿色对象出发,所有可到达的对象都在本阶段中标记。 3. Remark重标记,暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象。

因为并发标记阶段是和用户线程并发执行的过程,所以该过程中可能有用户线程修改某些活跃对象的字段,指向了一个未标记过的对象。在并发标记开始时不可达,但是并行期间引用发生变化,变为对象可达,这个阶段需要重新标记出此类对象,防止在下一阶段被清理掉。特别需要注意一点,这个阶段是以新生代中对象为根来判断对象是否存活的。 4. 并发清理,进行并发的垃圾清理。

新生代GC和老年代的GC是各自分开独立进行的。由于跨代引用的存在,CMS在Remark阶段必须扫描整个堆,同时为了避免扫描时新生代有很多对象,增加了可中断的预清理阶段用来等待Minor GC的发生。只是该阶段有时间限制,如果超时等不到Minor GC,Remark时新生代仍然有很多对象。新生代中对象的特点是"朝生夕灭",这样如果Remark前执行一次Minor GC,大部分对象就会被回收。我们的调优策略是,通过参数强制Remark前进行一次Minor GC,从而降低Remark阶段的时间。

单次执行时间>200ms的GC停顿消失

G1垃圾收集器:

目标减少swt时间,全年代回收器,取消物理分代,将整个堆空间分成一块一块相等的小内存,region,默认2048个,最大值,大小必须是2的倍数。5%给新生代。新生代和老年代由不同的region组成,不一定连续。可以设置gc停顿时间,每个region有一个回收价值(存活率,预计耗时等)组合一些有价值的region进行回收,region是动态变化的。

当新生代的region占比到60%就会就行垃圾回收,复制。年龄达到阈值,或者年龄大于50%以上的对象(比如1-5占50%,那6岁以上的进入),进入老年代。大对象存放在称为humongous的region,就行新生代或者老年代回收的时候顺带回收h。

默认老年代占比45%会垃圾回收(新生代和大对象也会回收),复制算法,可调。过程:初始标记(stw,gcroot能连到的标记),并发标记(遍历整个层,原始快照),最终标记(根据原始快照重新标记),混合回收(stw,可以分多次执行,先回收30,用户线程,再30个。默认分八次可改,避免停顿时间过长。)

GC ROOT

方法区中的静态属性,方法区的中的常量,虚拟机中的局部变量,本地方法栈,native修饰的方法指向的对象。

mysql

事务特性

原子性,2一致性(Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;

隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;

持久性(Durabilily): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然)来保证事务的原子性

脏读:当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是"脏数据",依据"脏数据"所做的操作可能是不正确的。

不可重复读: 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

幻读: 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

读取未提交: 允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。读不会加任何锁。而写会加排他锁,并到事务结束之后释放。

读取已提交: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。会通过MVCC获取当前数据的最新快照,不加任何锁,也无视任何锁

MVCC版本的生成时机: 是每次select时。这就意味着,如果我们在事务A中执行多次的select,在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读

可重复读: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读

可串行化: 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。select转化为select ... lock in share mode执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低。

mvcc

MVCC是指多版本并发控制。MVCC是在并发访问数据库时,通过对数据进行多版本控制,避免因写锁而导致读操作的堵塞,从而很好的优化并发堵塞问题。

MVCC的两个实现核心是undo log一致性视图,通过undo log来保存多版本的数据,通过一致性视图来保存当前活跃的事务列表,将两者结合和制定一定的规则来判断当前可读数据。

为什么InnoDB要回表查询,那为什么不像MyISAM一样使用非聚簇索引直接就能把数据查出来?

只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。

将查询的字段放到联合索引里面进去

Innodb把表中所有数据都存放在主索引的叶子节点里,在往表里插入数据时,可能会导致主索引结构发生变化(分裂或合并的操作),也就导致了数据地址的变化,所以为什么要再回表一次确保拿到正确的数据。而myisam的做法使得B+树结构发生变化时,还需要同步更新其他的索引。

InnoDB二级索引存储主键值而不是存储行指针的优点与缺点 
优点

减少了出现行移动或者数据页分裂时二级索引的维护工作(当数据需要更新的时候,二级索引不需要修改,只需要修改聚簇索引,一个表只能有一个聚簇索引,其他的都是二级索引,这样只需要修改聚簇索引就可以了,不需要重新构建二级索引)

缺点:

二级索引体积可能会变大,因为二级索引中存储了主键的信息。二级索引的访问需要两次索引查找。第一次通过查找 二级索引 找二级索引中叶子节点存储的 主键的值;第二次通过这个主键的值去 聚簇索引 中查找对应的行

聚簇索引和非

⾮聚簇索引:B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,⾸先按照 B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的 值为地址读取相应的数据记录。

聚簇索引:其数据⽂件本身就是索引⽂件。他的表数据⽂件本身就是按B+Tree组织的⼀个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据⽂件本身就是主索引。⽽其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值⽽不是地址,这也是和MyISAM不同的地⽅。在根据主索引搜索时,直接找到key所 在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再⾛⼀遍主索引。因此,在设计表的时候,不建议使⽤过⻓的字段作为主键,也不建议使⽤⾮单调的字段 作为主键,这样会造成主索引频繁分裂。

聚簇索引默认是主键,如果表中没有定义主键,InnoDB 会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。

如果我们使用哈希作为底层的数据结构,它对于处理范围查询或者排序性能会非常差,只能进行全表扫描并依次判断是否满足条件。使用 B+ 树其实能够保证数据按照键的顺序进行存储,也就是相邻的所有数据其实都是按照自然顺序排列的,使用哈希却无法达到这样的效果,因为哈希函数的目的就是让数据尽可能被分散到不同的桶中进行存储,我们总是要从根节点向下遍历子树查找满足条件的数据行,这个特点带来了大量的随机 I/O,也是 B 树最大的性能问题。

索引优缺点

第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 第二,可以大大加快数据的检索速度。 第三,可以加速表和表之间的连接。 第四,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

缺点:第一,创建索引和维护索引要耗费时间。 第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间。 第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。

在经常需要搜索的列上,可以加快搜索的速度;在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构; 在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度; 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的; 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间; 在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。

1.对于在查询过程中很少使用或参考的列,不应该创建索引。2.对于那些只有很少数据值的列,不应该创建索引。  3.对于那些定义为image,text和bit数据类型的列,不应该创建索引。因为这些数据类型的数据列的数据量要么很大、要么很小,不利于使用索引。  4.当修改性能远大于检索性能,不应该建立索引。

索引失效

联合索引排序的原理:先对第一个字段进行排序,在第一个字段相同的情况下考虑第二个字段,然后在第二个字段相同的情况下才考虑第三个字段...

违背了最左匹配原则可能会范围查询右边失效

单字段有索引,WHERE条件使用多字段(含带索引的字段),例 SELECT * FROM student WHERE name ='张三' AND addr = '北京市'语句,如果name有索引而addr没索引,那么SQL语句不会使用索引。

like,SQL语句中,使用后置通配符会走索引,例如查询姓张的学生(SELECT * FROM student WHERE name LIKE '张%'),而前置通配符(SELECT * FROM student WHERE name LIKE '%东')会导致索引失效而进行全表扫描。

如果条件中有or,即使其中有条件带索引也不会使用(因此SQL语句中要尽量避免使用OR)。要想使用OR,又想让索引生效,只能将OR条件中的每个列都加上索引。

mysql 事务在commit会发生什么?

申请锁资源,对所要修改 这行数据上排他锁

将需要修改的data pages读取到缓存

记录当前的数据到undo log

记录修改后的数据到redo log

将缓存中的数据更改

首先redo log prepare,然后写入binlog,最后redo log commit

大表优化

第一优化你的sql和索引;第二加缓存,memcached,redis;第三做主从复制或主主复制,读写分离,可以在应用层做,效率高,也可以用三方工具,第三方工具推荐360的atlas;第四mysql自带分区表,对sql做优化;第五如果以上都做了,那就先做垂直拆分,将一个大的系统分为多个小的系统,也就是分布式系统;第六才是水平切分。

一致性

(1)先淘汰缓存

(2)再写数据库(这两步和原来一样)

(3)休眠1秒,再次淘汰缓存。这么做,可以将1秒内所造成的缓存脏数据,再次删除。

慢查询优化基本步骤

0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE

1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高

2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)

3.order by limit 形式的sql语句让排序的表优先查

4.了解业务方使用场景

5.加索引时参照建索引的几大原则

6.观察结果,不符合预期继续从0分析

Java

Java判断两个类是否相同除了判断类本身,还需要判断类加载器是否相同,因此使用不同的类加载器进行加载就OK了

面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。

面向过程解决问题方式:把解决问题的过程,拆成一个个方法,通过一个个方法的执行解决问题。 
面向对象解决问题方式:先抽象出对象,然后用对象执行方法的方式解决问题。

面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低。

封装,继承,多态。封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

封装的优点:

(2)让使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制方法,限制对属性的不合理操作; 
(3)便于修改,增强代码的维护性和健壮性; 
(4)提高代码的安全性和规范性; 
(5)使程序更加具备稳定性和可拓展性。

多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。

多态的特点:

对象类型和引用类型之间具有继承(类)/实现(接口)的关系;

引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

栈和队列

栈:先进后出,可以用单链表实现。应用场景:当问题只关心最近一次的操作,而且需要在O(1)的时间内查找到更前的一次操作时使用栈。

队列:先进先出,可以用双链表实现。应用场景:按一定顺序处理数据,而且数据在不断变化的时候

Copyonwrite

CopyOnWrite当向一个容器中添加元素的时候,不是直接在当前这个容器里面添加的,而是复制出来一个新的容器,在新的容器里面添加元素,添加完毕之后再将原容器的引用指向新的容器。读和写可以并行执行,加快程序相应时间。适用于写操作非常少的场景,而且还能够容忍读写的暂时不一致CopyOnWriteArrayList/Set

树和二叉树

它是由n(n>0)个有限节点组成一个具有层次关系的集合。每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树;

二叉树是一种特殊的有序树:每个节点至多有两个分支(子节点),分支具有左右次序,不能颠倒。

平衡二叉树,普通二叉查找树,在新增/删除等操作后,会变得又高又瘦,会让查找/新增/删除操作的时间复杂度变大,而平衡二叉树在新增/删除等操作后,通过旋转(左旋和右旋)。依然会保持矮矮胖胖的形态,使树的高度维持在log n附近。这种形态就是平衡,会使查找速度更快。

红黑树:自平衡二叉树。节点是红色或黑色。根是黑色。所有叶子都是黑色(叶子是NIL节点)。每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

String,Stringbuffer,StringBuilder的区别

String:final和private两者保证了不可变,一旦创建就不可以修改。因此在每次对 String 类型进行改变的时候其实都等同于在堆中生成了一个新的 String 对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间。

StringBuffer:继承自AbstractStringBuilder,是可变类。StringBuffer是线程安全的。可以通过append方法动态构造数据。

StringBuilder:继承自AbstractStringBuilder,是可变类。StringBuilder是非线性安全的。执行效率比StringBuffer高。

扩容,原先长度*2+2,复制。

Java中的"+"对字符串的拼接,将String转成了StringBuilder后,使用其append方法进行处理的。

hashmap

用自定义类作为key,必须重写equals()和hashCode()方法:因为自定义类中的equals() 和 hashCode()都继承自Object类。Object类的hashCode()方法返回这个对象存储的内存地址的编号。而equals()比较的是内存地址是否相等。

在 JDK1.8 中,由"数组+链表+红黑树"组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树链表红黑树在达到一定条件会进行转换:

链表长度超过 8 且数据总量大于等于 64 才会转红黑树

链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),

hashcode,异或其右移十六位的值,高位运算的算法。这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

put方法

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤; ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

链表插入元素时,JDK1.7 使用头插法插入元素,在多线程的环境下有可能导致环形链表的出现,扩容的时候会导致死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

threshold 阈值。阈值=容量*加载因子。默认12。当元素数量超过阈值时便会触发扩容。

扩容:

长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成"原索引+oldCap"

Hash表的大小始终为2的n次幂,因此可以将取模转为位运算操作,提高效率,容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。

1.如果我两个对象的hashcode一样,equals不一样,put进Map会发生什么,拿出来的时候呢?

如果两个对象的hashcode一样,equals不一样,会正常put进Map,只不过会发生hash碰撞,以链表或者红黑树的形式挂在同一个桶中,而且因为二者的equals结果不一样,也可以正常取出。

2.hashcode不一样,equals一样会发生什么?

如果两个对象的equals一样的话,从业务逻辑上讲我们是想要把这两个对象认为是同一个对象的。但是由于二者的hashcode不一样,在put进Map中的时候,二者会被映射到不同的位置,即不同的桶中,没有调用equals比较的机会。所以这时两个业务上相等的对象会被放进不同的位置,出现错误。这就是为什么在重写hashCode()时,生成的哈希值依据应该是equals()中用来比较是否相等的字段。

一旦重写了equals()函数(重写equals的时候还要注意要满足自反性、对称性、传递性、一致性),就必须重写hashCode()函数。而且hashCode()的生成哈希值的依据应该是equals()中用来比较是否相等的字段,即equals()相同的的元素其hashCode()也要相同才能安全放入HashMap中。

put:数据不一致问题。假设有两个线程A和B,都在执行put任务,线程A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后遍历该桶的链表直到尾结点(JDK1.8之后HashMap变成了尾插),就在要插入数据之前,线程A的CPU时间片用完了,此时调度器调度线程B开始执行,与A不同的是,线程B成功将元素put进了Map中,而且线程Bput的元素和线程A的hash值是一样的。那么当线程A再次被调度的时候,它持有过期的链表尾,但是线程A对此一无所知,直接进行了插入操作,此时线程Bput的数据就会被覆盖掉,造成了数据不一致问题。

哈希表每一个单元中设置链表,某个数据项对的关键字还是像通常一样映射到哈希表的单元中,而数据项本身插入到单元的链表中。

ConcurrentHashMap

CAS+synchronized机制的锁。如果元素要put的桶是空的,那么就利用CAS进行元素的添加,失败就循环尝试。如果桶不为空,发生了hash碰撞,就对头结点加synchronized锁来进行后续的操作。

hashtable

HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中。

HashTable类和HashMap用法几乎一样,底层实现几乎一样,只不过HashTable的方法添加了synchronized关键字确保线程同步检查,效率较低。

Arraylist 与 LinkedList 区别?

是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全;

底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构

插入和删除是否受元素位置的影响:①ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e)方法的时候,ArrayList会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList采用链表存储,所以对于add(E e)方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element) 时间复杂度近似为o(n)因为需要先移动到指定位置再插入。

是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

内存空间占用:ArrayList的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

array list扩容,首先是10,然后乘以1.5倍扩容。Arrays.copyOf

CopyOnWriteArrayList:线程安全。写入时复制。意思就是大家共同去访问一个资源,如果有人想要去修改这个资源的时候,就需要复制一个副本,去修改这个副本,而对于其他人来说访问得资源还是原来的,不会发生变化。底层数组实现。

ReentrantLock,独占锁,多线程运行的情况下,只有一个线程会获得这个锁,只有释放锁后其他线程才能获得。数组被volatile修饰,可见性,也就是一个线程修改后,其他线程立即可见。

添加:获得独占锁,将添加功能加锁。获取原来的数组,并得到其长度。创建一个长度为原来数组长度+1的数组,并拷贝原来的元素给新数组。追加元素到新数组末尾。指向新数组。释放锁

线性表和二叉树和ArrayList

1. 链表,分为单向和多向链表

1) 基本特征:内存中不连续的节点序列,节点之间通过next指针彼此相连。每个节点的next指针都指向下一个节点,最后一个节点的next指针为NULL。

2) 基本操作:插入、删除、遍历。

应用:linkedlist

2.二叉树

单向线性链表里,每个节点可以向后找到多个其他节点,这也是一个数据结构,这个数据结构叫树。

线性表:属于逻辑结构中的线性结构,它包括顺序表和链表

顺序表:线性表中的一种,它是用数组来实现的一种线性表,所以它的存储结构(物理结构)是连续的。

有序数组的优势在于二分查找,然而链表的优势在于数据项的插入和数据项的删除。但是在有序数组中插入数据就会很慢,同样在链表中查找数据项效率就很低。

二叉树也是一种常用的数据结构。有序二叉树天然具有对数查找效率;

ArrayList 的底层是object数组,相当于动态数组。三种初始化方式

int newCapacity = oldCapacity + (oldCapacity >> 1),

反射

反射是框架的灵魂,可以在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。

使用场景:注解,动态代理

四种方式:1.知道具体类的情况下可以使用:类.class;2.通过对象实例.getClass()获取。3.通过Class.forName()传入类的路径获取:4.通过类加载器xxxClassLoader.loadClass()传入类路径获取。通过类加载器获取 Class 对象不会进行初始化,

Lamda和匿名内部类

取代匿名内部类,允许把函数作为一个方法的参数。

通过传递内部类实例,来调用函数式接口方法。就是传递个函数指针,

在 Java 中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。

在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。

匿名内部类也就是没有名字的内部类

正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写

但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口

异常和错误

检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

深浅拷贝

1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

抽象类和接口

都不能被实例化

1) 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势;接口中只能有抽象的方法。 
2) 一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类;但是一个类可以实现多个接口。

接口的设计目的,是对类的行为进行约束(更准确的说是一种"有"约束,因为接口不能规定类不可以有什么行为)而抽象类的设计目的,是代码复用。

错误和异常

Exception 是 Java 程序运行中可预料的异常情况,我们可以获取到这种异常,并且对这种异常进行业务外的处理。

Error 是 Java 程序运行中不可预料的异常情况,这种异常发生以后,会直接导致 JVM 不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如 OutOfMemoryError、

设计模式职责

单一职责原则(一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分)开闭原则 (通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。)里氏替换原则(继承规则)依赖倒置原则(依赖抽象接口,而不是具体对象)接口隔离原则(接口按照功能细分)迪米特法则 (类与类之间的耦合底)

单例模式

饿汉 在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用。这时候,单例类的唯一实例就被创建出来了。

懒汉 每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。

双重检查锁 检查变量是否被初始化(不去获得锁,syn)。获取锁。再次检查变量是否已经被初始化,否就初始化一个对象。多个线程同时了通过了第一次检查,并且其中一个线程首先通过第二次检查并实例化了对象,剩余通过了第一次检查的线程就不会再去实例化对象。这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。

静态内部类 加载一个类时,其内部类不会同时被加载。当且仅当其某个静态成员被调用时发生。才会对单例进行初始化,不能被反射破坏;由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。

枚举 创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。反射无法破坏。

分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

CAP 原则又称 CAP 定理

,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

分区容错性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。

分布式事务的一致性

两阶段提交

顾名思义就是要分两步提交。存在一个负责协调各个本地资源管理器的事务管理器,本地资源管理器一般是由数据库实现,事务管理器在第一阶段的时候询问各个资源管理器是否都就绪?如果收到每个资源的回复都是 yes,则在第二阶段提交事务,如果其中任意一个资源的回复是 no, 则回滚事务。

tcc

解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。

同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。

数据一致性,有了补偿机制之后,由业务活动管理器控制一致性

Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)

Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。

Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 。

会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口,所以代码实现复杂度相对较高。

浏览器输入URL,发生了什么

1.DNS域名解析2.三次握手建立tcp连接3.发送http请求,服务器处理请求并发回响应结果4.浏览器渲染5.四次挥手

DNS查询算法

主机向本地域名服务器的查询一般都是采用递归查询,即如果主机所询问的本地域名服务器不知道被查询域名的IP地址,那么本地域名服务器就以DNS客户的身份,向其他根域名服务器继续发出查询请求报文,而不是让该主机自己进行下一步的查询。

本地域名服务器向根服务器的查询通常采用迭代查询,即当根域名服务器收到本地域名服务器收到本地域名服务器发出的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地域名服务器"下一次应向哪个域名服务器进行查询"。

分享不易,感谢大家的阅读