java面试大全(7w字,更新中)

1.杂题

1.maven依赖冲突的解决方法

依赖冲突的原因

举个例子,A依赖于B与C,B依赖于D的1.0版本,C依赖于D的2.0版本,这就导致加载的时候到底会引入那个版本的jar包的问题

解决方法:

1.用idea提供的MavenHelper查看是哪个依赖冲突

2.找到pom文件下的Dependency Analyzer

3.然后点击exclude排除依赖冲突

 或者直接excludsion排除掉

 2.Idea无法下载springboot源码

1.cmd 进入项目的根目录

2.2、执行命令 mvn dependency:resolve -Dclassifier=sources

3.Int表示的最大数

2^31-1

int表示是4字节,32位,第一位代表正负号,所以是2^31-1

4.重写重载区别

重载:同一个类,方法名相同,参数列表不同

重写:子类父类,参数列表相同,返回值类型相同,访问权限要子类大于父类

5.I/O多路复用

一个或者多个线程来处理多个socket的技术,无需创建更多的线程,减小系统开销。

实现io多路复用的模型总共有三种,分别是select,poll,epoll

select:轮询遍历,轮询fd数组,随fd数量增多,性能会下降,On

poll:和select一样,但使用链表存fd,On

epoll:使用b+树存储fd,基于回调去获取连接,O1。缺点:只有linux系统支持

而我们Nginx,Redis的轮询也会优先采取epoll模型

6.new Integer(100)和Integer.valueOf(100)区别

一个是new对象,一个是从缓存中拿

-128~127

7.CPU彪增如何解决

先top命令 找到占用率最高的进程

ps -mp 2308 -o Thread,tid,time 找到占用率最高的线程 发现是2320 线程,再转换为10进制

jstack 2308 | grep 910

分析


 8.MVC各层

MVC是一种常用的软件架构模式,它将应用程序分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。各层技术如下:

1. 模型层(Model):用于管理应用程序中的数据和相关业务逻辑,通常使用数据库和数据访问技术实现。常用的技术包括JPA、Hibernate、MyBatis等。

2. 视图层(View):用于呈现应用程序的用户界面,通常使用HTML、CSS、JavaScript等技术实现。在Web应用中,常用的技术包括JSP、Thymeleaf、Freemarker等。

3. 控制器层(Controller):用于协调模型层和视图层之间的交互,处理用户的请求并相应地调用模型层的方法。常用的技术包括SpringMVC、Struts、Servlet等。

好处

  1. 易于维护和扩展:MVC 架构将应用程序按照职责分为三个部分,每个部分之间的耦合性较低,因此更加容易维护和扩展。比如,修改模型不会影响视图和控制器,修改控制器也不会影响模型和视图。

  2. 模块化设计:MVC 架构将应用程序按照职责分为三个部分,每个部分之间的耦合性较低,可以独立地开发和测试每个部分,从而实现模块化的设计。

  3. 视图和模型分离:MVC 架构将视图和模型分离,视图只负责显示数据,模型只负责处理数据。这种分离使得可以方便地修改视图或模型,而不会影响到另一个部分。

  4. 多人协作:使用MVC架构可以实现模块化的设计,每个人负责一个模块,这样不同的人负责不同的部分,在执行任务时可以提高效率,提高项目的开发效率。

9.JWT实现登录功能

分为社交登录和账号密码登录

用户输入账号密码后,后端进行验证,查询数据库进行匹配

有签名,防止参数被篡改

 如何续命

双token方案

登录成功以后,后端返回 access_token 和 refresh_token,客户端缓存此两种token;

使用 access_token 请求接口资源,成功则调用成功;如果token超时,客户端携带 refresh_token 调用token刷新接口获取新的 access_token;

后端接受刷新token的请求后,检查 refresh_token 是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的 access_token 返回给客户端。

客户端携带新的 access_token 重新调用上面的资源接口。

客户端退出登录或修改密码后,注销旧的token,使 access_token 和 refresh_token 失效,同时清空客户端的 access_token 和 refresh_toke。

10.cookie,session,token区别

他们主要的作用就是登录验证

首先咱们http是无状态的,什么是无状态,就是服务器记不住东西,如果想让服务器有记忆,那么cookie就出现了


1. Cookie

浏览器发给服务器请求,比如输入账号密码,服务器会set-cookie后返回给客户端,可以设置过期时间,但cookie(账号密码)保存在客户端不安全。

2. Session
session是存储在服务端的,是基于cookie的,浏览器第一次发请求时,比如输入账号密码,在服务端会创建一个jsessionid

{
    jsessionId:63sa754d4f//存在服务器的内存里
    username:123
    password:456
}

和域名绑定,只是针对某个域名,发请求才携带cookie,分布式项目需要解决子域的session共享问题, springsession解决

然后返回给客户端一个jsessionid,之后每次请求都会携带jsessionid,服务端就去做验证

3. Token

通常存储在客户端,可以是浏览器的本地存储,例如LocalStorage或SessionStorage,

Token通常存储在客户端,可以是浏览器的本地存储,例如LocalStorage或SessionStorage,也可以是在客户端内存中,每次请求都会携带token,服务端会根据相应的秘钥对其进行验证

11.登录如何做

使用jwt做

首先,前端把敏感信息通过公钥加密,然后再传到后端。后端返回一个jwt,留作下次前端访问时携带。这时会有一个问题,如果此时jwt被黑客截取,那么黑客不就可以永远登录了吗?

如果黑客成功截取了第一次从后端传递给前端的 JWT,那么他可以使用该 JWT 进行登录。这是因为 JWT 的核心就是利用签名机制来防止篡改和伪造,而在第一次生成 JWT 并传递给前端的过程中,JWT 的签名是根据服务器端的密钥生成的,这个密钥是黑客不知道的。但是,一旦黑客获取了这个 JWT,他就可以使用该 JWT 进行登录,因为 JWT 中包含了用户的身份信息以及有效期等信息,后端会根据这些信息来判断用户是否有访问权限。

为了避免黑客利用被截取的 JWT 进行登录,我们可以采取以下措施:

1.使用https做为网络协议进行传输

2.定期更换秘钥,如果秘钥变了,黑客再登录时,后端验签肯定不行

3.缩短jwt的时间

黑客的问题解决了,那么jwt续命的问题还没解决,可以使用双token方案

最后还有一个问题,总是被ddos攻击怎么办?

可以增加服务器带宽,也可以验证你是不是人,也可以弄一个黑白名单封ip。但没啥用,直接接入高防服务器,让他们去解决。

12.单点登录

单点登录就是,登录一位置的系统,其余多系统同时登陆的技术

单点登录(Single Sign-On,SSO)是一种身份认证和授权机制,允许用户使用一组凭证(如用户名和密码)登录到多个应用程序中。

  1. 使用cookie来做

  2. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数;

  3. sso认证中心发现用户未登录,将用户引导至登录页面;

  4. 用户输入用户名密码提交登录申请;sso认证中心校验用户信息,如果成功,就创建授权令牌,之后保存一个cookie, 留下一个登录痕迹;

  5. sso认证中心带着令牌跳转会最初的请求地址(系统1);

  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效;

  7. sso认证中心校验令牌,返回有效,注册系统1;

  8. 系统1使用该令牌创建与用户的会话,并把令牌保存在会话里,返回受保护资源;

  9. 用户访问系统2的受保护资源;

  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数;

  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌;

  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效;

  13. sso认证中心校验令牌,返回有效,注册系统2;系统2使用该令牌创建与用户的局部会话,返回受保护资源。

比较麻烦,不安全,信息存在用户端

2.OAuth2.0单点登录

成本比较高,信息存在服务器端,比较安全

14.如何对接短信接口

1.引依赖

2.复制一手工具类

3.把秘钥放入请求头中

4.把电话和短信内容和密码放入map中

5.调用工具类的方法给第三方平台发请求,

13.社交登录

可以做微博,qq等第三方登录,扫码或者输入账号密码

 

14.调优经验

1.查询调优

少用连表查询,多用vo封装。因为连表查询是非常慢的,效率非常低。

在特别耗时等功能,比如发短信,发邮件。使用异步的方式

2.jvm调优

大部分jvm调优就是减少fullgc的次数,因为fullgc代表着整个堆的垃圾回收,而垃圾回收会伴随着stw,用户体验感不好。

如何降低fullgc,根据具体问题具体分析,可以设置年轻代和老年代比例,也可以设置伊甸园区和from to区的比例,也可以更换垃圾回收器。这些都是具体问题具体分析。

3.数据库调优

总共分7步

1.sql优化

减少对数据库的访问次数,小表驱动大表,行裁剪,列裁剪,truncate(截断)代替delete,unionall(组合两张字段相同的表,不去重)代替union,exists代替distinct

2.索引优化

3.表结构优化

4.数据量实在太大就算走索引也很慢,就需要分库分表

分库(垂直拆分):一般不会这么做,会使事务问题变复杂

分表(水平拆分):按条件,时间,地区等分成多个表,但中间会加一个代理层mycat,shardingsphere

5.基本设置优化

连接数设置,慢查询设置

6.硬件优化

包括服务器硬件和网络带宽,这一部分是运维来做,我就不细说了

4.架构的优化

比如把常用的数据放在缓存中,比如使用负载均衡和水平扩展可以高效的处理请求,用nginx做动静分离,把静态页面都放在nginx,使用nginx来分担服务器压力,使用keepalived做故障转移

15.接口幂等性问题

接口幂等性问题是指在一次或多次请求后,返回结果不变的问题。常见的情况包括网络超时、客户端重试、服务端异常等原因导致同一个请求被多次执行。如何处理接口幂等性问题,以下是一些建议:

1.前端的按钮只能点击一次

2.增加一个状态字段,比如正在支付的订单状态为进行中,如果用户重复支付,因为订单id相同,查数据库如果订单正在支付就返回订单正在支付中.

3.加唯一索引,在重复添加数据时会出现异常

4.token机制,后端生成一个全局唯一token,加入redis,每次调用接口就带着这个token,之后从redis中拿出令牌对比,两个令牌相同就删除token.但需要保证这三个操作的原子性问题如果服务端由于网络延迟导致接口重复调用,因为令牌每次都是一个,所以也不会有幂等性问题,比如说生成订单的时候就同时生成一个防重令牌token,放入redis中,提交订单就携带这个token,再去redis查,最后再对比.可以解决订单重复提交的问题

5.乐观锁,加一个版本号字段,update的时候去判断版本号

16.流的分类

17.String为什么是不可变类

这是因为String类具有不可变性,即一旦创建了一个String对象,它的值就不能被修改。如果String类可以被继承,那么子类可能会修改父类中的字符串值,从而破坏了String类的不可变性。

String类的不可变性有以下几个好处:线程安全:由于String对象的值不可变,因此多个线程可以同时访问同一个String对象,而不需要担心线程安全问题。

18.反射的原理

jvm通过类加载器把类加载到内存中,一旦加载进来就会产生一个Class对象,我们就可以通过newInstance的方法来动态的创建对象

19.动态代理的原理

动态代理的原理是通过反射机制,在运行时动态地生成代理类。代理类实现了与被代理对象相同的接口,并在代理类中维护了一个被代理对象的引用。当代理类的方法被调用时,它会将方法调用转发给被代理对象,并在调用前后执行一些额外的逻辑,如记录日志、统计执行时间等。

Java中提供了两种动态代理方式:基于接口的动态代理和基于类的动态代理。基于接口的动态代理是通过Java的反射机制在运行时创建一个实现了指定接口的代理类,而基于类的动态代理是通过继承一个指定类来创建代理类。

动态代理的优点在于它能够在运行时动态地为对象添加额外的功能,而不需要修改原始类的代码。这种方式可以提高代码的复用性和灵活性,同时也可以减少代码的重复。

20.Arrays.sort的原理

一种混合排序算法,是多个排序算法合一起的混合排序。比如在某某区间到某某区间用什么排序,在某某区间到某某区间又用什么排序。这个排序算法不仅仅是java常用的算法,别的语言也用这个算法,说明这个算法的强大。

21.面向对象和面向过程的区别

面向对象(Object-oriented programming,OOP)和面向过程(Procedure-oriented programming,POP)是两种不同的编程范式。

面向过程是一种以过程为中心的编程方式,以实现为核心,将系统分解成若干个可单独独立实现的过程(函数或方法)。程序的执行流程是从一个过程转移到另一个过程,同时一些数据通过参数进行传递。对于复杂的问题,面向过程可能会导致代码繁琐和难以扩展。

面向对象则是一种以对象为中心的编程方式,将系统看作一个由对象组成的集合,对象之间通过消息传递来协同工作。每个对象都有自己的状态和行为,状态是对象的属性,行为则是对象的方法。对象可以根据需要随时创建、销毁和发送消息。面向对象的思想更具有灵活性和维护性。

面向对象的优势主要体现在下面几个方面:

  1. 可维护性:OOP 使得代码编写更加清晰、简洁,便于维护和扩展。

  2. 可重用性:OOP 支持代码的复用,使得开发人员可以编写和测试一些通用的、可重用的类,从而提高开发效率。

  3. 可扩展性:OOP 具有高度的灵活性和扩展性,对象之间的关系虽然复杂,但是可以在不影响其他部分的情况下添加新功能、新对象和新类。

  4. 松耦合:OOP 可以将封装性和继承带来的低耦合性高度融合,对象只与其它必需通信的对象产生联系,系统更加灵活。

  5. 代码的真实性:面向对象的语言中,对象模型往往是很贴切于问题本身的,因此编写出来的代码更有代表性,更能反映出问题的真正面貌。

面向对象具有很多优点,尤其适用于需要复杂数据类型或涉及许多对象交互的程序设计。同时,它也并不意味着就是万能的,不同的需求和场景可能需要不同的编程范式。

22.面向对象的三大特征

  • 封装:使数据更加安全。可以使用private,public等关键字控制访问权限

  • 继承:继承是让一个类具有父类的功能的一种机制。减少代码的重复,使代码更加简洁,提高代码的重用性和可维护性。

  • 多态:是同一方法名可以作用在不同的对象上,产生了不同的结果。它分为编译时多态(重载)和运行时多态(重写)。

23.跨域

跨域产生必须要同时满足两种情况,必须得是ajax请求,请求的域不同(协议,ip,端口)

在服务端设置响应头,来允许跨域, 

做可以通过nginx进行代理,让前后端的域相同

24.接口防刷怎么做

1.限制访问频率:通过生成一个带有过期时间的 token,限制客户端请求接口的频率。客户端在每次访问接口的时候,需要携带上该 token,后端就可以根据 token 来验证请求是否合法。如果 token 过期或者不存在,就拒绝该次请求。

2.前端滑动验证码

3.黑名单:如果总是这个ip违反规则那么就将这个ip拉入黑名单

25.浮点数为什么丢失精度

在计算机内存中,数字都是以二进制形式存储的,而浮点数使使用科学计数法来表示一个数值,浮点数在计算机内部分三部分。正负号,阶码(指数),尾数

BigDecimal对象包含两部分:整数部分和小数部分。其中整数部分存储为一个BigInteger对象,也就是了一种类似数组的数据结构来存储整数部分,小数部分存储为一个int类型的数字,就是将小数扩大N倍,转成整数后再进行计算,同时结合指数,得出没有精度损失的结果。

26.分布式项目如何生成全局唯一id

SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高

  1. 初始化需要设置数据中心 ID 和机器 ID;
  2. 获取当前时间戳(毫秒级);
  3. 如果时间戳和上次相同,表示在同一毫秒内,此时自增序列号,但需要先和 "4095" 进行与运算,得到序列号的后 12 位,如果为 0 则等待下一毫秒再生产 ID;
  4. 如果时间戳不同,则序列号归零,等待在新的时间戳内生成 ID;
  5. 返回生成的全局唯一 ID。

27.向浏览器发送一个请求的具体过程

1:DNS解析,得到IP地址

浏览器得到是一个域名,需要把这个域名转化成IP地址才能找到服务器。DNS解析就是将域名转化为IP地址的过程。
解析的过程总的来说,是先在本地缓存里寻找域名对应的IP地址,没有找到就去域服务器递归寻找,找到后返回给浏览器并存到本地缓存。
2:浏览器根据IP地址,访问服务器,建立TCP连接
3:建立完TCP连接后,浏览器向服务器发送http请求
4:服务器返回http响应给浏览器
5:浏览器根据响应渲染页面呈现给用户
6:浏览器关闭TCP连接

28.Java序列化原理

Java 序列化机制是将一个 Java 对象转化为字节序列,便于在网络上传输或者保存到文件中。Java 序列化的原理大致分为以下几个步骤:

  1. 对象输出流(ObjectOutputStream)会将对象转化为字节序列。在将对象序列化为字节序列之前,需要先校验对象类是否可序列化。如果对象实现了 Serializable 接口,就可以被序列化。如果没有实现 Serializable 接口,就会抛出 NotSerializableException 异常。

  2. 对象输出流会遍历对象的属性和方法,将它们的名称和值序列化到字节数组中。

  3. 对象输出流采用的是流式写入,将序列化后的字节序列写入到文件或者网络中。可以使用文件输出流或者网络输出流实现这个操作。

  4. 反序列化时,需要使用对象输入流(ObjectInputStream)将字节序列反序列化为对象。对象输入流会读取字节序列,并根据字节序列重建对象。如果字节序列不符合对象的属性和方法结构,就会抛出 InvalidClassException 异常。

  5. 在反序列化对象时,需要注意序列号 ID, 通过 serialVersionUID 来校验当前对象的版本是否一致。如果版本不一致,可以通过自定义的 writeObject 和 readObject 方法进行序列化版本的控制,从而保证反序列化的对象与序列化时的对象是一致的。

总的来说,Java 序列化机制就是通过将 Java 对象的属性和方法转化为字节序列,再将字节序列写入文件或者网络中,从而实现数据的持久化和传输。

serialVersionUID的作用

Java 序列化就是将 Java 对象转化为可存储或传输的格式,这个格式可以是二进制流、XML、JSON 等。Java 序列化中的序列号 ID(SerialVersionUID)是一个 64 位的 hash 值,用于在对象序列化和反序列化时判断对象的版本一致性。

当我们序列化一个对象时,Java 会自动为这个对象分配一个序列号 ID。然后在将对象序列化的时候,将这个对象的类型和序列号 ID 一并写入序列化的文件中。这样在反序列化对象时,Java 就可以根据文件中的序列号 ID 获取这个对象的类型,然后校验当前对象的序列号 ID 是否与文件中的序列号 ID 一致。

如果当前对象的序列号 ID 与文件中的序列号 ID 不一致,Java 将抛出 InvalidClassException 异常。这是因为在反序列化时,Java 无法匹配到正确的类定义,导致反序列化失败。

因此,序列号 ID 主要用于保证序列化和反序列化时对象的版本一致性,防止出现对象类的不兼容问题。

如何将GBK编码转成UTF8

  1. 将 GBK 编码的字符串转换成字节数组
byte[] gbkBytes = gbkStr.getBytes("GBK");
  1. 将字节数组使用 UTF-8 编码转换成字符串。
String utf8Str = new String(gbkBytes, "UTF-8");

29.包装类的好处

  1. 可以在集合中存储基本类型的值。集合类只能存储对象,不能存储基本数据类型。将基本数据类型转换成对应的包装类型后,可以在集合类中存储基本类型的值。

  2. 提供了一些额外的方法。包装类可以调用一些基本类型所没有的方法。例如,Integer类提供的toHexString()方法可以将整数转换成16进制字符串。面向对象的理念中,应该是某些操作本该由区别明确的类负责,故应该为基本类型补充相关的方法。

  3. 允许 null 值。基本类型不能为null值,而包装类允许为null值。当需要使用到null值时,使用包装类更加方便。

  4. 使代码更具可读性和可维护性。使用包装类可以使代码更具可读性和可维护性。同时,也可以使代码更具可重用性,使得开发工作更加简单和高效。

综上所述,使用包装类相比于基本数据类型,具有更多的灵活性和可扩展性,同时也使得代码更加易读、易维护。然而,在计算或比较时,使用基本数据类型通常会更加高效。

