实习面试遇到的问题之复盘结果

1.Comparable和Comparator区别比较:

  • Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。(在这个类中实现排序)

  • 而Comparator是比较器接口,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。(我重新创建一个新的类让其继承comparator接口,专门用来排序)

  • Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。

两种方法各有优劣, 用Comparable 简单, 只要实现Comparable 接口的对象直接就成为一个可以比较的对象,但是需要修改源代码。 用Comparator 的好处是不需要修改源代码, 而是另外实现一个比较器, 当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了, 并且在Comparator 里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

2.ConcurrentHashMap的key和value都不能为null:

源码

if (key == null || value == null) throw new NullPointerException();

二义性

假定ConcurrentHashMap也可以存放value为null的值。那不管是HashMap还是ConcurrentHashMap调用map.get(key)的时候,如果返回了null,那么这个null,都有两重含义:

1.这个key从来没有在map中映射过。

2.这个key的value在设置的时候,就是null。

为什么map允许value=null

对于HashMap的正确使用场景是在单线程下使用。

在单线程中,当我们得到的value是null的时候,我可以用hashMap.containsKey(key)方法来区分上面说的两重含义。

所以当map.get(key)返回的值是null,在HashMap中虽然存在二义性,但是结合containsKey方法可以避免二义性。

为什么ConcurrentHashMap不允许

ConcurrentHashMap的使用场景为多线程。

反证法来推理,假设concurrentHashMap允许存放值为null的value。

这时有A、B两个线程。

线程A调用concurrentHashMap.get(key)方法,返回为null,我们还是不知道这个null是没有映射的null还是存的值就是null。

我们假设此时返回为null的真实情况就是因为这个key没有在map里面映射过。那么我们可以用concurrentHashMap.containsKey(key)来验证我们的假设是否成立,我们期望的结果是返回false。

但是在我们调用concurrentHashMap.get(key)方法之后,containsKey方法之前,有一个线程B执行了concurrentHashMap.put(key,null)的操作。那么我们调用containsKey方法返回的就是true了。这就与我们的假设的真实情况不符合了。也就是上面说的二义性。

对于key不能为null

源码就是这样。。

3.为什么重写equals()方法时,必须要求重写hashCode()方法

equals() 方法和 hashcode() 方法间的关系是这样的:

1、如果两个对象相同,那么它们的 hashCode 值一定要相同;

2、如果两个对象的 hashCode 相同,它们并不一定相同

如果重写了 equals() 而未重写 hashcode() 方法,可能就会出现两个没有关系的对象 equals 相同(因为equal都是根据对象的特征进行重写的),但 hashcode 不相同的情况。因为此时 Student 类的 hashcode 方法就是 Object 默认的 hashcode方法,由于默认的 hashcode 方法是根据对象的内存地址经哈希算法得来的,所以 stu1 != stu2,故两者的 hashcode 值不一定相等。

根据 hashcode 的规则,两个对象相等其hash值一定要相等,矛盾就这样产生了。

4.MySQL联合索引时的最左前缀匹配原则:

MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 a=“3” and=“” b=“4” c>5 and d=6,如果建立(a,b,c,d)顺序的索引,d是无法使用索引的,abc是会走索引的,但是d无法走索引.如果建立(a,b,d,c)的索引则都可以使用到,a、b、d的顺序可以任意调整。

=和in可以乱序,比如 a=1 and b=2 and c=3 建立(a,b,c)索引可以任意顺序,MySQL的查询优化器会帮你优化成索引可以识别的形式。

5.MySqL中的B+树结构

B 树

因为内存的易失性。一般情况下,我们都会选择将 user 表中的数据和索引存储在磁盘这种外围设备中。但是和内存相比,从磁盘中读取数据的速度会慢上百倍千倍甚至万倍,所以,我们应当尽量减少从磁盘中读取数据的次数。另外,从磁盘中读取数据时,都是按照磁盘块来读取的,并不是一条一条的读。如果我们能把尽量多的数据放进磁盘块中,那一次磁盘读取操作就会读取更多数据,那我们查找数据的时间也会大幅度降低。如果我们用树这种数据结构作为索引的数据结构,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说的一个磁盘块。我们都知道平衡二叉树可是每个节点只存储一个键值和数据的。那说明什么?说明每个磁盘块仅仅存储一个键值和数据!那如果我们要存储海量的数据呢?

可以想象到二叉树的节点将会非常多,高度也会极其高,我们查找数据时也会进行很多次磁盘 IO,我们查找数据的效率将会极低!

img

为了解决平衡二叉树的这个弊端,我们应该寻找一种单个节点可以存储多个键值和数据的平衡树。也就是我们接下来要说的 B 树。

B 树(Balance Tree)即为平衡树的意思,下图即是一棵 B 树:

img

图中的 p 节点为指向子节点的指针,二叉查找树和平衡二叉树其实也有,因为图的美观性,被省略了。

图中的每个节点称为页,页就是我们上面说的磁盘块,在 MySQL 中数据读取的基本单位都是页,所以我们这里叫做页更符合 MySQL 中索引的底层数据结构。

从上图可以看出,B 树相对于平衡二叉树,每个节点存储了更多的键值(key)和数据(data),并且每个节点拥有更多的子节点,子节点的个数一般称为阶,上述图中的 B 树为 3 阶 B 树,高度也会很低。

基于这个特性,B 树查找数据读取磁盘的次数将会很少,数据的查找效率也会比平衡二叉树高很多。

假如我们要查找 id=28 的用户信息,那么我们在上图 B 树中查找的流程如下:

  • 先找到根节点也就是页 1,判断 28 在键值 17 和 35 之间,那么我们根据页 1 中的指针 p2 找到页 3。
  • 将 28 和页 3 中的键值相比较,28 在 26 和 30 之间,我们根据页 3 中的指针 p2 找到页 8。
  • 将 28 和页 8 中的键值相比较,发现有匹配的键值 28,键值 28 对应的用户信息为(28,bv)。

B+ 树

B+ 树是对 B 树的进一步优化。让我们先来看下 B+ 树的结构图:

img

根据上图我们来看下 B+ 树和 B 树有什么不同:

B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。

之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB。

如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

另外,B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。

一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。

②因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。

那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。

有心的读者可能还发现上图 B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。

其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。

也就是说上图中的 B+ 树索引就是 InnoDB 中 B+ 树索引真正的实现方式,准确的说应该是聚集索引(聚集索引和非聚集索引下面会讲到)。

通过上图可以看到,在 InnoDB 中,我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。

MyISAM 中的 B+ 树索引实现与 InnoDB 中的略有不同。在 MyISAM 中,B+ 树索引的叶子节点并不存储数据,而是存储数据的文件地址。

聚集索引 VS 非聚集索引

在上节介绍 B+ 树索引的时候,我们提到了图中的索引其实是聚集索引的实现方式。

那什么是聚集索引呢?在 MySQL 中,B+ 树索引按照存储方式的不同分为聚集索引和非聚集索引。

这里我们着重介绍 InnoDB 中的聚集索引和非聚集索引:

①**聚集索引(聚簇索引):**以 InnoDB 作为存储引擎的表,表中的数据都会有一个主键,即使你不创建主键,系统也会帮你创建一个隐式的主键。

