java面试整理
java面试题-编程导航
java基础
1、JDK 和 JRE 和 JVM 分别是什么,有什么区别?
[
- JDK(Java Development Kit):JDK 是 Java 开发⼯具包,包含了编写、编译、调试和运行 Java 程序所需的 所有⼯具和组件,比如编译器(javac)、Java API、调试⼯具等。JDK 是针对 Java 开发⼈员的,它包含了 JRE,还有编译器和其他⼯具,可以用来编写和调试 Java 程序。
- JRE(Java Runtime Environment):JRE 是 Java 运行时环境,包括了 Java 虚拟机(JVM)和 Java 标准类 库(Java API)。JRE 是针对 Java 应用程序的,它提供了在计算机上运行 Java 应用程序所需的最小环境。
- JVM(Java Virtual Machine):JVM 是 Java 虚拟机,是 Java 程序运行的环境。JVM 负责将 Java 代码解释或 编译为本地机器代码,并在运行时提供必要的环境⽀持,比如内存管理、垃圾回收、安全性等。JVM 的主要 作用是将 Java 代码转换为可以在计算机上运行的机器码,并负责程序的执行。
- 综上所述,JDK、JRE 和 JVM 之间的区别可以总结如下:
- JDK 是 Java 开发⼯具包,包括了编译器、Java API、调试⼯具等,用于开发 Java 应用程序。
- JRE 是 Java 运行时环境,包括了 Java 虚拟机和 Java 标准类库,用于在计算机上运行 Java 应用程序。
- JVM 是 Java 虚拟机,是 Java 程序运行的环境,负责将 Java 代码转换为可以在计算机上运行的机器码,并提 供必要的环境⽀持
2、什么是字节码?采用字节码的最大好处是什么?
字节码是 Java 程序编译后的中间代码,是⼀种可移植的⼆进制代码,可以在任何⽀持 Java 虚拟机(JVM)的平台 上运行。字节码通过将 Java 源代码编译为字节码指令序列,使得 Java 程序可以跨平台运行,即使是在不同的操作 系统和硬件平台上也可以运行。
字节码采用中间代码的形式,相比于直接将程序编译为特定平台上的机器码,有以下几个好处:
可移植性:由于字节码是中间代码,所以可以在任何⽀持 JVM 的平台上运行,使得 Java 程序具有很好的可移植性。这也是 Java 跨平台的重要特性之⼀。
安全性:由于字节码需要在 JVM 中运行,所以可以对字节码进行安全检查,以确保程序不会对系统造成威胁。
性能:由于字节码是⼀种紧凑的⼆进制格式,相比于直接编译为机器码,可以更快地加载和传输,同时也可以在运行时进行动态优化,提高程序的执行效率。
可读性:相比于直接编译为机器码,字节码具有更好的可读性,可以方便地进行反汇编和调试
因此,采用字节码作为中间代码的最大好处是提高了 Java 程序的可移植性、安全性、性能和可读性。这也是 Java 跨平台和安全性等特性的基础。
3、Java 和 C++、Go 语言的区别,各自的优缺点?
java
- 优点:
- 简单易学代码可读性强
- 跨平台⼀次编写可以在多个操作系统上运行
- 面向对象⽀持继承、多态等特性 丰富的类库
- 可以快速开发应用程序
- 自动内存管理,减少了内存泄漏的可能性
- 缺点:
- 由于JVM的存在,运行速度相对较慢
- 对于实时性要求较高的场景,Java的表现可能不如C++和Go
C++
- 优点:
- 速度快,适合编写需要高性能的应用程序
- 应用广泛,特别是在游戏开发、操作系统和嵌⼊式系统开发方面
- 灵活性高,可以直接访问硬件和内存
- 缺点
- 学习难度较高,需要掌握指针、内存管理等底层知识
- 容易出现内存泄漏和指针错误等问题
- 编写代码过程中需要更多的⼿动管理,相比 Java 更容易出错
go
- 优点:
- 高并发,天生⽀持协程,能够轻松编写高效的并发程序
- 简单易学,语法简洁,上⼿容易
- 静态类型语言,可以避免⼀些潜在的运行时错误
- 快速编译,可以快速构建和部署应用程序
- 缺点:
- 缺乏丰富的类库,与 Java 和 C++ 相比有些不足
- 在⼀些性能要求极高的场景中可能不如 C++ 表现
- 语言本身还比较年轻,相关生态和⼯具还需要进⼀步完善
使用场景:
JAVA
- 适合开发企业级应用程序、后端服务等
C++
- 适合开发需要高性能和高可靠性的应用程序,特别是在游戏开发、操作系统和嵌⼊式系统开发方面。
GO
- 适合开发高并发的后端服务、微服务、容器化应用程序等
4、JDK 动态代理和 CGLIB 动态代理的区别是什么?
JDK和CGLib动态代理区别
- JDK动态代理具体实现原理
- 通过实现 InvocationHandler 接⼝创建自⼰的调用处理器;
- 通过为 Proxy 类指定 ClassLoader 对象和⼀组 interface 来创建动态代理;
- 通过反射机制获取动态代理类的构造函数,其唯⼀参数类型就是调用处理器接⼝类型;
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参⼊;
JDK 动态代理是面向接口的代理模式,如果被代理目标没有接⼝那么 Spring 也⽆能为⼒,Spring 通过 Java 的反射 机制生产被代理接⼝的新的匿名实现类,重写了其中 AOP 的增强方法。
- CGLib动态代理
利用 ASM 开源包,对代理对象类的 class 文件加载进来,通过修改其字节码生成⼦类来处理。
- 两者对比:
JDK 动态代理是面向接口的。 CGLib 动态代理是通过字节码底层继承要代理类来实现,因此如果被代理类被 final 关键字所修饰,会失败。 如果要被代理的对象是个实现类,那么 Spring 会使用 JDK 动态代理来完成操作(Spirng 默认采用 JDK 动态代理实 现机制); 如果要被代理的对象不是个实现类,那么 Spring 会强制使用 CGLib 来实现动态代理。
5、Java 中 final 关键字有什么用
在 Java 中,final 关键字用于表示⼀个不可变的常量或⼀个不可变的变量。
在 Java 中,final 关键字可以修饰类、方法和变量,作用如下:
- final 修饰类,表示该类不能被继承。final 类中的方法默认都是 final 的,不能被子类重写。
- final 修饰方法,表示该方法不能被子类重写。
- final 修饰变量,表示该变量只能被赋值⼀次。final 修饰的变量必须在声明时或构造函数中初始化,且不能再 被修改。常用于定义常量
另外,使用 final 修饰的变量在编译时就已经确定了其值,因此在运行时访问时比非 final 变量更快。
使用 final 关键字可以带来⼀些好处,例如:
- 安全性:将变量声明为 final 可以防⽌它被改变,从而提高安全性。
- 可读性:将常量声明为 final 可以提高代码的可读性,因为常量的值不会被修改。
- 优化:final 变量在编译时被转换成常量,这可以提高程序的性能。
6、Java 中 hashCode 和 equals 方法是什么?它们和 == 各有什么区别?
[
7、什么是反射机制?说说反射机制的优缺点、应用场景?
Java的反射机制是指在运行时获得类的信息,创建类的对象,调用其中的方法和属性。
优点
- 可以动态的获取类信息。
- 可以动态的创建类对象。
- 可以动态的调用类对象的方法以及属性。
缺点:
- 不安全,反射能够获取所有类对象,包括私有的,破坏了程序的封装性。
- 效率低,反射的效率较低。
应用场景:
- 动态代理,因为不确定需要代理的类,所以需要通过反射动态的获取
- RPC 框架,RPC 框架就是动态的生成类对象,然后调用方法的。
8、Java 访问修饰符 public、private、protected,以及无修饰符(默 认)的区别?
[
9、String 和 StringBuffer、StringBuilder 的区别是什么?
- String 和 StringBuffer/StringBuilder 是 Java 中两种不同的字符串处理方式,主要的区别在于 String 是不可变的 对象,而 StringBuffer 和 StringBuilder 则是可变的对象。
- String 对象⼀旦被创建,就不可修改,任何的字符串操作都会返回⼀个新的 String 对象,这可能导致频繁的对象创建和销毁影响性能。而 StringBuffer 和 StringBuilder 允许进行修改操作,提供了⼀种更高效的字符串处理方式。
- StringBuffer 和 StringBuilder 的主要区别在于线程安全性和性能方面。StringBuffer 是线程安全的,所有方法都是同步的,因此可以被多个线程同时访问和修改。而 StringBuilder 不是线程安全的,适用于单线程环境下的字符串处理,但是相比于 StringBuffer,StringBuilder 具有更高的性能。
- 因此,当字符串处理需要频繁修改时,建议使用 StringBuffer 或 StringBuilder;而当字符串处理不需要修改时, 可以使用 String。
10、什么是 Java 内部类?内部类的分类有哪些 ?内部类有哪些优点和应用场景?
创建与获取
1 | / 1、私有内部类 => 在外部类中编写方法,对外提供内部类对象 |
优点
- 可以隐藏实现细节。
- 便于编写和维护,提高代码的可读性和可维护性。
- 使用内部类解决 Java单继承问题
- 可以更换的对外部类进行扩展
- 注:JDK16 之前成员内部类⾥不能定义静态变量
MySql数据库
1、什么是数据库事务?讲⼀下事务的 ACID 特性?
数据库事务是指数据库管理系统(DBMS)中的⼀个操作序列,这些操作必须作为⼀个不可分割的单元执行,即要 么全部执行成功,要么全部失败回滚。事务通常涉及到对数据库中的数据进行读写操作。 事务的 ACID 特性指四个关键特征:原子性(Atomicity)、⼀致性(Consistency)、隔离性(Isolation)和持久 性(Durability)
原子性
原子性是指事务是⼀个不可再分割的⼯作单元,事务中的操作要么都发生,要么都不发生。可采用“A向B转账”这个 例⼦来说明解释。 在 DBMS 中,默认情况下⼀条 SQL 就是⼀个单独事务,事务是自动提交的。只有显式的使用 start transaction 开启⼀个事务,才能将⼀个代码块放在事务中执行。
一致性
⼀致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的⼀致性。 如 A 给 B 转账,不论转账的事务操作是否成功,其两者的存款总额不变(这是业务逻辑的⼀致性,⾄于数据库关 系约束的完整性就更好理解了)。
隔离性
多个事务并发访问时,事务之间是隔离的,⼀个事务不应该影响其它事务运行效果。
在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另⼀事务修改它之前的状 态,要么是另⼀事务修改它之后的状态,事务不会查看到中间状态的数据。
事务最复杂问题都是由事务隔离性引起的。完全的隔离性是不现实的,完全的隔离性要求数据库同⼀时间只执行⼀ 条事务,这样会严重影响性能。
有四种隔离级别:
第⼀种隔离级别:Read uncommitted(读未提交)
解决了更新丢失,但还是可能会出现脏读
第⼆种隔离级别:Read committed(读提交)
解决了更新丢失和脏读问题
第三种隔离级别:Repeatable read(可重复读取)
解决了更新丢失、脏读、不可重复读、但是还会出现幻读
第四种隔离级别:Serializable(可序化)
解决了更新丢失、脏读、不可重复读、幻读(虚读)
持久性
这是最好理解的⼀个特性:持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。(完成的事务是系统永久的部分,对系统的影响是永久性的,该修改即使出现致命的系统故 障也将⼀直保持)
2、MySQL 日志有了解过吗?binlog、redolog、undolog 分别有什么作 用、有什么区别?
MySQL 是⼀款流行的关系型数据库,其日志是其关键功能之⼀。MySQL 包括三种类型的日志,分别是 binlog、 redolog 和 undolog,它们分别有不同的作用和特点。
binlog
是 MySQL 中的⼆进制日志文件,用于记录 MySQL 服务器上的所有更新和修改操作。它可以记录所有的 DDL和 DML操作,包括对表结构的更改、数据的插⼊、修改、删除等等。binlog是在事务提交后生成的,因此可以用于恢复 数据库。
redolog
用于恢复数据,保证数据的⼀致性和持久性。当 MySQL 发生修改时, redolog 会将这些操作记录下来,并写⼊磁盘。这样,当 MySQL 发生宕机或崩溃时,通过重放 redolog 就可 以恢复数据。
undolog
用于回滚操作。当 MySQL 发生事务回滚时,undolog 会记录这些操作并将其写⼊磁盘。这样,当 MySQL 需要回滚时,通过重放 undolog 就可以回滚事务。
区别
- 相同点:
- binlog 和 redolog 都是 MySQL 中的⼆进制日志。
- 不同点
- binlog 是 MySQL 记录所有的操作,而redolog 则是用于保证数据的⼀致性和持久性。
- binlog 是逻辑日志,redolog 是物理日志。
- binlog 记录的是SQL语句,而redolog 记录的是数据页的修改。
- 以 binlog 可以跨平台使用,而redolog 不能。
- undolog 是用于回滚操作的,而redolog 是用于恢复数据的。
3、数据库索引是什么,有什么作用,什么场景适合使用索引?
数据库索引是⼀种数据结构,就像书的目录⼀样,它可以帮助我们快速定位到想要的数据。
优点
- 可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
- 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。
缺点
- 需要占用物理空间,数量越大,占用空间越大。
- 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。
什么场景适合使用索引?
- 频繁使用的列,主键、外键。
- 字段有唯⼀性限制的,比如商品编码,可以使用唯⼀索引。
- 经常用于 WHERE 查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是⼀个字段,可以建联合索引。
- 经常用于 GROUP BY 和 ORDER BY 的字段,这样在查询的时候就不需要再去做⼀次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的
什么时候不适合创建索引?
- WHERE 条件,GROUP BY,ORDER BY ⾥用不到的字段,索引的价值是快速定位,如果起不到定位的字段 通常是不需要创建索引的,因为索引是会占用物理空间的。
- 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么⽆论搜索哪个值都可能得到⼀半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有⼀个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它⼀般会忽略索引,进行全表扫描。
- 表数据太少的时候,不需要创建索引。
- 经常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
4、你是怎么做 MySQL数据备份的?比如怎么恢复半个月前的数据?
MySQL 数据备份有多种方法,包括逻辑备份、物理备份、全备份和增量备份。如果你想要恢复半个月前的数据,你需要先确保你有半个月前的备份文件,然后使用相应的⼯具或命令 进行还原。
逻辑备份
逻辑备份是使用 SQL 语句来导出和导⼊数据库的数据的方法。
优点:是简单易用,可以跨平台和存储引擎,可以部分备份和恢复。
缺点:速度慢,占用空间大,可能影响数据库性能。
- MySQL 提供了⼀个原生的逻辑备份⼯具叫做 mysqldump。你可以使用它来备份整个数据库实例、单个数据库或 单张表。例如,如果你想要备份⼀个叫 ytt 的数据库,你可以在命令行窗⼝输⼊:
1 | mysqldump -u 用户名 -p 密码 --database ytt > ytt.sql |
- 如果你想要恢复这个文件到数据库中,你可以在命令行窗⼝输⼊:
1 | mysql -u 用户名 -p 密码 < ytt.sql |
这样就会执行 SQL 文件中的语句,将数据还原到数据库中。
物理备份
物理备份是直接拷贝数据库的数据文件、配置文件和日志文件。
优点:速度快,占用空间小,不影响数据库性能。
缺点:恢复复杂,需要关闭数据库服务,可能导致数据不⼀致。
MySQL 有⼀个开源的物理热备⼯具叫做 Percona XtraBackup。它可以在不锁表的情况下备份 InnoDB、XtraDB 和 MyISAM 存储引擎的表,并且支持增量备份。你可以使用它来备份整个数据库实例或单个数据库。
例如,如果你想要全量备份⼀个叫 ytt 的数据库,你可以在命令行窗⼝输⼊:
1 | innobackupex --user=用户名 --password=密码 --databases="ytt" /backup |
如果你想要恢复这个目录到数据库中,你需要先准备好数据文件,然后拷贝到对应的数据目录中,并更改权限和所有者。具体步骤如下:
1 | innobackupex --apply-log /backup/2023-02-27_00-00-00 cp -r /backup/2023-02-27_00-00 |
请注意,在恢复之前,你需要停止MySQL 服务,并确保数据目录为空。
全备份和增量备份
全备份是指备份数据库中的所有数据,不管数据是否有变化。它的优点是恢复简单,不需要其他文件。它的缺点是占用空间大,耗时长,影响数据库性能。
增量备份是指只备份上次全备份或增量备份后发生变化的数据,需要开启 binlog 日志功能. 它的优点是占用空间小,耗时短,不影响数据库性能,它的缺点是恢复复杂,需要依赖 binlog 日志文件和全备份文件。
5、⼀条 SQL 语句在 MySQL 中的执行过程是怎样的?
[
[
- 连接器:客户端首先进行身份验证,如果没通过直接返回。
- 查询缓存:如果身份验证不通过则查询缓存。缓存这个原理很容易理解,就是把常用的数据放到更高效的地方便于 查询。 查询缓存也有缺点,就是每当数据更改的时候就要重新设置缓存,在 MySQL8.0 已经将查询缓存去掉。
- 分析器:分析器先会做“词法分析”。把你输⼊的内容进行识别,知道字符分别代表什么,有什么含义然后进行语法分析,如果你的 SQL 语句不符合 MySQL 语法就会收到错误提醒。
- 优化器:优化器作用就是决定使用哪个索引,决定 join 表的连接顺序。 优化器会选择它自己认为最高效的方案, (也代表它不⼀定能选择出最优的方案)。
- 执行器:执行器还是先会判断有没有执行的权限,如果有权限的话才会执行下⼀步。遍历满足条件的行,并把组成的记录集作为结果集返回给客户端。
6、MySQL 中的索引是怎么实现的?B+ 树是什么,B 树和 B+ 树的区别, 为什么 MySQL 要用 B+ 树?
MySQL 中的索引采用B+树实现。
B+树是⼀种多路搜索树,是对B树的⼀种改进。
B树和B+树的主要区别在于 :
B+树只存储数据的索引信息,并且把叶子节点存储在同⼀层上,中间节点只起到索引作用,查询时只需要遍历⼀次树就能找到目标数据,因此B+树具有更好的查询性能和更少的磁盘I/O次数,B+树的叶⼦节点都连接成 了⼀个有序的链表,因此可以很方便地进行范围查询等操作。适合于数据库等存储大量数据的场景。
B树是⼀种平衡树,它的每个节点都存储有序的关键字,并且每个节点的子节点数目介于m/2和m之间。B树中的每个节点既可以存储数据,也可以存储索引信息,因此B树可以减少I/O次数,适合于文件系统等存储大量数据的场景。但是B树的查询性能相对较低,因为每个节点都可能存储数据,查询时需要遍历多个节点才能找到目标数据。
在MySQL中使用B+树作为索引的数据结构,主要是因为B+树的查询性能好、⽀持范围查询、⽀持数据分页等操作,适用于存储海量数据的场景。此外,B+树的索引结构相对简单,易于实现和维护,能够满足高并发、高可用性的数据库要求
7、MySQL 事务有哪些隔离级别、分别有什么特点,以及 MySQL 的默认隔离级别是什么?
- 读未提交(Read Uncommitted):事务可以读取未提交的数据,可能会读到脏数据,会导致幻读、不可重 复读、脏读等问题;
- 读已提交(Read Committed):只能读取已经提交的数据,可以避免脏读问题,但是可能会遇到不可重复读、幻读问题。
- 可重复读(Repeatable Read):保证同⼀个事务中多次读取同⼀数据的结果是⼀致的,避免了脏读和不可重复读问题,但是可能会遇到幻读问题。
- 序列化(Serializable):最高的隔离级别,可以避免所有并发问题,但是并发性能非常低,开销很大。
MySQL 的默认隔离级别是可重复读(Repeatable Read)。
- 脏读:⼀个事务读到了另⼀个事务未提交的数据。
- 不可重复读:同⼀个事务多次读取同⼀数据得到不同结果。
- 幻读:指同⼀个事务前后读取的数据集合不⼀致。
8、意向锁是什么?有什么作用?它是表级锁还是行级锁?
在使用 InnoDB 引擎的表⾥时对某些记录加上「共享锁」之前,需要先在表级别加上⼀个「意向共享锁」
在使用 InnoDB 引擎的表⾥时对某些记录加上「独占锁」之前,需要先在表级别加上⼀个「意向独占锁」
也就是,当执行插⼊、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。
意向锁的作用
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。
表锁和行锁是满足读读共享、读写互斥、写写互斥的。
作用:为了快速判断表里是否有记录被加锁
意向锁是表级别锁
9、MVCC 是什么?InnoDB 是如何实现 MVCC 机制的?
MVCC 是指多版本并发控制(Multiversion Concurrency Control),是⼀种并发控制机制,常用于数据库系统 中,用于实现事务的并发控制。它允许在同⼀时间多个事务对同⼀个数据集合进行读取操作,同时防止数据不⼀致和其他并发问题。
InnoDB 是 MySQL 中最常用的存储引擎之⼀,它的 MVCC 实现是通过在每行记录中添加两个隐藏的列,分别记录行的创建时间和过期时间,以此来判断事务对该行记录的可见性。当⼀个事务需要读取⼀行记录时,InnoDB 首先 读取这行记录的创建时间和过期时间,并根据这些信息判断该行记录是否可键。如果创建时间早于当前事务的开始时间,且过期时间晚于当前事务的开始时间,那么该行记录对当前事务可见。
在 InnoDB 中,MVCC 主要是通过实现以下几个机制来实现的:
- 事务版本号:每个事务都有⼀个唯⼀的版本号,用来标识该事务的创建时间。
- 读取视图:每个事务在开始时都会创建⼀个读取视图,记录该事务开始时间和其他信息。在事务执行期间,所有读取操作都要检查该视图,以确定读取哪些版本的数据。
- undo 日志:在事务执行期间,如果对数据进行修改,那么会先将原始数据复制⼀份到 undo ⽇志中。这样, 在回滚操作时就可以使用 undo ⽇志中的数据来还原原始数据。
- 快照读取:在某些情况下,事务需要读取⼀个数据的历史版本,而不是当前版本。这时可以使用快照读取来实现,即在读取时根据事务开始时间和 undo ⽇志来读取历史版本的数据。
10、覆盖索引和联合索引是什么?讲⼀下索引的最左前缀匹配原则。
覆盖索引
- 是指 SQL 中 查询 的所有字段,在索引 B+ Tree 的叶子节点上都能找得到的那些索引,从⼆级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
联合索引
- 通过将多个字段组合成⼀个索引,该索引就被称为联合索引。
最左前缀匹配原则
- 联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停⽌匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段⽆法用到联合索引。注意,对于 >=、<=、BETWEEN、 like 前缀匹配的范围查询,并不会停⽌匹配。
11、什么是 MySQL 执行计划?如何获取执行计划并对其进行分析?
MySQL 执行计划是 MySQL 查询优化器分析 SQL 查询时生成的⼀份详细计划,包括表如何连接、是否走索引、表扫描行数等。通过这份执行计划,我们可以分析这条 SQL 查询中存在的问题(如是否出现全表扫描),从而进行针对优化。
- id:SELECT 查询的序列号,表示执行select子句的顺序(id 相同,执行顺序从上到下;id 不同,值越大越先执行;如果是并集结果,则 id 值为 NULL
- select_type:查询类型,来区分简单查询、联合查询、子查询等。常见的类型有:
- SIMPLE:简单查询,不包含表连接或子查询
- PRIMARY:主查询,外层的查询
- SUBQUERY:子查询中第⼀个 SELECT
- UNION:UNION 后面的 SELECT 查询语句
- UNION RESULT:UNION 合并的结果
- table:查询用到的表名
- partitions:匹配的分区,没有分区的表为 NULL
type ★:连接的类型,常见的类型有(性能从好到差):
system:存储引擎能够直接知道表的行数(如 MyISAM)并且只有⼀行数据
const:通过索引⼀次找到,通常在用主键或唯⼀索引时出现
eq_ref:用主键或唯⼀索引字段作为连接表条件
ref:用普通索引的字段作为连接表条件
range:对索引列进行范围查询
index:利用索引扫描全表
all:全表扫描
- possible_keys:可能用到的索引
- key ★:实际使用的索引,没有使用为 NULL
- key_len:索引字段的最大长度,在满足需求下越短越好
- ref:当使用索引等值查询时,与索引作比较的列或常量
- rows ★:预计扫描的行数,值越大,查询性能越差
- filtered:表示返回结果的行数占需读取行数的百分比
- Extra ★:有关查询执行的其他信息
- using index:使用覆盖索引,不用回表查询
- using where:使用 where ⼦句来过滤结果集
- using temporary:使用到临时表来存储中间结果,可能会导致性能问题
- using filesort:查询需要进行文件排序操作,可能会导致性能问题
- using index condition:先根据能用索引的条件获取符合条件的数据行,然后在根据其他条件去过滤数据。
12、MySQL 支持哪些存储引擎?默认使用哪个?MyISAM 和 InnoDB 引 擎有什么区别,如何选择?
MySQL ⽀持多种存储引擎,包括 InnoDB、MyISAM、MEMORY、CSV 等。默认情况下,MySQL 使用的存储引 擎是 InnoDB。
MyISAM 和 InnoDB 是 MySQL 中 最常用 的两种存储引擎,它们有以下区别:
- 锁定方式不同:MyISAM 使用表级锁定,而 InnoDB 使用行级锁定。在并发访问时,InnoDB 的锁定方式更加精细,可以避免锁定整个表,提高了并发性能。
- 数据完整性不同:MyISAM 不支持事务和外键约束,而 InnoDB ⽀持事务和外键约束,可以保证数据的完整 性和⼀致性。
- 读写性能不同:MyISAM 的读写性能相对较高*高,适合于读密集型应用;而 InnoDB 的写性能相对较高**,适合于写密集型应用。
- 空间利用率不同:MyISAM 不⽀持行级别的存储,存储空间利用率较低;而 InnoDB ⽀持行级别的存储,存储空间利用率更高。
选择应用场景:
- 如果应用主要是读操作,可以考虑选择 MyISAM 引擎,以提高读取性能。
- 如果应用主要是写操作或需要⽀持事务和外键约束,可以考虑选择 InnoDB 引擎,以保证数据的完整性和⼀致性。
- 如果需要高性能和高可用性,可以考虑选择使用 MySQL 集群或使用多个副本实例,并将数据分布在不同的节点上。
Spring 框架
1、Spring 框架是什么?使用 Spring 框架有哪些好处?
介绍
Spring 框架是⼀个开源的 Java 应用程序框架,用于构建企业级应用程序。它提供了全面的基础设施⽀持,包括控制反转(IoC)、依赖注⼊(DI)、AOP(面向切面编程)等功能,使开发者可以更加专注于业务逻辑的实现,而不必处理底层的框架代码。
- Spring 框架由多个模块组成,其中核心模块是 Spring Framework,它提供了许多常用的工具和服务
Spring 框架的主要优点具体如下:
- 方便解耦,简化开发。 Spring 就是⼀个大工厂,可以将所有对象的创建和依赖关系的维护交给 Spring 管理。
- 方便集成各种优秀框架。 Spring 不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如 Struts2、Hibernate、MyBatis 等)的直接支持。
- 降低 Java EE API 的使用难度。 Spring 对 Java EE 开发中非常难用的⼀些 API(JDBC、JavaMail、远程调用等)都提供了封装,使这些 API 应用的 难度大大降低。
- 方便程序的测试。 Spring ⽀持 JUnit,可以通过注解方便地测试 Spring 程序。AOP 编程的⽀持。
- Spring 提供面向切面编程,可以方便地实现对程序进行权限拦截和运行监控等功能。
- 声明式事务的支持。 只需要通过配置就可以完成对事务的管理,而无须⼿动编程。
2、Spring 的两大核心概念是什么?简单讲⼀下你对它们的理解
IOC:
- IOC(Inverse Of Controll,控制反转):就是原来代码里面需要自己手动创建的对象,依赖,反转给Spring来帮忙实现。我们需要创建⼀个容器,同时需要⼀种描述来让容器知道要创建的对象与对象之间的关系。
- 在Spring中BeanFactory就是IOC容器,在Spring初始化的时候,创建容器,并将需要创建对象和对象的关系 (xml,注解)通过BeanDefinitionReader加载到BeanDefinition中并保存在BeanDefinitionMap中,然后再 由IOC容器创建bean对象.
两种bean的注册方式
- 通过@Bean+@Configuration的方式直接定义要创建的对象与对象的关系
- 通过@Component定义类,这种方式必须使用@ComponetScan定位Bean扫描路径
AOP
- AOP 在面向对象编程(oop)思想中,我们将事物纵向抽成⼀个个的对象。而在面向切面编程中,我们将⼀个个的对象某些类似的方面横向抽成⼀个切面,对这个切面进行⼀些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP底层是动态代理,如果是接口采用JDK 动态代理,如果是类采用CGLIB方式实现动态代理。
3、什么是 IOC,简单讲⼀下 Spring IOC 的实现机制?
IOC:
- IOC(Inverse Of Controll,控制反转):就是原来代码里面需要自己手动创建的对象,依赖,反转给Spring来帮忙实现。我们需要创建⼀个容器,同时需要⼀种描述来让容器知道要创建的对象与对象之间的关系。
两种bean的注册方式
- 通过@Bean+@Configuration的方式直接定义要创建的对象与对象的关系
- 通过@Component定义类,这种方式必须使用@ComponetScan定位Bean扫描路径
IOC的创建:
- 在Spring中BeanFactory就是IOC容器,在Spring初始化的时候,创建容器,并将需要创建对象和对象的关系 (xml,注解)通过BeanDefinitionReader加载到BeanDefinition中并保存在BeanDefinitionMap中,然后再 由IOC容器创建bean对象。
Bean的生命周期
- bean 的实例化:spring 启动后,会查找和加载需要被 spring 管理的 Bean,并且实例化
- bean 的属性注入(bean 的初始化):bean被实例化后将 Bean 的引用和值注⼊到 bean的属性中
- 查看是否调用⼀些 aware 接⼝,比如 BeanFactoryAware,BeanFactoryAware,ApplicationContextAware 接⼝,分别会将 Bean 的名字,BeanFactory 容器实例,以及 Bean 所在的上下文引用传⼊给 Bean。
- 在初始化之前,会查看是否调用了 BeanPostProcessor 的预初始化方法,可以对 bean 进行扩展。
- 调用InitializingBean 的 afterPropertiesSet()方法:如果 Bean 实现了 InitializingBean 接⼝,spring 将调用他们的afterPropertiesSet()方法,类似的,如果 Bean 使用 init-method 生命了初始化方法的话, 这个方法也会被调用。
- 初始化成功之后,会查看是否调用 BeanPostProcessor 的初始化后的方法:如果 Bean 实现了 BeanPostProcessor 接⼝,spring 就将调用他们的 postprocessAfterInitialization()方法。可以对 bean 进行扩展。
- bean的正常使用:可以被应用程序正常使用了,他们将驻留在上下文中,直到应用的上下文被销毁。
- bean的销毁:**调用 DisposableBean 的destory()方法:如果 Bean 实现 DisposableBean 接⼝,spring 将用 他的 destory()方法,相同的,如果 Bean 使用了 destory-method 生命销毁方法,该方法也会被调用。(但由 于 bean 也分为单例和多例,单例 bean 会随着 IOC 容器的销毁而销毁,多例的 bean 不会随着 IOC 容器的销毁 而销毁,他是通过 JVM ⾥面的垃圾回收器负责回收**)
4、Spring 框架中都用到了哪些设计模式?
- 单例模式:Spring 的 Bean 默认是单例模式,通过 Spring 容器管理 Bean 的生命周期,保证每个 Bean 只被创建⼀次,并在整个应用程序中重用。
- 工厂模式:Spring 使用工厂模式通过 BeanFactory 和 ApplicationContext 创建并管理 Bean 对象。
- 代理模式:Spring AOP 基于动态代理技术,使用代理模式实现切面编程,提供了对 AOP 编程的⽀持。
- 观察者模式:Spring 中的事件机制基于观察者模式,通过 ApplicationEventPublisher 发布事件,由 ApplicationListener 监听事件,实现了对象间的松耦合。
- 模板方法模式:Spring 中的 JdbcTemplate 使用了模板方法模式,将⼀些固定的流程封装在父类中,子类只 需实现⼀些抽象方法即可。
- 策略模式:Spring 中的 HandlerInterceptor 和 HandlerExecutionChain 使用了策略模式,允许开发者自定义处理器拦截器,按照⼀定顺序执行。
- 责任链模式:Spring 中的过滤器和拦截器使用了责任链模式,多个过滤器和拦截器按照⼀定顺序执行,每个过滤器和拦截器可以拦截请求或者响应并做出相应的处理。
5、Spring、SpringMVC、SpringBoot 三者之间是什么关系?
- Spring 是⼀个 Java 的轻量级应用框架,提供了基于 IoC 和 AOP 的支持,用于构建企业级应用。Spring 有多 个模块,包括 Spring Core、Spring Context、Spring JDBC、Spring Web 等,每个模块提供了不同的功能。
- SpringMVC 是 Spring 框架的⼀部分,是基于 MVC 设计模式的 Web 框架,用于构建 Web 应用程序。它提供了控制器、视图解析器、数据绑定、异常处理等功能,使得开发 Web 应用变得更加简单。SpringMVC 还⽀持 RESTful 架构。
- SpringBoot 是基于 Spring 框架的⼀个开发框架,用于快速构建独立的、生产级别的 Spring 应用程序。它通过自动配置和约定优于配置的方式,简化了 Spring 应用程序的配置和开发过程。SpringBoot 集成了很多常用的第三方库和⼯具,例如 Spring Data、Spring Security、Thymeleaf、Logback 等,可以极大地提高开发效 率。
因此,SpringBoot 可以看作是在 Spring 的基础上,通过自动配置和约定优于配置的方式,提供了更加简单、快速 的开发体验。而 SpringMVC 则是 Spring 框架中用于构建 Web 应用程序的模块。
6、有哪些注解可以注入Bean?@Autowired 和 @Resource 的区别?
哪些可以注入bean
- @Autowired:自动注入,按照类型自动装配,如果有多个同类型的 Bean,则需要通过 @Qualifier 指定具体 的 Bean。
- @Resource:Java 自带的注入方式,按照名称自动装配,默认是按照属性名称进行匹配,如果需要按照 Bean 的名称进行匹配,可以使用 @Resource(name=”beanName”)。
- @Inject:和 @Autowired 类似,也是按照类型进行自动装配,但是 @Inject 注解是 JSR-330 提供的,而 @Autowired 注解是 Spring 框架提供的。
- @Value:用于注入配置文件中的属性值,可以指定默认值
- @Component:用于声明⼀个 Bean,作用类似于 XML 中的 标签。
注⼊ Bean 的时候最好使用接⼝类型作为注⼊对象,这样可以避免因为具体实现类变更导致注⼊失 败的问题
7、Spring 如何处理线程并发问题,ThreadLocal 你了解过吗?
同步关键字 synchronized:使用synchronized 关键字可以对共享资源进行加锁,从而保证多线程访问时的同步性。但是,synchronized 对性能会产生⼀定的影响,并且容易导致死锁等问题。
Lock 接口:Lock 接口提供了比synchronized 更加灵活的加锁方式,并且可以防止死锁问题的发生。但是, Lock 接⼝的使用相对较复杂,需要⼿动进行加锁和解锁操作。
- ThreadLocal 类:ThreadLocal 类提供了线程本地变量的功能,可以让每个线程拥有自己的变量副本,从而避免了多个线程之间的共享问题。但是,ThreadLocal 类的使用需要注意内存泄漏问题。
关于ThreadLocal
ThreadLocal 是 Java 中的⼀个线程局部变量,它可以为每个线程提供⼀个独立的变量副本,避免了多线程之间的数据共享和数据竞争问题。
ThreadLocal 可以在每个线程中存储⼀个对象,并且每个线程只能访问自⼰的对象,而不会影响其他线程中的对 象。
ThreadLocal 主要用于解决线程安全问题,例如在 Web 应用中,可以使用 ThreadLocal 存储用户的会话信息,这 样每个线程就可以独立地访问自己的会话信息,避免了多个线程之间的数据共享和数据竞争问题。
在 Spring 中,ThreadLocal 通常用于存储和传递⼀些与线程相关的上下文信息,例如当前登录用户的信息、请求 的 IP 地址等。可以将 ThreadLocal 对象定义为⼀个 Bean,然后在需要使用时注⼊到其他 Bean 中使用。
8、Spring 中的 BeanFactory 和 ApplicationContext 有什么区别和联系?
BeanFactory 是 Spring 容器的超级接⼝。ApplicationContext 是 BeanFactory 的子接⼝
区别
- BeanFactory 是 Spring 框架的基础设施,用于管理 Bean 的生命周期和依赖关系,提供了 IoC 和 DI 功能。 ApplicationContext 是 BeanFactory 的扩展,提供了更多的功能,例如国际化⽀持、AOP ⽀持等。
- BeanFactory 是延迟加载的,即只有在获取 Bean 时才会进行实例化,可以减少系统资源的占用。而 ApplicationContext 在启动时会⽴即加载所有的 Bean,导致启动时间较长。
- BeanFactory 是单例模式,即在整个应用中只有⼀个 BeanFactory 实例。而 ApplicationContext 可以有多个实例,并且可以通过⽗子容器的方式组织起来,方便模块化开发。
联系
- BeanFactory 和 ApplicationContext 都是用于管理 Spring Bean 的容器,可以管理 Bean 的生命周期和依赖 关系,提供了 IoC 和 DI 功能。
- ApplicationContext 是 BeanFactory 的扩展,提供了更多的功能,例如国际化⽀持、AOP ⽀持等,同时也支持 BeanFactory 的所有功能。
- BeanFactory 和 ApplicationContext 都可以管理单例 Bean 和原型 Bean,可以控制 Bean 的作用域和生命周期。
9、讲⼀讲 Spring 框架中 Bean 的生命周期?
bean的生命周期有五个阶段
- 创建前准备:主要是扫描 spring 中的上下文和配置(解析和查找)bean 的⼀些扩展配置。
- 创建实例:在这个阶段调用 bean 的构造器进行实例的创建。这是通过反射进行实例的创建的,并且会扫描和解析 bean 的⼀些属性。
- 依赖注入:实例化的 bean 可能依赖其他的 bean,这个阶段就是注入实例的 bean 依赖的 bean,如用注解 @Autowired 进行依赖的注⼊。
- 容器缓存:这个阶段中,将实例化的 bean 放⼊容器和 spring 的缓存中,此时开发者可以使用实例化的 bean。
- 销毁实例:当 spring 的上下文关闭时,这些上下文的 bean 也会被销毁。
10、Spring 支持哪几种事务管理类型,Spring 的事务实现方式和实现原理是?
- 编程式事务管理:在代码中显式地编写事务管理相关代码,如开启事务、提交事务、回滚事务等。
声明式事务管理:使用 AOP 技术,在代码中通过配置进行声明,从而实现对事务管理的控制。
注解式事务管理:基于声明式事务管理,使用注解的方式进行事务的管理。
Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring 是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过binlog 或者 redolog 实现的。
- 事务管理器(Transaction Manager):Spring 事务管理器是事务的核心组件,它负责管理事务的生命周期和事务的状态。Spring 事务管理器提供了多种实现,包括 JDBC、Hibernate、JPA、JTA 等,开发⼈员可以根据自己的需求来选择适合的事务管理器。
- 事务代理(Transaction Proxy):Spring 使用代理模式来实现事务管理,即通过代理对象来拦截被代理对象的方法调用,以实现事务的开启、提交或回滚等操作。Spring 事务代理有两种实现方式:基于 AOP 的代理 和基于动态代理的代理。
- 事务切面(Transaction Aspect):Spring 事务切面是⼀组通知(Advice)和切点(Pointcut)的组合,用于定义事务的行为,例如事务的开启、提交或回滚等操作。Spring 事务切面可以基于注解或 XML 配置来实现,开发⼈员可以根据自⼰的需求来选择适合的方式。
- 事务同步器(Transaction Synchronization):Spring 事务同步器用于管理事务的提交或回滚时需要执行的⼀些操作,例如清理缓存、释放资源等。Spring 事务同步器通过 Synchronization 接⼝和 TransactionSynchronizationManager 类来实现,可以实现对事务的后置处理。
11、什么是 Spring 的依赖注入,依赖注入的基本原则以及好处?
依赖注⼊(Dependency Injection,简称 DI)是 Spring 框架中的⼀种设计模式,用于实现控制反转(Inversion of Control,简称 IoC)。它是⼀种将对象之间的依赖关系从硬编码中解耦的方法。通过依赖注入,Spring 框架可以在运行时自动为类的属性、构造函数或方法参数提供所需的依赖对象,从而实现对象之间的松耦合。
原则
- 高层模块不应该依赖低层模块。它们都应该依赖抽象。
- 抽象不应该依赖具体实现。具体实现应该依赖抽象。
好处
- 灵活性:通过依赖注入,对象之间的关系不再由程序员⼿动控制,而是由 Spring 容器动态地维护,可以灵活地组合和切换不同的对象实现。
可测试性:依赖注入可以使各个对象之间的依赖关系更加松散,方便进行单元测试和集成测试。
可扩展性:依赖注入将各个对象之间的依赖关系解耦,方便对系统进行扩展和升级。
- 可维护性:依赖注入可以减少重复代码,降低了代码的复杂度,使代码更加易于维护和修改。
12、什么是 AOP?有哪些实现 AOP 的方式?Spring AOP 和 AspectJ AOP 有什么区别?
AOP:Aspect Orlented Programming 面向切面编程;
实现区别:
- Spring AOP 是基于动态代理实现的,通过 JDK 动态代理或 CGLIB 代理来生成代理对象,只能用于方法级别的 拦截和处理,在运行时织入。
- AspectJ AOP 是⼀个更加完整的 AOP 框架,基于字节码操作,⽀持更多的切面表达式和切面类型,可以在编译期、类加载期进行织⼊,性能更加高效
实现方式:
- 静态代理:⼿动编写代理类,实现增强逻辑。需要为每个目标类编写代理类,代码量大
- JDK 动态代理:利用反射机制生成代理类,只能用于实现了接⼝的类
- CGLIB 动态代理:利用字节码生成目标类的⼦类(即代理类),可以代理未实现接⼝的类
- AspectJ AOP:使用 AspectJ 在编译期或类加载期对目标类进行织⼊
Redis缓存
1、什么是 Redis?Redis 有哪些特点?Redis 有哪些常见的应用场景?
Redis 是基于内存的键值型(key - value)的 NoSQL 数据库(非关系型数据库)。key ⼀般是 String 类型,而 value z支持丰富的数据类型,包括String、Hash、List、Set、SortedSet 这五种基本类型,此外还有 GEO、 BitMap、HyperLogLog 等其他类型。
redis有哪些特点?
- 读写性能优异
- 基于内存,内存的访问速度是比磁盘快很多的
- 采用单线程模型,不存在多线程的上下文切换,不需要考虑锁的问题。
- 使用 IO 多路复用模型,让 Redis 不需要创建额外的线程来监听客户端的大量请求,减少性能的消耗。
- 内置了多种优化过的数据结构实现
- 所有操作命令都是原子性的
- ⽀持事务
- 允许多个命令按顺序执行并不会被打断
- 不⽀持回滚
- 支持数据持久化
- RDB:通过创建快照来获得存储在内存⾥面的数据在某个时间点上的副本
- AOF:执行完更改数据的命令后,会将该命令记录到日志中
- ⽀持分布式部署
Redis有哪些常见的应用场景?
- 数据缓存
- 将热点数据缓存到 Redis,提高数据读取和系统响应速度。
- 将用户凭证(如 token)存⼊ Redis,实现单点登录。
- 分布式锁
- 利用 Redis 的 setnx 命令实现互斥,对于 setnx 命令,如果 key 不存在则会设置 key 的值并返回 1,否 则直接返回 0。
- 因此,可以通过 setnx 命令去尝试获取锁,获取锁成功才能继续执行相应业务。
- 此外,还得给锁设置⼀个过期时间,防⽌系统出现问题导致锁⽆法释放。
- 如果需要更复杂的需求,可以采用 Redisson,它⾥面提供了多种分布式锁的实现。
- 限时业务的运用
- Redis 中可以使用 expire 命令设置⼀个键的生存时间,到时间后 Redis 会删除它。利用这⼀特性可以运用在限时的优惠活动信息、⼿机验证码等业务场景。
- 全局唯⼀ID
- Redis 由于 incrby 命令可以实现原⼦性的递增,所以可以用于唯⼀分布式序列号的生成。
- 消息队列
- 基于 List 实现:利用 LPUSH 和 RPOP;
- 基于 PUB/SUB 实现;
- 基于 Stream 实现。
- 共同关注
- 由于 Set ⽀持交集、并集、差集等功能,可以实现共同关注、共同爱好等功能。
- 排行榜
- 由于 SortedSet 是⽀持排序的,可以用于各种排行榜的场景。
- 统计活跃用户
- 利用 BitMap,以⽇期为 key,用户 ID 为 offset,当⽇活跃过就设置为 1。
- 页面统计UV
- 将访问指定页面的每个用户 ID 添加到 HyperLogLog 中,调用 PFCOUNT 获取。
因为我的项目中也有使用到Redis,在这里大胆猜测⼀下面试官会问到: 在你的项目中是怎么使用Redis的?
- 在我的项目中使用 Redis 配合 Spring 的 redis-session,将用户的登录态存入 redis,实现单点登录。
- 对于系统的首页,为了加快响应速度,设置定时任务将首页数据缓存到 Redis中,避免了首页加载过慢用户体验差的情况。
- 对于用户加⼊队伍的限制,使用 Redisson 作分布式锁,防⽌了用户恶意请求加⼊超出限制数量的队伍。
2、讲⼀下 Redis 的单线程模型,IO 多路复用是什么?
- Redis 是⼀款基于内存的高性能键值存储系统,采用单线程模型的设计。在 Redis 中,所有客户端的请求都是由⼀个单线程来处理的,这个单线程不断地从客户端套接字中读取命令请求,并将命令请求放⼊⼀个请求队列中。接 着,Redis 的事件处理器会按照⼀定的规则选择⼀个请求进行处理,处理完之后将响应结果返回给客户端。
单线程模型的优点是可以避免多线程并发访问共享数据时的竞争和死锁问题,简化了系统的设计和调试。此外,由 于 Redis 的内存访问速度非常快,因此单线程处理请求也能够保证足够的性能。
IO多路复用:
- ⼀个线程中同时监听多个文件描述符,⼀旦某个文件描述符就绪,就⽴即处理对应的事件。在 Redis 中,采用的是基于 epoll 的 IO 多路复用技术,可以实现高效的事件监听和响应。
- 在 Redis 中,客户端的请求是由⼀个单线程来处理的,而IO 操作却是通过 epoll 多路复用技术实现的。这种设计方式既能充分利用 CPU 的计算能力,又能够保证足够的 IO 处理能力,从而实现了高效的键值存储服务。
3、Redis 基础类型中的 String 底层实现是什么?
String 底层实现是 SDS,也就是动态字符串。
Redis当中的动态字符串主要是对C语言中的做了⼀个封装,使得SDS具有动态扩容、O(1)复杂度的长度计算的特点。
并且避免了C字符串缓冲区溢出(C字符串不记录自身长度)。
当 SDS API 需要对 SDS 进行修改时,API 会先检查 SDS 的空间是否满足修改所需的要求,如果不满足的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小。
SDS主要由三个部分组成:
- len 实际使用长度
- free buf数组中未使用字节的数量
- char buf[] 用于保存字符串
4、如何使用Redis 实现⼀个排行榜?
Redis 实现排行榜是 Redis 中⼀个很常见的场景,主要使用的是 ZSet 进行实现,下面是为什么选用 ZSet:
- 有序性:排行榜肯定需要实现⼀个排序的功能,在 Redis 中有序的数据结构有 List 和 ZSet;
- 支持分数操作:ZSet 可以对集合中的元素进行增删改查操作,十分贴合排行榜中用户分数动态变化的场景, 而 List 并不能针对分数进行操作,只有其中的 value 进行操作;
- 支持范围查询:ZSet 可以按照分数进行范围查询,如排行榜中的 Top10 需求就可通过该特性进行实现;
- 支持去重:由于 ZSet 属于 Set 的特殊数据结构,因此同样拥有 Set 不可重复的特性,对于排行榜中不可出现重复项的需求也十分贴合,而 List 只能手动去重。
1 | ZADD scores 95 "王五" |
在springBoot整合
1 | // 添加学生成绩 |
5、Redis 的持久化机制有哪些?说说各自的优缺点和应用场景?
redis中的持久化主要分为RDB和AOF
RDB:
- RDB 持久化机制会在指定的时间间隔内对数据集做快照,将快照存储到磁盘中。
- RDB 的优缺点:对于数据恢复速度比较快,同时存储的文件也比AOF 小,缺点是可能会出现数据丢失的情况,因为快照的时间间隔越长,数据丢失的可能性就越大。、
AOF
- AOF 持久化机制则会将每⼀次的写操作都追加到⼀个文件中,这个文件就是 AOF 文件。
- AOF 文件保存了 Redis 服务器接收到的所有写命令,当 Redis 重启时,就可以使用 AOF 文件重建数据集。 AOF 的优缺点:数据丢失的可能性比 RDB 低,缺点是文件比 RDB 大,恢复速度比较慢。
RDB+AOF混合使用
在混合持久化机制中,使用 AOF 持久化机制保存每次写操作,使用 RDB 持久化机制保存指定时间点的数据快照, 以此来达到数据恢复速度和数据完整性的平衡。
应用场景
- RDB 适合用于数据集比较大,数据恢复速度比较快的场景,例如备份、灾难恢复等;
- AOF 适合用于数据集比较大,数据完整性比较重要的场景,例如⾦融、电商等行业。
6、如何用Redis 实现分布式 Session?
分布式 session 指在多个服务器间共享 session,我们可以使用 redis 来存储 session 来实现该功能。 在 redis 中我们通常使用 Hash 来存储 session。
具体步骤
- 用户登录成功后,将 Session 存到 redis 中
- 将key设置为⼀个全局 id,格式可以采用“session:token”,其中 token 为 sessiond 的唯⼀标识。
- 将 session 的唯⼀标识 token 以 cookie 的形式返回给客户端,客户端在后续请求中都会携带这个 cookie。
- 后续请求中,服务器拿到客户端传来的 cookie,并根据它的值,也就是 token,去 redis 找对应的 session 数据。
- 用户退出登录后,将 session 删除。
7、讲⼀下 Redis 中的内存淘汰机制、有哪些内存淘汰策略?
Redis 中的内存淘汰机制包括
- 定期删除:Redis 可以设置⼀个定时器,定期扫描键空间中的键,并删除已经过期的键。
- 惰性删除:当⼀个键过期时,Redis 不会⽴即删除该键,而是等到该键被访问时再删除。
- 内存淘汰策略:当 Redis 内存占用达到上限时,会根据内存淘汰策略来选择⼀些键进行删除,以腾出更多的 内存空间。
redis的内存淘汰策略包括
- noeviction:禁⽌删除键,即不做任何操作。
- allkeys-lru:从所有的键中选择最近最少使用的键进行删除。
- allkeys-random:从所有的键中随机选择⼀些键进行删除。
- volatile-lru:从已设置过期时间的键中选择最近最少使用的键进行删除。
- volatile-random:从已设置过期时间的键中随机选择⼀些键进行删除。
- volatile-ttl:从已设置过期时间的键中选择剩余时间最短的键进行删除。
8、Redis 6.0 之后为何引⼊了多线程?6.0 之前为什么不使用多线程?
在 Redis 6.0 之前,Redis 是单线程的,这是因为 Redis 的主要瓶颈是在 CPU 上。但是随着硬件的发展,现代服务 器的 CPU 核心数已经达到了几十个,这就导致 Redis 单线程模型无法充分利用多核处理器的性能。因此,Redis 6.0 引⼊了多线程,以提高 Redis 在多核处理器上的性能。
在6.0之前不用多线程的原因
- Redis 单线程模型相对简单,容易维护和调试,代码逻辑也比较清晰。
- Redis 的主要瓶颈在于 CPU,而不是 I/O,因此采用多线程模型并不能显著提高性能。
- Redis 是⼀个内存型数据库,它的性能主要受到 CPU 和内存带宽的限制。采用多线程模型会增加线程之间的竞争和锁等开销,反而可能降低 Redis 的性能。
9、Redis 有哪些数据类型?基础数据结构有几种?你还知道哪些 Redis 的高级数据结构?
基础:
高级:
10、Redis 为什么快?
- 内存存储:Redis 的数据都是存储在内存中的,相比于硬盘存储的数据库,内存存储速度更快。
- 单线程模型:Redis 使用单线程模型处理所有的请求,避免了多线程之间的线程切换和竞争等开销,提高了处理请求的效率。
- 非阻塞 I/O:Redis 使用非阻塞 I/O 处理网络通信,当⼀个客户端请求到来时,Redis 不会⼀直等待客户端的响应,而是会先处理其它的请求,这样就避免了 I/O 阻塞带来的性能问题。
精简高效的数据结构:Redis 内置了多种高效的数据结构,如哈希表、跳表等,这些数据结构的实现非常精简高效,减少了 Redis 对内存和 CPU 的占用,从而提高了 Redis 的性能。
持久化策略:Redis 支持多种持久化策略,如 RDB(快照)和 AOF(追加式文件)等,这些策略可以将内存 中的数据保存到硬盘中,以保证数据的持久性和安全性。同时,Redis 可以将数据以压缩的方式存储在硬盘中,减少了硬盘的占用,提高了数据的读写速度。
11、如何使用 Redis 实现分布式锁?
redis实现分布式锁的方式有两种:
- 通过 redis 提供的 setnx 进行实现,往 redis 中使用 setnx 插⼊ key 时,如果 key 存在,则返回 0,可以通过 插⼊ key 的返回值进行判断来实现分布式锁。
- 通过使用 Redission(客户端)来实现分布式锁。可以调用 Redission 提供的 api,即 lock(),unlock()方法进 行加锁和锁的释放。此外, Redission 还提供了 watchdog,即看门狗,来对加锁的 key 每隔 10 s对该 key 进行续时,(从而避免锁的过期)。
- Redission 的所有指令是通过 lua 脚本进行实现的,lua 脚本可以保证所有指令执行的原子性
12、如何用 Redis 中的 HyperLogLog 统计页面 UV?
- 创建⼀个 HyperLogLog 数据结构:使用 Redis 的 PFADD 命令创建⼀个 HyperLogLog 数据结构,用于记录 所有访问过该页面的用户的信息。例如,使用以下命令创建⼀个名为 “page_uv” 的 HyperLogLog:
1 | PFADD page_uv user1 user2 user3 |
- 统计 HyperLogLog 的基数:使用 Redis 的 PFCOUNT 命令可以统计 HyperLogLog 的基数,即访问过该⻚面 的唯⼀用户数。例如,使用以下命令可以统计名为 “page_uv” 的 HyperLogLog 的基数:
1 | PFCOUNT page_uv |
- 定期合并 HyperLogLog:为了保证统计结果的准确性,需要定期合并多个 HyperLogLog。使用 Redis 的 PFMERGE 命令可以将多个 HyperLogLog 合并为⼀个 HyperLogLog。例如,使用以下命令可以将名为 “page_uv1” 和 “page_uv2” 的 HyperLogLog 合并为⼀个名为 “page_uv” 的 HyperLogLog:
1 | PFMERGE page_uv page_uv1 page_uv2 |
计算机网络
1、简述计算机网络七层模型和各自的作用?
计算机网络七层模型是⼀个把网络通信协议分为七个层次的标准模型,其目的是为了让计算机网络的设计和管理更加灵活和模块化。这个模型被称为OSI模型(Open System Interconnection Model),它由国际标准化组织 (ISO)于1984年发布,是⼀个开放的标准模型。
每个层次都有自己的独立功能和责任,这种分层的方式使得每个层次都可以独立⼯作,同时还能够很好地协调上下层之间的数据传输,而不需要依赖于其他层次的实现细节。以下是每个层次的具体功能和责任:
- 物理层:主要负责通过物理媒介传输比特流。物理层规定了物理连接的规范,包括电缆的类型、接口的规范等。
- 数据链路层:主要负责把数据分成数据帧进行传输,并对错误进行检测和纠正。数据链路层还负责物理地址的分配、数据流量控制、错误校验等。
- 网络层:主要负责数据在网络中的传输,包括路由选择、分组转发、数据报文的封装等。网络层还处理数据包的寻址和控制流量等。
- 传输层:主要负责数据传输的可靠性和流量控制等,同时还包括分段、组装、连接建⽴和断开等功能。传输层 的最重要的两个协议是 TCP 和 UDP。
- 会话层:主要负责建立、管理和终止会话,提供会话控制和同步等服务。会话层还负责处理多个应用程序之间 的数据交换。
- 表示层:主要负责数据格式转换、加密解密、压缩解压等服务。表示层使得应用程序可以使用不同的数据格式和编码,同时还提供了数据的安全性和完整性保护等服务。
- 应用层:主要提供各种服务和应用程序,如电子邮件、文件传输、远程登录、Web 浏览等。应用层服务可以使用不同的协议实现,如 HTTP、SMTP、FTP、TELNET 等
2、HTTP 是哪⼀层的协议?简述它的作用?
- HTTP(Hypertext Transfer Protocol)是应用层协议,是互联网上使用最广泛的协议之⼀。它基于客户端-服务器模型,通过在客户端和服务器之间传输文本数据来进行通信。HTTP通常使用TCP作为传输层协议,也可以使用 TLS/SSL进行加密。
- HTTP的作用:是定义了客户端和服务器之间的通信方式,使得客户端可以向服务器请求资源,并且服务器可以向客 户端发送响应结果。HTTP使用URL(Uniform Resource Locator)来定位资源,通过请求方法(如GET、POST、 PUT、DELETE等)来描述对资源的操作,通过请求头和响应头来传递附加信息,如编码格式、内容类型、Cookie 等
主要特点
- 简单易用:HTTP 协议采用文本格式传输数据,易于人类阅读和编写,使用简单。
- 无状态:HTTP 是⼀种无状态协议,每次请求和响应之间相互独立,服务器不会保存任何客户端信息,客户端需要自行维护会话状态。
- 可扩展性:HTTP 允许通过扩展头部信息和请求方法等方式进行扩展,支持自定义数据传输格式和协议。
- 非连接型:HTTP 是⼀种非连接型协议,每个请求和响应之间相互独立,不存在长期的连接状态。
3、HTTP 有哪些常见的状态码?
- 1xx:提示信息,中间状态,实际用到的比较少
- 2xx:表示服务器成功处理了客户端的请求
- 200 OK
- 204 No Content 也是成功,但响应头没有 body 信息
- 206 Partial Content 分块下载,断点续传,表示返回的 body 只是⼀部分
- 3xx:表示客户端的请求资源发生了变动,需要客户端用新的 URL 重新发送请求,也就是重定向。
- 301 Moved Permanently 永久重定向,请求的资源不存在了
- 302 Found 临时重定向,请求资源还在
- 304 Not Modified 不具有跳转的含义,缓存重定向,用于缓存控制。
- 4xx:客户端发送的报文有误,服务器无法处理
- 400 Bad Request 客户端请求的报文有错误
- 403 Forbidden 服务器禁止访问。
- 404 Not Found 服务器上不存在
- 5xx:客户端请求报文正确,但是服务器内部发生了错误,属于服务端的错误码
- 500 Internal Server Error 服务器内部错误
- 501 Not Implemented 请求的功能还不⽀持,即将开业敬请期待
- 502 Bad Gateway 服务器作为⽹关和代理返回的错误码
- 503 Service Unavaliable 服务器很忙
4、TCP 和 UDP 协议有什么区别,分别适用于什么场景?
5、HTTP 协议中 GET 和 POST 有什么区别?分别适用于什么场景?
参数传递方式不同
- GET 请求参数是在 URL 中以键值对的形式传递的
- 而 POST 请求参数是在请求体中以键值对的形式传递的。
参数传递大小不同
- 因为 URL 长度有限制,不同的浏览器和服务器对 URL 长度的 限制不同,⼀般为 2048 个字符
- 而 POST 请求参数没有大小限制,因为它们是以请求体的形式传递的。
- 安全性不同
- GET 请求的参数是明文传输的,因为参数在 URL 中,如果涉及敏感信息(如密码),容易被窃取或暴露 在浏览器历史记录、代理服务器日志等地方。
- 而 POST 请求的参数在请求体中传输,相对安全⼀些,但是也需要注意参数加密和防⽌ CSRF 攻击等问 题
- GET 和 POST 适用的场景不同:
- GET 请求适用于获取数据,如浏网页、搜索等。因为 GET 请求参数以明文形式传输,容易被拦截和篡 改,所以不适用于提交敏感信息的操作。
- POST 请求适用于提交数据,如登录、注册、发布内容等。因为 POST 请求参数在请求体中传输,相对 安全⼀些,可以提交敏感信息,但是需要注意参数加密和防⽌ CSRF 攻击等问题。
6、简述 TCP 三次握手、四次挥手的流程?为什么需要三次握手?为什么需要四次挥手?
三次握⼿的过程如下
- 第⼀次握手:客户端向服务器发送 SYN 报文,请求建立连接。
- 第⼆次握手:服务器收到客户端的 SYN 报文,向客户端发送 SYN+ACK 报文,表示可以建立连接。
- 第三次握⼿:客户端收到服务器的 SYN+ACK 报文,向服务器发送 ACK 报文,表示连接已经建⽴。
为什么需要三次握手:
三次握⼿的目的是为了确认双方的收发能⼒和同步初始序列号。
四次挥手的过程如下
第⼀次挥手:客户端向服务器发送 FIN 报文,请求关闭连接。
第⼆次挥手:服务器收到客户端的 FIN 报文,向客户端发送 ACK 报文,表示收到关闭请求。
第三次挥⼿:服务器向客户端发送 FIN 报文,请求关闭连接。
第四次挥手:客户端收到服务器的 FIN 报文,向服务器发送 ACK 报文,表示收到关闭请求。
为什么需要四次挥手?
四次挥手的目的是为了保证数据的完整性和可靠性。在关闭连接之前,双方需要确保所有数据都已经传输完毕,因 此需要通过四次挥手的过程进行确认和处理。
总结:三次握⼿的本质是确认通信双方收发数据的能⼒ ,四次挥⼿的目的是关闭⼀个连接。
操作系统
1、什么是进程和线程?它们有哪些区别和联系?
在操作系统中,进程是指⼀个正在执行中的程序,而线程是进程的⼀部分,是⼀个程序中执行的代码片段。
2、死锁是什么?如何预防和避免死锁?
死锁是什么?
- 多线程编程中,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,如果应用不当,会造成两个线程都在等待对方释放锁,没有外力的作用下,这些线程会⼀直互相等待,就没办法继续运行,这就是发生了死锁。
产生死锁的四个必要条件
- 互斥条件:多个线程不能同时使用⼀个资源。
- 持有并等待条件:线程A在等待资源2的同时并不会释放自己已经持有的资源1。
- 不可剥夺条件:在自己使用之前不能被其他线程获取。
- 环路等待条件:两个线程获取资源的顺序构成了环形链
预防和避免死锁的措施
- 避免资源独占:尽量避免⼀个进程在获得了某些资源后再次请求其他资源,而应该将所有需要的资源⼀次性申请到位。
- 避免资源持有和等待:当⼀个进程占用了⼀些资源并等待另⼀些资源时,其他进程就⽆法使用这些资源,容易引发死锁。因此尽可能减少资源持有和等待时间。
- 避免资源互斥:有些资源在同⼀时间只能被⼀个进程占用,比如打印机、磁带机等,需要采用⼀些技术⼿段来避免资源互斥的问题。
- 引⼊资源剥夺策略:当⼀个进程请求的资源被其他进程占用时,可以采取剥夺资源的策略,即暂停占用该资源的进程,直到该资源被释放后再恢复该进程的执行。
- 引⼊进程抢占策略:当⼀个进程等待时间过长时,可以采取抢占其资源的策略,即中断正在执行的进程,强制释放其占用的资源。
例⼦1:线程之间如何避免死锁
- 线程A和线程B获取资源的顺序要⼀样
- 线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候
- 线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B
- 线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
例⼦2:MySQL 如何避免死锁
- 设置事务等待锁的超时时间
- 开启主动死锁检测
3、线程间有哪些通信方式?
- 共享内存:多个线程共享同⼀块内存空间,通过对内存的读写操作实现线程间的信息交换。可以使用synchronized 关键字或 Lock 接⼝等机制来确保线程安全。
- 消息队列:⼀个线程向消息队列中放⼊⼀条消息,另⼀个线程从消息队列中取出消息。
- 管道:管道是⼀种特殊的流,用于在线程之间传递数据。Java 中的 PipedInputStream 和 PipedOutputStream 类 就是管道的实现。
- 信号(Signal):信号是⼀种异步通信方式,进程收到信号后,会根据信号的类型做出相应的处理。
- RPC调用:远程过程调用(RPC)是⼀种跨网络进行的远程调用,可以实现在不同的线程或机器之间进行信息交换。
4、什么是零拷贝?说⼀说你对零拷贝的理解?
零拷贝(Zero-Copy)是⼀种高效的数据传输技术,它可以将数据从内核空间直接传输到应用程序的内存空间中, 避免了不必要的数据拷贝,从而提高了数据传输的效率和性能。
传统:
传统的数据传输方式中,当应用程序需要从磁盘、网络等外部设备中读取数据时,操作系统需要先将数据从外部 设备拷贝到内核空间的缓冲区,然后再将数据从内核空间拷贝到应用程序的内存空间中,这个过程中需要进行两次数据拷贝,浪费了大量的 CPU 时间和内存带宽。
零拷贝
数据可以直接从外部设备复制到应用程序的内存空间中,避免了中间的内核空间缓冲区,减少了不必要的数据拷贝,提高了数据传输的效率和性能。
分布式
1、什么是分布式?为什么需要分布式?
分布式是指在多台计算机上协同⼯作的系统,这些计算机通过网络连接在⼀起,共同完成⼀个任务。
分布式系统能够有效地解决单台计算机处理能力不足、系统容易宕机、数据存储容量有限等问题,同时能够提高系统的可靠性、可用性和性能,适用于数据量较大、并发量高、访问频繁的场景。
分布式系统还可以通过横向扩展的方式提高系统的性能和可靠性,同时降低单点故障的风险,提高了系统的可伸缩性,方便进行升级和维护。
在分布式系统中,由于数据和计算任务被分布在多台计算机上,不同计算机之间需要进行通信和协调,因此需要解决分布式⼀致性、负载均衡、故障恢复、数据共享和安全等问题,同时需要考虑数据的⼀致性和可靠性。因此,分布式系统的设计和实现比单机系统更加复杂和困难,需要考虑到多个因素的综合影响。
2、什么是网关,网关有哪些作用?
网关(Gateway)是连接两个或多个不同网络的设备,可以实现协议的转换、数据的转发和安全策略的实现等功能。简单来说,网关是设备与路由器之间的桥梁,由它将不同的网络间进行访问的控制,转换,交接等等。 常见的网关有应用网关、协议网关、安全网关等。
网关的作用
- 实现协议的转换:不同网络之间通常使用不同的协议,通过网关可以实现协议的转换,使得不同网络之间能够相互通信。
- 提供数据转发功能:网关可以对传输的数据进行过滤、路由、转发等处理,确保数据的可靠传输。
- 实现安全策略:网关可以对传输的数据进行加密、认证、授权等操作,保证数据的安全性和可靠性。
- 提供缓存功能:网关可以将⼀部分数据缓存起来,提高数据的访问速度和响应性能。
- 支持负载均衡:网关可以将请求分配到不同的服务器上,实现负载均衡,提高系统的可用性和性能。
- 实现访问控制:网关可以对访问进行控制,防止未授权的访问和攻击,提高系统的安全性。
3、Dubbo 是什么?是否了解过它的架构设计?
Apache Dubbo 是⼀款高性能的 Java RPC 框架。其前身是阿⾥巴巴公司开源的⼀个高性能、轻量级的开源 Java RPC 框架,可以和 Spring 框架⽆缝集成。
什么是RPC ?
RPC 全称为 remote procedure call,即远程过程调用
比如两台服务器 A 和 B,A 服务器上部署⼀个应用,B 服务器上部署⼀个应用,A 服务器上的应用想调用B 服务器 上的应用提供的方法,由于两个应用不在⼀个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。
Dubbo架构图
调用关系说明
- 1.服务容器负责启动,加载,运行服务提供者。
- 2.服务提供者在启动时,向注册中心注册自己提供的服务。
- 3.服务消费者在启动时,向注册中心订阅自己所需的服务。
- 4.注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 5.服务消费者,从提供者地址列表中,基于负载均衡算法,选⼀台提供者进行调用,如果调用失败,再选另⼀台调用。
- 6.服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送⼀次统计数据到监控中心。
4、什么是分布式的 CAP 理论?
分布式的 CAP 理论是指在分布式系统中,⼀致性(Consistency)、可用性(Availability)和分区容错性 (Partition Tolerance)这三个指标⽆法同时满足的问题。具体来说:
- ⼀致性(Consistency):指多个副本之间数据保持⼀致,即在⼀个副本上的写操作会立即同步到其他所有副本,所有副本的数据都是最新的,保持强⼀致性。
- 可用性(Availability):指系统在任何时候都能对外提供服务,即系统随时能够响应用户请求,不会因为节点故障或其他原因而导致服务中断。
- 分区容错性(Partition Tolerance):指系统在出现网络分区(节点之间失去联系)时,仍能够继续⼯作,保证数据的⼀致性和可用性。
CAP 理论指出,⼀个分布式系统只能同时满足其中的两个指标,无法同时满足三个。
然而实际上,分区容错性是⼀定要满足的,因为不可能只要出现分区问题时整个系统就完全无法使用。因此,在分布式系统中,我们需要考虑的是当出现分区问题时,选择的是⼀致性还是可用性,即 CP 还是 AP。
- CP 架构:当系统出现分区故障时,客户端发送的任意请求都会被卡死或超时,保证数据的强⼀致性。如 Zookeeper。
- AP 架构:当系统出现分区故障时,客户端依旧能获取数据,但有的是新数据,有的是旧数据。如 Eureka。
5、什么是 RPC?目前有哪些常见的 RPC 框架?实现 RPC 框架的核心原理是什么?
RPC 是远程过程调用(Remote Procedure Call)的缩写,是⼀种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
RPC 的主要功能目标是让构建分布式计算/应用更加容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。
常见的RPC框架
- gRPC:是由 Google 开发的⼀个基于 HTTP/2和Protocol Buffers 的高性能、跨语言的 RPC 框架,支持多种编程语言,如:java、C++、Python 等
- Dubbo:是由阿里巴巴开发的⼀个基于 java 的高性能、轻量级的 RPC 框架,支持多种协议和注册中心,如: zookeeper、Nacos 等等。
- Thrift:是由 FaceBook 开发的⼀个基于⼆进制协议和多种传输层的 RPC 框架,支持多种编程语言,如: java、c++、Python 等
RPC框架的核心原理
RPC 框架的核心原理是通过代理、序列化、网络传输、反序列化、反射等技术,实现远程过程调用的透明化。核心流程如下:
- 客户端通过代理对象(Proxy)调用远程服务,代理对象将调用信息(比如方法名、参数等)进行序列化 (Serialzation),转换成字节流;
- 客户端通过网络传输(Transport)将序列化厚的字节流发送给服务端,服务端收到字节流后进行反序列化 (Deserialization),还原成调用信息。
- 服务端通过反射(Reflection)根据调用信息找到对应的服务信息(Service)和方法(Method),并执行方 法,得到返回结果。
- 服务端将返回结果进行序列化,转换成字节流,通过网络传输发送给客户端,客户端接收到字节流后,进行反序列化,还原成返回结果。
- 客户端通过代理对象返回结果给调用者,完成远程过程调用。
6、什么是注册中心?如何实现⼀个注册中心?
注册中心是服务实例信息的存储仓库,也是服务提供者和服务消费者进行交互的桥梁。它主要提供了服务注册和服务发现这两大核心功能。在⼀个分布式系统中,不同的服务会以微服务的形式运行在不同的机器上,它们需要相互通信以完成业务逻辑。而注册中心则充当了服务之间的“黄页”,记录了所有可用的服务及其网络地址,方便其他服务进行查找和调用。
当⼀个新的服务启动时,它会向注册中心注册自己的网络地址和⼀些元数据信息(例如服务名称、版本号、健康状态等),注册中心会将这些信息存储在自己的数据中心中。当其他服务需要调用这个新服务时,它们可以通过向注册中心查询来获取该服务的地址和元数据信息,然后与该服务建立网络连接。 常见的注册中心包括 ZooKeeper、Consul、Eureka 、Nacos 等等
如何实现一个注册中心
- 设计数据模型:设计注册中心的数据模型,包括服务的元数据信息、服务实例的网络地址等。
- 实现服务注册:当服务启动时,它需要向注册中心注册自己的元数据信息和网络地址。可以通过 REST API、 RPC 等方式实现服务的注册。
- 实现服务发现:当⼀个服务需要调用其他服务时,它需要向注册中心查询目标服务的网络地址。可以通过REST API、RPC 等方式实现服务的发现。
- 实现健康检查:为了保证服务的可用性,注册中心需要定期检查服务实例的健康状况,并将不健康的实例从服务列表中移除。
- 实现高可用:注册中心是⼀个分布式系统的核心组件,需要保证高可用性。可以采用主从复制、集群等方式实 现注册中心的高可用性。
- 实现安全机制:注册中心涉及到服务的元数据信息和网络地址等敏感信息,需要采取合适的安全措施。
7、什么是分布式的 BASE 理论,它与 CAP 理论有什么联系?
BASE 理论是对 CAP 理论的延伸,核心思想是即使无法做到强⼀致性(Strong Consistency,CAP 的⼀致性就是强 ⼀致性),但应用可以采用适合的方式达到最终⼀致性(Eventual Consitency)。
- Basically Available(基本可用):分布式同再出现不可预知故障的时候,允许损失部分可用性。
- Soft state(软状态):软状态也称弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
- Eventually consistent(最终⼀致性):最终⼀致性强调的是系统中所有的数据副本,在经过⼀段时间上的同步后,最终能够达到⼀个⼀致的状态。因此,最终⼀致性的本质是需要系统保证最终数据能够达到⼀致,而不需要实时保证系统数据的强⼀致性。
CAP 与 BASE 关系
BASE是对 CAP 中⼀致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强⼀致性(Strong consistency),更具体地说,是对 CAP 中 AP方案的⼀个补充。其基本思路就是:通过业务,牺牲强⼀致性而获得可用性,并允许数据在⼀段时间内是不⼀致的,但是最终达到⼀致性状态。
消息队列
1、什么是消息队列?消息队列有哪些应用场景?
消息队列是⼀种用于异步通信的机制,用于在不同的应用程序之间传递消息。消息队列通常由消息生产者、消息队列和消息消费者三部分组成。
消息生产者将消息发送到消息队列中,而消息消费者则从消息队列中接收消息。消息队列负责存储和管理消息,确保消息传递的可靠性和稳定性。在实现过程中,消息队列还会提供⼀些额外的功能,如消息过滤、消息路由、消息持久化等。
消息队列的特点:
- 异步通信:消息生产者和消息消费者之间采用异步通信模式,发送方无需等待接收方的响应即可继续执行。
- 解耦合:消息队列可以将消息生产者和消息消费者解耦合,使得它们之间的关系更加灵活。
- 可靠性:消息队列通常会提供⼀些保证消息传递可靠性的机制,如消息持久化、重试机制等。
- 缓冲:消息队列可以缓冲来自多个消息生产者的消息,使得消息消费者可以按照自己的节奏进行消费,从而有效地平衡生产者和消费者之间的处理速度。
消息队列的应用:
- 异步任务处理:通过将任务发送到消息队列中,异步处理任务,提高系统的并发性能和吞吐量。
- 解耦合系统:将不同的业务逻辑拆分成不同的服务,通过消息队列实现服务之间的通信,提高系统的可维护性和可扩展性。
- 流量削峰:将流量通过消息队列分散到不同的服务中,避免单个服务被高并发流量打垮。
- 日志收集:通过将日志消息发送到消息队列中,将日志收集和分析与业务逻辑解耦合,提高系统的可靠性和可维护性。
- 应用解耦:将不同的应用程序通过消息队列进行集成,实现应用之间的解耦合和数据交换。
2、有哪些主流的消息队列,它们分别有什么优缺点、各自的适用场景是什么?
3、有哪些常见的消息队列模型?分别适用于什么场景?
- 点对点模型(Point-to-Point Model):这种模型中,消息生产者将消息发送到⼀个队列中,消息消费者从该队列中接收消息。⼀个消息只会被⼀个消费者接收,消费者在处理完消息之后会从队列中删除它。这种模型适用于需要保证消息只被一个消费者处理的场景,例如订单系统、日志处理等。
- 发布-订阅模型(Publish-Subscribe Model):这种模型中,消息生产者将消息发送到⼀个主题(Topic) 中,多个消息消费者可以订阅该主题并接收到所有的消息。每个消息可以被多个消费者接收,消费者在处理完 消息之后不会从主题中删除它。这种模型适用于需要将消息广播给多个消费者的场景,例如新闻订阅、实时数据分析等。
- 请求-应答模型(Request-Response Model):这种模型中,消息生产者发送⼀个请求消息到⼀个队列中, 消息消费者从该队列中接收请求并返回⼀个响应消息。⼀个请求只会被⼀个消费者接收并处理,处理完成后返 回响应消息给消息生产者。这种模型适用于需要请求-响应模式的场景,例如远程过程调用、微服务通信等。
- 推拉模型(Push-Pull Model):这种模型中,消息生产者将消息推送到⼀个队列中,消息消费者从该队列中拉取消息。消息生产者将消息发送到队列中,消费者按需从队列中拉取消息进行处理。这种模型适用于需要灵活控制消息消费速度的场景,例如数据采集、视频流传输等。
可以将点对点模型和请求-应答模型结合使用,实现异步的 RPC 调用。另外,在选择消息队列模型时,还需要考虑 消息传输的可靠性、顺序性、延迟等因素。
设计模式
1、设计模式是什么?为什么要学习和使用设计模式?
设计模式:是⼀套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
项目中合理地运用设计模式可以完美解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式描述 了⼀个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因
设计模式分为三大类:
- 创建型模式:共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式:共7种:适配器模式、装饰器模式、代理模式、桥接模式、外观模式、组合模式、享元模式。
- 行为型模式:共11种:策略模式、模板方法模式、观察者模式、责任链模式、访问者模式、中介者模式、迭代器模式、命令模式、状态模式、备忘录模式、解释器模式。
2、什么是单例模式?使用单例模式有什么好处?有哪些常用的单例模式实现方式?各自的应用场景是什么?
单例模式是⼀种创建型设计模式,它确保⼀个类只有⼀个实例,并提供⼀个全局访问点来访问该实例。单例模式的目的是确保类的⼀个唯⼀实例,因此其他类可以轻松地从⼀个可知的地方访问它。
单例模式有以下好处
- 节省系统资源:在系统中,如果有多个实例会造成资源浪费,而使用单例模式可以减少这种浪费。
- 简化了对象访问:单例模式提供了⼀个全局的访问点,因此可以简化访问过程。
常见的单例模式实现方式
- 饿汉式单例模式:在类加载时创建单例对象。缺点是不支持延迟加载。
- 懒汉式单例模式:在第⼀次使用时才创建单例对象。缺点是需要考虑线程安全问题。
- 双重检查锁单例模式:在第⼀次使用时创建单例对象,并使用双重检查锁定来确保线程安全。
- 枚举单例模式:在枚举类型中创建单例对象,可以防止反射和序列化攻击。
应用场景
- 数据库连接池:通过单例模式,可以确保系统中只有⼀个数据库连接池。
- 日志记录器:可以使用单例模式记录系统日志,这样可以确保系统中只有⼀个日志记录器。
- 配置文件管理器:可以使用单例模式来管理应用程序的配置文件,这样可以避免重复读取配置文件的开销。
- 线程池:可以使用单例模式来确保系统中只有⼀个线程池。
4、什么是工厂模式?使用工厂模式有什么好处?工厂模式有哪些分类?各自的应用场景是什么?
工厂模式
- 把对象的构造交给⼀个类,这样在对象构造过程中,调用者不需要知道对象的产生过程。降低了代码的耦合度,同时体现了面向对象的封装的特征。
- 好处
- 松耦合
- 封装
- 可拓展
- 复用
分类
简单工厂模式 (⼀个工厂生产所有的对象 )
- 如果它负责的对象太多,简单工厂容易庞大,变成超级类。
- 简单⼯⼚的拓展是竖向拓展(拓展需要访问⼯⼚内部,职责不够单⼀)
- 因此,他适合在’简单场景’ - 对象少且固定
工厂方法模式(一个工厂一个对象)
- 工厂方法是横向拓展,由于工厂对象1对1,耦合度没有任何减少,要生产类就要知道那个对应的工厂。
- 当需要生产新的产品时,⽆需更改既有的工厂,只需要添加新的工厂即可。保持了面向对象的可扩展性(纵向,横向都很不错),符合开闭原则。
- 因此,工厂方法模式适用于对象多而且可能增加的场景。
抽象工厂模式 ( 抽象接口工厂实现 )
- 抽象工厂是定义了⼀个接口,然后由具体工厂去实现抽象方法,突出特点就是运用了多态,耦合够松。
- 缺点也很明显,在接口不变的情况下,无论是横向拓展性还是松耦合都很好,但是⼀旦接口有新的功能增加, 所有的工厂实现都需要纵向拓展。即横向拓展能⼒强,但是纵向拓展能⼒=0
- 因此,抽象工厂模式适合产品族(即有相似属性)的生产
并发编程
1、并发和并行有什么区别?同步和异步有什么区别?
2、什么是 BIO、NIO、AIO?
BIO、NIO、AIO 都是 Java 中网络编程的 I/O 模型。
- BIO(Blocking IO )是JDK1.4之前的传统IO模型,特点就是同步阻塞等待数据,直到数据读取完毕才会返回结果, 线程会⼀直阻塞在 read/write 方法上,不能处理其他的 IO 请求,它的并发性能比较差。
- NIO(Non-Blocking IO)是Java 1.4 之后新增的 IO 模型,它⽀持同步非阻塞式的 IO 操作。NIO 采用了多路复用器来处理 IO 请求,通过⼀个线程处理多个 IO 请求,实现了高并发处理。NIO 主要有三个核心概念:Selector、 Channel、Buffer。Selector 负责监听多个 Channel 上的事件,Channel 可以理解为对原始 IO 的封装,Buffer 则 是对数据的封装。
- AIO(Asynchronous IO)是Java 1.7 之后新增的 IO 模型,它⽀持异步非阻塞 IO 操作。与 NIO 不同的是,AIO 在 进行读写操作时不需要像 NIO ⼀样⼀直轮询,而是通过回调函数的方式在数据准备好后通知应用程序进行数据的读取,这样可以更加高效地利用系统资源,提高吞吐量。但是 AIO 在处理小文件和小数据量时的性能并不如 NIO。
3、线程的生命周期是什么,线程有几种状态,什么是上下文切换?
从传统操作系统层面,线程有五大基本状态,包括:创建、就绪、运行、阻塞和终止状态。
上下文切换:
指将当前线程的状态保存下来,并将 CPU 资源切换到另⼀个线程上运行的过程,通常由操作系统进行管理和控制的。需要注意,上下文切换需要花费⼀定的时间和系统资源,线程的上下文切换次数要尽量减少,以提高系统的性能。
4、synchronized 关键字是什么,有什么作用?
- synchronized 是 Java 中的⼀个关键字,用于实现线程同步。具体来说,synchronized 用于修饰方法或代码块, 使得同⼀时刻只能有⼀个线程访问被修饰的代码,其他线程需要等待当前线程执行完毕后才能访问。
- synchronized 主要用于解决多线程并发访问共享资源时出现的线程安全问题。
- 如果多个线程同时访问⼀个共享资源,就会出现多个线程同时修改这个资源的情况,从而导致数据不⼀致等问题。 而使用synchronized 可以保证同⼀时刻只有⼀个线程访问该资源,从而避免了线程安全问题。
- synchronized 的作用不仅限于线程同步,它还可以保证可见性和有序性,即保证在同⼀个锁上,⼀个线程修改了共享变量的值之后,另⼀个线程能够立即看到修改后的值,并且在多个线程执行顺序上保证了⼀致性。
- 需要注意的是,使用 synchronized 会带来⼀定的性能损失,因为每次进⼊同步块时都需要获得锁,这会增加线程的等待时间和上下文切换的开销。同时,如果同步块的代码执行时间很短,也会增加不必要的性能开销。因此,需 要根据具体情况来判断是否需要使用 synchronized。
系统设计
1、如何设计⼀个点赞系统?
从需求上面
- 点赞类别:帖子、评论、or 其他。
- 点赞限制
- 仅登录用户还是游客都可以点赞?
- 可以⽆限点赞还是每个用户仅限点1次?
- 点赞是否通知用户
- 通知频率:每次点赞都通知 1 次,还是满 5 次合并通知1 次
- 通知样式:点赞帖子和点赞评论,通知的内容要怎么区分显示
从数据库设计上
- 点赞数量:是单独一张表?还是在帖⼦表直接加⼀个字段?
- (前者的好处是,这样如果例如有其他类别的内容需要加点赞功能,就不用再修改数据库,后者的好处 是后台统计功能方便查数据,不需要查两张表)
- 点赞记录表:记录点赞操作记录,哪个用户点赞了哪个帖子。
从性能设计考虑
- 点赞是个高频操作,肯定不能每次都直接操作数据库,需要加⼀层缓存
- 是单独独立⼀个key,如 {post_id}_like_num 记录
- 还是随着跟其他数量字段,⼀起构造⼀个key 如 {post_info}_num ⼀个hash字段
- 这里的设计会影响如何保持数据库和缓存⼀致性的问题
一致性考虑
- 看是否需要强⼀致性,如果不需要的话,就不需要加锁,这样可以减轻实现负担和提高⼀定的系统性能。
2、如何在 10 亿个数据中找到最大的 1 万个?(提示:最小堆)。
使用最小堆来解决:
使用最小堆来解决: ⾸先,我们可以把这 10 亿个数据划分成多个小数据块,每个小数据块包含 1 万个数据。然后,我们可以遍历这些小数据块,对于每个小数据块,我们可以构建⼀个大小为 1 万的最小堆。当遍历到⼀个新的数据时,我们可以把它 与最小堆的堆顶元素进行比较。如果这个数据比堆顶元素大,那么我们可以把堆顶元素删除,并把这个数据插⼊到 最小堆中。这样,当我们遍历完所有的小数据块之后,最小堆中存储的就是最大的 1 万个数据了。
具体实现过程如下
- 初始化⼀个大小为 1 万的最小堆,把第⼀个小数据块的数据插⼊到堆中。
- 读取下⼀个小数据块的数据,把这些数据依次与最小堆的堆顶元素进行比较。
- 如果数据比堆顶元素大,那么删除堆顶元素,并把这个数据插⼊到堆中。
- 如果数据比堆顶元素小,那么忽略这个数据。
- 重复步骤 2 直到所有的小数据块都被遍历完。 2. 最终,最小堆中存储的就是最大的 1 万个数据。
这种方法的时间复杂度是 O(NlogK),其中 N 是数据总数,K 是需要找到的最大的数据数量。因为最小堆的大小是 固定的,所以时间复杂度与数据总数 N 没有关系,只与需要找到的数据数量 K 有关系。因此,这种方法可以快速 找到最大的 1 万个数据,而且可以处理⾮常大的数据集。
3、有几台机器存储着几亿的淘宝搜索日志,假设你只有⼀台 2g 的电脑, 如何选出搜索热度最高的⼗个关键词?
对于 top k 类文本问题,通常比较好的方案是【分治+hash/trie 树+小顶堆】,即先按照 hash 方法把数据集分解成多个小数据集,然后使用 trie 树或者 hash 统计每个小数据集中的词频,随后用小顶堆求出每个数据集中出现频率最高的前 K 个数,最后在所有 top K 中求出最终的 top K。
拆分成 n 多个文件:以⾸字母区分,不同⾸字母放在不同文件,如果某个文件长度仍过长,继续按照次⾸字母进行拆分。这样⼀来,每个文件的每个数据长度基本相同且⾸字母相同,就能保证数据被独立的分为了 n 个文件,且各个文件中不存在关键词的交集。
分别词频统计:对于每个文件,使用 hash 或者 Trie 树进行进行词频统计.。
小顶堆排序:依次处理每个文件,并逐渐更新最大的⼗个词
JVM
1、请你介绍下 JVM 内存模型,分为哪些区域?各区域的作用是什么?
堆:
- 堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆 内存是由自动内存管理系统也就是垃圾收集器回收。
虚拟机栈:
- 每个线程运行时所需要的内存,称为虚拟机栈 每个栈由多个栈帧(Frame)组成,对应着每次方法调 用时所占用的内存 每个线程只能有⼀个活动栈帧,对应着当前正在执行的那个方法。
本地方法栈:
- 与虚拟机栈发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务, 执行每个本地方法的时候,都会创建⼀个栈帧用于存储局部变量表,操作数栈,动态链接,方法出⼝等信息。
程序计数器(PC寄存器)
- 程序计数器区域是⼀块较小的区域,它用于存储线程的每个执行指令,每个线程都有自⼰的程序计数器
方法区(元空间):
- 方法区是可提供各条线程共享的运行时内存区域。存储了每⼀个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括⼀些在类、实例、接⼝初始 化时用到的特殊方法。在JDK1.8之前的版本⾥,代表JVM的⼀块区域。在1.8版本以后,这块区域的名字改 了,叫做“Matespace”
2、什么是双亲委派模式?有什么作用?
双亲委派模式(Parent-Delegate Model)是 Java 类加载器(ClassLoader)在加载类时所采用的⼀种设计模式。
这种模式的核心思想是:当⼀个类加载器收到类加载请求时,⾸先不会尝试自⼰加载这个类,而是将请求委派给其⽗类加载器。依次递归,直到最顶层的启动类加载器(Bootstrap ClassLoader);如果⽗类加载器⽆法加载该 类,⼦类加载器才尝试自⼰去加载。
作用:
- 避免重复加载类:如果⼀个类已经被某个类加载器加载过了,那么其他类加载器就不需要再次加载该类,因为 类的定义只需要加载⼀次就可以了。
- 确保类的唯⼀性:通过双亲委派模式,可以保证不同的类加载器加载同⼀个类时,得到的都是同⼀个 Class 对 象,从而保证了类的唯⼀性。
- 安全性:通过双亲委派模式,可以保证核心 Java API 不会被⾮法替换,从而提高了 Java 应用的安全性。
3、常见的垃圾回收算法有⼏种类型?他们对应的优缺点是什么?
常见的垃圾回收算法:
Mark-Sweep(标记-清除)算法
标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是首先通过可达性分析,标记出所有需要回收的对象,清除阶段就是回收被标记的对象所占用的空间。
优点:速度较快;缺点:内存不连续,会造成内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空 间时⽆法找到足够的空间而提前触发新的⼀次垃圾收集动作。
Coping(复制)算法
为了解决Mark-Sweep算法的缺陷,他将可用内存按照容量划分为大小相等的两块,每次只使用⼀块。当这⼀块的内存用完了,就将还存活的对象复制到另⼀块上面,然后再把已使用的内存空间⼀次清理掉,这样⼀来就 不容易出现内存碎⽚的问题。
这种算法虽然实现简单,运行高效且不容易产生内存碎⽚,但是却对内存空间的使用做出了高昂的代价,因为 能够使用的内存缩减到原来的⼀半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果 存活对象很多,那么Copying算法的效率将会大大降低。
Mark-Compact(标记-整理)算法(压缩算法)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和MarkSweep⼀样,但是在完成标记之后,他不是直接清理可回收对象,而是将存活对象都向⼀端移动,然后清理 掉端边界以外的内存
Generation Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾回收器采用的算法。他的核心思想是根据对象存活的生命周期将内存划分为若⼲个不同的区域。⼀般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾回收时,只有少数对象需要被回收,而新生代的特点是每次垃圾回 收时都有大量的对象需要被回收。
目前大部分垃圾回收器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但实际上不是按照1:1的比例来划分新生代的空间,⼀般来说是将新生代划分为⼀块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的⼀块Survivor空间。当新生代的Eden Space和From Space空间不足时就会发生⼀次GC,进行GC后,Eden Space和From Space区的存 活对象会被挪到To Space,然后将Eden Space和From Space进行清理。如果To Space⽆法足够存储某个对象,则将这个对象存储到老生代。在进行GC后,使用的便是Eden Space和To Space了,如此反复循环。当对 象在Survivor区躲过⼀次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老年代中。
- 而由于⽼年代的特点是每次回收都只回收少量对象,⼀般使⽤的是标记-整理算法(压缩法),当⽼年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更⻓