2.设计模式

1.设计模式的7大原则

  • 单一职责原则:一个类只有一个职责
  • 接口隔离:一个接口只有一类职责
  • 依赖倒转:高层模块不应该依赖低级模块,应该依赖他们的抽象或者实现(接口定义规范把细节留给他们的实现类)

        代码案例:

public class DependencyInversion {
    public static void main(String[] args) {
        //客户端无需改变
        Person person = new Person();
        person.receive(new Email());
        person.receive(new WeiXin());
    }
}
//定义接口
interface IReceiver {
    String getInfo();
}
class Email implements IReceiver {
    public String getInfo() {
        return "电子邮件信息: hello,world";
    }
}
//增加微信
class WeiXin implements IReceiver {
    public String getInfo() {
        return "微信信息: hello,ok";
    }
}
//方式 2
class Person {
    //这里我们是对接口的依赖,而不是直接依赖实现类。
    public void receive(IReceiver receiver) {
        System.out.println(receiver.getInfo());
    }
}
  • 里氏替换:子类可以继承父类的扩展,但不要改变父类原有功能
  • 开闭原则:对扩展开放,对修改关闭
  • 迪米特法则:一个类对自已依赖的类知道越少越好
  • 合成复用原则:多用合成和聚合方式,少用继承

扩展:类与类之间的关系

依赖(一个类用到了其他类)

泛化(继承)

实现

聚合(set注入)

组合(构造注入)

2.设计模式分类

  1. 创建型模式
    创建型模式专注于对象的创建方式,包括单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。
  • 单例模式:保证系统中只有一个对象实例,提供全局访问点。
  • 工厂方法模式:定义一个用于创建对象的接口,交由子类决定实例化的类。
  • 抽象工厂模式:提供一个创建相关或依赖对象的接口,而无需指定它们具体的类。
  • 建造者模式:将一个复杂对象的构建与它的表示分离,使同样的构建过程可以创建不同的表示。
  • 原型模式:通过复制现有的实例来创建新的对象,而不是在代码中直接创建。
  1. 结构型模式
    结构型模式关注类和对象的组合,包括适配器模式、桥接模式、组合模式、装饰器模式、外观模式、享元模式和代理模式。
  • 适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原本接口不兼容的类可以一起工作。
  • 桥接模式:将抽象部分和它的实现部分分离,使它们可以独立地变化。
  • 组合模式:将对象组合成树形结构,以表示“部分-整体”的层次结构,并可以通过一个对象实例来操作整个树形结构。
  • 装饰器模式:动态地给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活。
  • 外观模式:为子系统中的一组接口提供一个统一的入口,使得在复杂的系统中能够更容易地进行操作。
  • 享元模式:运用共享技术有效地支持大量细粒度的对象。
  • 代理模式:为一个对象提供一个代用品或占位符,以便控制对它的访问。
  1. 行为型模式
    行为型模式关注对象如何相互交互、解耦对象之间的职责分配,包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式。
  • 责任链模式:避免请求发送者和接收者之间的耦合关系,使处理请求的对象连成一条链,并沿着这条链传递该请求,直到有对象处理为止。
  • 命令模式:将请求封装成对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录日志,以及支持可撤销的操作。
  • 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
  • 迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示方式。
  • 中介者模式:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  • 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,之后就可以将该对象恢复到原先保存的状态。
  • 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
  • 状态模式:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
  • 策略模式:定义一系列算法,把它们一个个封装起来,并且使它们可相互替换,本模式让算法的变化独立于使用它的客户。
  • 模板方法模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
  • 访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

3.单例

双重检查

class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
    }
    private static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

3.计算机网络

1.OSI 7层模型和TCP/IP五层模型

应用层

最靠近用户的一层,为用户直接提供各种网络服务,比如我们看到的图片,听的音乐,看的视频常见的有HTTP,HTTPS协议 http是基于tcp和udp的

表示层

把人类看到的图片听到的声音用计算机编码表示,可以实现数据加密

高版本qq给低版本qq发表情,低版本qq解密不了

会话层

加两个app之间建立连接,比如美团只能用微信支付,淘宝只能用支付宝支付

qq发信息对方应该用qq接收,而不是微信

传输层

建立端到端的连接,比如我的电脑到你的电脑

建立TCP和UDP连接

TCP:适合传输那种对完整度要求高的文件,丢包就打不开的那种,有可靠性

UDP:可以接受少量丢包,对延迟要求很高,快,无可靠性

网络层

选择传递数据的最佳路径,选择合适的路由进行传输 ,ip协议

数据链路层

定义数据该以什么样的形式进行传输,比如网线是电信号,光纤是光信号

物理层

定义了传输介质,比如网线,光纤,什么样的东西才叫网线,什么样的东西才叫光纤



2.TCP和UDP的区别

UDP(面向报文)

1. 面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了

  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2. 有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

3. UDP是面向报文的

没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。

UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

4. 不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

从上面的动态图可以得知,UDP只会把想发的数据报文一股脑的丢给对方,并不在意数据有无安全完整到达。

4.UDP支持一对一,一对多,多对一和多对多的交互通信

5. 头部开销小,传输数据报文时是很高效的。

UDP 头部包含了以下几个数据:

  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口

  • 整个数据报文的长度

  • 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误

因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的

TCP(面向字节流)

引言:当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。

 TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流

  • CLOSED:初始状态,表示TCP连接是”关闭着的”或”未打开的”

  • LISTEN:表示服务器端的某个SOCKET处于监听状态,可以接受客户端的连接

  • SYN_RCVD:表示服务器接收到了来自客户端请求连接的SYN报文。这个状态是在服务端的,但是它是一个中间状态,很短暂,平常我们用netstat或ss的时候,不太容易看到这种状态,但是遇到SYN flood之类的SYN攻击时,会出现大量的这种状态,即收不到三次握手最后一个客户端发来的ACK,所以一直是这个状态,不会转换到ESTABLISHED

  • SYN_SENT:这个状态与SYN_RCVD状态相呼应,,它是TCP连接客户端的状态,当客户端SOCKET执行connect()进行连接时,它首先发送SYN报文,然后随机进入到SYN_SENT状态,并等待服务端的SYN和ACK,该状态表示客户端的SYN已发送

  • ESTABLISHED:表示TCP连接已经成功建立,开始传输数据

1.TCP连接过程

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

这里可能大家会有个疑惑:为什么 TCP 建立连接需要三次握手,而不是两次?

这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

客户端向服务端建立连接确认了,但是服务端向客户端没有确认

2. TCP断开链接

  • FIN_WAIT_1:这个状态在实际工作中很少能看到,当客户端想要主动关闭连接时,它会向服务端发送FIN报文,此时TCP状态就进入到FIN_WAIT_1的状态,而当服务端回复ACK,确认关闭后,则客户端进入到FIN_WAIT_2的状态,也就是只有在没有收到服务端ACK的情况下,FIN_WAIT_1状态才能看到,然后长时间收不到ACK,通常会在默认超时时间60s(由内核参数tcp_fin_timeout控制)后,直接进入CLOSED状态

  • FIN_WAIT_2:这个状态相比较常见,也是需要注意的一个状态,FIN_WAIT_1在接收到服务端ACK之后就进入到FIN_WAIT_2的状态,然后等待服务端发送FIN,所以在收到对端FIN之前,TCP都会处于FIN_WAIT_2的状态,也就是,在主动断开的一端发现大量的FIN_WAIT_2状态时,需要注意,可能时网络不稳定或程序中忘记调用连接关闭,FIN_WAIT_2也有超时时间,也是由内核参数tcp_fin_timeout控制,当FIN_WAIT_2状态超时后,连接直接销毁

  • CLOSE_WAIT:表示正在等待关闭,该状态只在被动端出现,即当主动断开的一端调用close()后发送FIN报文给被动端,被动段必然会回应一个ACK(这是由TCP协议层决定的),这个时候,TCP连接状态就进入到CLOSE_WAIT

  • LAST_ACK:当被动关闭的一方在发送FIN报文后,等待对方的ACK报文的时候,就处于LAST_ACK的状态,当收到对方的ACK之后,就进入到CLOSED状态了

  • TIME_WAIT:该状态是最常见的状态,主动方在收到对方FIN后,就由FIN_WAIT_2状态进入到TIME_WAIT状态

  • CLOSING:这个状态是一个比较特殊的状态,也比较少见,正常情况下不会出现,但是当双方同时都作为主动的一方,调用 close() 关闭连接的时候,两边都进入FIN_WAIT_1 的状态,此时期望收到的是ACK包,进入 FIN_WAIT_2 的状态,但是却先收到了对方的FIN包,这个时候,就会进入到 CLOSING 的状态,然后给对方一个ACK,接收到 ACK 后直接进入到 CLOSED 状态。

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次挥手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。(A告诉B我要释放连接了)

第二次挥手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。(B回答好的,然后A->B连接就释放)

第三次挥手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

第四次挥手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

拓展:2和3可以合在一起吗

不可以,也许服务端还要发数据

3.TCP怎么保证可靠性传输TCP(传输控制协议)

  1. 应答机制:发送方发送数据后,接收方会发送一个确认消息(ACK)来告诉发送方数据已经接收到。如果发送方没有收到确认消息,它会重发数据,直到接收方确认接收到数据。

  2. 序列号和确认号:每个TCP报文段都有一个序列号和确认号。序列号表示报文段中第一个字节的编号,而确认号表示期望收到的下一个字节的编号。这些序列号和确认号可以防止数据包丢失或乱序。

  3. 滑动窗口:TCP使用滑动窗口机制来控制发送方和接收方之间的数据流量。发送方会根据接收方的窗口大小来发送数据,而接收方会根据自己的处理能力来调整窗口大小。这样可以避免发送方发送过多数据导致接收方无法处理。

  4. 超时重传:如果发送方没有收到确认消息,它会等待一段时间(超时时间),然后重新发送数据。如果多次重发后仍未收到确认消息,则认为连接已经中断。

  5. 流量控制:TCP使用流量控制机制来防止发送方发送过多的数据导致接收方无法处理。接收方会告诉发送方自己的窗口大小,发送方会根据窗口大小来发送数据。

通过这些机制,TCP可以保证数据的可靠性传输,即使在网络环境不稳定的情况下也能保证数据的完整性和可靠性。

tcp滑动窗口,拥塞控制

TCP(Transmission Control Protocol)是一种面向连接的传输协议,它通过滑动窗口和拥塞控制等机制实现高效可靠的数据传输。

滑动窗口是 TCP 实现流控制的重要机制,它允许发送方和接收方在数据传输过程中控制发送和接收的数据量。发送方和接收方各自维护一个滑动窗口,用于控制数据的流动。滑动窗口的大小可以动态调整,它是由发送方和接收方之间的通信延迟和网络带宽等因素决定的。

TCP 还实现了拥塞控制机制,通过控制发送方的发送速率,以避免网络出现拥塞情况。TCP 通过拥塞窗口来限制发送方的数据发送速率,即通过控制滑动窗口的大小来实现拥塞控制。在拥塞控制过程中,TCP 通过慢启动、拥塞避免和快重传等算法,根据网络状况动态调整拥塞窗口的大小,以提高网络传输效率和可靠性。

TCP 滑动窗口和拥塞控制机制的综合作用,使得 TCP 协议在数据传输方面具有良好的性能和可靠性,尤其适合用于可靠传输的应用场景。

4.HTTP1.0、HTTP1.1、HTTP2.0、HTTP3.0

HTTP1.0默认使用 Connection:cloose,浏览器每次请求都需要与服务器建立一个 TCP 连接,服务器处理完成后立即断开 TCP 连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。

HTTP1.1默认使用 Connection:keep-alive长连接),避免了连接建立和释放的开销;通过 Content-Length 字段来判断当前请求的数据是否已经全部接受。不允许同时存在两个并行的响应。

HTTP2.0新特性

(1)二进制传输

http2.0将请求和响应数据分割为更小的帧,并且它们采用二进制编码(http1.0基于文本格式)。多个帧之间可以乱序发送,根据帧首部的流表示可以重新组装。

(2)Header压缩

Http2.0开发了专门的“HPACK”算法,大大压缩了Header信息。

(3)多路复用

http2.0中引入了多路复用技术,很好的解决了浏览器限制同一个域名下的请求数量的问题。

多路复用技术可以只通过一个TCP链接就可以传输所有的请求数据。

(4)服务端推送

HTTP2.0在一定程度上改不了传统的“请求-应答”工作模式,服务器不再完全被动地响应请求,也可以新建“流”主动向客户端发送消息。(例如,浏览器在刚请求html的时候就提前把可能会用到的JS,CSS文件发送给客户端,减少等待延迟,这被称为“服务端推送Server Push”)

Http3.0 抛弃tcp

Google在推行SPDY的时候意识到了上述http2.0一系列问题,于是又产生了基于UDP协议的“QUIC”协议,让HTTP跑在QUIC上而不是TCP上。从而产生了HTTP3.0版本,它解决了“队头阻塞”的问题

  • 特点:

(1)实现了类似TCP的流量控制,传输可靠性的功能。

(2)实现了快速握手功能(QUIC基于UDP,UDP是面向无连接的,不需要握手和挥手,比TCP快)

(3)集成了TLS加密功能

(4)多路复用,彻底解决TCP中队头阻塞的问题(单个“流”是有序的,可能会因为丢包而阻塞,但是其他流不会受到影响)

5.https

https是在应用层和传输层之间加了一个安全层,应用层与安全层进行加密,安全层再与传输层进行交互

1.客户端把它支持的加密算法传给服务端

2.服务端给客户端一个公钥和他的数字证书

3.客户端验证证书,发现这个公钥是我自己的,之后生成随机的对称秘钥,用公钥加密

4.用私钥解密获得秘钥

5.传输数据

签名:由私钥生成

传输的时候传数据和它独有的签名,别人收到用签名和数据对比,如果一样则安全,不一样就丢弃

证书:保护公钥不被篡改

对称性加密算法和非对称性加密算法

对称性加密算法:
对称性加密算法使用同样的密钥,用于加密和解密数据。由于密钥需要在双方之间共享,因此需要确保密钥在传输过程中不被泄露。对称性加密算法一般采用比较快速的加密算法,因此在加密大量数据时通常比非对称加密算法更具有优势。下面是几个常见的对称性加密算法:

  • DES算法:Data Encryption Standard,是一种基于密码学的对称性加密算法,用于加密电子数据。
  • AES算法:Advanced Encryption Standard,是一种基于密码学的高级对称性加密算法,用于加密电子数据。
  • Blowfish算法:是一种基于密码学的对称性加密算法,可以用来加密小到大的数据块。它的优点是速度快、安全、经过广泛的测试。

非对称加密算法:
非对称加密算法使用不同的密钥,其中一个用于加密数据,另一个用于解密数据。加密密钥被公开,而解密密钥则需要存储在安全的地方。非对称加密算法通常用于加密短消息、数字签名等需要高度安全性的数据。下面是几个常见的非对称加密算法:

  • RSA算法:是一种非对称加密算法,广泛用于数字签名和安全信任建立等领域。它能够支持多种密钥长度,通常用于加密短消息。
  • DSA算法:数字签名算法,用于在数字文档上确定其作者身份的一种标准算法。
  • ElGamal算法:是由ElGamal发明的一种非对称加密算法,在加密过程中能够提供完全性证明。

散列算法:

  1. MD5 (Message Digest Algorithm 5):MD5 是一个广泛使用的散列算法之一。它接收字符串作为输入,并生成具有固定大小的唯一的散列值。通常,它会生成一个 128 位长的十六进制数字,用于验证数据的完整性。

  2. SHA (Secure Hash Algorithm):SHA 是另一个常用的散列算法。它有几种不同的变体,但最广泛使用的变体是 SHA-256 和 SHA-512。SHA-256 生成一个 256 位长的散列值,而 SHA-512 生成一个 512 位长的值。

  3. MurmurHash:MurmurHash 是一个比 MD5 和 SHA 更快的散列算法。它在散列大量数据时非常高效,并且生成的散列值具有良好的随机性和均匀分布性。它被广泛用于哈希表等需要高效哈希函数的场景。

https为什么安全

HTTPS的安全性建立在Transport Layer Security(TLS)协议(前身是Secure Sockets Layer(SSL)协议)上。

HTTPS之所以安全,是因为使用了一些加密技术和安全机制,包括:

  1. 数据传输加密:HTTPS使用公钥加密和私钥解密的方式对数据进行传输加密,防止数据传输过程中被窃取或篡改。

  2. 客户端验证:HTTPS实现了双向认证,即客户端和服务器双方都需要进行验证,确保通信双方的身份和合法性。

  3. 数据完整性保护:HTTPS使用数据摘要技术和数字签名机制来保护数据的完整性。在数据传输结束后,接收方可以通过验证数据摘要和数字签名来确定数据是否被篡改。

  4. 防止中间人攻击:HTTPS使用非对称加密算法来保证数据传输的安全性,避免中间人攻击。这是因为HTTPS使用公钥加密和私钥解密的方式,数据只能由私钥持有者读取,即使在传输过程中被截获,攻击者也无法解密。

综上所述,HTTPS之所以安全,是因为它使用了多种加密技术和安全机制,确保了数据在传输过程中的保密性、完整性和合法性。这些技术和机制为网站和用户之间的数据传输提供了强有力的安全保障。

4.操作系统

1.linux的进程状态 ????

*R (TASK_RUNNING),可执行状态。* running

*S (TASK_INTERRUPTIBLE),可中断的睡眠状态。*sleep

*D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。*

*T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态*stop

*Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。*zombie

*:X (TASK_DEA

D - EXIT_DEAD),退出状态,进程即将被销毁。*exit

2.分段,分页机制

分段分页是操作系统中的内存管理机制,用于将物理内存和逻辑内存进行映射,以实现多道程序的并发执行和保护内存空间的安全性

考虑最原始,最直接的情况,程序中访问的地址都直接对应于物理地址。

这种方式有以下几个问题:两个使用的地址有交集的程序没法同时进行,就回导致各个程序之间无法隔离。

所以引入了分段机制。那么什么是分段机制呢? 将内存分成一段一段的(段大小不固定),为程序被分配某个段之后,程序便只能访问固定的段,无法访问其他地址

那么分页就是

将地址分为大小固定的页(一般为4096字节),按页为单位进行映射。连续的线性地址可以映射到不连续的的物理内存上

段式管理:

优点:消除了内部碎片,提高了对物理内存的利用率;将应用按逻辑分段,人们可以编写不同类型的代码,可以方便的进行共享或保护。

缺点:会产生大量的外部碎片,使得操作系统难以分配空闲空间。

页式管理:

优点:消除了外部碎片,提高了对物理内存的利用率,利于操作系统管理空闲空间。

缺点:仍然会产生内部碎片,尽管每个页碎片不超过页的大小;页表过大,占用大量空间,可以采用多级页表思想解决。

段页式管理:

优点:同时具备段式和页式的所有优点。

缺点:需要更多的硬件支持;当TLB未命中,需要更多的时间访问内存。

3.操作系统底层文件的管理通常涉及到以下几个方面:

文件系统通常会将文件的数据分成多个数据块(Data Block)存储,每个数据块的大小通常为 4KB 或 8KB。当文件的大小超过一个数据块时,文件系统会将文件的数据分成多个数据块存储,并记录每个数据块的地址和大小等信息。

当应用程序需要访问文件时,操作系统会根据文件系统的目录结构找到文件的位置,并读取文件的数据块。如果文件的数据块在内存中,则直接读取内存中的数据;否则,操作系统会从磁盘等外存中将数据块读取到内存中,然后再读取数据。

5.mysql

1.Mysql的存储引擎

首先数据库的存储引擎是基于表的而不是基于数据库的,存储引擎就是数据的存储方式

然后mysql支持很多存储引擎,但我们常用的有3种

1.Innodb:支持事务,支持行锁,支持外键,底层是两个文件,一个里面存表结构,一个里面存数据和索引

2.myisam:不支持事务,但查询快,,底层是三个文件,平均分布io,获得更快的速度