这是因为 InnoDB 是把数据存放在 B+ 树中的,而 B+ 树的键值就是主键,在 B+ 树的叶子节点中,存储了表中所有的数据。

这种以主键作为 B+ 树索引的键值而构建的 B+ 树索引,我们称之为聚集索引。

②**非聚集索引(非聚簇索引):**以主键以外的列值作为键值构建的 B+ 树索引,我们称之为非聚集索引。

非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。

明白了聚集索引和非聚集索引的定义,我们应该明白这样一句话:数据即索引,索引即数据。

利用聚集索引和非聚集索引查找数据

前面我们讲解 B+ 树索引的时候并没有去说怎么在 B+ 树中进行数据的查找,主要就是因为还没有引出聚集索引和非聚集索引的概念。

下面我们通过讲解如何通过聚集索引以及非聚集索引查找数据表中数据的方式介绍一下 B+ 树索引查找数据方法。

利用聚集索引查找数据

img

还是这张 B+ 树索引图,现在我们应该知道这就是聚集索引,表中的数据存储在其中。

现在假设我们要查找 id>=18 并且 id<40 的用户数据。对应的 sql 语句为:

MySQL

select * from user where id>=18 and id <40

其中 id 为主键,具体的查找过程如下:

①一般根节点都是常驻内存的,也就是说页 1 已经在内存中了,此时不需要到磁盘中读取数据,直接从内存中读取即可。

从内存中读取到页 1,要查找这个 id>=18 and id <40 或者范围值,我们首先需要找到 id=18 的键值。

从页 1 中我们可以找到键值 18,此时我们需要根据指针 p2,定位到页 3。

②要从页 3 中查找数据,我们就需要拿着 p2 指针去磁盘中进行读取页 3。

从磁盘中读取页 3 后将页 3 放入内存中,然后进行查找,我们可以找到键值 18,然后再拿到页 3 中的指针 p1,定位到页 8。

③同样的页 8 页不在内存中,我们需要再去磁盘中将页 8 读取到内存中。

将页 8 读取到内存中后。因为页中的数据是链表进行连接的,而且键值是按照顺序存放的,此时可以根据二分查找法定位到键值 18。

此时因为已经到数据页了,此时我们已经找到一条满足条件的数据了,就是键值 18 对应的数据。

因为是范围查找,而且此时所有的数据又都存在叶子节点,并且是有序排列的,那么我们就可以对页 8 中的键值依次进行遍历查找并匹配满足条件的数据。

我们可以一直找到键值为 22 的数据,然后页 8 中就没有数据了,此时我们需要拿着页 8 中的 p 指针去读取页 9 中的数据。

④因为页 9 不在内存中,就又会加载页 9 到内存中,并通过和页 8 中一样的方式进行数据的查找,直到将页 12 加载到内存中,发现 41 大于 40,此时不满足条件。那么查找到此终止。

最终我们找到满足条件的所有数据,总共 12 条记录:

(18,kl), (19,kl), (22,hj), (24,io), (25,vg) , (29,jk), (31,jk) , (33,rt) , (34,ty) , (35,yu) , (37,rt) , (39,rt) 。

下面看下具体的查找流程图

img

利用非聚集索引查找数据

img

读者看到这张图的时候可能会蒙,这是啥东西啊?怎么都是数字。如果有这种感觉,请仔细看下图中红字的解释。

什么?还看不懂?那我再来解释下吧。首先,这个非聚集索引表示的是用户幸运数字的索引(为什么是幸运数字?一时兴起想起来的:-)),此时表结构是这样的。

img

在叶子节点中,不再存储所有的数据了,存储的是键值和主键。对于叶子节点中的 x-y,比如 1-1。左边的 1 表示的是索引的键值,右边的 1 表示的是主键值。

如果我们要找到幸运数字为 33 的用户信息,对应的 sql 语句为:

select * from user where luckNum=33

查找的流程跟聚集索引一样,这里就不详细介绍了。我们最终会找到主键值 47,找到主键后我们需要再到聚集索引中查找具体对应的数据信息,此时又回到了聚集索引的查找流程。

下面看下具体的查找流程图

img

在 MyISAM 中,聚集索引和非聚集索引的叶子节点都会存储数据的文件地址。

6.http各个版本的区别

HTTP1.0