3.memery:速度最快,只存储表结构,剩下都存在内存

MyIsam为什么比innodb快

  1. 由于innodb支持事务,所以会有mvvc的一个比较。这个过程会浪费性能。

  2. 查询的时候,innodb叶子节点存储的是数据块,查询时找到数据块才能找到对应行.MyIsam叶子节点存储的是磁盘地址,所以,查询的时候查到的最后结果不是聚簇索引树的key,而是会直接去查询磁盘。

  3. 锁的一个损耗,innodb锁支持行锁,在检查锁的时候不仅检查表锁,还要看行锁。

Innodb底层的四大组件

1.Buffer Pool
用来缓存数据的内存池,缓存的是最热门的数据页。在 InnoDB 存储引擎中,数据存储在由具体页组成的数据文件中,而 Buffer Pool 则缓存着热门的数据页,以避免频繁的磁盘 I/O 操作。Buffer Pool 的大小可以通过参数 innodb_buffer_pool_size 进行设置,一般建议为总内存的 60~80%。

什么是double_write?

当数据还在缓冲池中的时候,当机器宕机了,发生了写失效,有Redo Log来进行恢复。但是如果是在从缓冲池中将数据刷回磁盘的时候宕机了呢?

这种情况叫做部分写失效,此时重做日志就无法解决问题。

当 InnoDB 存储引擎要将脏数据页写入磁盘时,它首先会将数据页的副本写入缓冲区,再将数据页的副本写入磁盘的数据文件中。

2.Change Buffer
Change Buffer 相当于一个写入缓存,由于 InnoDB 存储引擎在处理写入操作时需要先定位到对应的磁盘页然后再更新数据,而 I/O 操作往往是数据库性能的瓶颈,因此 Change Buffer 用来缓存更新数据的那一部分,以减少 I/O 操作。Change Buffer 缓存的数据是未被写入磁盘的,并且会在 Buffer Pool 内存不足时转移到磁盘。

由 Insert Buffer 和 Update Buffer 两部分组成。Insert Buffer 用于缓存插入操作所产生的记录,而 Update Buffer 用于缓存更新和删除操作所产生的修改。

当一个事务需要插入、删除或更新记录时,InnoDB 不会直接写入磁盘,而是首先将修改缓存到 Change Buffer 中,InnoDB 会将修改的数据页标记为“脏页”,稍后会将 通过double_write合并到对应的磁盘页中。因为缓存到 Change Buffer 中的修改可以批量写入磁盘,因此在缓存中的修改可以减少磁盘 I/O 操作,提高了事务的处理速度。

Change Buffer 在某些条件下并不适用,例如内存不足、更新批量过大或者对于主键更新频繁等情况下。在这些情况下,Change Buffer 会被禁用,修改直接落盘到磁盘进行更新。

3.自适应哈希索引
Adaptive Hash Index(简称 AHI)则是 InnoDB 存储引擎用来加速查询的一种索引。自适应哈希索引的主要思想是为频繁访问的数据建立 hash 索引,在查询时能很快地定位到对应的数据块,避免了在 B+ 树中进行多次磁盘 I/O 操作,进而提高了查询效率。自适应哈希索引采用的是一种自适应性策略,能够动态地调整索引的大小、数量和位置,从而适应数据访问的变化。

4.Log Buffer
Log Buffer 是 InnoDB 存储引擎中用来加速日志写入操作的缓存区域,它缓存着待写入到磁盘上的 redo log,可以减少频繁的磁盘操作,从而提高性能。Log Buffer 的大小可以通过参数 innodb_log_buffer_size 进行设置。

2.事务的四大特性ACID

事务通常以BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束

首先,什么是事务,我理解为一些操作的集合

1.原子性: undolog

事务包含的操作,是一个原子性操作,要么都成功,要么都失败

2.一致性: undolog

事务执行前后必须处于一致性状态

 拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

3.隔离性: MVCC

事务之间相互独立,一个事务执行不能被其他事务干扰

即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

4.持久性: redolog

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的

3.事务在并发条件下产生的问题

1.脏读:

A事务读取了B事务未提交的数据

2.不可重复读:

A事务对自己未操作的数据进行多次读取,结果发现数据不一致的情况(破坏一致性)

3.幻读:

有的人说幻读是,A事务查询得到了n条数据,事务b此时对表进行增加或者删除,之后事务A有进行了查询,发现条数不一样

但我不这么,我是这样理解的

A事务查某条记录是否存在,结果不存在,然后他插入数据,插不进去

12.事务的隔离级别

主要就是读取的是哪个版本的数据

1.读未提交(read uncommitted)

脏读,不可重复读,幻读都有可能发生

2.读已提交(read committed) 每次快照读

避免脏读rc

  每次执行快照读的时候生成readview

3.可重复读(repeatable read) 一次快照读

避免脏读,不可重复读rr,在快照读时避免幻读,当前读时会产生幻读.

  第一次执行快照读的时候生成readview

当执行update、delete、insert是当前读

4.串行化(serializable)

当前读,因为加入共享读锁

避免脏读,不可重复读,幻读

当前读:

  select for update 
  insert,update,delete

快照读:某一时间点的数据,就像对数据拍了一张照片

4.innodb中主键索引和非主键索引都是聚簇索引吗

不是

聚簇索引具有唯一性

由于聚簇索引是将数据跟索引结构放到一块,因此一个表仅有一个聚簇索引

总结:当有主键时聚簇索引,因为一张表中没有主键索引,那么聚簇索引会使用第一个唯一索引(此列必须为not null),如果以上情况都不满足,那么InnoDB会生成一个隐藏的聚簇索引。

5.事务的原理,回滚的实现

有一个日志,undolog

当有事务对数据进行操作时,会在undolog上产生一条记录

他记录了当前事务的id和回滚指针

完了他就实现了回滚

6.mysql主从复制

1.为什么需要主从复制

1.如果有一句sql语句需要锁表导致暂时不能够使用读服务,那么很影响性能,使用主从复制,让主库负责写,从库负责读

2.做数据的热备,相当于给主库做一个备份

3.业务量大,I/O访问频率过高,降低磁盘的访问频率,提高单个机器的I/O量

2.主从复制原理

1.主服务器将数据的改变记录记录在binlog日志,当主服务器上的数据发生改变,将改变写入binlog日志(binlog的写入必须要等待事务完成之后,才会传入备库)

2.从服务器在一定时间间隔内对主服务器的binlog日志探测是否改变,如果发生改变,则开始一个I/O线程请求主服务器的事件

3.主库接收到请求就为开一个log dump线程,用来给从库传binlog,写到本地的relay-log(中继文件)文件中

传完,从库开启一个sql的线程,把读到的都执行一遍,与主服务器保持一致

img

4.复制延迟如何解决

不可能解决,可以在两个mysql服务器之间添加缓存.

5.如何实现读写分离

主从复制依赖于读写分离

1.通过一个mysql代理(lua脚本写的,可以识别出读和写的sql语句,然后进行转发),但不常用,因为性能

不高img

6.Binlog的三种形式的优缺点

  1. Statement-Based Logging(基于语句的日志)

该方式 Binlog 记录的是 MySQL 执行的 SQL 语句。这种日志记录方式比较简单,记录的数据量相对较小,存储空间开销较小,对 CPU 的消耗也相对较小。这种方式可减轻系统的磁盘 I/O 压力,因为它只需要写入 Binlog 中的 SQL 语句即可。

但是由于基于语句的方式是记录 SQL 语句,因此只要语句相同,执行结果也相同。但是,同一 SQL 语句的多次执行结果在不同时间或环境下可能有所不同,所以在恢复数据时稍有风险。

比如地理位置不同就会导致时间不同

  1. Row-Based Logging(基于行的日志)

该方式 Binlog 记录针对每行数据所执行的修改操作的结果。由于该方式是对每行数据进行记录的,所以可以准确地恢复出原操作,使得数据更具备一致性。而且它能够记录插入和删除操作,甚至是对于原始状态没有改变的 UPDATE 操作都记录了修改前和修改后的状态。 

但是这种记录方式产生的日志数据量较大,占用存储空间也高,对 CPU 的消耗也相对较高。

一条修改和删除的sql语句可能伴随着多个表的行的变动,日志需要记录每个表每个行的变动导致文件变大

  1. Mixed Logging(混合日志)

该方式结合了基于语句和基于行的记录方式,使得使用组合方式可以充分发挥它们的长处。MySQL 可以自己判断一个请求的 SQL 语句是否适合由 Statement-Based Logging 方式进行记录,如果 SQL 操作适合于采用 Row-Based Logging 方式,MySQL 将自动采用 Row-Based Logging。

当 MySQL 无法确定使用哪种日志格式时(例如,涉及到表中 BLOB 或 TEXT 类型),它将使用 Mixed Logging。这种方式既保证了数据的完整性,也减小了存储空间和 CPU 占用的开销。

综上所述,每种日志记录方式都有其自身的优缺点。Statement-Based Logging 简单、存储空间和 CPU 占用小,但是恢复时有一定的风险;Row-Based Logging 记录准确,恢复数据时风险较小,但是存储空间比较大,对 CPU 占用也较高;而 Mixed Logging 将两种方式组合使用,可以在存储空间和 CPU 占用方面取得一个折中的结果,但是可能会牺牲一定的查询性能。因此,我们需要根据不同的应用场景来选择适合的 Binlog 日志记录方式。

7.谈一下你对MVCC的理解

MVCC是多版本并发控制(Multi-Version Concurrency Control)的缩写,是一种并发控制机制。MVCC机制通过创建多个版本的数据,来实现多个事务并发地读取或修改同一数据,从而提高并发性和性能。

在MVCC机制中,每个事务操作的数据都被保存在多个版本上,并根据时间戳进行管理。当一个事务读取数据时,它会读取其中的某个版本。当进行更新操作时,会将新的版本存储到数据中,并将旧的版本作为历史版本保存。在整个事务过程中,每个事务都只能看到它启动时的数据版本,从而保证了事务的隔离性和一致性。并且,MVCC机制还可以避免死锁的情况,提高了并发性能。

如何解决幻读

MVCC采用了多版本的方式来管理数据,读取操作不需要获取锁,而是通过版本号来确定读取的数据。每个事务看到的数据都是该事务启动时刻数据库中的数据快照,这样就可以避免了幻读的问题。每个事务都会创建一个时间戳(Transaction ID),读操作是基于该时间戳来获取数据版本的,并发事务之间不会相互影响。如果在事务中进行更新操作,更新的数据版本会被保存到历史版本中,而读取操作则会通过版本号来获取数据,从而实现了事务的隔离性和一致性。

在高并发的情况下,MVCC机制会产生大量的历史数据,频繁的读写操作可能会导致性能问题,应该根据实际情况采取适当的措施进行优化和调整。

8.索引分类

按数据结构分类可分为:B+tree索引、Hash索引、Full-text索引。 按物理存储分类可分为:聚簇索引、二级索引(辅助索引)。 按字段特性分类可分为:主键索引、普通索引、前缀索引。 按字段个数分类可分为:单列索引、联合索引(复合索引、组合索引)

聚簇索引:聚簇索引可以直接找到数据innodb

非聚簇索引:至少经历两次查找,先找到聚簇索引,再找到数据(MyIsam)

9.执行计划

MySQL的执行计划(Execution Plan)是指MySQL优化器根据SQL语句所生成的查询计划,描述了MySQL如何执行该查询语句。执行计划显示了MySQL选择的查询方式、所使用的索引、表连接顺序、数据读取方式等详细信息,能够帮助我们理解MySQL的查询优化过程和性能瓶颈,从而进行优化。

在MySQL中,我们可以通过EXPLAIN语句来查看执行计划。EXPLAIN语句可以在SELECT、DELETE、INSERT、REPLACE和UPDATE语句前加上,例如:

``` EXPLAIN SELECT * FROM table_name WHERE column1 = 'value'; ```

执行该语句后,MySQL会返回一个表格,其中包含了查询的详细信息,例如:

| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | SIMPLE | table_name | NULL | ref | column1_index | column1_index | 767 | const | 1 | 100.00 | NULL |

在表格中,每一行对应一个访问表的操作,其中包含了该操作所用的索引、访问方式、读取行数、过滤条件等信息。通过分析执行计划,我们可以判断查询中是否使用了索引,索引是否命中,是否存在全表扫描等问题,从而优化查询性能。

10.索引结构

注意:innodb只支持B tree ,Memery支持hash

  • BTREE 索引 :

    • 最常见的索引类型,大部分索引都支持 B 树索引。

  • HASH 索引:

    • 只有Memory引擎支持 , 使用场景简单 。

  • R-tree 索引(空间索引):

    • 空间索引是MyISAM引擎的一个特殊索引类型,主要用于地理空间数据类型,通常使用较少,不做特别介绍。

  • Full-text (全文索引) :

    • 全文索引也是MyISAM的一个特殊索引类型,主要用于全文索引,InnoDB从Mysql5.6版本开始支持全文索引。

11.B+tree和B tree区别

B+树在非叶子节点只存key和指针,而B树是kv一起存,这就使B+树更加扁平,查询速度更快,I/O更加稳定,通过主键查询只需要1-3次I/O

B+树在相邻的叶子节点都有链表指针,更加适合范围查询

img

12.一颗B+树可以存多少数据

InnoDB页的大小默认是16KB:

  • 假设一条记录大小为1KB,则一个数据页中可以存16条数据(忽略页中的其他数据结构)

  • 假设主键为int,指针大小为6字节,则一个索引页中可以存储16KB/(4B+6B)≈1600个索引

所以,两层的B+树可以存储:16*1600=2万条数据;三层的B+树可以存储:16*1600*1600=3000多万条数据。

如果表只有2个字段,就会更多

13.如何设计索引

选择查询频率高的作为索引,通常where、join、order by子句的列等

索引并不是越多越好,索引过多也会影响性能

能使用联合索引时就使用联合索引

最后explain一下看看索引用没用上,type至少是range

1.能使用联合索引就使用联合索引,单个索引会失效

2.最左前缀, 联合索引使用时顺序乱了,索引失效

3.计算,函数,类型转换索引失效

4.范围查询右侧的索引失效

5.不等于,不为空,不像导致索引失效

6.like以%开头导致索引失效

7.OR 前后存在非索引的列,索引失效

9.建立多个单列索引时即使查询条件用到了多个条件,最终也只会用到一个索引

最左前缀原则

联合索引是(a,b,c),那么只有查询条件中包含(a)或者(a,b)或者(a,b,c)的情况下才能够使用联合索引

14.select的执行顺序

from组装数据

join把需要join的表读入内存

on 将每一行和on条件进行匹配

where筛选

group by 分组

having和聚合函数 分组后再筛

select 选择查询字段

distinct去重

orderby

limit

where和having

WHERE 子句通常比 HAVING 子句效率高,因为 WHERE 子句在查询之前对数据进行过滤,只将符合条件的行传递给 GROUP BY 子句进行分组,从而减少了分组的数据量以及聚合函数的计算次数。相比之下,HAVING 子句需要在查询之后对聚合后的数据再进行过滤,因此需要处理更多的数据量,而且聚合函数的计算成本也较高,所以它的查询效率一般比 WHERE 子句低。

那什么时候用having?

HAVING 子句通常会使用到聚合函数,如 AVG()SUM()MAX()MIN() 等函数,这些函数的计算成本较高,因为需要对数据进行聚合运算。相比之下,WHERE 子句只是根据表中的列来进行简单的判断或筛选操作。

15.数据库优化

总共分7步

1.sql优化

减少对数据库的访问次数,小表驱动大表,行裁剪,列裁剪,避免使用HAVING子句, HAVING 只会在检索出所有记录之后才对结果集进行过滤,这个处理需要排序,总计等操作。如果能通过WHERE子句限制记录的数目,那就能减少这方面的开销。unionall(组合两张字段相同的表,不去重)代替union,exists代替distinct,用exists代替in

2.索引优化

3.表结构优化

4.数据量实在太大就算走索引也很慢,就需要分库分表

分库(垂直拆分):一般不会这么做,会使事务问题变复杂

分表(水平拆分):按条件,时间,地区等分成多个表,但中间会加一个代理层mycat,shardingsphere

5.基本设置优化

连接数设置,慢查询设置

6.硬件优化

包括服务器硬件和网络带宽,这一部分是运维来做,我就不细说了

16.关键字

1.exists和distinct区别

DISTINCT语句用于从查询结果集中删除重复行,但它需要对整个结果集进行排序和去重操作,这可能会影响查询的性能。

EXISTS语句用于检查子查询是否返回任何行,它只检查子查询是否返回任何行,而不需要对整个结果集进行排序和去重操作。

举个例子,假设有两个表A和B,需要查询A表中不重复的列1值。可以使用DISTINCT语句来实现:

 SELECT DISTINCT col1 FROM A;

也可以使用EXISTS语句来实现:

 SELECT col1 FROM A WHERE EXISTS (SELECT 1 FROM B WHERE A.col1 = B.col1);

2.聚合函数

count sum avg min max

3.IN,BETWEEN,NOT,IS NULL的用法

 SELECT * FROM table WHERE column_name IN ('value1', 'value2', 'value3')
 SELECT * FROM table WHERE column_name BETWEEN 'value1' AND 'value2'
 SELECT * FROM table WHERE NOT column_name = 'value'
 SELECT * FROM table WHERE column_name IS NULL

17.索引失效

1.能使用联合索引就使用联合索引,单个索引会失效

2.最左前缀, 联合索引使用时顺序乱了,索引失效

3.计算,函数,类型转换索引失效

4.范围查询右侧的索引失效

5.不等于,不为空,不像导致索引失效

6.like以%开头导致索引失效

7.OR 前后存在非索引的列,索引失效

9.建立多个单列索引时即使查询条件用到了多个条件,最终也只会用到一个索引

18.mysql日志

1.undolog

回滚日志,只有innodb有

2.redolog

重做日志,物理日志

3.binlog逻辑日志

记录增删改的逻辑变更,以二进制存储,事务提交才记录

4.relaylog

主从复制用,用于数据同步

5.日志记录顺序

1.将数据从磁盘读到buffer pool中

2.在undolog记录旧值以便回滚

3.执行引擎更新buffer pool中的数据

4.将buffer pool中更新的数据页,写到redo log buffer中

5.生成redo log日志文件

6.写bin log

19.mysql 为什么不能用binlog来做数据恢复?

因为binlog是逻辑日志,以二进制的形式记录了sql语句,只有在事务提交时才记录

而redolog 是物理日志,记录的是页的变化,一直在记录

20.distinct, group by ,exist的区别

distinct:是对查询结果进行扫描,适用于纯去重操作,如果有唯一索引就会更快

group by: 对查询结果进行扫描,适用于数据量较大的操作,并且聚合(平均值,总和)

exist: 需要子查询,效率取决于子查询的规模和索引等等方面

21.Mysql 3000w条数据同步到es中需要注意什么问题

  1. 数据同步方式:同步数据时可以选择增量同步或全量同步。增量同步需要考虑同步的数据变化和以何种方式将变化的数据同步到Elasticsearch中;全量同步可能会导致性能瓶颈和宕机等问题。

  2. Elasticsearch节点部署:Elasticsearch需要部署在独立的节点上,部署的节点数和硬件配置对同步流程和查询性能都会有影响,需要根据数据量和业务需求来决定节点数和硬件配置。

  3. 数据类型适配问题:MySQL和Elasticsearch是两个不同的数据库,需要对数据类型进行适配,例如日期类型、数字类型和字符串类型等。同时,需要考虑到中文分词和英文分词等问题。

如何同步

分为全量同步和增量同步

1.全量同步:可以使用

使用Datax进行全量数据同步

不使用第三方框架也可以直接对mysql的数据库需要同步的表进行查询,封装成对象. 向mq发消息

es获取消息来同步

2.增量同步:

可以使用 MySQL Binlog 来进行 MySQL 数据同步到 Elasticsearch 

使用 Binlog 数据同步 Elasticsearch,业务方就可以专注于业务逻辑对 MySQL 的操作,不用再关心数据向 Elasticsearch 同步的问题,减少了不必要的同步代码,避免了扩展中间表列的长耗时问题。

可以使用github提供的开源项目 go-mysql-elasticsearch 实现数据同步

公司的所有表的 Binlog 数据属于机密数据,不能直接获取,可以采用接入 Kafka 的形式提供给使用方,并且需要使用方申请相应的 Binlog 数据使用权限。获取使用权限后,使用方以 Consumer Group 的形式读取。

可以保证了 Binglog 数据的安全性,但会有数据的书序性和完整性的问题

1). 顺序性

通过 Kafka 获取 Binlog 数据,首先需要保证获取数据的顺序性。严格说,Kafka 是无法保证全局消息有序的,只能局部有序,所以无法保证所有 Binlog 数据都可以有序到达 Consumer。

但是每个 Partition 上的数据是有序的。为了可以按顺序拿到每一行 MySQL 记录的 Binglog,我们把每条 Binlog 按照其 Primary Key,Hash 到各个 Partition 上,保证同一条 MySQL 记录的所有 Binlog 数据都发送到同一个 Partition。

如果是多 Consumer 的情况,一个 Partition 只会分配给一个 Consumer,同样可以保证 Partition 内的数据可以有序的 Update 到 Elasticsearch 中。

2). 完整性

考虑到同步程序可能面临各种正常或异常的退出,以及 Consumer 数量变化时的 Rebalance,我们需要保证在任何情况下不能丢失 Binlog 数据。

利用 Kafka 的 Offset 机制,在确认一条 Message 数据成功写入 Elasticsearch 后,才 Commit 该条 Message 的 Offset,这样就保证了数据的完整性。而对于数据同步的使用场景,在保证了数据顺序性和完整性的情况下,重复消费是不会有影响的。

22.Mysql的执行流程

查询缓存往往效率不高,所以在MySQL8.0之后就抛弃 了这个功能。

23.查询千万级别的数据量的瓶颈

1.内存溢出问题

首先对于这么多的数据如果一次行查询会占用很多的内存,将数据分批进行查找

select * from test limit 1000000,10

select id from test limit 1000000,1

select * from test where id>=(select id from test limit 1000000,1)limit 10

每次查询一些数据,对于深分页可以采用子查询的形式 ,最后将数据合并到一起

2.异步查询

将每个查询做异步的处理最后合并成想要的数据

3.效率问题

分库分表

分库(垂直拆分):一般不会这么做,会使事务问题变复杂

分表(水平拆分):按条件,时间,地区等分成多个表,但中间会加一个代理层mycat,shardingsphere

24.索引覆盖和索引下推

索引中已经包含了所有需要读取的列数据的查询方式称为覆盖索引,无需回表

下推:索引的使用是在存储引擎中进行的,而数据记录的比较是在Server层中进行的。就是过滤的动作由下层的存储引擎层通过使用索引来完成,而不需要上推到Server层进行处理。

25.update,insert触发行锁还是表锁

如果使用的是 InnoDB 存储引擎,并且如果 UPDATE 语句有 WHERE 子句,并且 WHERE 中的条件只涉及到某一行或少数几行,那么就会使用行级锁定来进行锁定,即只锁定需要更新的行。如果没有指定 WHERE 子句或者 WHERE 子句中的条件涉及到表中的多行,那么就会对整个表进行锁定,即使用表锁定。这样可能会影响并发性能和系统的响应时间。因此,在使用 UPDATE 语句时,应该尽量避免对整个表进行锁定。

对于insert如果插入的行不存在主键重复等冲突情况,那么就会使用行级锁定来进行插入操作,即只锁定插入的那一行。这样就可以避免不必要的锁定,提高并发性能。如果使用的是 InnoDB 存储引擎,并且插入的行不存在主键重复等冲突情况,那么就会使用行级锁定来进行插入操作,即只锁定插入的那一行。这样就可以避免不必要的锁定,提高并发性能。

在 MyISAM 存储引擎中,只支持对整个表进行锁定,不存在行级锁定的概念。因此,在 MyISAM 中进行 UPDATE,INSERT 操作时,会对整个表进行锁定,容易导致锁定冲突和性能瓶颈。

26.为什么innodb有页的概念而MyIsam没有

InnoDB 存储引擎中引入了页的概念,这是因为 InnoDB 是一种支持事务的存储引擎,需要通过页的概念来实现一些事务相关的功能。

具体来说,页是 InnoDB 存储引擎中管理数据的最小单位,页的大小通常为 16KB(可以通过参数设置调整),所有的数据操作都是以页为单位进行处理的。由于 InnoDB 支持事务,需要记录数据的修改历史,因此 InnoDB 引入了多版本并发控制(MVCC)技术,通过在页内记录行的版本信息来实现。

而 MyISAM 存储引擎是一种不支持事务的存储引擎,它不需要在所存储的数据上手动维护行的版本历史,因此没有必要引入页的概念。MyISAM 存储引擎采用的是表锁定的方式进行并发控制,因此它不需要像 InnoDB 那样在每行数据上维护锁定信息。

27.innodb行锁的原理

 

 28.Mysql日志参数如何调优

MySQL 日志参数的调优是 MySQL 数据库性能调优的重要方面之一。主要的 MySQL 日志包括二进制日志、错误日志、查询日志、慢查询日志和日志缓冲等。

在调优 MySQL 日志参数时,建议遵循以下几点原则:

  1. 合理选择日志输出级别,避免输出过多的日志信息,影响系统性能。

  2. 合理调整日志文件的切分大小和数量,以免磁盘空间被过多占用,影响数据库的正常运行。

  3. 采用异步日志写入模式,将日志写入缓冲区,再异步将缓存区的日志批量写入磁盘,避免频繁的磁盘读写操作。

  4. 针对不同类型的日志,采用不同的存储引擎,以达到最优的性能和存储效率。

针对不同类型的 MySQL 日志参数,可以进行如下的调优:

  1. 二进制日志(binlog)参数调优:主要包括 max_binlog_size 和 binlog_cache_size 这两个参数。前者用于控制二进制日志文件的大小,后者用于控制二进制日志缓存的大小。

  2. 错误日志(error log)参数调优:主要包括 log_error 和 log_error_verbosity 两个参数,前者用于设置错误日志的输出文件名,后者用于控制错误日志的输出级别和详细程度。

  3. 查询日志(query log)参数调优:主要包括 general_log 和 slow_query_log 这两个参数。前者用于开启或关闭查询日志,后者用于开启或关闭慢查询日志记录。

  4. 慢查询日志(slow query log)参数调优:主要包括 long_query_time 和 log_queries_not_using_indexes 两个参数。前者用于设置慢查询的时间阈值,后者用于记录没有使用索引的慢查询语句。

  5. 日志缓冲(log buffer)调优:主要包括 log_buffer_size 和 innodb_log_buffer_size 两个参数。前者用于控制查询日志和慢查询日志的缓冲大小,后者用于控制 InnoDB 存储引擎的日志缓冲大小。

调整这些参数需要了解数据库的实际情况,参考官方文档和其他运维人员的实践经验,综合考虑系统负载、数据库大小、查询量、I/O 等综合因素,从而达到最优的性能和效果。

29.锁的分类

1.按锁的粒度划分

  1. 全局锁:就是对整个数据库实例加锁。全局锁的典型使用场景是:做全库逻辑备份 。 
  2. 表级锁:对整个表进行加锁
  3. 行级锁  :只有innodb存储引擎有 ,对一行数据进行加锁  

2.按锁的兼容性划分

  1. 共享锁(Shared Locks):在读取某个数据行时,如果该数据行已经被其他事务加了共享锁,则当前事务可以获得共享锁,以允许读操作继续进行,但是不能对数据行进行更新操作,以避免数据不一致的情况    SELECT...LOCK IN SHARE MODE

  2. 排它锁(Exclusive Locks):在修改某个数据行时,如果该数据行已经被其他事务加了共享锁或排它锁,则当前事务必须等待其他事务释放锁后才能加锁,以允许数据行的修改操作。

    SELECT...FOR UPDATE

3.按锁的模式划分

  1.  记录锁:当程序对一行记录进行更新时,MySQL 会为该行记录加上记录锁,其他事务无法修改该行。
  2. 间隙锁:加在两个索引之间当对索引上的数据区间进行查询时,MySQL 会对索引上的间隙加锁,以阻止其他事务向这个间隙中插入数据。between...and
  3. 临界锁:临键锁(Next-Key)简单理解是 “记录锁+间隙锁” 的组合,Next-Key lock与record lock加锁的粒度一样,都是加在一条索引记录上的。一个next-key lock=对应的索引记录的record lock+该索引前面的间隙的gap lock,通过临键锁可以解决幻读的问题。
  4.  自增锁:  自增锁(auto-inc Locks)是一种特殊的表级锁,主要用于事务中插入自增字段(AUTO_INCREAMENT),也就是我们最常用的自增主键id。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务都必须等待插入到该表中,以便第一个事务插入的行接收连续的主键值。

4.按加锁机制分,乐观锁,悲观锁

6.JVM

1.什么是垃圾,什么是内存溢出,什么是内存泄漏

垃圾:在运行中的没有任何指针指向的对象

内存溢出:我要用的内存大于系统给的

内存泄漏:不再使用的对象无法被回收

内存泄漏是怎样发生的(可以联合ThreadLocal一起说)

1.静态集合类造成内存泄漏

2.指针忘记断开引起内存泄漏

3.资源未关闭

2.垃圾标记算法

1.引用计数算法(java不用,python用)

只要有任何一个对象引用了A,则A的引用计数器就加1

引用失效,引用计数器-1

当引用计数器为0时则判断为垃圾

特点:效率高

致命缺点:循环引用

2.可达性分析算法

就看对象可不可达,有GCroot出发,不可达全都是垃圾

什么是GCroot

栈中变量引用的对象

静态变量引用的对象

被锁住的对象

一些常见的异常对象,比如NullPointerException,OutOfMemoryError

3.Stop the world的原因

判断对象是否是垃圾时,必须保障在一致性快照中进行,不管垃圾回收器怎么优化虚拟机在垃圾回收时都会发生stw

4.对象的finalization机制

对象回收之前总会先调用对象的finalize()方法,用于资源释放,比如关闭文件

不要自己调用这个方法,垃圾回收器会自动调用

可以会导致对象复活,重写finalize方法

只能被执行一次

5.判断对象是否可回收

1.如果对象没有引用链,则进行第一次标记

2.然后再看看 finalize方法

6.垃圾清除算法

1.标记清除算法(Mark-Sweep)

CMS

老年代

当堆中有效空间被耗尽时,就会停止整个程序(stw),然后标记,然后清除

标记:标记所有可达的对象,保存在对象头中,

清除:那么未被标记的对象都是垃圾,都要被回收

缺点:效率太低

这种方式清理出的内存是不连续的,产生内存碎片,需要维护一个空间列表

2.复制算法(在垃圾多的时候用)

serial

新生代

整出来一块区域B,遍历所有可达的对象(不标记),每遍历一个复制一个,以保证连续的空间,之后销毁A区内所有的对象

只是复制(改变指针指向而已)

img

3.标记压缩整理算法

老年代

第一阶段和标记清除算法一样,从根节点开始标记所有被引用的对象

第二阶段把所有存活的对象压缩到内存的一边

最后清理边界所有空间img

优点:空间用得少,不堆积空间碎片

缺点:最慢

4.增量收集算法

如果一次性将所有垃圾回收,需要造成很长时间的停顿,如果一次处理一部分,那么就可以垃圾回收线程和用户线程交替执行

优点:减少stw时间

缺点:性能下降

5.分代收集

根据对象的生命周期将内存划分为几部分

6.分区算法G1

就是把一整个堆空间分成很多小块,每个小块相互独立,这种算法的好处是可以控制一次回收多少个小块

img

7.垃圾回收器

吞吐量优先的同时,尽量降低stw

img

1.serial 垃圾回收器

串行

用户端

2.ParNew 垃圾回收器 新生代

并行

只能够处理新生代

服务端

3.parallel垃圾回收器

并行

吞吐量优先

4.CMS 垃圾回收器 标记清除(并发) 老年代

优点:

  1. 停顿时间短 - 用户线程和垃圾回收线程并发执行,stw时间较短

  2. 吞吐量大 - 在并发标记和清除过程中,CMS充分利用了多核处理器和多线程,在垃圾回收过程中最大程度地减少了暂停时间。同时,CMS与G1等回收器相比,跨多个处理器核心的访问模式更少,因此对于多核处理器可以获得更好的性能提升。

缺点:

  1. 空间碎片 - CMS使用标记-清除算法回收垃圾,当对象存活时间很久时,可能会产生大量的空间碎片,这些空间碎片可能会导致大对象无法得到分配(连续空间不足)。

5.G1

降低停顿时间的同时,具有高吞吐量的特征

优点:

  1. G1把整个Java堆划分为大小相等的子区域(Region),每个Region可以灵活设置属于哪个代(Eden、Survivor、Old)优先清理那些垃圾比较多的Heap区域;

  2. G1采用了多线程和并发的方式处理垃圾回收,同时,G1也能够有效地利用多个CPU;

  3. G1会对各个Region的使用情况动态跟踪,根据实际状况动态调整各个区域的回收时间,从而使得垃圾整理的代价更加均衡。

缺点:

  1. 初始标记暂停时间较长:G1通过初始标记阶段,记录所有GC Roots直接关联的对象,需要暂停线程才能完成,这个过程会比较耗时;

  2. 需要更多的空间:G1在管理Java堆内存时,需要一些额外的空间进行一些标记或整理操作,因此,相对于其他的收集器,G1可能需要更多的空间。

  3. 垃圾回收算法较为复杂:因为G1在内部需要维护大量的数据结构,同时,G1的垃圾回收算法也比较复杂,导致了一些性能上的开销。

6.ZGC 标记压缩整理算法(都是并发的)

优点:

  1. 低延迟:ZGC 第一大目标就是减少停顿时间。通过使用分代压缩技术,处理更大的对象而不会引起对象移动,从而避免了常规垃圾回收器不可避免的内存移动带来的延迟问题。

  2. 高吞吐:它是并行处理,并利用了现代 CPU 多核技术。

  3. 自适应:ZGC 根据需要动态分配内存,当堆空间达到预设值时进行自动垃圾回收。ZGC 也会自动调整垃圾回收过程中的线程数量以进行更好的负载平衡,从而减少延迟

缺点:

  1. 垃圾回收成本较大,需要更多的CPU和内存资源。

  2. 初始化,或者分配一大块连续内存的任务,可能需要较长时间。

img

8.堆

img

几乎所有的对象啊都在伊甸园区分配空间,若对象太大会直接进入老年代,老年代还放不下,直接报oom,绝大多数对象都是在新生代销毁

当伊甸园区满了会触发minorGC,不满不触发

GC时会stw,用户线程会停止,不被使用的对象会被干掉,还在使用的对象会进入幸存者0区(S0),年龄计数器加一

年龄计数器的作用:判断对象神魔时候进入老年代,当年龄计数器到达15(默认)就会从幸存者区晋升到老年代

谁空谁是to区

fullGC代表整个堆和方法区的回收

为什么要分代不分不行吗?

因为大约百分之70~99都是临时对象,如果堆全满了再进行垃圾回收,那么就很浪费性能

9.目前jdk默认垃圾回收器

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

10.创建对象方式

1.new

2.反射:

  Class<?> obj1 = Object.class;
  
  Class<?> obj2 = obj.getClass();
  
  Class<?> obj3 = Class.forName("java.lang.Object");

最后再用 obj1.newInstance()调用无参构造器

也可以获得构造器对象

img

3.clone();

4.反序列化

11.对象头有什么

运行时元数据,

HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳

类型指针

指向方法区

12.对象包括什么

运行时元数据,

HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳

类型指针

指向方法区img

13.对象的创建过程

判断类是否加载,分配内存,设默认值,设对象头,初始化

1.判断对象的类是否加载,链接,初始化

虚拟机遇到一个new指令,首先区检查能否在常量池中定位到类的符号引用,并检查这个符号代表的类是否被加载解析初始化

如果没有,那么在双亲委派机制下,去查找.class文件,如果没找到对应的.class文件,则抛出ClassNotfoundException,如果找到,

则进行类加载

2.为对象分配内存,如果内存规整用指针碰撞

如果内存不规整,用空间列表

虚拟机维护一个列表,记录哪块内存块是可用的分配的时候再从列表中安到一个足够大的空间划分给对象实例

3.为每个线程分配一个TLAB

4.初始化分配到的空间,为所有属性设置默认值

5.设置对象对象头

6.初始化(init)

14.引用

1.强引用:

就是new出来的对象

永远不会被回收

   Object object=new Object();

2.软引用 :

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它

应用:如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出这时候就可以使用软引用

   String str=new String("abc");                                     // 强引用
   SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

3.弱引用:

只要发现就回收

   String str=new String("abc");    
   WeakReference<String> abcWeakRef = new WeakReference<String>(str);

4.虚引用

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动

15.双亲委派机制

当一个类加载器需要加载一个类时,它首先会将这个任务委托给它的父类加载器去完成,如果父类加载器还存在父类加载器,那么它会继续向上委托,直到委托给最顶层的启动类加载器为止。只有当父类加载器无法完成这个任务时,子类加载器才会尝试自己去加载这个类。

16.minor gc,full gc频繁怎么处理

使用工具分析jstats对gc进行采样,然后jmap dump导出堆镜像

对gc日志进行分析,查看dump文件

调整堆内存大小,这个操作不是一次就完成,可以进行试错

最后发现是sftp上传工单文件时,每次都会new出来一个sftpclient出来,必须主动掉用shutdown才会关闭,导致的fullgc

最后把这个sftpclient做成单例的就好了

17.内存模型

1.类加载器

把字节码文件加载进内存 ,只负责加载,至于能不能运行由执行引擎来决定

1.类加载的过程

1.加载阶段:把.class文件加载进内存

2.链接阶段:

验证:保证加载的类不会危害虚拟机

准备:为类变量赋默认值(即0),不包括final修饰的类变量

解析:将常量池中的符号引用转换为直接引用

3..初始化阶段

执行类构造器方法<clinit>(),为类属性赋默认值

2.如何自定义类加载器

只需要继承ClassLoader类,覆写findClass方法即可

2.程序计数器

存储下一条指令地址 ,线程私有,每一个每一个线程都有自己的程序计数器,生命周期和当前线程保持一致

3.虚拟机栈

线程私有的内存区域, 当一个方法被调用时,就会在该线程的虚拟机栈中压入一个对应的栈帧,当该方法执行完毕后,该栈帧也会被出栈。每个栈帧包括

局部变量表:定义一个数组,用来存储变量

操作数栈:用来做变量操作的栈

动态链接:在运行期间符号引用转换为直接引用(多态)

方法返回地址:记录了调用该方法的程序计数器的值

4.堆

线程共享的一块内存区域,用于存储Java对象。在Java堆中,对象的分配和回收都是由垃圾回收器完成的。Java堆中存储的数据主要包括实例变量、数组以及其他对象引用。

5.方法区

类型信息,jit代码缓存,域信息,方法信息

jdk1.6前静态变量在永久代,jdk1.7就已经把静态变量和字符串常量池,放到了堆中,因为放在堆中在垃圾回收时可以及时回收内存,jdk1.8将永久代变为元空间,因为永久代内存很难控制,给小了报oom,给大了还浪费对永久代调优很难,而元空间的内存大小可以按需调整

6.执行引擎

将字节码解释或编译成为机器指令

7.本地方法栈

主要用于执行本地方法,即由Java调用本地的C语言动态库等操作接口。

7.集合

1.hashmap在jdk1.7和1.8的区别

数组+链表 头插法

数组+链表+红黑树 尾插法

2.hashmap链表什么时候变为树

当数组长度大于64并且链表长度大于8

3.hashmap工作原理

HashMap是Java中最常用的数据结构之一,它基于哈希表实现,可以快速的查找。