优点
  1. 简单
    HTTP基本的报文格式就是 header + body。
  2. 灵活和易于扩展
    HTTP协议里的各类请求方法`状态码、头字段等都是可以自定义扩展的。
    同时 HTTP 由于是工作在应用层( OSI 第七层),则它下层可以随意变化。
  3. 应用广泛和跨平台
缺点
  1. 无状态

服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息(无状态的问题解决方法有cookie/session/token)

​ 2.不安全

  • 明文传输

  • 不验证通信方的身份

  • 无法证明报文的完整性

安全问题HTTPS得到了解决。

​ 3.无连接(短连接)

浏览器的每次请求都要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接。

每个TCP只能发送一个请求。发送数据完毕,连接就关闭。如果还要请求其他资源,就需要再建立一个连接。

TCP三次握手是一个很耗费时间的过程,所以HTTP/1.0性能比较差。

请求报文
  • 请求行由请求方法、URL 和 HTTP协议版本三个字段组成。中间由空格隔开。例如:GET /index.html HTTP/1.1。

HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。

常见的:

  1. GET
  • 当客户端要从服务器读取文档时,点击网页上的链接或者通过在浏览器的地址栏输入网址来浏览网页,都是GET方式。
  • GET方法要求服务器将URL定位的资源放在响应报文的数据部分,会送给客户端。
  • 使用GET方法时,请求参数和对应的值附加在URL后面,以一个(?)代表URL的结尾与请求参数的开始,传递参数长度受限制。例如:/index.jsp?id=100&op=bind.

​ 2.POST

  • POST方法将请求的参数封装在HTTP请求数据中,以名称/值的方式出现,可以传输大量数据,对传送的大小没有限制,也不会显示在URL中。

​ 3.HEAD

  • HEAD就像GET,只不过服务端接受到HEAD请求后只返回响应头,而不会发送响应内容。当我们只需要查看某个页面的状态的时候,使用HEAD是非常高效的,因为在传输的过程中省去了页面内容。

GET和POST的区别?

  1. GET方法是请求从服务器获取资源,可以是图片、文本、页面、视频等。 POST是向URI指定的资源提交数据,数据就放在请求报文的BODY里。
  2. GET请求会把请求的数据附在URL后。POST提交是将数据放在报文的body里。 因此GET提交的数据会在地址栏中显示出来,而POST提交地址栏不会改变。
  3. 是否安全且幂等

安全和幂等:

  • 安全:请求方法不会破坏服务器上的资源
  • 幂等:多次执行相同的操作,结果都是相同的。

GET是安全且幂等,因为它的操作是只读的,无论操作多少次,服务器上的数据都是安全的,且每次结果都相同。
POST 因为是新增或提交数据,因此它会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,也是不幂等的。

  1. 安全性:

POST的安全性要比GET的安全性高。

注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的Security的含义,

比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存, (2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了。

  1. POST传输的资源更大,数据类型更多。
  2. get 获取资源的速度更快!!

HTTP 1.1改进

  1. 缓存处理

在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。

​ 2.带宽优化及网络连接的使用

HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

​ 3.错误通知的管理

在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

​ 4.HOST头处理

在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。

​ 5.长连接

在早期的HTTP1.0中,每次http请求都要建立一个TCP连接,创建连接的过程需要消耗资源和时间,为了减少资源消耗,就需要重用连接。

后来的HTTP1.0 与 HTTP1.1 中,引用了重用连接的机制,在HTTP请求头中加入Connection:keep-alive,告诉对方这个响应完成后不要关闭,下一次继续使用。

HTTP1.0需要保持长连接要在头部信息中加入此参数,HTTP1.1 默认长连接,可以不加。如果不需要长连接,则在头部信息加上:Connection:close,接受到请求的客户端就会自动关闭连接。

长连接会一直保持吗?

不会! 一般服务端都会设置keep-alive超时时间,超过指定的时间间隔,服务端主动关闭连接。

同时,服务端还会设置最大请求数,比如最大请求数为300,只要超过最大请求数,即使没到超时时间,都会主动关闭连接。

参数content-length,指明响应体数据的大小,浏览器收到如数的响应知道响应完成,就可以关闭连接。

​ 6.管道网络传输

采用长连接的方式,管道传输成为了可能。长连接有两种工作方式:非流水线方式和流水线方式。

  • 非流水线方式:客户端在收到前一个响应之后才能发出下一个请求。在TCP连接建立后,客户端每访问一次对象都要消耗一个RTT。优点:比非持续连接的两倍RTT节省了建立TCP连接所需的一个RTT。 缺点:服务器发送完一个对象之后,其TCP连接处于空闲状态,浪费服务器资源。
  • 流水线方式:客户在收到HTTP响应之前,就接着发送新的请求。服务器可以持续发送响应报文。优点:只需要一个RTT。使TCP连接中的空闲时间减少,提高文档下载效率。

但是服务器会按照顺序,回应请求。如果前面的请求特别慢,后面的请求就会排队等待。称为队头堵塞

HTTP 2.0 改进

  1. 头部压缩

如果发送多个请求,头部使一样的或是相似的,协议会消除重复的部分。

HPACK算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

  1. 二进制格式

报文采用了二进制格式。

头信息和数据体都是二进制,统称为:头信息帧和数据帧。

这样虽然对用户不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率。

  1. 数据流

数据包不是连续发送的,同一个连接里面的连续的数据包,可能属于不同的回应。必须对包进行标记,指出属于哪个回应。

每个请求或回应的所有数据包,称为一个数据流(Stream)。

每个数据流都标记着一个独一无二的编号,其中规定客户端发出的数据流编号为奇数, 服务器发出的数据流编号为偶数

客户端还可以指定数据流的优先级。优先级高的请求,服务器就先响应该请求。

  1. 多路复用

HTTP/2 是可以在一个连接中并发多个请求或回应,而不用按照顺序一一对应。

移除了 HTTP/1.1 中的串行请求,不需要排队等待,也就不会再出现「队头阻塞」问题,降低了延迟,大幅度提高了连接的利用率。

举例来说,在一个 TCP 连接里,服务器收到了客户端 A 和 B 的两个请求,如果发现 A 处理过程非常耗时,于是就回应 A 请求已经处理好的部分,接着回应 B 请求,完成后,再回应 A 请求剩下的部分。
HTTP/2 为了解决HTTP/1.1中仍然存在的效率问题,**HTTP/2 采用了多路复用。即在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应。**能这样做有一个前提,就是HTTP/2进行了二进制分帧,即 HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码。

也就是说,老板可以同时下达多个命令,员工也可以收到了A请求和B请求,于是先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。A请求的两部分响应在组合到一起发给老板。

而这个负责拆分、组装请求和二进制帧的一层就叫做二进制分帧层。
除此之外,还有一些其他的优化,比如做Header压缩、服务端推送等。

HTTPS 改进

HTTPS 在 HTTP 与TCP之间加入了 SSL/TLS协议。

在这里插入图片描述

HTTPS如何解决 HTTP不安全的问题?

  • 混合加密的方式实现了信息的机密性,解决了窃听的风险。
  • 摘要算法保证数据的完整性,能够为数据生成独一无二的指纹,指纹用于校验数据的完整性,解决了篡改的风险。
  • 将服务器公钥放入数字证书中,解决了冒充的风险。

HTTP 与 HTTPS 区别?

  1. HTTP是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS解决了HTTP不安全的缺陷,在TCP与HTTP之间加入了SSL/TLS协议,保证报文能够加密传输。
  2. HTTP连接建立相对简单,TCP三次握手之后可进行HTTP传输。而HTTPS在三次握手之后,还要进行SSL/TLS握手过程,才可以进入加密传输。
  3. HTTP的端口号是80,HTTPS端口号是443.
  4. HTTPS需要向CA(证书权威机构)申请数字证书,保证服务器是可信的。

7.t1 t2 t3三个线程,按顺序串行执行如何实现?

除了使用newSingleThreadPool整一个单工作线程的线程池/使用一个阻塞队列之外.

方法一:**用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。**为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

//Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。join方法必须在线程start方法调用之后调用才有意义。如果一个线程没有start,那它也就无法同步。这是由于只有执行完start方法才会创建线程。join才会有意义。

public static void main(String[] args) {
        final Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("t1");
            }
        });
        final Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    // 引用t1线程,等待t1线程执行完
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2");
            }
        });
        Thread t3 = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    // 引用t2线程,等待t2线程执行完
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t3");
            }
        });
        
        //这里三个线程的启动顺序可以任意,大家可以试下!
        t1.start();
        t2.start();
        t3.start();
    }

8.Thread.sleep(0)有什么作用

Thread.Sleep(0)作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。感觉就是和yeild差不多,但是没有优先级的限制.


wait、notify、notifyAll、sleep、join、yeild、interrupt的总结:

  • 首先按照分类来说:

    wait、notify、notifyAll都是java.lang.Object的方法,都是用于协调多个线程对共享数据的存取,所以必须在Synchronized语句块内使用这三个方法。

    sleep(long)、join、yeild、interrupt都是Thread类的静态方法,跟所没关系

  • 各个方法的区别:

    • wait方法:释放CPU资源,释放锁资源,进入等待池中
    • notify方法:随机唤醒一个等待池中的线程进入锁池(注意:只有锁池中的线程才可以去竞争并获取锁)
    • notifyAll方法:唤醒等待池中的所有线程进入锁池
    • sleep(long)方法:释放CPU资源,不释放锁资源.线程进入阻塞状态,睡眠时间后自动转入就绪状态
    • yeild方法:释放CPU资源,让同等优先级的线程执行(只保证当前线程放弃CPU,但不保证其他线程一定能抢到CPU)
    • interrupt方法:会给受阻的线程(sleep/wait/join)发送中断信号,使其退出阻塞状态,如果线程正常运行本身没有被阻塞,则不起作用
    • join方法:主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行
  • 注意:notify可能会导致死锁(原因是如果使用notify唤醒一个线程从等待池进入锁池,并获得锁资源.如果线程执行完之后释放了锁.但是并没有再次执行notify语句,就可能会导致此时锁池中没有线程,而等待池中的线程有没有被唤醒的,从而导致死锁.)

  • 注意:wait用于锁机制,sleep不是,这就是为啥sleep不释放锁,wait释放锁的原因,sleep是线程的方法,跟锁没半毛钱关系,wait,notify,notifyall 都是Object对象的方法,是一起使用的,用于锁机制

park和unpark的区别:都是LockSupport类中的方法

  • LockSupport.park()暂停当前线程

  • LockSupport.unpark()恢复暂停的线程(可以在线程暂停前和线程暂停后调用,都可以恢复暂停的线程)

9.为什么notify()和notifyAll()不在Thred类中,而是在Object类中

Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。简单的说,由于wait ()、notify ()和notifyAll ()都是锁级别的操作,用于锁机制中,而锁是属于对象的,所以把他们定义在Object类中

10.同步方法和同步代码块的区别

  • 同步方法默认用this或者当前类的实例化对象作为锁;
  • 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;

总结一:同步方法中锁对象是this或者当前类的class对象,而同步代码块中可以自由的选择锁对象.

总结二:同步方法中,是将整个方法中的内容作为同步.而同步代码块可以更加的细化,对部分代码进行同步.

11.Spring中写了事务的注解,但没有生效是什么原因? 事务没有回滚是什么原因?

原因是,我们并没有添加 @EnableTransactionManagement 来开启事务管理,所以 @Transactional 没生效。当我们在 TransactionConfiguration 这个类上面加上 @EnableTransactionManagement 注解之后,再执行 Application1 的main方法,可以看到数据没有插入即事务被回滚了。

12.MySQL中的悲观锁和乐观锁的sql实现语句,行锁中死锁现象

  • 悲观锁的实现,通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update来实现悲观锁。
  • 乐观锁的实现不依靠数据库提供的锁机制,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号,一种是通过时间戳。(给表加一个版本号或时间戳的字段,读取数据时,将版本号一同读出,数据更新时,将版本号加1。当我们提交数据更新时,判断当前的版本号与第一次读取出来的版本号是否相等。如果相等,则予以更新,否则认为数据过期,拒绝更新,让用户重新操作)

MySQL死锁怎么实现:两个事务(事务一和事务二),事务一对A行进行请求(对A上行锁),然后再请求B.而事务二先对B行进行请求(对B上行锁),然后再请求A.两者就成死锁.

13.单例模式(double-checked)

//懒汉式 不推荐使用的原因是在获取时getInstance()每次都要加锁,每次获取对象,哪怕之后单例已经创建好了,获取的时候也要加锁,这样的效率太低
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

总结:懒汉式锁的范围太大了,效率低,要会使用double-checked的方式创建单例,首先两次判断单例是否为空,为的就是不要重复创建单例.在第一次为空的时候在上锁,减小锁的范围,提高并发效率.然后将单例修饰为volatile是为了防止创建单例时的指令重排,分配内存和引用内存空间先后顺序不能乱.不然会出现先引用内存空间,还没有分配内存,新的线程发现单例存在,获取的时候拿到null

//双检锁/双重校验锁(DCL,即 double-checked locking)推荐!!!!
//是懒汉式的改进,首先缩小synchronized上锁的范围,
public class Singleton {  
    private volatile static Singleton singleton;//用volatile的原因是防止指令重排序,instance = new SingleInstance()这行代码并不是原子性的,也就是说,这行代码需要处理器分为多步才能完成,其中主要包含两个操作,分配内存空间,引用变量指向内存,由于编译器可能会产生指令重排序的优化操作,所以两个步骤不能确定实际的先后顺序,如果在没有分配内存的时候,先引用指向内存,那么此时别的线程发现单例不为空,但是获取到的值却是null
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {//第一次判断 如果不为空直接返回singleton对象
            synchronized (Singleton.class) {  
                if (singleton == null) {//第二次判断 是为了防止singleton重复创建  t1创建的同时 t2在外面获取锁 然后t1创建完了 t2进来如果不判断就又重复创建
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}
//枚举的方式创建单例 也不会有线程安全的问题 因为在字节码文件中是一个final static的INSTANCE,是一个静态成员变量,所以也没有并发的问题.反射破坏不了单例,同时枚举类一般默认实现序列化接口,并且无法通过反序列化破坏单例.枚举属于饿汉式.如果需要给枚举单例加一些单例创建时的初始化逻辑,就加一个构造方法即可.
public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

14.JMM

JMM就是Java内存模型(java memory model)。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。线程之间的通信机制有两种共享内存消息传递,Java线程之间的通信采用的是过共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),

Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

如果听起来抽象的话,我可以画张图给你看看,会直观一点:

img

每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主存。这是 Java 内存模型定义的线程基本工作方式。

java内存模型主要是保证共享内存的原子性/可见性/有序性.

要解决共享对象可见性这个问题,我们可以使用java volatile关键字,volatile原理是基于CPU内存屏障指令实现的

原子性由synchronize关键字上锁来解决.

有序性通过volatile和synchronize解决,虽然说使用synchronized的代码块,还是可能发生指令重排,但是因为synchronized可以保证只有一个线程执行,所以最后的执行结果还是正确的。

15.redis的基本数据结构与底层实现,String如何存储,list的底层

Redis中所有的数据都是key-value键值对的形式存放的,一共有五种基本数据结构,对应的就是value中可以存放五种数据结构,分别是String/list/hash/set/zset

img

简单动态字符串(Simple Dynamic String,SDS)

Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。 其实SDS等同于C语言中的char * ,但它可以存储任意二进制数据,不能像C语言字符串那样以字 符’\0’来标识字符串的结 束,因此它必然有个长度字段。

SDS保存了字符串的长度,所以获取字符串长度的时间复杂度就是O(1)

struct sdshdr {
 // 记录buf数组中已使用字节的数量
 // 等于sds所保存字符串的长度
 int len;
 
 // 记录buf数组中未使用字节的数量
 int free;
 
 // 字节数组,用于保存字符串
 char buf[];
}

SDS 相比C字符串的优势:

  • SDS保存了字符串的长度,而C字符串不保存长度,需要遍历整个数组(找到’\0’为止)才能取到字符串长度。
  • 修改SDS时,检查给定SDS空间是否足够,如果不够会先拓展SDS 的空间,防止缓冲区溢出。C字符串不会检查字符串空间是否足够,调用一些函数时很容易造成缓冲区溢出(比如strcat字符串连接函数)。
  • SDS预分配空间的机制,可以减少为字符串重新分配空间的次数。

List

节点底层结构(可以看出是双向)

typedef struct listNode {
 // 前置节点
 struct listNode *prev;
 // 后置节点
 struct listNode *next;
 // 节点的值
 void *value;
} listNode;

list底层结构(双向链表实现,并且同样在链表的结构中带有链表长度的信息)

typedef struct list {
 // 表头节点
 listNode *head;
 // 表尾节点
 listNode *tail;
 // 链表所包含的节点数量
 unsigned long len;
 // 节点值复制函数
 void *(*dup)(void *ptr);
 // 节点值是放过函数
 void (*free)(void *ptr);
 // 节点值对比函数
 int*match)(void *ptr, void *key);
} list;

特性

  • 链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。
  • 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以 Redis的链表实现是双端链表。
  • 每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。
  • 因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表
  • 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。

Hash

hash结构里其实是一个字典,有许多的键值对.

typedef struct dictht {
 // 哈希表数组
 dictEntry **table;
 // 哈希表大小
 unsigned long size;
 // 哈希表大小掩码,用于计算索引值
 // 总是等于size-1
 unsigned long sizemark;
 // 该哈希表已有节点的数量
 unsigned long used;
} dichht;

哈希表节点的结构体如下:

typeof struct dictEntry{  
   void *key;//键
   union{  //不同键对应的值的类型可能不同,使用union来处理这个问题
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;//每个哈希表节点都有一个next指针
}

哈希冲突的解决方式:Redis的哈希表使用开链法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用 这个单向链表连接起来,这就解决了键冲突的问题。

为了让哈希表的装载因子维持在一个合理的范围之内,需要对哈希表的大小进行扩展或者收缩,这叫做rehash。字典中总共有两个哈希表dictht结构体,ht[0]用来存储键值对,ht[1]用于rehash时暂存数据,平时它指向的哈希表为空,需要扩展或者收缩ht[0]的哈希表时才为它分配空间。

Set

set的实现一般使用intSet或者字典实现.注意set中的元素不可以重复.

intset

只有当数据全是整数值,而且数量少于512个时,才使用intset,intset是一个由整数组成的有序集合,可以进行二分查找。

字典

不满足intset使用条件的情况下都使用字典(拉链法),使用字典时把value设置为null。

(字典的方式其实很好理解,就像java中的set可以使用hashmap实现是一样的)

Zset

zset中的元素同样不可以重复,并且支持排序.底层中增加了一个权重参数score,可以是集合按照score进行排序(zset中的每个元素包含数据本身和一个对应的分数(score)

zset的数据本身不允许重复,但是score允许重复

zset底层实现原理:压缩表和跳表

  1. 数据少时,使用ziplist:ziplist占用连续内存,每项元素都是(数据+score)的方式连续存储,按照score从小到大排序。ziplist为了节省内存,每个元素占用的空间可以不同,对于大的数据(long long),就多用一些字节来存储,而对于小的数据(short),就少用一些字节来存储。因此查找的时候需要按顺序遍历。ziplist省内存但是查找效率低。(省内存这方面的理解,感觉就是不像跳表一样节点中存放的信息比较多)
  2. 数据多时使用跳表

关于跳表的结构:

跳表是基于一条有序单链表构造的,通过构建索引提高查找效率,空间换时间,查找方式是从最上面的链表层层往下查找,最后在最底层的链表找到对应的节点:

在这里插入图片描述

插入和删除的时间复杂度是 O(logn)

插入:逐层查找位置,然后插入到最底层链表。注意需要维护索引与原始链表的大小平衡,如果底层结点大量增多了,索引也相应增加,避免出现两个索引之间结点过多的情况,查找效率降低。同理,底层结点大量减少时,索引也相应减少。

删除:如果这个结点在索引中也有出现,那么除了要删除原始链表中的结点,还要删除索引中的这个结点。

16.redis中一千万个key,找到对应的一百个

17.程序计数器的大小

  • 又叫PC寄存器,(Program Counter Register),寄存器结构,PC需要能访问所有的指令,所以它的长度由内存指令存储器的地址位数决定

18.拆箱和装箱底层实现

自动装箱的底层原理:自动装箱实际上调用的是Integer中的静态方法valueOf(),将基本数据类型的int数值包装成了一个Integer对象

Integer integerVal = 0;//等效于Integer integerVal = Integer.valueOf(0);

自动拆箱的底层原理:自动拆箱的底层实际上调用的是Integer对象的intValue(),得到对象内的int变量的数值,然后给赋值给变量

int intVal = new Integer(0);//等效于int intVal = new Integer(0).intValue();

19.Spring初始化bean的方式

20.MySql主从复制原理

一:概念

复制是将主库的**DDL(数据库模式定义语言)DML(数据操纵语言)**操作通过二进制日志传递到复制服务器(从库)上,然后从库对这些日志重新执行(重做),从而使得主库和从库保持数据一致。

*MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点*。MySQL 默认采用****异步复制****方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表.

注意:由于 mysql 实现的异步复制,所以主库和从库数据之间存在一定的差异,在从库执行查询操作需要考虑这些数据的差异,一般只有更新不频繁和对实时性要求不高的数据可以通过从库查询,实行要求高的仍要从主库查询。

二:MySQL主从复制的主要用途

1 读写分离

在开发工作中,有时候会遇见某个sql 语句需要锁表,导致暂时不能使用读的服务,这样就会影响现有业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。

2 数据实时备份,当系统中某个节点发生故障时,可以方便的故障切换(主从切换)

提高数据安全-因为数据已复制到从服务器,从服务器可以终止复制进程,所以,可以在从服务器上备份而不破坏主服务器相应数据;

3 高可用(HA)
  • 因为数据库服务器中的数据都是相同的,当Master挂掉后,可以指定一台Slave充当Master继续保证服务的运行,因为数据是一致性的(如果当插入时Master就挂掉,可能不一致,因为同步也需要时间)当然这种配置不是简单的把一台Slave充当Master,毕竟还要考虑后续的Slave的数据同步到Master。
  • 在主服务器上执行写入和更新,在从服务器上向外提供读功能,达到读写分离的效果,也可以动态地调整从服务器的数量,从而调整整个数据库的性能。
  • 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。
4架构扩展

随着系统中业务访问量的增大,如果是单机部署数据库,就会导致I/O访问频率过高。有了主从复制,增加多个数据存储节点,将负载分布在多个从节点上,降低单机磁盘I/O访问的频率,提高单个机器的I/O性能。

三:MySQL主从形式

1 一主多从和一主一从

提高系统的读性能

img

一主一从和一主多从是最常见的主从架构,实施起来简单并且有效,不仅可以实现HA(高可用),而且还能读写分离,进而提升集群的并发能力。

2 多主一从(从5.7开始支持)

img

多主一从可以将多个mysql数据库备份到一台存储性能比较好的服务器上。

3双主复制

双主复制,也就是互做主从复制,每个maste(主)既是master,又是另外一台服务器的slave(从)。这样任何一方所做的变更,都会通过复制应用到另外一方的数据库中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B5SlVsZe-1651216324857)(C:\Users\hanxiao\AppData\Roaming\Typora\typora-user-images\image-20220425151606953.png)]

4 级联复制

img

级联复制模式下,部分slave的数据同步不连接主节点,而是连接从节点。因为如果主节点有太多的从节点,就会损耗一部分性能用于replication(复制),那么我们可以让3~5个从节点连接主节点,其它从节点作为二级或者三级与从节点连接,这样不仅可以缓解主节点的压力,并且对数据一致性没有负面影响。级联复制下从节点也要开启binary log(bin-log)功能

四:主从复制原理

MySQL主从复制涉及到三个线程,一个**(log dump thread)运行在主节点,其余两个(I/O thread,SQL thread)**运行在从节点,如下图所示

img

主节点 log dump 线程

当从节点连接主节点时,主节点会为其创建一个log dump 线程,用于发送和读取bin-log的内容。在读取bin-log中的操作时,log dump线程会对主节点上的bin-log加锁,当读取完成,在发送给从节点之前,锁会被释放。主节点会为自己的每一个从节点创建一个****log dump 线程

从节点 I/O线程

当从节点上执行start slave命令之后,从节点会创建一个I/O线程用来连接主节点,请求主库中更新的bin-log。I/O线程接收到主节点的blog dump进程发来的更新之后,保存在本地**relay-log(中继日志)**中。

从节点 SQL线程

SQL线程负责读取relay-log中的内容,解析成具体的操作并执行,最终保证主从数据的一致性。

对于每一个主从连接,都需要这三个进程来完成。当主节点有多个从节点时,主节点会为每一个当前连接的从节点建一个log dump进程,而每个从节点都有自己的I/O进程SQL进程

从节点用两个线程将从主库拉取更新和执行分成独立的任务,这样在执行同步数据任务的时候,不会降低读操作的性能。比如,如果从节点没有运行,此时I/O进程可以很快从主节点获取更新,尽管SQL进程还没有执行。如果在SQL进程执行之前从节点服务停止,至少I/O进程已经从主节点拉取到了最新的变更并且保存在本地relay日志中,当服务再次起来之后,就可以完成数据的同步。

要实施复制,首先必须打开Master端的binary log(bin-log)功能,否则无法实现

因为整个复制过程实际上就是Slave 从Master 端获取该日志然后再在自己身上完全顺序的执行日志中所记录的各种操作。如下图所示:

img

复制的基本过程
  1. 在从节点上执行sart slave命令开启主从复制开关,开始进行主从复制。从节点上的I/O 进程连接主节点,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;
  2. 主节点接收到来自从节点的I/O请求后,通过负责复制的I/O进程(log dump 线程)根据请求信息读取指定日志指定位置之后的日志信息,返回给从节点。返回信息中除了日志所包含的信息之外,还包括本次返回的信息的bin-log file 的以及bin-log position(bin-log中的下一个指定更新位置);
  3. 从节点的I/O进程接收到主节点发送过来的日志内容、日志文件及位置点后,将接收到的日志内容更新到本机的relay-log(中继日志)的文件(Mysql-relay-bin.xxx)的最末端,并将读取到的binary log(bin-log)文件名和位置保存到****master-info**** ****文件****中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log 的哪个位置开始往后的日志内容,请发给我”;
  4. Slave 的 SQL线程检测到relay-log 中新增加了内容后,会将relay-log的内容解析成在主节点上实际执行过SQL语句,然后在本数据库中按照解析出来的顺序执行,并在****relay-log.info****中记录当前应用中继日志的文件名和位置点。

五:主从复制的模式

MySQL 主从复制默认是异步的模式。MySQL增删改操作会全部记录binlog中,当slave节点连接master时,会主动从master处获取最新的bin-log文件。并把bin-log存储到本地的relay-log中,然后去执行relay-log的更新内容。

1异步模式(mysql async-mode)

异步模式如下图所示,这种模式下,主节点不会主动推送bin-log到从节点,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理,这样就会有一个问题,主节点如果崩溃掉了,此时主节点上已经提交的事务可能并没有传到从节点上,如果此时,强行将从提升为主,可能导致新主节点上的数据不完整。

img

2半同步模式(mysql semi-sync)

介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay-log中才返回成功信息给客户端(只能保证主库的bin-log至少传输到了一个从节点上,但并不能保证从节点将此事务执行更新到db中),否则需要等待直到超时时间然后切换成异步模式再提交。相对于异步复制,半同步复制提高了数据的安全性,一定程度的保证了数据能成功备份到从库,同时它也造成了一定程度的延迟,但是比全同步模式延迟要低,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。如下图所示:

img

半同步模式不是mysql内置的,从mysql 5.5开始集成,需要master 和slave 安装插件开启半同步模式。

3.全同步模式

指当主库执行完一个事务,然后所有的从库都复制了该事务并成功执行完才返回成功信息给客户端。因为需要等待所有从库执行完该事务才能返回成功信息,所以全同步复制的性能必然会收到严重的影响。

4.异步模式,全同步模式,半同步模式的对比图

img

5 GTID复制模式

在传统的复制里面,当发生故障,需要****主从切换*,需要找到bin-log和pos点(指从库更新到了主库bin-log的哪个位置,这个位置之前都已经更显完毕,这个位置之后未更新),然后将主节点指向新的主节点,相对来说比较麻烦,也容易出错。在MySQL 5.6里面,不用再找bin-log和pos点,我们只需要知道主节点的ip,端口,以及账号密码就行,因为复制是自动的,MySQL会通过内部机制*GTID****自动找点同步。

基于GTID的复制是MySQL 5.6后新增的复制方式.

*GTID (global transaction identifier)* 即全局事务ID, 保证了在每个在主库上提交的事务在集群中有一个唯一的ID.

5.1 GTID复制原理

在原来基于日志的复制中, 从库需要告知主库要从哪个偏移量进行增量同步, 如果指定错误会造成数据的遗漏, 从而造成数据的不一致.

而基于GTID的复制中, 从库会告知主库已经执行的事务的GTID的值, 然后主库会将所有未执行的事务的GTID的列表返回给从库. 并且可以保证同一个事务只在指定的从库执行一次.*通过全局的事务**ID**确定从库要执行的事务的方式代替了以前需要用**bin-log**和**pos**点确定从库要执行的事务的方式*

21.redis和Mysql数据不一致的问题(缓存和数据库一致性解决方案)

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

如来解决?这里给出两个解决方案,先易后难,结合业务和技术代价选择使用。

1.第一种方案:采用延时双删策略(数据库更新前删一次缓存,更新完之后再删一次缓存)

在写库前后都进行redis.del(key)删除缓存操作,并且设定合理的超时时间。

伪代码如下

public void write(String key,Object data){
    redis.delKey(key);
    db.updateData(data);
    Thread.sleep(500);
    redis.delKey(key);
} 

2.具体的步骤就是:

1)先删除缓存

2)再写数据库

3)休眠500毫秒

4)再次删除缓存

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

3.设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

4.该方案的弊端

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

2.第二种方案:异步更新缓存(基于订阅binlog的同步机制)

1.技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL:增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

2.Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

22.jdk7和jdk8的区别

23.rabbitMq会出问题吗

24.代码解析

public static void main(String[] args) {
        List<Integer> nums=new ArrayList<>();//debug发现nums={ArrayList@484}
        add(nums,1);
        for (Integer num : nums) {//这里的nums={ArrayList@484}
            System.out.println(num);//所以最终啥也打不出来
        }
    }

    public static void add(List<Integer> nums,int i){
        nums=new ArrayList<>();debug发现nums={ArrayList@485}
        nums.add(i);//这个i会添加到nums={ArrayList@485}中
    }
}

//上述代码主要考察的是jvm内存模型中栈和堆的相关问题,经过debug之后会发现nums虽然传入的是nums={ArrayList@484},但是在add方法中引入了新的nums={ArrayList@485}并且是一个局部变量,所以执行回到主函数之后并没有在nums={ArrayList@484}中加入任何的数据.
private static class User{
        public String name;

        @Override
        public boolean equals(Object o) {
            return this.name.equals(o);
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }
    }


    public static void main(String[] args) {
        HashMap<User,Integer> map=new HashMap<>();
        User zhangsan=new User();
        zhangsan.name="zhangsan";
        map.put(zhangsan,1);//这里使用的是"zhangsan"这个名字的hashCode进行的插入
        zhangsan.name="lisi";
        Integer value = map.get(zhangsan);//这里查找的时候使用的是"lisi"这个名字的hashCode进行查找
        System.out.println(value);

    }
}
//同样上述代码会返回null.主要考察的是hashMap中的hash值查找.在插入的时候使用的是"zhangsan"这个名字的hashCode进行的插入,之后将名字设为了"lisi"并用"lisi"这个名字的hashCode进行查找,从User类中可以看到重写的hashCode方法中两者的hashCode是不同的.因此从hashCode的角度来看,插入的时候使用的是hashCode1而查找的时候使用的hashCode2进行查找,因此肯定找不到值,所以结果对应为null.

25.setNX锁的是key还是value

锁的是key,只有key不存在的时候才可以上锁.当key存在则代表锁存在,就无法上锁.

26.零拷贝

27.redis限流

①基于Redis的setnx的操作(相当于计数器的方式)

在限流的主要目的就是为了在单位时间内,有且仅有N数量的请求能够访问我的代码程序。依靠setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期时间(expire).比如需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,然后查询有几个setnx即可,当请求的setnx数量达到20时候即达到了限流效果.

当然这种做法的弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题

②基于Redis的数据结构zset(相当于滑动窗口的方式)

滑动窗口就是记录一个滑动的时间窗口内的操作次数,操作次数超过阈值则进行限流。

使用zset数据结构,key是一个唯一ID,然后score和value记录的都是放入时的时间戳,当进行判断的时候可以将不在滑动窗口内的时间即(当前时间-过期时间)的socre全部删除,然后统计在这个时间窗口内的所有值,来判断操作数.

这种方案有一定的缺点,因为它要记录时间窗口内所有的行为记录,如果这个量很大,比如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流的,因为会消耗大量的存储空间,因此我们可以使用漏斗限流

③基于Redis的令牌桶算法

令牌算法是以固定速度往一个桶内增加令牌,当桶内令牌满了后,就停止增加令牌。上游请求时,先从桶里拿一个令牌,后端只服务有令牌的请求,所以后端处理速度不一定是匀速的。当有突发请求过来时,如果令牌桶是满的,则会瞬间消耗桶中存量的令牌。如果令牌还不够,那么再等待发放令牌(固定速度),这样就导致处理请求的速度超过发放令牌的速度。

也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。

依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码

依靠List的leftPop来获取令牌

// 输出令牌
public Response limitFlow2(Long id){
     Object result = redisTemplate.opsForList().leftPop("limit_list");
     if(result ==null){
	 return Response.ok("当前令牌桶中无令牌");
 }
 return Response.ok(articleDescription2);
 }

再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成

// 10S的速率往令牌桶中添加UUID,只为保证唯一性
 @Scheduled(fixedDelay = 10_000,initialDelay = 0)
 public void setIntervalTimeTask(){
 redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
 }

综上,代码实现起始都不是很难,针对这些限流方式我们可以在AOP或者filter中加入以上代码,用来做到接口的限流,最终保护你的网站。

28.慢查询

超过 long_query_time 参数设定的时间阈值(默认10s),就被认为是慢的,是需要优化的。慢查询被记录在慢查询日志里。慢查询日志默认是不开启的,通过查看slow_query_log 是否开启慢查询日志.

慢查询解读

在这里插入图片描述
第一行:记录时间
第二行:用户名 、用户的IP信息、线程ID号
第三行:执行花费的时间【单位:毫秒】、执行获得锁的时间、获得的结果行数、扫描的数据行数
第四行:这SQL执行的时间戳
第五行:具体的SQL语句

慢查询分析工具

慢查询的日志记录非常多,要从里面找寻一条查询慢的日志并不是很容易的事情,一般来说都需要一些工具辅助才能快速定位到需要优化的SQL语句,下面介绍两个慢查询辅助工具mysqldumpslow和pt_query_digest.

除此之外还可以使用Explain

Explain分析慢查询SQL

分析mysql慢查询日志 ,利用explain关键字可以模拟优化器执行SQL查询语句,来分析sql慢查询语句,下面我们的测试表是一张137w数据的app信息表,我们来举例分析一下;

SQL示例如下:

-- 1.185s
SELECT * from vio_basic_domain_info where app_name like '%陈哈哈%' ;

这是一条普通的模糊查询语句,查询耗时:1.185s,查到了148条数据;
  我们用Explain分析结果如下表,根据表信息可知:该SQL没有用到字段app_name上的索引,查询类型是全表扫描,扫描行数137w。

mysql> EXPLAIN SELECT * from vio_basic_domain_info where app_name like '%陈哈哈%' ;
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table                 | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | vio_basic_domain_info | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1377809 |    11.11 | Using where |
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

各列属性的简介:

  • id:SELECT的查询序列号,体现执行优先级,如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
  • select_type:表示查询的类型。
  • table:输出结果集的表,如设置了别名,也会显示
  • partitions:匹配的分区
  • type:对表的访问方式
  • possible_keys:表示查询时,可能使用的索引
  • key:表示实际使用的索引
  • key_len:索引字段的长度
  • ref:列与索引的比较
  • rows:扫描出的行数(估算的行数)
  • filtered:按表条件过滤的行百分比

优化LIMIT分页的一些方法

在系统中需要分页的操作通常会使用limit加上偏移量的方法实现,同时加上合适的order by 子句。如果有对应的索引,通常效率会不错,否则MySQL需要做大量的文件排序操作。

一个非常令人头疼问题就是当偏移量非常大的时候,例如可能是limit 1000000,10这样的查询,这是mysql需要查询1000000条然后只返回最后10条,前面的1000000条记录都将被舍弃,这样的代价很高,会造成慢查询。

优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。

对于下面的查询:

-- 执行耗时:1.379s
SELECT * from vio_basic_domain_info LIMIT 1000000,10;

Explain分析结果:

mysql> EXPLAIN SELECT * from vio_basic_domain_info LIMIT 1000000,10;
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------+
| id | select_type | table                 | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra |
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------+
|  1 | SIMPLE      | vio_basic_domain_info | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1377809 |   100.00 | NULL  |
+----+-------------+-----------------------+------------+------+---------------+------+---------+------+---------+----------+-------+
1 row in set, 1 warning (0.00 sec)

该语句存在的最大问题在于limit M,N中偏移量M太大,导致每次查询都要先从整个表中找到满足条件 的前M条记录,之后舍弃这M条记录并从第M+1条记录开始再依次找到N条满足条件的记录。如果表非常大,且筛选字段没有合适的索引,且M特别大那么这样的代价是非常高的。

那么如果我们下一次的查询能从前一次查询结束后标记的位置开始查找,找到满足条件的10条记录,并记下下一次查询应该开始的位置,以便于下一次查询能直接从该位置 开始,这样就不必每次查询都先从整个表中先找到满足条件的前M条记录,舍弃掉,再从M+1开始再找到10条满足条件的记录了。

处理分页慢查询的方式一般有以下几种

思路一:构造覆盖索引

**通过修改SQL,使用覆盖索引,比如我需要只查询表中的app_name、createTime等少量字段,那么我秩序在app_name、createTime字段设置联合索引,即可实现覆盖索引,无需全表扫描。(尽量不要使用select*这样的语句,全表扫描很垃圾)**适用于查询列较少的场景,查询列数过多的不推荐。
耗时:0.390s

mysql> EXPLAIN SELECT app_name,createTime from vio_basic_domain_info LIMIT 1000000,10;
+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+---------+----------+-------------+
| id | select_type | table                 | partitions | type  | possible_keys | key          | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | vio_basic_domain_info | NULL       | index | NULL          | idx_app_name | 515     | NULL | 1377809 |   100.00 | Using index |
+----+-------------+-----------------------+------------+-------+---------------+--------------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
思路二:优化offset

无法用上覆盖索引,那么重点是想办法快速过滤掉前100w条数据。我们可以利用自增主键有序的条件,先查询出第1000001条数据的id值,再往后查10行;适用于主键id自增的场景。
耗时:0.471s

SELECT * from vio_basic_domain_info where 
  id >=(SELECT id from vio_basic_domain_info ORDER BY id limit 1000000,1) limit 10;
12

原理:先基于索引查询出第1000001条数据对应的主键id的值,然后直接通过该id的值直接查询该id后面的10条数据。下方EXPLAIN 分析结果中大家可以看到这条SQL的两步执行流程。

mysql> EXPLAIN SELECT * from vio_basic_domain_info where id >=(SELECT id from vio_basic_domain_info ORDER BY id limit 1000000,1) limit 10;
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table                 | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | PRIMARY     | vio_basic_domain_info | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |      10 |   100.00 | Using where |
|  2 | SUBQUERY    | vio_basic_domain_info | NULL       | index | NULL          | PRIMARY | 8       | NULL | 1000001 |   100.00 | Using index |
+----+-------------+-----------------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
2 rows in set, 1 warning (0.40 sec)

29.MySQL间隙锁、Next-Key Lock

间隙锁+Next-Key Lock可以完美的解决innodb在RR情况下的幻读.

总体来说,就是MySQL innoDB引擎要在RR隔离级别之下解决幻读的问题,所以引入了间隙锁。

在进行当前读的情况下,对读出的数据的附近的一整个范围(“间隙”)进行加锁,保证满足查询条件的记录不能被插入。

在快照读(snapshot read)的情况下,MySQL通过MVCC(多版本并发控制)来避免幻读。

快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。主要应用于无需加锁的普通查询(select)操作。

在当前读(current read)的情况下,MySQL通过next-key lock来避免幻读。

当前读,读取的是记录的最新版本,并且会对当前记录加锁,防止其他事务发修改这条记录。加行共享锁(SELECT … LOCK IN SHARE MODE )、加行排他锁(SELECT … FOR UPDATE / INSERT / UPDATE / DELETE)的操作都会用到当前读

innoDB的间隙锁只存在于 RR 隔离级别

间隙锁在innoDB中的唯一作用就是在一定的“间隙”内防止其他事务的插入操作,以此防止幻读的发生:

  • 防止间隙内有新数据被插入。
  • 防止已存在的数据,更新成间隙内的数据。

innoDB支持三种行锁定方式:

  • 行锁(Record Lock):锁直接加在索引记录上面(无索引项时演变成表锁)(单个行记录上的锁)
  • 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别的。(锁定一个范围,但不包括记录本身)
  • Next-Key Lock :行锁和间隙锁组合起来就是 Next-Key Lock。(锁定一个范围,并且锁定记录本身)是一个前开后闭的区间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cgLik9KW-1651216324858)(https://ask.qcloudimg.com/http-save/yehe-3487219/7d140adbc44a8c2f0fcf4a30560a8a57.png?imageView2/2/w/1620)]

从上面这个主键索引来看,行锁就是对id=10这一行数据进行加锁,这就是行锁.而间隙锁就是对(5,10)这个范围进行加锁.而next-key lock就是对(5,10]这个范围进行上锁.


innoDB默认的隔离级别是可重复读(Repeatable Read),并且会以Next-Key Lock的方式对数据行进行加锁。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

当查询的索引含有唯一属性(唯一索引,主键索引)时,Innodb存储引擎会对next-key lock进行优化,将其降为record lock,即仅锁住索引本身,而不是范围。

何时使用行锁,何时产生间隙锁

  1. 只使用唯一索引查询,并且只锁定一条记录时,innoDB会使用行锁。
  2. 只使用唯一索引查询,但是检索条件是范围检索,或者是唯一检索然而检索结果不存在(试图锁住不存在的数据)时,会产生 Next-Key Lock。
  3. 使用普通索引检索时,不管是何种查询,只要加锁,都会产生间隙锁。
  4. 同时使用唯一索引和普通索引时,由于数据行是优先根据普通索引排序,再根据唯一索引排序,所以也会产生间隙锁。

(有可能是历史版本),不用加锁。主要应用于无需加锁的普通查询(select)操作。

在当前读(current read)的情况下,MySQL通过next-key lock来避免幻读。

当前读,读取的是记录的最新版本,并且会对当前记录加锁,防止其他事务发修改这条记录。加行共享锁(SELECT … LOCK IN SHARE MODE )、加行排他锁(SELECT … FOR UPDATE / INSERT / UPDATE / DELETE)的操作都会用到当前读

innoDB的间隙锁只存在于 RR 隔离级别

间隙锁在innoDB中的唯一作用就是在一定的“间隙”内防止其他事务的插入操作,以此防止幻读的发生:

  • 防止间隙内有新数据被插入。
  • 防止已存在的数据,更新成间隙内的数据。

innoDB支持三种行锁定方式:

  • 行锁(Record Lock):锁直接加在索引记录上面(无索引项时演变成表锁)(单个行记录上的锁)
  • 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读或以上级别的。(锁定一个范围,但不包括记录本身)
  • Next-Key Lock :行锁和间隙锁组合起来就是 Next-Key Lock。(锁定一个范围,并且锁定记录本身)是一个前开后闭的区间

[外链图片转存中…(img-cgLik9KW-1651216324858)]

从上面这个主键索引来看,行锁就是对id=10这一行数据进行加锁,这就是行锁.而间隙锁就是对(5,10)这个范围进行加锁.而next-key lock就是对(5,10]这个范围进行上锁.


innoDB默认的隔离级别是可重复读(Repeatable Read),并且会以Next-Key Lock的方式对数据行进行加锁。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

当查询的索引含有唯一属性(唯一索引,主键索引)时,Innodb存储引擎会对next-key lock进行优化,将其降为record lock,即仅锁住索引本身,而不是范围。

何时使用行锁,何时产生间隙锁

  1. 只使用唯一索引查询,并且只锁定一条记录时,innoDB会使用行锁。
  2. 只使用唯一索引查询,但是检索条件是范围检索,或者是唯一检索然而检索结果不存在(试图锁住不存在的数据)时,会产生 Next-Key Lock。
  3. 使用普通索引检索时,不管是何种查询,只要加锁,都会产生间隙锁。
  4. 同时使用唯一索引和普通索引时,由于数据行是优先根据普通索引排序,再根据唯一索引排序,所以也会产生间隙锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值