是通过对key做哈希映射来实现的快速查找。每次查询只需获取到key的hash值,在数据较少时时间复杂度是O1,数据量较大是时间复杂度是Ologn。

4.hashmap put流程

1.一开始先看看数组是不是空,

如果是空

就扩容

如果不是

就计算 出索引值(数组长度-1后和hash按位与) -------------i = (n - 1) & hash

2.再看看索引位置是不是null

如果是则直接插入

\3. 再看看数组长度大没大于16/32/64

是则扩容

否则结束

如果索引位置不是空

再看看key是否与之前的key相同

是则调用equals方法比较value

不是,看看当前索引位置是否是树节点

是则向红黑树中插入

不存在,则遍历链表,插入

4.最后看看链表长度大没大于8

大于则扩容,扩不了就转化为红黑树

5.HashMap 和 Hashtable 的区别

1.线程是否安全

2.效率

3.hashtable的键不能有null

4.hashtable初始大小为11,扩容为2n+1

hashmap初始大小是16,扩容是每次扩大为原来的两倍

6.hashmap和concurrenthashmap区别

hashmap:在jdk1.7是 分段的数组+***链表*** ,jdk1.8的时候跟HashMap1.8的时候一样都是基于数组+链表/红黑树

线程不安全

currenthashmap:在jdk1.7是 分段的数组+***链表*** ,jdk1.8的时候跟HashMap1.8的时候一样都是基于数组+链表/红黑树

在jdk1.7的时候是使用分段所segment,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率

在jdk1.8的时候摒弃了 Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronizedCAS 来操作。synchronized只锁定当前链表或红黑二叉树的首节点。

为什么这么优化

锁的粒度 首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。 Hash冲突 JDK1.7中,ConcurrentHashMap从过二次hash的方式

(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。 扩容 JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。

7.hashmap在1.7时如何成为死链,在jdk1.8时如何丢数据

1.7

首先说下闭环产生的原因:1.7的HashMap在扩容复制时,采用的是头插入法,这会导致扩容时原数组中的链表反转,即将原来的正向,复制成反向链表。

img

而在多线程环境下,可能存在其他线程完成了扩容复制操作,完成了后的数组链表变成了原来的反向链表。

由于可见性的保证,当前线程继续复制时,复制的时其他线程已经完成了的反向链表,但当前线程又会将其反转,导致又变成了正向链表,那么在当前线程复制过程中会既有正向的链表又有反向的链表,从而可能产生闭环的情况。

1.8

put方法调用resize有线程安全问题,然后就丢数据

8.迭代器遍历集合报错问题(List,Map)

   举个例子:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。

    迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。    看异常ConcurrentModificationException,JDK中是这么介绍该异常的:当检测到一个并发的修改,就可能会抛出该异常,一些迭代器的实现会抛出该异常,以便可以快速失败。但是你不可以为了便捷而依赖该异常,而应该仅仅作为一个程序的侦测

解决方法:不用迭代器

9.hashmap如何扩容

由于数组长度改变,原来算出的hash值就不对了。需要把所有元素重新拿出来,再算一遍hash值。从左到右,从上到下。

10.HashMap,LinkedHashMap,TreeMap有什么区别?

LinkedHashMap保存了插入顺序;

TreeMap实现SortMap接口,可以对key进行排序

11.说一说copyOnWriteList

CopyOnWriteList是Java集合框架中的一种线程安全的List实现,它通过复制整个List来实现线程安全,即在修改List时,先将原有的List复制一份,然后在新的List上进行修改操作,最后将新的List赋值给原有的List。

具体来说,当一个线程需要修改CopyOnWriteList时,它会先复制整个List,然后在新的List上进行修改操作,最后将新的List赋值给原有的List。由于复制操作是在新的List上进行的,所以不会影响其他线程对原有List的访问。

CopyOnWriteList的优点是线程安全,不需要使用锁来保护共享资源,因此可以提高程序的并发性能。

CopyOnWriteList的缺点是在修改List时需要复制整个List,因此会占用较大的内存空间和CPU资源。

总的来说,CopyOnWriteList是一种适用于读多写少场景的线程安全List实现,它通过复制整个List来实现线程安全,避免了使用锁的性能问题,但在修改List时需要复制整个List,会占用较大的内存空间和CPU资源。

12.ConcurrentHashMap的put流程

Java 7的ConcurrentHashMap的put操作分为以下步骤:

  1. 根据key的哈希值,计算出要插入的桶的索引位置。

  2. 使用锁来保证该桶的线程安全,加锁成功则可以进行操作,否则需要重试。

  3. 遍历该桶中的链表,查找是否已经存在该key值。

  4. 如果找到该key,则使用新的value替换旧的value。

  5. 如果没有找到该key,则在该桶的链表末尾添加新的键值对。

  6. 如果链表长度超过阈值,则需要将该链表转换为红黑树,以提高查找效率。

  7. 操作完成后,释放锁。

Java 8的ConcurrentHashMap的put操作则使用了更加先进的锁分离技术来提高并发性能,流程如下:

  1. 根据key的哈希值,计算出要插入的桶的索引位置。

  2. 使用long类型的baseCount和StampleNode类型的node来进行锁分离,获取一个hash值和一个node节点。

  3. 如果节点为空,则调用doHelpSpread方法帮助迁移,具体来说,会尝试取到锁,并根据当前迁移状态重新保证迁移的正确性。

  4. 如果节点已经被初始化,则使用synchronize-on-the-contention-point (SCTP)锁分离,即只根据当前桶的状态(链表或红黑树)加锁。

  5. 如果链表长度超过阈值,则需要将该链表转换为红黑树,以提高查找效率。

  6. 操作完成后,释放锁,更新计数器统计插入的元素个数。

13.ConcurrentHashMap的如何扩容

在 JDK 1.7 中,ConcurrentHashMap 的扩容流程是通过 Segment 进行的,它是一个分段锁实现的数组,每个 Segment 可以看作是一个独立的小型哈希表,每个 Segment 中包含若干个 HashEntry,每个 HashEntry 是一个哈希桶,存储具有相同哈希值的键值对。在进行扩容时,会对所有 Segment 依次进行扩容操作,它的大致流程如下:

  1. 扩容时首先会将 ConcurrentHashMap 容量的大小翻倍,然后为每个 Segment 分配新的 HashEntry 数组,将旧的 HashEntry 数组中的键值对重新分配到新的数组中,此时需要对每个 Segment 进行独立的扩容操作。

  2. 在进行 Segment 扩容时,会获取该 Segment 的独占锁,然后将所有 HashEntry 中的键值对全部重新分配到新的 HashEntry 中,这个过程中会对每个键值对进行重定位操作,将它们分配到新的 HashEntry 中去,如果有多个键值对映射到同一个新的 HashEntry,则将它们组织成链表。

  3. 所有 Segment 扩容完成后,会将新的 Segment 数组赋值给 ConcurrentHashMap。

而在 JDK 1.8 中,ConcurrentHashMap 的扩容流程进行了大量改进,实现了无锁分段扩容,可以同时对多个 Segment 进行扩容,它的流程大致如下:

  1. 扩容时会将 ConcurrentHashMap 的容量大小翻倍,并且为每个 Segment 分配新的 HashEntry 数组,将旧的 HashEntry 中的键值对重新分配到新的数组中,这个过程是在多个线程中同时进行的,每个线程负责处理一个或多个 Segment。

  2. 在对一个 Segment 进行扩容时,会首先将该 Segment 对应的 HashEntry 数组中的所有元素复制到新的 HashEntry 数组中,这个过程是无锁的,通过 CAS 操作实现。

  3. 扩容完成后会将新的 Segment 数组设置为 ConcurrentHashMap 的属性,这个过程也是无锁的。

可以看出,在 JDK 1.8 中,ConcurrentHashMap 的扩容过程更加高效、并发,大大提升了它的性能和并发能力。

8.JUC

1.创建线程的方式

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

   @Slf4j(topic = "c.Test1")
   public class Test1 {
       public static void main(String[] args) {
   //        线程和任务结合
           Thread t= new Thread(() -> log.debug("running"));
           t.setName("线程1");
           t.start();
           log.debug("running");
       }
   }

2.实现runnable接口,实现run方法

   @Slf4j(topic = "c.Test2")
   public class Test2 {
       public static void main(String[] args) {
           //任务
           Runnable task2 = () -> log.debug("running");
           //线程
           //把线程和任务分开
           Thread t = new Thread(task2, "t2");
           t.start();
       }
   }

其实他俩本质一样,因为Thread类也实现了runnable接口

但第种方式把线程和任务分开了,更加灵活

3.实现Callable接口重写call方法

futuretask也间接实现runnable接口所以可以用于当成任务

用来获得任务的执行结果,异步执行

   @Slf4j(topic = "c.Test3")
   public class Test3 {
       public static void main(String[] args) throws ExecutionException, InterruptedException {
           // 任务
           FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
               @Override
               public Integer call() throws Exception {
                   log.debug("多线程任务");
                   Thread.sleep(100);
                   return 100;
               }
           });
           // 主线程阻塞,同步等待 task 执行完毕的结果
           
           new Thread(futureTask,"我的名字").start();
           log.debug("主线程");
           log.debug("{}",futureTask.get());
       }
   }
   

Future的局限性

future不知道什么时候任务会完成,如果想要获取结果必须要get,如果get不到线程会一直阻塞影响效率,没有回调函数

2.讲一下CAS

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

一个 CAS 涉及到以下操作: 我们假设资源对象的原本值为V,线程旧的预期值A,需要修改的新值B,

  1. 比较 A 与 V 是否相等。(比较)

  2. 如果比较相等,将 B 写入 V。(交换)

  3. 返回操作是否成功。

那么如何保证CAS的原子性呢

cpu原生支持cas,本来就是原子的,上层进行调用即可

3.ABA问题,怎么解决

CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。

如何解决ABA问题 加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会出现老的值。

4.项目中用过乐观锁吗

用过,就是用户发短信,我们这边就会产生一个短信编号。短信编号的生成是并发的,所以我们就用了CAS校验,就加一个版本号,判断是否是当前版本。

1.项目中用过多线程吗

用过在Excel导入时,用多线程,用线程池整4个线程。因为有安全问题,就把任务整的不一样,第一个线程从1开始,第二个线程从2开始。。。步长为4.这样安全问题迎刃而解

ExecutorService executorService = Executors.newFixedThreadPool(4);

// 向线程池提交不同的任务 executorService.submit(new TaskA()); executorService.submit(new TaskB()); executorService.submit(new TaskC());

executorService.submit(new TaskD());

在发短信时用的异步处理

在发短信时,使用异步处理可以提高程序的响应速度和吞吐量,避免阻塞主线程。在Java中,可以使用多线程或异步编程框架实现异步处理。下面是一个使用Java多线程实现异步处理的示例代码:

private ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建线程池

public void sendSms(String phoneNumber, String message) { executorService.execute(() -> { // 提交任务到线程池 // 发送短信的具体逻辑 System.out.println("Sending message to " + phoneNumber + ": " + message); }); } }

5.聊一下悲观锁

synchronized是悲观锁,但这个jdk自带的锁,经过jdk版本的迭代已经很不错了。jdk1.6引入了轻量级锁和偏向锁,大大的提高了效率。

6.锁升级

无锁 偏向锁 轻量级锁 重量级锁

7.sleep,wait,park,join,yield区别

sleep:线程进入阻塞状态,Thread类的方法,后面接时间可以自己唤醒,不释放锁

wait:是Object类的方法,必须配合synchronized一起使用,释放锁

park:是lock锁的方法,通过信号量实现阻塞,不释放锁

join:底层是wait

yield:将自己线程的时间片分给别的线程,但不太靠谱,只能降低概率

8.死锁

一个线程想要同时获得多把锁导致死锁

t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁

t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

避免死锁:放一个加锁时限,优化加锁策略

9.synchronized和Lock的实现原理与区别

synchronized

有一个monitor对象,他是操作系统级别不是java级别,获得锁的线程在monitor对象的owner的位置

被阻塞的线程在monitor对象的等待队列中

当获得锁的线程的时间片用完时,会将owner致为空,然后去等待队列中去唤醒被阻塞的线程img

而Lock锁的实现原理就是

CAS AQS park unpark

至于Lock锁相对于synchronized的区别就是

可中断 可以设置超时时间 可以设置为公平锁,解决线程饥饿问题 支持多个条件变量

所以lock锁更加灵活

synchronizedLock 是 Java 中用于实现线程同步的两种机制。

synchronized 是 Java 语言内置的关键字,可以在方法或代码块上加锁,保证同一时间只有一个线程可以执行该方法或代码块。synchronized 关键字的实现原理是基于 Java 对象头中的监视器(Monitor)和管程(Monitor Control Block,MCB)实现的。每个 Java 对象都有一个唯一的监视器和一个对应的 MCB,监视器用于控制并发访问对象,MCB 用于记录线程状态和等待队列信息。当一个线程尝试获取一个被 synchronized 关键字保护的对象时,它会进入到该对象的等待队列中,直到获取到对象的监视器才能执行相应的操作。

Lock 是 Java 中用于实现线程同步的另一种机制,它提供了比 synchronized 更加灵活和可控的线程同步方式。Lock 接口提供了 lock()unlock() 两个方法,分别用于获取锁和释放锁。与 synchronized 不同的是,Lock 接口提供了可重入锁(ReentrantLock)和公平锁(FairLock)等高级锁的实现,可以更加灵活地控制锁的获取和释放。

AQS原理

Lock 的实现原理是基于 Java 的 AbstractQueuedSynchronizer(AQS)框架实现的,AQS 通过对状态变量的修改和线程等待队列来实现对锁的管理。当一个线程尝试获取一个 Lock 保护的资源时,它会先判断状态变量是否允许获取该资源,如果允许就直接获取资源;如果不允许,该线程会进入到等待队列中等待,直到状态变量允许它获取该资源。

总的来说,synchronizedLock 都可以用于实现线程同步,但是它们的实现原理和使用方式不同。synchronized 是 Java 内置的关键字,使用起来简单方便,但是灵活性不够;Lock 是 Java 提供的接口,提供了更加灵活和可控的线程同步方式,但是使用起来比 synchronized 更加复杂。在实际开发中,应根据具体的业务需求和性能要求选择适合的线程同步机制。

10.ThreadLocal作用

可以将数据缓存在我们的线程内部,就是缓存在我们线程内的一个map里面threadlocalmap

一个线程可以有多个threadlocal对象,用threadlocalmap储存 ,通过get方法取出

内存泄漏

因为线程在一直用,所以map也一直被占用,垃圾回收不收

只需要把不用的数据remove掉就行

为什么要设计为弱引用

因为ThreadLocal的东西是经常使用的,不需要每次垃圾回收都看一遍。

使用场景

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离,【你访问你的,我访问我的,不会相互干扰】

3、Spring的事务(ThreadLoacl和AOP)

4、数据库连接,Session会话管理。

11.volatile关键字的作用

1.保证可见性,不保证原子性

2.保证有序性,被volatile修饰的不会被编译器优化

原理:每次对的共享变量的读取和修改都会同步到主存中

1.final

final修饰的变量不可被改变,是线程安全的

但final修饰的引用类型不是线程安全的

2.为什么被synchronized修饰的变量是可见的

线程获得锁的时候会清空工作内存,把主内存的数据同步过来。数据修改后再同步到主内存中。

12.JMM内存模型

就是一种概念,规定了内存如何管理和分配,保证了线程之间的可见性,有序性,原子性

它定义了三种内存

\1. 主内存(Main Memory):所有线程共享的内存区域,包含了所有的变量值。

\2. 工作内存(Working Memory):每个线程独有的内存区域,线程的操作都在工作内存中进行。线程之间的数据不能直接共享,需要通过主内存来进行通信。

\3. 本地内存(Native Memory):在 Java 程序中使用 Native 方法时,需要使用本地内存来存储数据。

JMM 确保线程之间的操作具有可见性、有序性和原子性:

\1. 可见性:一个线程修改的数据,对于其他线程是可见的,即线程之间能够正确地共享变量值。

\2. 有序性:在 Java 中,编译器和处理器会对指令进行重排序。JMM 通过禁止特定类型的重排序,保证了程序中的顺序性。

\3. 原子性:JMM 通过提供原子性操作,保证了对于多线程中的共享变量,对它的操作是原子性的,即不会出现数据不一致的情况。

总之,JMM 为多线程环境下的程序提供了一种可靠的内存模型,确保了程序中的操作能够按照正确的顺序进行,并保证了线程之间数据的可见性和原子性。

13.ReentrantLock优点

可以设置超时时间,公平锁,支持条件变量,可中断

1.可重入锁

2.读写锁

3.信号量

4.闭锁

14.线程池

1.工作原理

线程池是一种多线程处理方式,其主要原理是通过预先创建多个线程并将它们放入线程池中,等待任务的到来。当任务到来时,线程池会从池中取出一个线程来执行该任务,当任务执行完成后,该线程会返回到线程池中等待下一个任务的到来。线程池的好处在于可以避免线程的频繁创建和销毁,提高了系统的性能和效率,同时还可以控制线程的数量,避免线程数量过多导致系统资源的浪费。线程池的组成主要包括任务队列、工作线程和线程池管理器。

img

2.线程池参数

核心线程永远不会死,救急线程在没有工作就会死 。在核心线程不够用时会再new出救急线程

  public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                                int maximumPoolSize,//     最大线程数
                                long keepAliveTime, //       生存时间,针对救急线程
                                TimeUnit unit,         //        时间单位,针对救急线程
                                BlockingQueue<Runnable> workQueue,   //   任务队列
                                ThreadFactory threadFactory,    // 线程工厂,起个好名字
                                RejectedExecutionHandler handler  //  拒绝策略
      ) 
  { ... }

3.线程池的流程

  1. 当有任务需要执行时,将任务提交到线程池。

  2. 线程池首先判断是否仍有空闲的核心线程,如果有,那么就将任务交给空闲的核心线程执行。

  3. 如果所有核心线程都在执行任务,那么线程池会将任务加入到任务队列中等待执行。任务队列可以是无界的(不限制大小)或有界的(有固定的大小限制)。

  4. 如果任务队列已满,那么线程池会判断是否仍有空闲的救急线程。若有,则将任务交给其中一个非核心线程进行处理,否则线程池会拒绝执行该任务。

  5. 当一个线程执行完任务后,它会继续从任务队列中获取新的任务执行。如果任务队列中没有任务了,线程将等待新的任务被提交到线程池。

  6. 当线程池不再被使用时,可以调用线程池的shutdown()方法来关闭线程池。线程池将会拒绝接受新的任务,并等待所有的任务执行完成后退出。

4.阻塞队列的分类

  1. ArrayBlockingQueue:基于数组实现的有界阻塞队列。插入、删除操作都是具有阻塞特性的。当队列已满时,会阻塞插入操作;当队列为空时,会阻塞删除操作。

  2. LinkedBlockingQueue:基于链表实现的有界或无界阻塞队列。如果传入构造函数的容量为正数,则是一个有界队列。插入、删除操作都是具有阻塞特性的。

  3. PriorityBlockingQueue:带优先级的无界阻塞队列。插入操作可以在任何时候进行,但是删除操作会等待队列中存在元素时才能进行。元素按照它们具有的优先级进行排序。

  4. SynchronousQueue:一种特殊的阻塞队列,每个插入操作必须等待一个相应的删除操作,否则插入操作就会被阻塞。同样,每个删除操作也必须等待一个相应的插入操作,否则删除操作就会被阻塞。

  5. DelayQueue:具有延迟特性的无界阻塞队列,用于调度系统。元素必须实现 Delayed 接口,并且队列会根据元素的延迟时间进行排序。

5.拒绝策略

img编辑AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。 CallerRunsPolicy:不抛异常,让调用者自己运行任务

DiscardPolicy:啥也不干 DiscardOldestPolicy:抛弃最老策略。

上面四种是jdk自带的拒绝策略

Dubbo:抛异常之前记录日志,方便定位错误

Netty:再new一个新线程

ActiveMQ:超时等待(60s)

tomcat线程是如何优化的

当线程数大于最大线程数时,不立即执行拒绝策略,而是放到队列里,之后再尝试一次,如果还是失败,就拒绝

6.JDK提供线程池缺点

  • Executors.newFixedThreadPool创建固定大小线程池,OOM

  • Executors.newSingleThreadExecutor 创建一个单线程的线程池,太慢了,堆积大量任务,OOM

  • Executors.newCachedThreadPool 创建n个线程的线程池 随机扩容 可能会创建出大量线程,导致OOM(内存溢出)

  • Executors.newScheduledThreadPool:如果提交的任务过多或者任务的执行时间过长,会导致线程池中的线程数不断增加,最终达到JVM的最大限制,导致程序抛出OutOfMemoryError。

7.异步编程方法

1.springboot注解@Async

在启动类@EnableAsync

在你要的异步方法上@Async

2.使用工具类jdk1.8提供的CompletableFuture

img

8.线程池抛出异常怎么处理

1.如果未捕获异常,线程会立刻终止,同时记录日志

2.如果捕获异常,有不同的策略处理,比如直接丢弃或者将任务返回给任务的提交者执行

3.咱们的CompleteFuture已经考虑到了这一点,以链式编程的形式对异常进行处理,可以自定义异常的处理逻辑

9.submit和excute的区别?

线程池的 submit()execute() 方法都是用来提交任务到线程池中执行的,它们之间的主要区别如下:

  1. 返回值不同:

    execute() 方法没有返回值,只能执行 Runnable 任务。

    submit() 方法可以执行 RunnableCallable 任务,并且可以返回一个 Future 对象,用来获取任务执行的结果或异常。

  2. 抛出异常不同:

    execute() 方法只能抛出 RuntimeExceptionError 异常。

    submit() 方法可以抛出任何类型的异常,需要通过 Future.get() 方法捕获并处理异常。

  3. 参数不同:

    execute() 方法只接受 Runnable 对象作为参数。

    submit() 方法可以接受 RunnableCallable 对象作为参数,还可以接受一个泛型参数用来返回结果。

  4. 返回的 Future 对象不同:

    execute() 方法没有返回值,也就不会返回 Future 对象。

    submit() 方法会返回一个 Future 对象,通过它可以获取任务的执行结果、取消任务、等待任务完成、获取任务执行的状态等信息。

一般来说,推荐使用 submit() 方法,因为它更加灵活,支持执行 Callable 任务,并且可以获取任务执行的结果和异常信息。如果只需要执行 Runnable 任务,也可以使用 execute() 方法,它的使用比较简单。

15.进程,线程,协程????

进程是操作系统分配资源的最小单位

线程是cpu调用的最小单位

协程:另外一种支持多线程的方式

16.java线程状态

new新建

runnable运行

blocked阻塞

waiting等待

timed_waiting超时等待

terminaled终止

17.线程之间如何通信

1.共享变量 :在使用共享变量时要注意线程安全问题,比如秒杀

2.wait notify

3.阻塞队列,一个线程放数据,一个线程拿数据

4.闭锁: 可以通过CountDownLatch来实现线程之间的协调,一个CountDownLatch可以使一个或多个线程等待其他线程完成任务后再执行。

5.信号量:信号量可以用于控制同时可以访问资源的线程数量,每个线程想要访问需要获取信号量

6.future接口 get方法获取上一个线程的结果

18.如何根据cpu核数来设置线程池线程数

程序的线程和cpu的线程(或者说核数)没有直接关系,一个线程在执行一系列指令,会申请cpu,内存资源,但不会一直占着cpu不放,只有需要使用cpu计算的时候,才会占用cpu,当io线程sleep时,会把cpu释放,其他的线程依然可以使用释放的cpu

这也是为什么一个8核的cpu,计算量大的线程 8个线程就能把cpu占到100%,而IO量特别大的线程,起50个线程都不会把这个8核的cpu占满。

java

java并发编程实践》书上说

线程池最优大小=CPU逻辑数CPU期望使用率任务比例(w等待时间、c计算时间)。

但也就是书上这么说

一般说来,大家认为线程池的大小经验值应该这样设置:(其中N为CPU的个数)

  • 如果是CPU密集型应用,则线程池大小设置为N+1

  • 如果是IO密集型应用,则线程池大小设置为2N+1

19.如何优雅的停止线程

1.interrupt()方法,对线程更新打断标记, 之后当检测到打断标记为true时,可以根据自己的业务来执行

2.使用一个标志位,运行时检查这个标志位.当需要终止线程时,将标记设置为true即可

9.Spring

1.Spring的优缺点是什么

优点:

1.集中管理对象,降低对象与对象之间的耦合度

2.在不修改代码的情况下,可以对业务功能增强

3.Spring极大的提高了开发效率,比如加个事务只需一个注解

4.方便了程序的测试

5.Spring粘合能力特别强,只需简单地配置就能使用第三方框架

...

缺点:

底层太复杂

2.谈谈你对Spring的理解

Spring是一个非常庞大的一个生态体系,

通常Spring指的是SpringFramework

他可以构建我们java应用所需的一切基础设施,

其中最重要的就是可以降低我们代码的耦合性

他是一个轻量级的容器框架,核心技术IOCAOP

  • IOC:控制反转

    控制:创建对象

    反转:将对象交给Spring容器管理

    IOC技术实现:DI(1.xml配置文件 2.注解)

    依赖注入

  • AOP:面向切面编程,可以对功能增强

  • 容器可以更方便的管理对象的生命周期

对SpringMVC的理解

SpringMVC是一个基于MVC(Model-View-Controller)模式的Web框架,用于开发Web应用程序。它是Spring框架的一部分,可以帮助开发者创建可维护、灵活、可扩展的Web应用程序。

在SpringMVC中,控制器(Controller)作为中心组件,接受来自客户端的请求,根据请求参数和业务逻辑处理结果,并且将处理结果封装成模型(Model)和视图(View)返回给客户端。总之,SpringMVC是一个灵活、可扩展、易于使用的Web框架,可以大大简化Web应用程序的开发工作。

对Springboot的理解

Spring Boot是一个基于Spring框架的快速开发脚手架,可以帮助开发者快速构建基于Spring的应用程序。它的主要目的是简化新项目的初始搭建和开发过程,提高开发效率。

相对于传统的Spring开发方式,Spring Boot在开发过程中能够自动配置一些常用的框架组件,例如数据源、Web服务器、消息队列等,开发人员只需要添加相应的依赖,即可获得这些默认的配置,使得开发过程更加高效。

Spring Boot的主要特点包括:

  1. 自动配置:基于约定优于配置的原则,框架能够根据项目依赖自动选择并配置合适的组件。

  2. 无侵入式开发:在开发过程中,不会限制开发人员使用的框架,支持与现有Spring项目无缝集成。

总之,Spring Boot是一个高效、灵活、方便的框架,可以使得开发人员更快地构建出高质量的Spring应用程序,因此在当下Java Web开发中受到了广泛的关注和应用。

2. Spring是如何创建对象的,IOC的流程

1.创建容器

2.加载配置并解析配置文件和注解配置信息,将他们封装成BeanDefination对象

3.调用Bean工厂后置处理器

4.实例化,创建初始对象

5.初始化

6.注册销毁方法:如果Bean实现了Spring提供的销毁接口DisposableBean,Spring IOC容器会将该Bean的销毁方法注册到容器中,在容器关闭时调用销毁方法。

6.获取完整对象

3.BeanFactory和ApplicationContext的区别

共同点:

都是容器,可以管理对象的生命周期

不同点:

  1. ApplicationContext不生产Bean而是通知BeanFactory来创建Bean,底层new DefaultListableBeanFactory();工厂

但ApplicationContext功能更多

<1>MessageSource, 提供国际化的消息访问 <2>资源访问(如URL和文件) <3>事件传递 <4>Bean的自动装配 <5>各种不同应用层的Context实现

  1. ApplicationContext加载配置文件时就创建对象(配置文件中所有bean)

BeanFactory在getBean时才会创建对象

4.常用注解及其作用

创建对象:

@Component: 一个component表示一个bean对象,属性value表示id值 ,默认是类名首字母小写

@Repository: dao层接口实现类上,持久层

@Service:表示业务层对象

@Controller:表示控制器对象


依赖注入:

@Value:注入值,可以放在成员变量或者set方法上,一般使用外部文件注入

@Autowired:自动注入引用类型,默认是byType形式 ,属性required默认true,必须要有与其对应的引用类型

@Qualifier:与@Autowired一起用是byName形式

img

@Resource:默认byName,jdk提供的注解,如果byName赋值失败那么就用byType


AOP:

@Before:前置通知

@AfterRunning:后置通知

@Around:环绕通知

@AfterThrowing:异常通知

@After:最终通知

@Transactional:配置事务

SpringMVC:

@RequestBody: 将前端发送的json数据,封装成对象或者数组或者map

@RequestParam: 是get请求封装数据

@PathVariable: 传递路径变量

@RequestMapping:为处理请求的 URL 指定方法或控制器类

不常用的:

  1. @Scheduled:配置定时任务,例如每小时执行一次任务。

  2. @Async:启用 Spring 异步任务。

5.SpringIOC的实现机制

简单工厂,反射

  1. 读取配置文件:在Spring中,我们通过XML文件或注解来配置Bean对象。Spring IOC会读取这些配置文件并用来初始化IOC容器。

  2. 初始化容器: 配置文件中定义的Bean,Spring IOC会通过反射机制来实例化这些Bean,并将它们存储在容器中。

  3. 注入依赖:在Spring IOC容器中,通过配置文件或注解声明Bean之间的依赖关系。Spring IOC通过这些依赖关系,将一个Bean注入到另一个Bean中。

  4. 获取Bean:在应用程序中需要使用Bean时,可以通过容器获取它们。

SpringAOP的原理

面向切面编程。它是一种编程思想,可以帮助我们在程序运行时,通过预定义的切点把逻辑织入到应用程序中。原理就是

AOP会动态生成代理对象,对目标对象进行增强,将我们预定义的通知代码织入到目标对象方法调用前、调用后、出现异常和方法返回后等不同时机执行,从而实现共性的横切逻辑。这样,我们就可以在不修改原有业务代码的基础上,增加、删除、修改一些共性的横切逻辑,提高我们的代码复用性和可维护性。

6.IOC和DI的区别

IOC控制反转是思想

DI是IOC的实现

7.BeanFactory作用

他是Spring中非常核心的接口,主要职责就是生产bean,实现了简单工厂的设计模式

有非常多的实现类,每个工厂职责不同,最强大的是DefaultListableBeanFactory,Spring底层就是通过它来生产Bean的

8.BeanDefinition作用

存储Bean的定义信息

9.SpringIOC的扩展点

Spring IOC容器的扩展点(Extension Points)可以让开发人员在应用程序启动和运行期间插入自定义代码,以满足各种特定的需求。以下是Spring IOC容器的扩展点:

\1. BeanPostProcessor:Bean后处理器接口是Spring IOC容器中最常用的扩展点之一。Bean后处理器接口允许开发人员在Bean实例化之后、初始化之前或之后拦截Bean的初始化过程,并对其进行自定义处理。Bean后处理器可以用于实现日志记录、性能监视、安全检查等方面的处理

\2. BeanFactoryPostProcessor:Bean工厂后处理器是在Spring容器实例化并加载所有Bean定义之后运行的扩展点。它可以修改或添加到容器中的Bean定义。可以通过实现BeanFactoryPostProcessor接口并将其注册到Spring IOC容器中来使用Bean工厂后处理器。

\3. BeanDefinitionRegistryPostProcessor:Bean定义注册表后处理器是BeanFactoryPostProcessor的一个子接口,它在BeanFactoryPostProcessor之前运行,并提供了修改Bean定义的能力。可以通过实现BeanDefinitionRegistryPostProcessor接口并将其注册到Spring IOC容器中来使用Bean定义注册表后处理器。

\4. InstantiationAwareBeanPostProcessor:实例化感知Bean后处理器接口是Bean后处理器接口的一个扩展,它提供了更高级的自定义Bean实例化的能力。InstantiationAwareBeanPostProcessor接口在实例化Bean对象之前或之后拦截并处理Bean实例化的过程。

\5. ApplicationContextInitializer:应用程序上下文初始化器接口是一个用于Spring应用程序上下文初始化的扩展点。ApplicationContextInitializer可以在应用程序上下文创建之前或之后拦截并对其进行自定义处理。可以通过实现ApplicationContextInitializer接口并将其注册到Spring IOC容器中来使用它。

\6. ApplicationListener:应用程序监听器接口是一个通用的Spring事件处理机制,用于在应用程序生命周期中处理各种事件。ApplicationListener接口可以在应用程序启动、关闭、部署、错误处理等各种事件发生时进行自定义处理。

通过使用这些扩展点,开发人员可以将自定义代码插入Spring IOC容器中的不同阶段,从而实现更高级的自定义处理。

10.代理

jdk动态代理:目标类必须要实现接口,如果没有实现接口就不行

为什么必须实现接口不能继承

因为继承了Proxy类,实现了代理的接口,由于java不能多继承,这里已经继承了Proxy类了,不能再继承其他的类,所以JDK的动 态代理不支持对实现类的代理,只支持接口的代理。

cjlib动态代理:

代理类继承目标类,覆盖其所有此方法,所以目标类和方法不能声明为final形式

性能上cjlib>jdk

11.Bean的生命周期

img

在Spring容器中,Bean的生命周期可以分为以下几个阶段:

\1. 实例化

\2. 属性赋值(Population):当Bean实例对象被创建后,Spring容器会自动为其设置属性值,也就是进行依赖注入(DI)。

\3. BeanPostProcessor的前置处理(Initialization(Post-Processing Before Initialization)):在Bean实例化和依赖注入完成后,如果有配置了BeanPostProcessor的实现类,那么它们的postProcessBeforeInitialization方法就会被调用,这个方法可以对Bean实例进行自定义的处理。

\4. 初始化(Initialization):在BeanPostProcessor的前置处理完成之后,Spring容器会对Bean进行初始化,也就是调用Bean的init-method方法(如果有配置)。

\5. BeanPostProcessor的后置处理(Initialization(Post-Processing After Initialization)):在Bean初始化完成之后,如果有配置了BeanPostProcessor的实现类,那么它们的postProcessAfterInitialization方法就会被调用,这个方法同样可以对Bean实例进行自定义的处理。

\6. 成品对象

\7. 销毁(Disposal):当Spring容器关闭时,会对所有的Bean实例进行销毁,也就是调用Bean的destroy-method方法(如果有配置)。

12.Spring是如何加载配置文件的

通过beanDefinationReader读进来,然后再转换成document,有了固定格式,遍历每一个子元素来进行解析操作

13.如何解决循环依赖问题

构造器注入无解

set注入三级缓存

1.重新设置依赖关系

2.通过@Lazy注解解决循环依赖:如果两个Bean之间的依赖关系是通过注解方式实现的,可以通过在其中一个Bean上使用@Lazy注解,使其延迟初始化,从而避免出现循环依赖。

   @Service
   public class UserService {
       
       private DepartmentService departmentSerivce;
    
       @Autowired
       public UserSerivce(@Lazy DepartmentService departmentService) {
           this.departmentService = departmentService;
       }
    
       public List<Department> list() {
           retern departmentService.list();
       }    
    
   }

3.修改yml

spring: main: allow-circular-references: true

1.三级缓存存什么?

一级缓存放成品对象

二级缓存放半成品对象

三级缓存放lamda表达式

Spring在创建bean实例时会调用getBean方法,而getBean方法先从一级缓存中查找,如果没找到就在二级缓存中查找,二级缓存,存的是半成品对象,如果二级缓存中有bean就返回,如果还没有就说明容器没有这个对象,就去创建对象.这个对象就是早期的对象,将它存入二级缓存.当这个早期的bean走完了bean的生命周期,就会将这个bean转移到一级缓存中.

说到这好像三级缓存没什么用,二级缓存就足矣解决循环依赖的问题,但前提是不用SpringAOP

没有三级缓存就无法实现SpringAOP,因为无法区分bean和代理bean

三级缓存用来存代理的bean,当调用bean的getbean方法时发现需要代理工厂来创建,这个时候就会把目标bean保存在三级缓存中,最终也会把这个bean同步到一级缓存

2.构造注入也可以通过@Lazy懒加载的方式进行解决

15.SpringMVC的执行流程

1.向服务器发出请求,请求被SpringMVC中的dispatcherservlet捕获

2.dispatcherservlet对url进行解析,得到url,看看url对应的映射是否存在

3.如果存在,调用handlermapping返回一条执行链(拦截器和handler等等)

4.然后handlermapping再调用处理器适配器handleradapter执行controller

  1. 然后返回一个ModelAndView对象,再会回到前端控制器dispatcherservlet

  2. 调用ViewResolver视图解析器,返回View

  3. 使用ModelAndView渲染视图

16.什么是Bean工厂的后置处理器

\1. 修改Bean的配置信息,例如修改Bean的作用域、懒加载等属性。

\2. 注册新的Bean定义,例如自定义Scope、PropertySource等。

\3. 扩展自定义的注解,例如增加新的注解,修改注解属性值等。

\4. 其他任何需要在BeanFactory实例化Bean之前进行自定义操作的场景。

17.Bean的后置处理器

Bean后置处理器的作用是对Bean实例进行加工处理,Bean后置处理器的主要接口有两个:BeanPostProcessor和BeanFactoryPostProcessor。

在Bean实例化的过程中,Spring容器会按照Bean的生命周期顺序调用Bean后置处理器的两个方法:

\1. postProcessBeforeInitialization(Object bean, String beanName)方法:在Bean实例化后、初始化前调用。该方法可以对Bean进行任何操作,但是需要注意的是,该方法返回的是一个Object类型的Bean实例,如果需要修改Bean的属性值,需要返回一个新的Bean实例。

\2. postProcessAfterInitialization(Object bean, String beanName)方法:在Bean实例化后、初始化后调用。该方法同样可以对Bean进行任何操作,但是需要注意的是,该方法也需要返回一个Object类型的Bean实例,如果需要修改Bean的属性值,需要返回一个新的Bean实例。

Bean后置处理器的应用场景包括:

\1. 实现AOP功能:可以在Bean实例化后、初始化前或初始化后对Bean进行代理操作,实现AOP功能。

\2. 实现数据校验:可以在Bean实例化后、初始化前或初始化后对Bean进行数据校验,确保数据的正确性。

\3. 实现国际化:可以在Bean实例化后、初始化前或初始化后对Bean进行国际化操作,确保应用程序支持多语言。

\4. 其他任何需要对Bean进行定制化操作的场景。

18.Spring事务的实现原理

spring事务核心就是AOP

spring事务分为编程式事务和声明式事务

编程式事务:不常用,就是自己写代码来控制事务

声明式事务:注解的方式来声明事务。当我们使用@Transactional注解时,Spring会自动为我们生成一个代理对象,在代理对象中添加了事务管理的代码。当我们调用代理对象的方法时,Spring会自动开启事务,并在方法执行完毕后提交或回滚事务。

19.Controller是线程安全的吗

不一定

如果是单例的,多个线程共享一个controller那么线程不安全

如果是多例的,每个线程都有自己的controller那么线程安全

20.Springboot自动装配原理

就是一个springboot的starter里面的autoconfigure的一个jar包,里面是各种自动配置类

springboot核心注解有三个

 @SpringBootConfiguration
 @EnableAutoConfiguration 这个注解就是springboot自动装配的核心注解,没有这个注解Spring不会扫描这个autoconfigure依赖中的自动配置类
 @ComponentScan 用于扫描类路径,Spring就会自动的将这些类上带有Controller等的注册成bean

Spring Boot启动的时候会找到@EnableAutoConfiguration下的@Import(AutoConfigurationImportSelector.class) 将自动配置选择器导入到配置类中

之后用工厂加载META-INF/spring.factories里面的AutoConfiguration

所以Springboot在一启动就会加载这么 多类,但并不是全都生效,所以他按需加载,你加依赖了才会注册对应的组件

因为一个@Condition的注解,条件装配,只要有一个条件不符合就加载不了

21.Spring的设计模式

1.简单工厂

Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。

2.工厂方法

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Spring中的FactoryBean就是典型的工厂方法模式

3.单例

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

4.原型

多个实例

5.适配器

Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。SpringMVC适配器

6.包装器

动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活

7代理

AOP

8.观察者

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。Spring中Observer模式常用的地方是listener的实现。如ApplicationListener。

9.策略

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

10.模板

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。

22.看了Spring源码后对你写代码有什么收获?

  1. 设计模式的应用:Spring 框架中大量地运用了设计模式,例如工厂模式、代理模式、单例模式、装饰器模式等,通过学习 Spring 源码,我更加深入地理解了这些设计模式的用途和优缺点,可以更加准确地选择和应用设计模式,提高代码的可维护性和扩展性。

  2. 规范化的编码风格:阅读 Spring 框架源码,可以看到 Spring 团队在编码风格、代码结构等方面有非常高的规范要求,例如类的划分、方法的命名、注释的规范等,通过学习 Spring 源码,我对自己编写代码的规范要求也更加严格,有利于代码的可读性和可维护性。

  3. 代码的模块化和解耦合:Spring 框架具有非常好的模块化结构和解耦合特性,学习 Spring 源码有助于我将代码拆分成不同的模块,实现更好的解耦合和高内聚,使得代码更加易于重用和扩展。

  4. 注解的应用:Spring 框架中有很多注解,例如 @Component、@Autowired、@Qualifier、@Transactional 等,这些注解可以有效地简化和提高代码的编写效率,通过学习 Spring 源码,我更加深入地了解了注解的原理和实现方式,可以更加灵活地应用注解,提高代码的可读性和可维护性。

学习 Spring 源码可以帮助我更加深入地理解编程的基本原理和设计模式,规范自己的编码风格,实现代码的模块化和解耦合,提高代码的可读性和可维护性,从而提高自己的代码编写和设计能力。

23.spring事务的隔离级别

spring的7种传播行为: 1.required:(默认传播行为),如果当前有事务,其他就用当前事务,不会新增事务。 例如:方法A调用方法B,它们用同一个事务。(如果B没有事务,它们会用同一个事务。)(只要有一个回滚,整体就会回滚)

2.requires_new:如果当前有事务,其他不会加入当前事务,会新增事务。即他们的事务没有关系,不是同一个事务。 如果其他没有事务,那么以当前事务运行。 例如:方法A调用方法B,它们用不同的事务。(B不会用A的事务,会新增事务。)

3.supports:当前没有事务,就以非事务运行。当前有事务呢?就以当前事务运行。 例如:方法A调用方法B,如果A没有事务,那么B就以非事务运行。 如果A有事务就以A事务为准。如果A没有事务,那么B就会以非事务执行。

4.mandatory:其他没有事务就会抛异常。当前没有事务抛出异常,当前有事务则支持当前事务。 支持当前事务,如果当前没有事务就会抛出异常。 例如:方法A调用方法B,如果方法A没有事务,那么就会抛出异常。

5.not_supported:以非事务执行。 例如:方法A调用方法B,方法B会挂起事务A以非事务方式执行。

6.never:以非事务执行,如果存在事务,抛出异常。 总是以非事务执行,如果存在事务,那么就抛出异常。

7.nested:如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。 如果当前事务不存在,那么其行为与Required一样。 例如:方法A中调用了方法B,B中try catch手动回滚,A不会回滚。

24.Spring如何引入外部配置文件

1.在application.yml里

spring.config.name: config spring.config.location: file:/myproject/config/ 2.在启动类上

  @SpringBootApplication
  @PropertySource(value = "file:/myproject/config/config.yaml", factory = YamlPropertySourceFactory.class)
  public class MyApplication {
      // ...
  }

25.构造注入的好处

  1. 构造注入可以保证依赖对象在类实例化时就已经准备好,而set注入是在类实例化后再设置的,所以构造注入可以更好地保证依赖对象的可用性。

  2. 通过构造注入,可以强制规定必须注入的依赖对象,而使用set注入则可以在没有所有必需依赖时创建对象。

26.Spring事务失效

1.方法不是public

2.方法抛出的异常不是spring的事务支持的异常,导致事务失效

3.catch捕获异常之后,没有再次抛出异常,导致事务失效

4.数据库不支持事务

5.这个类必须要被spring管理

27.Spring热部署的原理

Spring Boot DevTools

通过 WatchService 监测 Class 文件的变化,使用自定义的 RestartClassLoader 加载 Class 文件并替换旧的 Class 文件,再使用 Java Runtime.exec 方法在一个新的子进程中重启应用程序。

10.Redis

最大节点数16384

1.Redis到底是单线程还是多线程

如果只说命令处理部分是单线程,如果是整个redis是多线程。特别是在redis6.0以后在核心网络模型引入多线程,进一步提高我们的CPU利用率

核心网络模型的区别?

6.0前不支持多线程接收请求,6.0之后引入一个新的io多路复用的模型能多线程的接受请求,提高了redis的并发量和响应效率

1.为什么Redis命令部分还采用单线程

redis是纯内存操作,执行速度非常的快。它的性能瓶颈不是执行速度,而是网络延迟,变成多线程并不会带来多大的性能提升。

多线程会导致上下文切换,带来不必要的开销

多线程会有线程安全性问题,而我们一直都使用redis作为分布式锁,它变多线程了,那么分布式锁用什么?

2.redis单线程为啥快

主要原因就是数据保存在内存,还有一个io多路复用机制

2.Redis数据类型以及应用场景

一共8种数据类型,3种不常用,5种常用

\1. 字符串(String):作为缓存,可以存token,也可以存一些常用数据

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。

是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

不适合存储大量数据,因为存储大量数据时会出现吞吐量下降,和线程不安全的问题

吞吐量:存储集合时每次都需要对整个集合做序列化和反序列化,使性能下降

线程安全性问题:比如使用Redis的String类型存储list集合,得需要先准备好一个新的list集合,再set进去是两个操作,

而将list类型存储为list集合的形式,可以直接通过index进行操作,直接lset就可以,无需加锁

最后两者的效率,一个加锁一个不加锁效率这方面可能差10倍

\2. 哈希(Hash):比如用户信息、商品信息等。

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。

当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

\3. 列表(List):适用于存储一组有序的元素,比如最近查看的文章、历史记录等。

List的数据结构为快速链表quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存

当数据量比较多的时候才会改成quicklist。

\4. 集合(Set):适用于存储一组无序的元素,用于点赞的用户列表、标签列表等。

Set数据结构是dict字典,字典是用哈希表实现的。

\5. 有序集合(ZSet):适用于存储一组有序的元素,每个元素有一个分值,支持根据分值进行排序、添加、删除和获取元素等操作,比如排行榜、热门文章等。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

对比有序链表和跳跃表,从链表中查询出51

(1) 有序链表

img

要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

(2) 跳跃表

img

从第2层开始,1节点比51节点小,向后比较。

21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层

在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下

在第0层,51节点为要查找的节点,节点被找到,共查找4次。

从此可以看出跳跃表比有序链表效率要高

3.Redis持久化

1.RDB

将 Redis 在内存中的数据定时,修改次数,手动触发生成快照(snapshot)到磁盘上

默认为dump.rdb

优点:

  1. RDB 方案可以根据需求自动备份,可以定期执行快照备份

  2. RDB 方案在恢复数据时,速度较快,因为只需要把已保存好的 RDB 文件读入 Redis 数据库即可,不需要像 AOF 那样一个个恢复命令执行;

  3. RDB 方案的文件比 AOF 文件要小,因此占用的磁盘空间更小

  4. 当 Redis 服务需要迁移或备份时,RDB 方案比 AOF 更加方便,因为RDB文件可以直接复制到另一个 Redis 服务器上,而不必再恢复AOF命令序列。

2.AOF

日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,在处理写命令时,Redis 会先将写命令写入缓存区,再在一定条件下执行将缓存区的内容同步到 AOF 文件的操作。

默认是每秒记录一次,还可以设置每次执行命令都追加

AOF 持久化优点是数据更加可靠,在任意一时刻 Redis 服务停止时,数据文件中保存了最近的操作,可以使用最新的数据文件来恢复数据。缺点是它的文件可能会比 RDB 文件大,同时 AOF 文件写入会影响 Redis 的性能,增加了 I/O 操作时间。

默认不开启,默认为 appendonly.aof

3.应用场景

  1. 适用于数据量较小、对数据的可靠性要求不高、且要求对 Redis 的性能影响较小的场景:可以选用 RDB 方式。

  2. 适用于数据量较大、对数据的可靠性和完整性要求较高、且对 Redis 的性能影响可以接受的场景:可以选用 AOF 方式。

  3. 对于需要同时满足数据快照+日志持久化的场景:可以在 Redis 中同时开启 RDB 和 AOF 持久化方式。

aof重写: index=1 对incr index执行10遍,会直接记录index

建议使用rdb+aof, 在aof重写时

4.Redis应用问题

1.缓存穿透

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

1.redis命中率降低

2.一直查数据库(出现很多非正常url访问)

解决方案:

1.对空值进行缓存,(springcache可以配置对空数据缓存)img

2.布隆过滤器

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

3.进行实时监控

2.缓存击穿

key对应的数据存在,但在redis中过期,这个时候如果大量的请求来访问这个key,那么容易压垮数据库

比如吴亦凡过期了,之后大量人搜索吴亦凡

解决:

加锁,springcache默认是无锁的,可以通过一个注解加锁

    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)

3.缓存雪崩

和击穿不同的就是,大量key过期

1.保持缓存层的高可用性

使用Redis 哨兵模式或者Redis 集群部署方式,即便个别Redis 节点下线,整个缓存层依然可以使用。除此之外,还可以在多个机房部署 Redis,这样即便是机房死机,依然可以实现缓存层的高可用。

2.优化缓存过期时间

设计缓存时,为每一个 key 选择合适的过期时间,避免大量的 key 在同一时刻同时失效,造成缓存雪崩。

3.使用锁和队列

使用锁:在某个缓存即将失效时,可以使用分布式锁的方式,在多个应用实例中只有一个实例能够重新生成缓存。这样就可以保证只有一个请求从数据库加载数据,而其他请求都可以从缓存中获取数据,减轻了数据库的压力。

使用队列:在某个缓存即将失效时,可以将请求放入一个延迟处理队列中,例如 Redis 的“zset”数据类型,使用队列的方式使得请求能够得到有序的调度和处理。当缓存失效时,只有第一个请求会从数据库加载数据,其他请求继续等待。这样可以保证缓存的稳定性和数据库的稳定性,以及吞吐量的均衡性。

5.分布式锁

1.Redis AP 最终一致性

Redis 方法用setnx上锁,能上就返回1不能就返回0,delete释放锁 用expire设置过期时间解决死锁问题

redssion:lock unlock来锁和释放锁,并且对锁的续期问题有解决

redssion这个东西里面内置了一个看门狗的机制可以续期

它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。默认情况下,看门狗每隔10s就会进行一次续期,把锁重置成30秒

优点是效率最高,适合高并发的场景,

缺点:如果取不到锁,就会不断尝试,会对程序性能造成影响,但在集群模式中存在数据一致性问题,

比如在redis的哨兵模式下

  1. 客户端 1 在主库上执行 SET 命令,加锁成功

  2. 此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的)

  3. 从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!

可以用redlock算法进行解决,但增加了系统的复杂度和部署难度。

即便使用redlock这种方法在某些场景下也不会保证锁百分之百可用

  1. 网络故障:当某个 Redis 实例出现网络故障或连接异常时,它将无法正常执行加锁或解锁操作。

  2. 时钟偏差:Redlock 必须依赖一致的时钟来避免并发问题,如果时钟发生了偏差,它会导致分布式锁被误释放或提前释放。

  3. 竞争条件:在锁过期之前,多个客户端同时对一个资源进行加锁操作,这将会导致互相的竞争,从而导致某些锁的失效。

2.Zookeeper CP 强一致性

Zookeeper的分布式锁原理是利用了临时节点(EPHEMERAL)的特性。其实现原理:

  • 创建一个锁目录lock

  • 线程A获取锁会在lock目录下,创建临时顺序节点

  • 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁

  • 线程B创建临时节点并获取所有兄弟节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)

  • 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁

优点:

可靠性更好

  1. 节点选举机制:节点选举过程中,一旦发现领导者节点发生故障,Zookeeper 会通过多数机制重新选举新的领导者节点,从而保障了整个系统的可靠性。

  2. 架构设计:Zookeeper 采用了集中式架构设计,分布式系统中的所有数据必须由领导者节点进行处理,从而保障分布式环境中数据的一致性和可靠性。在实际应用中,可以通过增加 Zookeeper 集群的数量来提高整个系统的容错性和可靠性。

缺点,性能比redis低

zookeeper为什么具有强一致性

ZooKeeper 具有强一致性的特点,主要是由于其提供了两个关键实现: 原子广播 和 顺序一致性。

首先,ZooKeeper 的原子广播确保了所有服务器上的相同数据副本都会同步更新到最新值。 当一个客户端向 ZooKeeper 内部数据发送一个更改操作时,该操作会首先被发送到此 ZooKeeper 实例的 Leader 服务器,并且其保证了对事务操作的原子性。具体来说,在 ZooKeeper 中,每个节点都被分配了一个唯一的 zxid(ZooKeeper Transaction Id)数值,用于标识所有的事务。事务最终的状态会被所有节点按照相同的顺序进行执行,因此所有数据副本的状态都能在同一时间点达成一致,这样就实现了原子广播特性。

其次,ZooKeeper 实现了顺序一致性特性。 当所有的事务执行完毕时,ZooKeeper 保证将事务的操作结果按照顺序公布给所有客户端,客户端也可以通过监听 ZooKeeper 节点的变化从而获取最新的数据信息。这样就减少了因数据复制不同步误导客户端的情况,同时保证了数据的一致性。

由于原子广播和顺序一致性的实现,ZooKeeper 能够提供一个强一致性的服务。在分布式系统中,各个节点之间的通信、数据复制和同步等问题可以通过 ZooKeeper 实现,从而保证分布式节点的一致性。

3.mysql

  CREATE TABLE `database_lock`
  (
      `id`          BIGINT        NOT NULL AUTO_INCREMENT,
      `resource`    int           NOT NULL COMMENT '锁定的资源',
      `description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
      `updatetime`  TIMESTAMP              DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`),
      UNIQUE KEY `uiq_idx_resource` (`resource`)
  ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8mb4 COMMENT ='数据库分布式锁表';

其中update_time是用来判断加锁时间,用于后续定时任务解决使用!

当我们想要获得锁时,可以插入一条数据:

  INSERT INTO database_lock(resource, description) VALUES (1, 'lock');

resource字段做了唯一性约束,这样如果有多个请求同时提交到数据库的话,数据库可以保证只有一个操作可以成功,那么那么我们就可以认为操作成功的那个请求获得了锁

当需要释放锁的时,可以删除这条数据:

  DELETE FROM database_lock WHERE resource=1;

img

当项目中没有redis和zookeeper的时候又不想改变架构使用,可以最小化修改代码,降低接入成本。

但一般不使用

6.超卖问题

如何产生?

判断还有没有库存和库存-1不是原子的。

1.每次更新库存时可以带着之前的库存

img

2.用redis的话,把这两个操作写成lua脚本

7.redis和mysql一致性问题

保证数据一中一问题,根本就是最终一致性思想

1.先更新redis,再更新mysql,但如果更新mysql慢了,会造成数据库脏数据 不可取

a更新缓存, b更新缓存,更新mysql ,a更新mysql

\2. 双写模式先更新mysql,在更新redis,但如果多线程并发下很容易出现redis脏数据

img

3.先更新mysql,再删redis,多线程呢下也容易出现 redis脏数据

img

4.延迟双删,先删redis,在更新mysql在删redis,但第二次删除的时间点很难控制

如果对时效性要求不高的话就不用处理

读多写少就加读写锁, 如果写多的话就

5.使用canel框架,订阅mysql的binlog文件,对数据进行同步

Canel框架原理

首先呢,我们需要先启动canel的一个服务器端他去订阅到我们mysql主节点的binlog文件,

当mysql主节点binlog文件发生改变的话,就会将我们binlog文件中的数据同步给canel服务端,之后再同步给我们的canel客户端

如何canel客户端呢,再将我们的数据同步给我们redis当中

他会告诉你我们那张表,那一行数据发生变化,我们可以写代码,将数据同步给我们的redis

redis同步时间有一点点的延迟

大概30ms左右

8.redis增删如何对系统影响最小

  1. 批量操作:采用批量操作可以减少Redis客户端与服务器之间的通信次数,从而减轻系统负担。

  2. 异步操作:采用异步操作可以将Redis的增删操作放到后台执行,不影响系统的正常运行。

  3. 控制并发数:在高并发情况下,可以通过控制并发数来减少Redis的压力,避免系统崩溃。

  4. 设置redis集群

redis如何设置集群

一致性hash

可以用一致性hash对请求进行分配,

一致性哈希的基本思想是将所有的节点和数据都映射到一个环形空间中,然后将数据根据其哈希值映射到环上的某个位置,再将每个节点映射到环上的某个位置。这样,每个数据就可以通过其哈希值找到对应的节点,从而实现数据的分布式存储和处理。

当节点发生变化时,比如节点添加或删除,只需要重新计算受影响的数据的哈希值,然后将其映射到新的节点上即可,不需要重新计算所有数据的哈希值,避免了数据迁移的开销。

9.redis缓存失效策略

Redis缓存失效策略主要有以下几种:

  1. 定时失效:当缓存过期时,Redis会自动将其删除。

  2. 周期性失效:可以通过定时任务或者定时扫描的方式来周期性地清理缓存。

  3. 惰性失效:当缓存数据过期时,Redis不会立即将其删除,而是在下一次访问时再进行删除。这种策略可以减少缓存的删除操作,提高缓存的效率。

对于不同类型的缓存数据,可以采用不同的失效策略。对于访问频率比较高的缓存数据,可以采用惰性失效策略,而对于访问频率比较低的缓存数据,可以采用定时失效策略

10.Redis如何实现高可用,高扩展

1.主从复制,实现数据的备份和故障恢复。

2.哨兵模式,是一种监控和故障转移解决方案,支持快速检测 Redis 主从节点的状态,自动进行故障切换

3.分片,将数据分成多个分片存储在不同的物理节点上,通过一致性哈希算法将同一个键映射到同一个节点上,实现数据的读写操作和负载均衡。

Redis分片的最少数量6个,允许挂4个

3个主3个从

11.Redis主从复制

1.复制流程

img

2.哨兵模式

在哨兵模式下,当主节点出现故障时,哨兵会自动选举一个从节点(或者新建从节点)提升为新的主节点,并且通知其他从节点,使得整个 Redis 集群仍然可以服务。当发现从节点故障或服务不可用时,自动将该节点从 Redis 集群中移除并进行数据迁移,同时还能自动将已下线的节点重新加入到 Redis 集群中。

12.如何提高redis的并发数

1.调整redis的配置,增加redis的最大连接数,调整redis处理网络连接的线程数

2.使用连接池,连接池可以预先创建一定数量的 Redis 连接实例,在需要连接 Redis 时从连接池中获取连接并使用,避免了因频繁建立、断开连接而造成的性能瓶颈。

3.使用消息队列,先让请求任务进入消息队列,再从消息队列给redis

13.redis达到最大内存限制会发生什么

当 Redis 达到最大内存限制时,Redis 服务器对写入操作会产生影响,但读取操作仍可进行,这是因为 Redis 具有先进的数据淘汰策略。下面是 Redis 的数据淘汰策略:

  1. volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。

  2. volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。

  3. volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。

  4. allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。

  5. allkeys-random:从数据集中任意选择数据淘汰。

  6. no-enviction(不删除策略):当 Redis 内存使用达到最大值时,所有写入操作都将失败,但读取操作仍可以进行。

根据上述机制,可以看出当使用 no-enviction 不删除策略时,Redis 内存使用量达到最大值时,写入操作会受到限制,但读取操作仍可以进行,因此可以根据实际情况进行选择。另外,应该通过监控和预测 Redis 的内存使用情况,及时采取扩容或配合 LRU 数据淘汰策略等措施来避免出现内存使用达到最大值的情况。

14.Redis为什么推荐Redis主节点数目是奇数个

防止由脑裂造成的集群不可用(集群可用原则:可用节点数量 > 总节点数量/2)。对一个偶数折半必然会出现两个数相等(可用节点数量 = 总节点数量/2)。

在 Redis Cluster 中,为了保证 Redis 集群的高可用性和数据一致性,使用了 quorum(仲裁)机制来实现。quorum 机制指的是,每个 Redis 节点在每次执行写操作之前都要等待 quorum 机制的确认,只有在 quorum 机制确认后才能继续执行写操作,从而保证节点之间的数据一致性。

对于 Redis Cluster 集群而言,quorum 机制要求在集群中超过半数的节点正常运行,才能继续提供服务。因此,为了保证 quorum 机制的正确性和高可用性,需要确定一个合适的主节点数量,通常建议选择奇数个主节点。

假设 Redis Cluster 集群中有 N 个主节点,如果 N 是偶数,则某些情况下可能会发生节点之间的网络分区,导致集群无法达到 quorum。这是因为当发生网络分区时,每个子集中的节点数量可能都小于 or 大于 N/2,因此无法确定哪个子集中的节点应该被视为正确的节点,并提供写操作服务。

然而,如果选择奇数个主节点,即 N 为奇数,则子集中必定有一个节点超过半数,能够确定正确的节点。即使在发生网络分区的情况下,也能够确保至少有一个合法的子集,从而维护 quorum 机制的正确性和集群的高可用性。

综上所述,选择奇数个主节点并不是说一定可以杜绝 Redis Cluster 的脑裂问题,但是可以在一定程度上保障 Redis Cluster 集群的高可用性、数据一致性和安全性。

11.微服务

1.微服务和分布式的关系

微服务就是一种分布式的方案,每一个服务运行在自己的进程中,各个服务独立部署运行

若干个独立计算机的集合,对于用户来说就是一个单体,和集有不同集群是同一业务,分布式是不同业务

2.负载均衡算法

  1. 轮询(Round Robin):将请求依次分配给不同的后端服务器,循环进行。这是nginx默认的负载均衡算法。

  2. IP Hash:根据客户端的IP地址对后端服务器进行哈希运算,然后将请求发送到对应的服务器。这种算法可以确保同一个客户端的请求都被发送到同一个服务器处理,适用于需要保持会话一致性的应用场景。

  3. 最小连接数(Least Connections):将请求发送到连接数最少的服务器上,以平衡服务器的负载。这种算法适用于处理长连接的应用场景,例如视频流媒体。

还有就是加权,一般使用轮询,其他的会使请求分布不均

3.跨域

跨域请求流程: 非简单请求(PUT、DELETE)等,需要先发送预检请求

-----1、预检请求、OPTIONS ------> <----2、服务器响应允许跨域 ------ 浏览器 | | 服务器 -----3、正式发送真实请求 --------> <----4、响应数据 --------------

4.负载均衡原理

低版本是Ribbon,

高版本是loadbalance,必须排除ribbon

5.Nacos

1.Nacos的作用

服务注册发现,做配置管理,服务监控

img

2.Nacos服务注册发现原理

  1. 服务提供者在启动时,通过 Nacos 客户端 API 将自己的服务实例信息注册到 Nacos 服务端的注册中心。

  2. 服务消费者在启动时,通过 Nacos 客户端 API 调用 Nacos 服务端的服务发现接口,获取服务提供者的服务实例信息,并缓存到本地。

  3. 服务消费者调用服务时,通过本地缓存的服务实例信息,再DNS 解析主机名获取到服务实例的 IP 地址选择一台可用的服务提供者进行调用,如果做了集群就要负载均衡, 轮询,iphash,随机

  4. 当服务提供者宕机或下线时,Nacos 客户端 API 可以自动将服务实例信息从注册中心中删除,服务消费者在下一次请求时,将会获取最新的可用服务实例列表。

3.配置中心原理

@RefreshScope

当应用程序启动时,它会从Nacos服务器获取配置信息,并将其缓存在本地。此时,监听器会注册到Nacos服务器上,以便在配置发生变化时接收通知。当配置发生变化时,Nacos服务器会向监听器发送通知,监听器会接收到通知并将其传递给配置管理器。配置管理器会重新加载配置信息,并将其更新到应用程序中。

4.Nacos如何保证服务是可用的

nacos分为临时实例和永久实例,默认都是临时实例

临时:客户端主动向服务端发送心跳,每5秒一次,如果心跳超过15秒就说明是不健康的,时间间隔超过30秒就会永久剔除

永久实例:注册到nacos后会被持久化到本地,除非主动删除,否则一直存在.服务失效后也只是被标记成不健康实例.而且还是nacos主动到客户端发送心跳检测,会增加nacos的压力

5.Nacos如何处理数十万的服务注册的压力的

nacos收到服务注册的请求时,不会立刻去注册,而是将任务提交到一个阻塞队列中,之后通过线程池异步的来处理阻塞队列的任务

6.Nacos如何解决并发读写的冲突?

使用copyonwrite写,先拷贝出一份,别人来读的时候就读拷贝的那份.当写完后覆盖原来拷贝的实例列表

7.Nacos集群的强一致性和最终一致性

强一致性cp:nacos通过raft算法实现cp

  • 主节点负责接收客户端写请求并广播到其它从节点,从节点接收到写请求后向主节点发送确认消息。主节点收到了超过一半的确认消息后,将写操作应用到本地存储,并通知所有的备节点同步写操作的结果。

  • 当主节点正在进行数据写入并同步到备节点的过程中,如果收到了读请求,Nacos 会先将该读请求发送到主节点,等待主节点将写完成的数据同步到备节点后,再返回读请求的结果给客户端。

  • 当主节点发生故障时,会通过raft算法重新选举出一个主节点,在选举的过程中,每个参与节点都有一个唯一的 ID,按照 ID 大小来决定投票权重,最终得票最高的节点成为新的主节点。选举的过程中,集群对外不服务.

最终一致性ap:默认

ap的实现就简单多了,没有主从的概念,每个节点都可以处理写请求,当节点接收到写请求后,会异步全量复制给其他节点,如果有一个节点挂了,这个集群对外还是可用的

6.正向代理反向代理

正向代理:是客户端的代理,一般是客户端的架构设计,帮助客户端访问其无法访问的服务器资源

反向代理:是服务端的代理,是服务端的架构设计,帮助服务端做负载均衡,安全防护

正向代理和反向代理的作用和目的不同。正向代理主要是用来解决访问限制问题。而反向代理则是提供负载均衡、安全防护等作用。二者均能提高访问速度

7.微服务常用组件

服务注册发现,配置中心Nacos

客户端负载均衡Ribbon 和LoadBalancer

服务熔断sentinel

服务网关 gateway

RPC远程调用OpenFeign

链路追踪Skywalking

异步中间件消息队列mq和kafka

8.什么是熔断

某个服务请求响应时间过长或者出现异常情况(如网络故障)。为了避免服务的故障或者下游服务的雪崩效应(因某个服务出现问题而导致其它服务都受到影响),就有了服务的熔断。

通常会在受监控的服务中设置一个阈值,当服务的错误率达到一定的程度时,熔断机制会自动触发并中断该服务的调用,不再返回错误的结果。当服务熔断后,调用方可以使用降级服务(fallback)来获取默认的响应结果,从而避免出现雪崩效应

9.CAP理论

1.C - Consistency

一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态

响应会有延迟,当数据同步时对从数据库进行锁定,一定不能返回旧数据

2.A - Availability

可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误

就算没同步成功也要给出响应,哪怕是响应旧数据,就算旧数据也没有,也要响应一个默认值 ,但不能响应错误

3.P - Partition tolerance

分区容忍性:当网络中某些节点失联(即发生了分区),而剩余节点之间却一直能够正常运转时,系统依然能够正常工作和提供服务的能力。

如何实现分区容忍性? 1、尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。

2、添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。 可以配一主多从

分区容忍性分是布式系统具备的基本能力

4.CP 强一致性

CP模式是指在分布式系统中,当网络节点通信异常或者故障发生时,为了保证数据一致性,系统可以牺牲可用性。

此时,分布式系统会停止对外服务,等待节点通信正常后再次提供服务。CP通常应用于对数据一致性要求较高的分布式系统场景中,如金融和电商等领域。

5.AP 最终一致性

AP模式是指在分布式系统中,当网络节点通信异常或者故障发生时,为了保证系统可用性,系统可以牺牲数据的一致性。

此时,分布式系统仍会对外提供服务,但部分数据可能会出现不一致的情况。AP通常应用于对数据一致性要求不太高的分布式系统场景中,如社交和游戏等领域。

12.MQ

1.对mq的理解

他是消息队列,分布式系统异步通信的中间件,具有解耦,异步通信和削峰填谷等功能,并且具有很好的可靠性与java的异步接口不同的是,mq是多进程异步调用

2.mq宕机的话消息会消失吗

不会

我们使用的这些主流mq一般在默认的情况下将我们的这些message消息持久化到硬盘上,就算宕机后,重启服务也会瞬间读到内存里的

缓存策略

  1. 内存缓存

当消息进入队列时,首先将消息存储在内存中,然后由队列逐步将消息写入磁盘。这样做可以大大提高消息的处理速度和稳定性,同时也可以避免丢失重要消息。

  1. 磁盘缓存

为了避免内存队列的负荷过大,消息队列通常会对消息内容进行分片和压缩,然后将其存储在磁盘上。这样一来,可以提高消息的存储能力和系统的可靠性,并减少内存队列的使用,从而提高系统的稳定性和性能。

  1. 队列淘汰策略

队列淘汰策略指的是消息队列系统在队列达到一定长度或存储一定时间后,如何处理过期的消息。常见的淘汰策略包括FIFO(先进先出)、优先级、LRU(最近最少使用)、MQTT(可靠的消息传输)等。

3.如何解决消息堆积问题

消息堆积可以说是一个常见问题,尤其在高并发的情况下

我之前也想到过这种情况,就是咱们的这个producer生产者的消息投递速率与我们的consumer消费者消息速率明显是不匹配的,

比如咱们生产者一秒钟投递几万条消息,但是我们消费者只有一个,而且我们的消费者他的消费消息的过程是单个消费者消费的速率,

而单个消费者消费的速率是比较慢的,所以我们为了解决这个问题,

第一,把我们消费者做成集群的形式进行消费

第二,每一个消费者一般都采用批量的形式进行消费,这样的话他会提高我们的消费者消费的速率

如果说以后我们consumer消费者消费速率还是跟不上,我们可以对消费者横向扩张,不断地做集群

4.mq如何保证消息不丢失,可靠性如何保证

首先咱们使用mq异步处理会分成3个阶段

生产者发送消息给mq,mq发消息给消费者,消费者消费消息

1.首先生产者发送消息要保证消息不丢失,可以使用mq提供的发布确认模式

可以同步单个确认,批量确认虽然保证了可靠性但是影响效率,所以使用异步的消息确认机制,但是会增加代码的复杂度,

需要开启一个消息确认的监听器来监听哪些消息发送成功,哪些消息发送失败.如果一定时间内没收到确认消息,就抛出异常记录日志,或者触发重试的一些策略

2.当消息到达mq后需要配置持久化

3.当mq把消息发送给消费者,消费者消费完手动发送一个确认消息给mq,之后mq会删除这条消息

自动确认:消费者接收到消息就自动确认

手动确认:肯定确认和两个否定确认

4.事务机制:通过使用事务机制,可以将消息发送和消息确认操作封装成一个原子操作,以确保消息不丢失

5.如何保证我们mq消息顺序性

⼀个 queue,多个 consumer。⽐如,⽣产者向 RabbitMQ ⾥发送了三条数据,顺序依次是 data1/data2/data3,

压⼊的是 RabbitMQ 的⼀个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的⼀条,结果消费者2先执⾏完操作,把 data2存⼊数据库,然后是 data1/data3。这不明显乱了。

我们项目的解决方案是,

*拆分多个 queue*,每个 queue 一个 consumer,把需要保证顺序性的消息发送给同一个queue ,但相对而言性能就会有所下降

6.mq幂等性问题

mq不保证幂等性,需要我们自己的业务完成幂等新的操作

1.数据库使用唯一约束,如果重复操作数据库就会报错

2.使用乐观锁,加一个初始版本,比如转账这个业务,mq给消费者一开始0元,加10元这个消息,消费者先查数据库,目前是不是0元如果不是就证明已经处理过这个业务

mq发消息时携带一个全局唯一id,并存在redis中,消费者消费之前去redis中查这个id,如果没有证明已经被消费过处理完消息就删除这个id,

7.mq和kafka区别

  1. MQ 消息一般是一对一的发送和接收,Kafka 的消息通常是被分成多个分区进行处理的。

  2. 异步处理的支持不同:MQ 支持同步和异步两种消息处理方式,而 Kafka 只支持异步处理。

  3. 性能不同:Kafka 采用了更高效的 zero-copy IO 技术,它的性能更高,能够处理更多的消息。

  4. 适用场景不同:MQ 更适合实现点对点的消息传递场景,Kafka 更适合处理海量数据的实时处理场景。

mq适合比如mysql和redis同步, 发送短信等等点对点的异步处理

kafka适合大批量的数据,比如作为大数据hadoop的消息队列

8.MQ如何确定消费者消费了消息,生产者发布了消息

mq消费者消费失败怎么处理

通过消息的确认机制来判断生产者是否成功发送消息,以及消费者是否成功消费消息。

对于生产者发送消息来说,mq一般会提供发送确认机制,也就是将消息发送到消息队列后,消息队列会返回确认信息告诉生产者消息已经被成功接收,并已经持久化到磁盘中,此时可以认为消息已经成功发送。如果消息发送失败,消息队列会返回错误信息,生产者可以通过传递异常或者返回错误码的方式来处理。

如何防止重复消费

对于消费者消费消息来说,mq一般也会提供消息确认机制。消费端消费到消息后,通过向消息队列发送确认消息的方式来告诉消息队列该消息已经被成功消费。消息队列接收到确认消息后会将该消息从消息队列中移除,防止消息重复消费。如果消费过程中出现异常,可以将消息重新放入消息队列或者回滚事务。

消息确认机制常见有两种方式:自动确认和手动确认。自动确认是指在消息被发送到消息队列后,消息队列会自动发送确认消息给生产者,告诉生产者消息已经被成功接收。手动确认则是由消费者主动向消息队列发送确认消息,告诉消息队列已经消费了该消息。

总的来说,消息队列通过使用消息确认机制来保证消息的可靠性和顺序性。生产者可以通过发送确认机制来确保消息已经被成功发送,消费者也可以通过确认机制来确保消息已经被成功消费,从而实现消息队列中消息的正确传输和保证系统的稳定性。

9.消费者消费失败怎么处理

消费成功会返回CONSUME_SUCCESS

消费失败

  • 方式①:返回RECONSUME_LATER,消息将重试

  • 方式②:返回null,消息将重试

  • 方式③:直接抛出异常, 消息将重试

将消息放入重试队列进行重试,重试一定次数还是失败,就放出死信队列

10.rabbitmq交换机的3种模式

1.扇出交换机fanout:是一种点对面模式,忽略routingKey,消息经过交换机时为会同时向每个队列发送相同的消息

2.直接交换机direct:点对点模式

3.topic:通过正则表达式来判断某个消息应该发到哪个队列

11.死信队列

  1. 重试机制:消息发送失败,重试了一定次数就会放入死信队列, 之后由死信队列来触发重试策略

  2. 处理异常消息:当消息消费失败时,可以将这些消息投递到死信队列中,并记录日志,以便后续跟踪和处理。

  3. 延时队列:可以通过配置死信队列和TTL(Time-To-Live)属性,实现延时消息的发送和处理,当消息发送到队列中,为每个消息绑定一个过期时间,过期就放入死信队列中,由死信队列将消息投递给对应的消费者来进行消费

12.算法

1.排序算法

img

1.冒泡

0~1位置上谁大往右移,1~2位置上谁大往右移,直到最后,每轮排序都会把最大的放到右边

2.选择

在0~n-1位置上,找到最小值,把最小值和0位置上去。再遍历一遍。。。

3.插入

先在让0~0范围上有序,0~1范围上有序,0~2范围上有序。。。0~n-1范围上有序。每次都把为排序的那个数都插入到正确的位置上去。

4.希尔

希尔排序是插入排序的一种改进算法,分组插入。希尔排序的基本思想是:先将整个数组分成若干个子序列,对每个子序列进行插入排序,然后逐步缩小子序列的间隔,直到整个数组变为一个序列,再对该序列进行插入排序。

5.归并 适合要求稳定性的场景

先找中点,让左边有序,再让右边有序,最后merge到一起,整体就有序了

merge过程:

  1. 创建一个临时数组,用于存储合并后的数组。

  2. 定义三个指针,分别指向左边子数组的第一个元素、右边子数组的第一个元素和临时数组的第一个元素。

  3. 比较左右子数组的第一个元素,将较小的元素放入临时数组中,然后将指向该元素的指针向后移动一个位置。

  4. 重复步骤3,直到左右子数组中的一个为空。此时,将另一个子数组中剩余的元素全部放入临时数组中。

  5. 将临时数组中的元素复制回原数组的对应位置。

6.快排 适合大多数不要求稳定性的场景

1.0 partition:三个指针ijk,i代表当前位置,j=0和k=length-1代表小于区域和大于区域

[i]<num那么swap(i,j) i++ j++,如果[i]=num,i不动,j++(小于区域向右扩)

,如果[i]>num,那么swap(i,k),i不动k--。当i=k时,也就是撞上大于区域时,循环结束。

之后就在小于num的区域做递归,大于区域做递归。一次搞定一批数,所以快。

但他这个时间复杂度是n2,因为我能找到最差的例子。123456。每次的划分值都在最右侧,每次partition都搞定一个数。快排算法快不快主要是看你划分值找的谁,我故意找最差的他就是On2级别。所以就有了2.0版本

2.0 1.0版本问题就是划分值大的很偏,如果随机划分值,那么他就是一个On logn级别的算法

7.堆排 适合找前k大的场景

把数组变为大根堆,然后一直做heapify

什么是heapify,就是拿掉一个最大的数,之后把最右侧的数放到树头。重新把这棵树变成大根堆

2.树

1.二叉搜索树之AVL Tree

判断不平衡依据 : 当某个节点的左子树和右子树高度差大于1时, 树不平衡

如何平衡?

  • 左左 右旋

imgimg

  • 左右 左子树左旋,自己右旋

img
img
img
  • 右右 左旋

  • 右左 右子树右旋,自己左旋

2.红黑树

1.红黑树的优缺点

优点:判断平衡的依据不同,插入和删除时旋转次数更少

缺点:所占空间大于平衡二叉树

2.红黑树的特性

1.所有节点只有两种颜色

2.所有null视为黑色,红色节点不能相邻

3.从根到任何一个叶子节点,黑色数目一样(完美平衡)

3.红黑树添加时如何保持平衡

变色和旋转

四种情况(插入节点默认为红)

1.如果插入节点是根节点变黑即可

2.插入节点的父节点是黑色,直接插入

3.当红红相邻并且叔叔为红时,将父亲叔叔都变黑祖父变红然后对祖父做递归

4.当红红相邻并且叔叔为黑时,前三种直接变色即可,这种情况既要变色又要旋转

和AVL旋转逻辑一样,就多了变色,分为左左,左右,右右,右左

左左:把父亲变黑,爷爷变红,再对爷爷右旋

左右:先对父亲左旋,把自己变黑,爷爷变红再对爷爷右旋

右右:父亲变黑,爷爷变红再对爷爷左旋

右左:先对父亲右旋,把自己变黑爷爷变红,再对爷爷左旋

  • 8
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java狂魔哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值