面经

请求一个网页链接的过程

以下是请求一个网页链接的详细过程:

  1. DNS解析:

    • 客户端(通常是Web浏览器)获取目标网页的URL(统一资源定位符)。
    • 客户端提取URL中的域名部分,例如www.example.com。
    • 客户端向本地DNS服务器发起域名解析请求,询问目标网页的IP地址。
  2. TCP连接建立:

    • 客户端通过本地DNS服务器获取目标网页的IP地址。
    • 客户端使用目标网页的IP地址与服务器建立TCP连接。
    • 客户端向服务器发送一个TCP连接请求(称为三次握手),建立可靠的通信连接。
  3. 发送HTTP请求:

    • 客户端向服务器发送一个HTTP请求报文,包括以下内容:
      • 请求行:包含请求方法(GET、POST等)和请求的资源路径。
      • 请求头:包含附加的请求信息,如浏览器类型、语言偏好等。
      • 请求体(对于POST请求):包含提交的数据。
    • 客户端将HTTP请求报文发送到服务器端。
  4. 服务器处理请求:

    • 服务器接收到客户端发送的HTTP请求。
    • 服务器解析请求报文,获取请求的资源路径和其他相关信息。
    • 服务器根据请求的资源路径和服务器配置,处理请求并生成相应的响应。
  5. 服务器发送HTTP响应:

    • 服务器生成一个HTTP响应报文,包括以下内容:
      • 状态行:指示响应状态码(如200表示成功)和响应的文本描述。
      • 响应头:包含附加的响应信息,如内容类型、日期等。
      • 响应体:包含请求的资源或响应的数据。
    • 服务器将HTTP响应报文发送回客户端。
  6. 客户端接收HTTP响应:

    • 客户端接收到服务器发送的HTTP响应。
    • 客户端解析响应报文,提取响应状态码、响应头和响应体等信息。
  7. 渲染和显示网页:

    • 如果响应状态码为200(成功),客户端将根据响应的内容类型(如HTML、CSS、JavaScript等)对响应体进行解析。
    • 客户端渲染并显示网页内容,将HTML解析为可视化的网页,执行JavaScript代码,加载和显示网页中的资源(如图像、样式表等)。
  8. 关闭TCP连接:

    • 客户端和服务器之间的数据传输完成后,客户端和服务器通过TCP连接进行握手,关闭连接。

Redis 将数据存储在内存中,mysql把数据存在哪里?

MySQL将数据存储在磁盘上。它使用文件系统来管理数据文件,将数据持久地存储在硬盘上的文件中。MySQL的数据文件通常被称为表空间(tablespace),包括系统表空间和用户表空间。

系统表空间包含了MySQL的系统表和共享表数据。这些表存储了关于数据库和表结构的元数据信息。

用户表空间用于存储用户创建的表和数据。每个数据库都有一个对应的用户表空间,其中包含该数据库中所有表的数据。

在用户表空间中,每个表都有对应的表文件,用于存储表的数据和索引。这些表文件通常以.frm扩展名的文件存储表的定义,以及.ibd扩展名的文件存储表的数据和索引。

MySQL还使用日志文件来记录数据库的修改操作,包括事务日志和重做日志。这些日志文件位于磁盘上,用于保证数据的一致性和持久性。

需要注意的是,MySQL也可以配置为使用内存表(Memory Table),这种表将数据存储在内存中,但是它们的数据不是持久的,会在MySQL重启或关闭时丢失。而Redis则是一种基于内存的键值存储系统,它将数据完全存储在内存中,并通过持久化机制将数据定期写入磁盘以保证数据的持久性。

内存与磁盘的区别

内存(RAM)和磁盘(硬盘或固态硬盘)是计算机中用于存储数据的两个主要组件,它们在以下几个方面有着明显的区别:

  1. 工作原理:内存是计算机中的临时存储介质,用于存储正在运行的程序和数据。它通过电子电荷在集成电路中的状态变化来存储和访问数据,数据在断电后会被清空。磁盘是永久存储介质,用于长期存储数据。它利用磁性材料上的磁场来存储和读取数据,数据在断电后仍然保留。

  2. 速度:内存的读写速度非常快,可以以纳秒级别的速度访问数据。这使得内存非常适合于需要快速访问的数据和程序。磁盘的读写速度相对较慢,以毫秒级别甚至更长的时间访问数据。

  3. 容量:内存的容量通常比较有限,以几千兆字节(GB)为单位。磁盘的容量相对较大,可以达到几十到几千或更多的千兆字节(TB)。

  4. 成本:内存的成本相对较高,以每千兆字节(GB)为单位计算。磁盘的成本相对较低,以每千兆字节(GB)或每千兆位(GB)为单位计算。

  5. 数据持久性:内存是易失性存储,断电后数据会丢失。磁盘是非易失性存储,断电后数据仍然保留。

基于这些区别,内存通常用于临时存储正在运行的程序和数据,以提供快速访问速度。磁盘用于长期存储大量数据和文件,但相对访问速度较慢。计算机系统通常会使用内存作为缓存,将经常访问的数据存储在内存中,以提高整体性能。

B树

B树(B-tree)是一种自平衡的搜索树数据结构,广泛应用于文件系统、数据库和文件索引等领域。它具有高效的插入、删除和查找操作,同时适应大规模数据集的存储和访问。

B树的特点包括:

  1. 多路搜索树:B树是一种多路搜索树,每个节点可以存储多个键值对。相比于二叉搜索树,B树的节点可以拥有更多的子节点,从而减少树的高度,提高搜索效率。

  2. 自平衡:B树通过保持树的平衡性来保证高效的操作。在插入和删除操作后,B树会进行自动的平衡调整,确保树的高度在可接受的范围内,避免数据倾斜和性能下降。

  3. 有序存储:B树的节点中的键值对按照特定的顺序进行存储。这使得B树在范围查询和顺序遍历方面表现出良好的性能。

  4. 多级索引:B树支持多级索引结构,通过根节点、内部节点和叶节点的层次结构,可以快速定位和访问存储在树中的数据。

  5. 磁盘友好:B树的节点大小通常与磁盘页的大小相匹配。这意味着在磁盘上读取或写入一个节点时,可以一次性处理更多的键值对,减少磁盘I/O操作次数,提高性能。

总体而言,B树通过自平衡和多路搜索的特性,在处理大规模数据和频繁的插入、删除、查找操作时表现出较好的性能。它是许多常见数据存储系统的核心组件之一,用于高效地管理和访问数据。

http和https区别

HTTP(Hypertext Transfer Protocol)和HTTPS(Hypertext Transfer Protocol Secure)是用于在网络上传输数据的两种协议。它们之间的主要区别在于安全性和数据传输方式:

  1. 安全性:HTTP是明文传输协议,数据在传输过程中不经过加密处理,容易被恶意攻击者截获和窃听。HTTPS使用SSL(Secure Sockets Layer)或TLS(Transport Layer Security)协议对通信进行加密,确保数据在传输过程中的机密性和完整性,使得数据更加安全。

  2. 加密方式:HTTP不使用加密,数据以纯文本形式传输。HTTPS使用SSL或TLS协议对数据进行加密,使数据在传输过程中变得不可读。

  3. 端口号:HTTP默认使用80端口进行通信,而HTTPS默认使用443端口。这样使得网络服务器能够根据端口号来区分处理HTTP请求和HTTPS请求。

  4. 证书要求:HTTPS需要通过数字证书来验证服务器的身份。数字证书由可信任的第三方机构(如CA机构)签发,用于证明服务器的身份和确保通信的安全性。而HTTP不需要证书验证。

  5. 性能:HTTPS的加密和解密过程需要消耗更多的计算资源,因此比起HTTP会稍微慢一些。然而,随着计算机处理能力的提升,HTTPS的性能差距逐渐缩小。

综上所述,HTTPS相较于HTTP在数据传输过程中提供了更高的安全性和保护用户隐私的能力。因此,对于处理敏感信息和需要保密性的场景,使用HTTPS是更为推荐的选择。

Socket是什么

Socket(套接字)是计算机网络中用于实现网络通信的编程接口或抽象层。它允许应用程序通过网络发送和接收数据,实现不同设备之间的通信。

Socket提供了一种标准化的方式,使得应用程序能够通过网络进行数据传输。它基于网络协议栈(例如TCP/IP协议栈)来实现数据的分割、传输和重组。

通过使用Socket,应用程序可以创建一个网络套接字(socket),并通过该套接字与其他设备建立连接、发送和接收数据。套接字可以在不同设备之间的不同计算机上进行通信,允许客户端和服务器之间的双向数据传输。

在网络编程中,Socket通常使用一组API函数来进行操作,这些函数包括创建套接字、绑定套接字到特定的IP地址和端口、监听连接请求、接受连接、发送和接收数据等。

不同类型的套接字可以支持不同的网络协议和通信模式,例如TCP套接字用于可靠的面向连接的通信,UDP套接字用于无连接的不可靠通信。

总结来说,Socket是一种网络编程接口,允许应用程序通过网络进行数据传输和通信。它提供了一种标准化的方式来实现网络通信,使得应用程序能够在不同设备之间进行数据交换。


GET和POST区别:

  • GET用于获取资源,POST用于提交资源。
  • GET请求的参数会附加在URL的后面,以查询字符串的形式传递,而POST请求的参数在请求体中传递,不会在URL中显示。
  • GET请求对数据长度有限制,一般在URL长度上限范围内,而POST请求对数据长度没有限制。
  • GET请求的数据会被保存在浏览器的历史记录中,而POST请求的数据不会保存。
  • GET请求会被浏览器主动缓存,POST请求不会被缓存,需要再次确认是否提交。

HTTP和HTTPS区别:

  • HTTP是明文传输协议,数据在传输过程中不经过加密处理,容易被截获和窃听。HTTPS使用SSL/TLS协议对通信进行加密,保证数据的机密性和完整性。
  • HTTP默认使用80端口进行通信,而HTTPS默认使用443端口。
  • HTTP不需要证书验证,而HTTPS需要通过数字证书验证服务器的身份。
  • HTTPS的安全性更高,但相对而言会消耗更多的计算资源,性能稍低于HTTP。

十进制和二进制的转换:

  • 十进制转二进制:将十进制数不断除以2,直到商为0,然后将每次的余数从下往上依次排列,即为二进制表示。
  • 二进制转十进制:将二进制数从右往左依次乘以2的幂,对应位置上为1的位对应的幂相加,得到的和即为十进制表示。

判断一个数是否为2的幂:一个数如果是2的幂,则其二进制表示中只有一个1,其他位都是0。可以使用位运算进行判断,如果数n满足 n & (n-1) == 0,则它是2的幂。

MySQL调优:

  • 使用索引来加速查询。
  • 优化SQL语句,避免全表扫描和不必要的连接。
  • 适当调整数据库的参数设置,如缓冲区大小、连接数等。
  • 避免使用过多的触发器和存储过程,以减少数据库的负载。
  • 合理设计数据库表结构,避免冗余和重复数据。
  • 定期进行数据库备份和优化,清理无用数据和日志。

IO多路复用:

IO多路复用是一种通过单个线程来监听多个IO事件的机制,它可以同时监视多个文件描述符的可读、可写等事件状态。常见的IO多路复用模型包括select、poll和epoll。它可以提高程序的并发性能,避免使用多线程或多进程处理大量的并发连接。

HAVING和WHERE区别:

  • WHERE用于在查询中指定条件筛选行,它在对表进行分组之前过滤数据。
  • HAVING用于在查询的结果集上指定条件,它在对分组后的结果进行过滤。

Redis数据类型和数据结构:

Redis支持多种数据类型,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)和位图(Bitmap)等。

  • 有序集合(Sorted Set)使用跳表(Skip List)和字典(Hash Table)两种数据结构,跳表用于提供有序性,字典用于提供快速的成员查找和删除操作。
  • 跳表是一种特殊的链表结构,通过在每个节点上增加多个指针来实现快速的查找和插入操作。

第K大算法:

可以使用快速选择(QuickSelect)算法来找到数组中第K大的元素,其基本思想是选取一个基准元素,将数组划分为两部分,使得左边的元素都大于基准元素,右边的元素都小于基准元素。如果基准元素的位置等于K,则找到了第K大的元素;如果基准元素的位置大于K,则在左边继续查找;如果基准元素的位置小于K,则在右边继续查找。通过递归地进行划分和查找,最终可以找到第K大的元素。


MySQL默认引擎,解决了什么问题:

  • MySQL的默认存储引擎是InnoDB。
  • InnoDB引擎解决了多版本并发控制(MVCC)的问题,实现了更好的并发性能。
  • 它支持事务处理和ACID属性(原子性、一致性、隔离性和持久性),提供了更高的数据完整性和可靠性。
  • InnoDB还提供了行级锁定和外键约束等功能。

MVCC

MySQL写一个死锁场景:

-- 会话1
BEGIN;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

-- 会话2
BEGIN;
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;
SELECT * FROM table1 WHERE id = 1 FOR UPDATE;

-- 会话1
SELECT * FROM table2 WHERE id = 2 FOR UPDATE;

上述场景中,会话1获取了table1的行锁并等待table2的行锁,同时会话2获取了table2的行锁并等待table1的行锁,从而造成死锁。

一组有序可重复数组找到某个最后出现的数的索引(二分):

def find_last_index(arr, target):
left = 0
right = len(arr) - 1
index = -1

while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
index = mid
left = mid + 1
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1

return index

有1、3、9、27克的砝码各一个,能称出多重的物品,后追问如果添加81克、273克等(3的幂)各一个,能称出500克:

可以使用贪心算法解决该问题。首先,将重量为1的砝码放在一边,然后将剩下的砝码按从小到大的顺序放在另一边,将需要称重的物品放在一边,并根据需要逐步使用较大的砝码进行称重。如果添加了81克、273克等砝码,仍然可以按照相同的方法进行称重,因为这些额外的砝码是3的幂,可以覆盖到其他重量范围。这样,可以称出500克的物品。


线程安全机制:

线程安全是指在多线程环境下,多个线程同时访问共享资源时,保证对共享资源的操作能够正确执行而不会出现数据不一致或不确定的结果。

常见的线程安全机制包括:

  • 互斥锁(Mutex):通过锁机制保证在同一时刻只有一个线程可以访问共享资源。
  • 信号量(Semaphore):用于控制同时访问共享资源的线程数量。
  • 条件变量(Condition Variable):用于线程之间的通信和同步,允许线程等待某个条件的发生。
  • 原子操作(Atomic Operation):保证特定操作在执行过程中不会被中断,确保数据的原子性。

JVM类加载:

JVM(Java Virtual Machine)类加载是指将Java字节码文件加载到内存中,并解析成可执行的Java类的过程。
JVM采用了三级类加载器体系:启动类加载器(Bootstrap Class Loader)、扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader)。

类加载的过程包括:

  • 加载(Loading):将字节码文件加载到内存中,通常从磁盘或网络中获取字节码文件。
  • 验证(Verification):验证字节码文件的格式、结构和语义的正确性。
  • 准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值。
  • 解析(Resolution):将符号引用替换为直接引用,解析类之间的关系。
  • 初始化(Initialization):执行类的初始化代码,包括静态变量的赋值和静态块的执行。

TCP/IP如何实现可靠的数据传输:

TCP/IP协议通过以下方式实现可靠的数据传输:

  • 应用数据被分割成小的数据块(Segment),每个数据块都有序号。
  • TCP使用滑动窗口机制来管理发送和接收数据的缓冲区大小。
  • 接收方会对收到的数据进行确认,并发送确认消息给发送方,以确认数据的接收。
  • 发送方在一定时间内未收到确认消息,则认为数据丢失,并重新发送未收到确认的数据。
  • TCP使用序号和确认号来保证数据的有序性和完整性。
  • TCP使用流量控制和拥塞控制机制来控制数据的发送速率,避免网络拥塞和数据丢失。
  1. CAS(Compare and Swap):
    CAS是一种并发编程中的原子操作,用于实现无锁并发算法。它通过比较内存中的值与预期值是否相等,如果相等则将新值写入内存,否则不进行任何操作。

CAS操作包括三个参数:内存地址(或称为变量的引用)、预期值和新值。它的执行过程如下:

  • 读取内存地址中的值,与预期值进行比较。
  • 如果相等,则将新值写入内存地址,操作成功。
  • 如果不相等,则表示其他线程已经修改了内存中的值,CAS操作失败。

CAS操作在并发环境下可以保证数据的一致性和线程安全性,避免了锁的使用,减少了线程切换和调度的开销。然而,CAS操作也存在ABA问题,即在操作过程中可能发生其他线程修改了值并恢复成原来的值,导致CAS操作无法感知这种变化。为了解决ABA问题,通常会使用版本号或标记来增加额外的判断。


static关键字:

  • static关键字可以用于修饰类的成员(属性和方法),也可以用于修饰局部变量。
  • 当static关键字用于类的成员时,表示该成员属于类级别,而不是实例级别。类的所有实例共享同一个static成员。
  • 静态成员可以通过类名直接访问,不需要创建类的实例。
  • 静态属性在内存中只有一份拷贝,对于所有的实例都是共享的。
  • 静态方法不能访问非静态成员,只能访问静态成员。

类加载机制:

  • 类加载是指将类的字节码文件加载到内存中,并在JVM中生成对应的Class对象的过程。
  • 类加载机制分为加载、验证、准备、解析和初始化五个阶段。
  • 加载阶段:通过类加载器将字节码文件加载到内存中。
  • 验证阶段:验证字节码文件的正确性和安全性。
  • 准备阶段:为类的静态变量分配内存空间,并设置默认初始值。
  • 解析阶段:将符号引用替换为直接引用,建立类之间的关系。
  • 初始化阶段:执行类的初始化代码,包括静态变量的赋值和静态块的执行。

反射:

  • 反射是指在运行时动态地获取类的信息,并能够操作类或对象的属性、方法和构造函数。
  • 通过反射,可以在运行时创建对象、调用方法、获取和设置属性等,即使在编译时无法确定具体的类。
  • 反射提供了灵活性和扩展性,但由于涉及到动态解析和调用,性能上相对较低。

HashMap:

  • HashMap是Java中的一种数据结构,用于存储键值对。

  • 它基于哈希表实现,可以快速地进行插入、删除和查找操作。

  • HashMap允许null键和null值,但键不能重复(相同的键只能存在一个)。

  • HashMap的底层数据结构是数组和链表(或红黑树),通过哈希算法将键映射到数组的索引位置,解决哈希冲突的键值对通过链表(或红黑树)形成一个链表(或红黑树)节点。

  • HashMap的查找和插入操作的平均时间复杂度是O(1),但在最坏情况下可能会达到O(n)。

TCP和UDP的区别及介绍:

  • TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)是网络通信中常用的两种传输协议。
  • TCP是面向连接的协议,提供可靠的数据传输,保证数据的有序性和完整性,但传输效率相对较低。
  • UDP是无连接的协议,不提供可靠性保证,传输速度较快,但对数据的有序性和完整性没有保证。
  • TCP使用三次握手和四次挥手来建立和关闭连接,保证数据的可靠传输。
  • UDP适用于实时性要求高、数据量较小、丢失一些数据不影响的场景,例如音视频传输、实时游戏等。

TCP如何保证可靠的数据传输:

  • TCP使用以下机制保证数据的可靠传输:
    • 序号和确认号:TCP使用序号对数据进行编号,接收方通过确认号告知发送方已接收到的数据,确保数据的有序性和完整性。
    • 窗口机制:TCP使用滑动窗口机制控制发送和接收数据的窗口大小,根据网络的拥塞情况动态调整数据传输速率。
    • 确认机制:接收方在接收到数据后发送确认消息给发送方,以确认数据的接收。如果发送方在一定时间内未收到确认消息,则认为数据丢失,重新发送未收到
      确认的数据。
    • 超时重传:如果发送方在一定时间内未收到确认消息,则认为数据丢失,会重新发送未收到确认的数据,确保数据的可靠传输。
    • 拥塞控制:TCP使用拥塞控制机制来避免网络拥塞,通过动态调整窗口大小和发送速率来控制数据的传输。

给定一个数组,然后一个正数k,判断数组中是否存在子数组,使得子数组之和可以被正数K整除。

思路:使用前缀和的思想,使用一个Set容器存储前缀和与k的余数,当计算完每个位置的前缀和并取余后,如果发现Set中存在该余数,则证明一定存在一个子数组之和可以被k整除。这是因为如果两个位置的前缀和的余数相同,那么这两个位置之间的子数组之和必然是k的倍数。

说一下数组和列表是什么,以及区别?

数组和列表都是数据结构用于存储和组织一组数据的容器,但它们在实现方式和特性上有一些区别。

数组(Array):

  • 数组是一种固定大小的数据结构,它由相同类型的元素组成,这些元素在内存中是连续存储的。
  • 数组的访问是通过索引来实现的,可以通过索引快速访问和修改数组中的元素。
  • 数组的大小在创建时确定,并且通常不可更改。如果需要调整大小,需要创建一个新的数组,并将原数组的元素复制到新数组中。
  • 数组适合用于对元素的随机访问和修改,但在插入和删除元素时效率较低。

列表(List):

  • 列表是一种动态大小的数据结构,它可以容纳不同类型的元素,并且元素在内存中不一定是连续存储的。
  • 列表的访问可以通过索引或迭代器来实现,可以按需访问和修改列表中的元素。
  • 列表的大小可以根据需要动态调整,可以方便地插入和删除元素。
  • 列表适合用于需要频繁插入和删除元素的场景,但在随机访问元素时效率较低。

区别:

  1. 存储方式:数组中的元素在内存中是连续存储的,而列表中的元素在内存中不一定是连续存储的。
  2. 大小调整:数组的大小在创建时确定,并且通常不可更改,而列表的大小可以根据需要动态调整。
  3. 元素类型:数组要求存储的元素类型必须相同,而列表可以容纳不同类型的元素。
  4. 插入和删除操作:数组在插入和删除元素时效率较低,需要移动其他元素,而列表在插入和删除元素时效率较高,不需要移动其他元素。
  5. 随机访问效率:数组可以通过索引进行快速随机访问,而列表在随机访问元素时效率较低。

Spring Bean 生命周期:

  • 实例化:当容器加载Bean定义时,会实例化Bean对象。
  • 属性赋值:容器将属性值和对其他Bean的引用注入到Bean实例中。
  • 初始化:在Bean实例化和属性赋值完成后,可以执行自定义的初始化逻辑,例如调用自定义的初始化方法或实现InitializingBean接口。
  • 使用:Bean可以被容器或其他Bean使用。
  • 销毁:当容器关闭时,会销毁Bean对象,可以执行自定义的销毁逻辑,例如调用自定义的销毁方法或实现DisposableBean接口。

Spring Boot Starter .factorys文件:

  • 在Spring Boot中,自动配置的实现是通过Spring Boot Starter模块来实现的。
  • 在Starter模块的META-INF目录下,有一个名为spring.factories的文件,其中列出了自动配置类的全限定名。
  • Spring Boot在启动时,会根据spring.factories文件加载配置的自动配置类,并将其纳入到应用上下文中。

MySQL三个范式和索引建立与查询:

  • 第一范式(1NF)要求数据表中的每一列都是不可分割的最小数据单位,确保每个数据都是原子性的。
  • 第二范式(2NF)要求数据表中的非主键列完全依赖于主键,即每个非主键列都必须完全依赖于主键而不是部分依赖。
  • 第三范式(3NF)要求数据表中的非主键列之间没有传递依赖关系,即非主键列不依赖于其他非主键列。

ACID

  • 原子性(Atomicity):事务是一个原子操作,要么全部执行成功,要么全部回滚到事务开始前的状态。
  • 一致性(Consistency):事务在执行前后,数据的完整性必须保持一致,即满足预定义的规则和约束。
  • 隔离性(Isolation):多个并发事务之间应该相互隔离,每个事务都应该感觉不到其他事务的存在。
  • 持久性(Durability):一旦事务提交,对数据库的修改应该永久保存,即使系统故障也不应该丢失提交的事务。

具体实现事务的方式包括使用事务管理器、在代码中使用@Transactional注解等。

Redis缓存穿透、缓存击穿和缓存雪崩的解决方法:

  • 缓存穿透:指查询一个不存在于缓存和数据库中的数据,导致每次查询都穿透到数据库。解决方法包括在查询时进行空值缓存、布隆过滤器等。
  • 缓存击穿:指一个热点数据失效或被删除,导致大量请求直接打到数据库上。解决方法包括设置热点数据永不过期、互斥锁等。
  • 缓存雪崩:指缓存中的大量数据在同一时间失效,导致大量请求直接打到数据库上。解决方法包括设置缓存数据的随机过期时间、使用分布式缓存等。

Redis实现分布式锁和解决set nx命令的问题:

  • 使用Redis实现分布式锁可以利用Redis的原子性操作,通过设置一个特定的键来实现锁的获取和释放。
  • 在使用SET命令设置分布式锁时,可以设置NX(只在键不存在时才设置)和EX(设置键的过期时间)选项。
  • 问题:在分布式环境下,如果获取锁和设置过期时间两个操作不是原子性的,可能导致锁无法正常释放。
  • 解决方法:可以使用SET命令的NXEXPX选项组合,或者使用Redis的Lua脚本来保证获取锁和设置过期时间的原子性。

CMS(Concurrent Mark Sweep):

  • CMS是一种基于标记-清除算法的垃圾回收器,用于减少垃圾回收的停顿时间。
  • CMS采用并发标记和并发清除的方式进行垃圾回收,允许垃圾回收与应用程序并发执行,以减少停顿时间。
  • CMS的主要目标是减少垃圾回收的停顿时间,但可能会牺牲一些吞吐量。
  • CMS在进行垃圾回收时,会产生一些碎片,可能会导致内存空间的浪费。

Full GC的发生时机:

  • Full GC(Full Garbage Collection)指的是对整个堆内存进行垃圾回收的过程。
  • Full GC的发生时机包括:
    • 当老年代空间不足以容纳新生成的对象时,会触发Full GC。
    • 当调用System.gc()Runtime.getRuntime().gc()显式触发垃圾回收时,可能会触发Full GC。
    • 当永久代或元空间(在不同的Java版本中)空间不足时,会触发Full GC。

在代码中捕获OOM异常:

  • OOM(OutOfMemoryError)是Java中的错误(Error),而不是异常(Exception),表示内存溢出错误。
  • OOM通常发生在JVM无法分配足够的内存给对象使用时。
  • 由于OOM是错误而不是异常,它表示JVM处于不可恢复的状态,无法被捕获和处理。

算法题:二维数组逐行自增快速查找target:

  • 可以使用二分查找的思想进行解答。
  • 对于逐行自增的二维数组,可以将其看作是一个按顺序排列的一维数组。
  • 首先确定目标值在哪一行,然后在该行进行二分查找。
  • 如果找到目标值,返回true;否则,返回false。

ArrayList的并发修改异常(ConcurrentModificationException)

  • 当使用迭代器遍历ArrayList时,如果在遍历过程中通过集合的add、remove等方法修改了集合的结构(增加或删除元素),就会抛出ConcurrentModificationException异常。
  • 单线程情况下不会发生并发修改异常,因为在单线程环境下,遍历和修改操作是串行执行的。

Java的happens-before:

  • Happens-before是Java内存模型中的概念,用于定义操作之间的可见性和顺序关系。
  • Happens-before规则规定了在多线程环境下,对共享变量的写操作对于后续的读操作是可见的。
  • Happens-before关系保证了程序的顺序性和一致性,避免了由于指令重排等因素导致的数据不一致问题。

Redis的持久化机制:

  • AOF(Append-Only File)持久化:将每个写操作追加到AOF文件中,恢复时重新执行AOF文件中的命令。AOF持久化可以通过配置进行自动化的定期写入或者根据数据变化的时候执行。
  • RDB(Redis Database)持久化:将当前时刻的内存数据快照保存到磁盘上的二进制文件。RDB持久化可以通过手动执行SAVE或者BGSAVE命令,或者根据配置定期自动执行。

Redis集群方案:

  • Redis集群通过数据分片和复制来提供高可用性和可扩展性。
  • Redis Cluster使用分片来水平拆分数据到多个节点,每个节点负责管理其中的一部分数据。
  • Redis Cluster使用Gossip协议来进行节点之间的通信和信息交换,实现节点的自动发现和故障转移。
  • 集群模式下,Redis客户端会根据哈希算法将数据分散存储到不同的节点上,从而实现负载均衡和数据的高可用性。

Spring的IOC容器的作用:

  • IOC(Inversion of Control)是一种设计模式,它将对象的创建和依赖关系的管理交给容器来处理。
  • Spring的IOC容器负责创建、装配和管理应用程序中的对象(Bean),通过配置文件或注解的方式来描述对象之间的依赖关系。
  • IOC容器的作用是实现对象之间的解耦,使得应用程序的组件更加灵活、可维护和可测试。

对Spring的理解:

  • Spring是一个开源的、轻量级的企业级应用开发框架。

  • Spring提供了一个IOC容器和AOP等核心特性,以简化Java开发过程。

  • Spring可以用于构建各种类型的应用程序,包括Web应用、RESTful服务、批处理应用等。

  • Spring提供了丰富的功能和扩展,如事务管理、安全性、缓存、消息传递等。

适配器、装饰器和代理模式的区别:

  • 适配器模式:用于将一个类的接口转换成另一个客户端所期望的接口,使得原本不兼容的类能够协同工作。
  • 装饰器模式:动态地给对象添加额外的功能,通过包装(装饰)原始对象,而不是通过继承来扩展功能。
  • 代理模式:为其他对象提供一种代理以控制对这个对象的访问。代理类和被代理类实现相同的接口,通过代理类来控制和管理被代理类的访问。

TCP/IP四层模型:

  • 应用层:提供应用程序之间的通信和数据交换,包括HTTP、FTP、SMTP等协议。
  • 传输层:负责端到端的数据传输,包括TCP和UDP协议。
  • 网络层:处理网络中的数据包路由和转发,包括IP协议。
  • 数据链路层:负责物理链路的传输,包括以太网、WiFi等。

三次握手和四次挥手:

  • 三次握手是建立TCP连接的过程,用于确认双方的通信能力和同步连接序列号。
  • 四次挥手是关闭TCP连接的过程,用于双方协商断开连接并确认最后的数据传输。

HTTP请求的构成:

  • HTTP请求由请求行、请求头和请求体组成。请求行包括请求方法、URL和协议版本。
  • 请求头包括一些附加的信息,如请求头字段和值,用于传递请求的相关信息。
  • 请求体(可选)包含要发送给服务器的数据,例如表单数据或请求体内容。

消息队列的作用和RabbitMQ的设计:

  • 消息队列用于解耦和异步处理系统中的组件,提供了可靠的消息传递机制。
  • RabbitMQ是一种开源的消息队列系统,它实现了AMQP(高级消息队列协议),提供了可靠的消息传递、消息路由和消息排队等功能。
  • RabbitMQ的设计包括交换机(Exchange)、队列(Queue)和绑定(Binding),通过交换机和绑定规则将消息发送到特定的队列中。

最长无重复子串算法:

  • 最长无重复子串算法可以通过滑动窗口的方式来实现。

  • 使用两个指针表示滑动窗口的左右边界,遍历整个字符串。

  • 当右指针遇到重复字符时,移动左指针到重复字符的下一个位置,更新窗口的大小。

  • 在遍历过程中记录最大的窗口大小,即为最长无重复子串的长度。

什么是遍历和修改操作是串行执行

当说到遍历和修改操作是串行执行时,意味着在单线程环境中,这两个操作是按顺序依次执行的,不会发生并发或并行的情况。

具体来说,在单线程中执行遍历操作时,会按照集合的顺序依次访问每个元素,直到遍历完所有元素或满足某个条件。期间不会有其他线程干扰或修改集合的结构。

同样,在单线程中执行修改操作时,会依次对集合的元素进行修改,例如添加、删除或更新元素。在修改操作执行期间,也不会有其他线程同时对集合进行修改。

因此,在单线程环境下,遍历和修改操作是串行执行的,保证了操作的顺序性和一致性。但是需要注意的是,在多线程环境下,遍历和修改操作可能会引发并发修改异常(ConcurrentModificationException),需要使用相应的并发控制手段来保证线程安全性。

什么是快照

在计算机科学和数据库领域,快照(Snapshot)是指某一时刻系统或数据的静态副本或状态的表示。

在操作系统中,快照可以是对文件系统或虚拟机的状态的拍摄,记录了某一时刻文件系统或虚拟机中所有文件、目录和内存的状态。这样的快照可以用于备份和还原,以及系统恢复和故障排除。

在数据库中,快照是数据库在某一时刻的一致性视图。它是数据库在特定时间点的数据副本,反映了数据库中所有表、记录和索引的状态。数据库快照可以用于数据恢复、数据分析和并发控制。

快照的关键特点是它是在某个确定的时间点捕获的,因此它反映了该时间点系统或数据的准确状态。快照可以被用于恢复系统到该时间点的状态,或者作为参考点进行数据分析和比较。

需要注意的是,快照通常是只读的,即不能对快照进行修改。任何对原始系统或数据的修改都不会影响已经捕获的快照。因此,快照常常用于数据保护和故障恢复的目的。

IOC

IOC(Inversion of Control)是一种软件设计原则,也是Spring框架的核心概念之一。它实现了控制反转,即将对象的创建、依赖关系的管理和对象的生命周期的控制权从应用程序代码中转移到了容器(框架)中。

在传统的编程模型中,应用程序代码通常负责创建对象、解决对象之间的依赖关系以及管理对象的生命周期。这导致了代码的紧耦合和复杂性,使得代码难以维护、测试和扩展。

而IOC容器则通过将对象的创建和依赖关系的管理委托给容器来解决这些问题。应用程序只需要描述对象之间的依赖关系,而不需要直接处理对象的创建和管理。容器根据配置文件或注解来实现对象的创建、依赖注入和生命周期管理,从而实现了对象之间的解耦和灵活性。

IOC容器的工作原理是通过反射、配置文件或注解来实现对象的实例化和依赖注入。它可以根据配置文件或注解中的信息,自动创建对象,并将依赖的对象注入到目标对象中。这样,对象之间的依赖关系由容器来管理,应用程序代码只需要使用这些对象即可。

通过使用IOC容器,我们可以实现以下优势:

  • 松耦合:对象之间的依赖关系由容器来管理,减少了对象之间的直接耦合,提高了代码的可维护性和可测试性。
  • 可扩展性:通过配置文件或注解,可以很容易地添加、修改或替换对象的实现,而不需要修改应用程序的代码。
  • 简化开发:IOC容器自动处理对象的创建和依赖注入,减少了开发人员的工作量,提高了开发效率。

Spring框架是一个基于IOC容器的应用框架,它提供了强大的IOC功能以及其他丰富的特性,如AOP(面向切面编程)、事务管理、安全性等,使得开发者可以更轻松地开发复杂的企业应用。

为什么要有异步这个东西,解决了什么问题,举个代码例子

异步编程是一种编程范式,用于解决在某些情况下同步执行会导致性能问题或阻塞的情况。它可以提高系统的响应性能、资源利用率和并发处理能力。

异步编程的主要目的是通过将耗时的操作(如网络请求、文件读写、数据库查询等)放在后台进行,不阻塞主线程或其他任务的执行。这样可以使程序在等待这些操作完成的同时继续执行其他任务,提高了程序的吞吐量和响应速度。

异步编程可以解决以下问题:

  1. 阻塞问题:当程序需要等待耗时操作完成时,同步执行会导致阻塞,影响程序的执行效率和响应性能。异步编程通过非阻塞的方式处理耗时操作,允许程序继续执行其他任务,提高了程序的并发处理能力。
  2. 响应性能问题:在需要响应用户请求的应用中,同步执行可能导致用户长时间等待,给用户带来不良的体验。异步编程可以在后台处理耗时操作,使得系统能够更快地响应用户请求。
  3. 资源利用率问题:同步执行时,如果一个线程在等待耗时操作完成期间空闲,资源得不到充分利用。异步编程可以在等待时释放线程,使其能够执行其他任务,提高了资源的利用率。

以下是一个简单的Java代码例子,展示了如何使用异步编程(基于Java 8 CompletableFuture)进行网络请求的并发处理:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsyncExample {

public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
// 执行第一个网络请求
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Result from Request 1";
}, executor);

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
// 执行第二个网络请求
// 模拟耗时操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Result from Request 2";
}, executor);

// 异步等待两个请求都完成
CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2);

combinedFuture.thenRun(() -> {
// 当两个请求都完成时执行的操作
System.out.println("Both requests completed.");
System.out.println("Result 1: " + future1.join());
System.out.println("Result 2: " + future2.join());
// 可以继续执行其他任务
});

// 关闭线程池
executor.shutdown();
}
}

在上述代码中,我们使用CompletableFuture创建两个异步任务,分别模拟两个网络请求的耗时操作。然后,使用CompletableFuture.allOf等待两个任务都完成,然后执行后续操作。这样可以并发地处理多个网络请求,提高了程序的并发性能。

什么是编程范式

编程范式是一种编程的方法论或编程的风格,它规定了程序员在解决问题时应遵循的原则和思维方式。不同的编程范式强调不同的概念、原则和思想,以实现特定的目标和效果。

  1. 命令式编程范式:以算法为中心,通过编写一系列的命令来指导计算机执行任务。常见的命令式编程语言有C、Java等。
  2. 声明式编程范式:以描述问题的逻辑为中心,通过声明式语句来描述问题的解决方式,而不是直接指定计算机的执行步骤。常见的声明式编程范式包括函数式编程和逻辑编程。
  3. 函数式编程范式:将计算视为数学函数的求值过程,强调函数的纯粹性和不可变性。函数式编程语言如Haskell、Scala和Lisp属于这种范式。
  4. 面向对象编程范式:以对象为中心,将数据和操作封装在对象中,并通过对象之间的交互来解决问题。面向对象编程语言如Java、C++和Python属于这种范式。
  5. 面向过程编程范式:将程序看作是一系列的过程或函数,通过顺序执行这些过程来实现任务。C语言常用这种范式。
  6. 并发编程范式:用于处理并发和多线程编程的范式,例如使用锁、线程、消息传递等机制来实现并发任务。并发编程语言和框架如Java的并发包、Go和Erlang等属于这种范式。
  7. 泛型编程范式:通过参数化类型和抽象数据类型的概念,实现可重用和通用的代码。C++的模板和Java的泛型就是泛型编程的范式。

Spring Bean 是什么

在Spring框架中,Bean是一个由Spring容器管理的对象。Bean代表了应用程序中的一个组件,它可以是一个实例化的类对象、一个配置的数据源、一个服务等。

Spring Bean的特点和作用包括:

  1. 实例化和管理:Spring容器负责创建和管理Bean的生命周期,包括实例化、依赖注入、初始化、销毁等过程。
  2. 依赖注入:Spring容器通过依赖注入将Bean所需的依赖关系自动注入到Bean中,实现了松耦合和可测试性。
  3. 配置灵活性:Spring Bean的配置信息可以通过XML配置文件、注解或Java代码等方式进行定义和修改,提供了灵活的配置选项。
  4. AOP支持:Spring框架提供了面向切面编程(AOP)的支持,可以在Bean中定义切面和通知,实现横切关注点的解耦和复用。
  5. 事务管理:Spring框架提供了事务管理的功能,可以通过配置将事务应用到Bean的方法中,实现数据的一致性和事务的管理。
  6. 生命周期管理:Spring容器管理Bean的生命周期,包括初始化方法和销毁方法的回调,可以通过配置自定义Bean的初始化和销毁逻辑。

通过Spring Bean的管理和依赖注入,开发人员可以更加专注于业务逻辑的实现,而无需关注对象的创建和管理细节。同时,Spring Bean的配置灵活性也使得应用程序的组件可以方便地进行扩展和修改,提供了更好的可维护性和可测试性。

RESTful

RESTful是一种设计风格或架构风格,用于构建网络应用程序,特别是基于HTTP协议的Web服务。REST是Representational State Transfer的缩写,它强调将应用程序的功能以资源的形式进行建模,通过HTTP协议中的各种方法对资源进行操作和交互。

以下是RESTful的一些关键概念和原则:

  1. 资源(Resource):在RESTful架构中,所有的功能被视为资源,每个资源都可以通过一个唯一的URI进行标识,例如/users、/products等。
  2. 表述(Representation):资源的表述是指资源的具体数据表示形式,可以是JSON、XML、HTML等。客户端可以通过HTTP方法(GET、POST、PUT、DELETE等)对资源的表述进行操作。
  3. 统一接口(Uniform Interface):RESTful架构通过使用统一的接口规范,包括使用HTTP方法进行资源操作(GET、POST、PUT、DELETE)以及对资源的URI进行标识和访问。
  4. 状态无关(Stateless):RESTful架构是无状态的,每个请求都包含了足够的信息,服务器不需要存储客户端的状态信息。
  5. 超媒体驱动(HATEOAS):通过在响应中提供资源间的链接,客户端可以动态地发现和操作可用的资源,实现应用程序的自描述性和可扩展性。

使用RESTful架构设计的Web服务具有良好的可扩展性、可维护性和松耦合性。它能够提供清晰的接口设计、灵活的资源交互方式和统一的状态管理,使得不同系统之间的通信更加简洁和高效。同时,RESTful架构也符合Web标准,能够充分利用HTTP协议的各种功能和特性。

GET、POST、PUT、DELETE

GET、POST、PUT和DELETE是HTTP协议中定义的常用请求方法,用于对服务器资源进行不同类型的操作。

  1. GET:用于从服务器获取资源,常用于获取数据。GET请求是幂等的,即多次执行相同的GET请求应该返回相同的结果,不应该对服务器产生副作用。GET请求的参数可以通过URL的查询字符串传递,也可以通过请求体传递(例如在RESTful API中使用JSON格式的请求体)。

  2. POST:用于向服务器提交数据,常用于创建新的资源。POST请求不是幂等的,即多次执行相同的POST请求可能会创建多个资源。POST请求的参数通常包含在请求体中,用于传递需要提交的数据。

  3. PUT:用于更新服务器上的资源,常用于更新已存在的资源。PUT请求是幂等的,即多次执行相同的PUT请求应该产生相同的结果。PUT请求通常需要提供完整的资源表示,即需要将所有的属性值都传递给服务器。

  4. DELETE:用于删除服务器上的资源,常用于删除不再需要的资源。DELETE请求是幂等的,即多次执行相同的DELETE请求应该产生相同的结果。DELETE请求的URI通常包含要删除的资源的唯一标识,例如”/users/1”表示删除ID为1的用户。

这些请求方法在RESTful API设计中起着重要的作用,它们定义了对资源的不同操作类型,使得客户端能够以统一和标准的方式与服务器进行交互。根据具体的应用场景和需求,选择合适的请求方法来进行资源操作。

什么是BEAN

在计算机编程领域,”Bean”一词通常用于表示可重用的软件组件,特别是在Java编程语言中。在Java中,Bean是指符合一定规范的Java对象,它具有以下特征:

  1. 具有私有的成员变量(属性):Bean通常定义了一组私有的成员变量,用于存储对象的状态。
  2. 提供公共的访问方法:Bean通过公共的getter和setter方法,提供对成员变量的访问和修改。
  3. 符合JavaBean规范:JavaBean是一种特殊类型的Bean,它需要满足一些规范,如具有无参构造函数、实现可序列化接口等。

Bean的概念旨在提供一种可重用和可组合的编程模型,通过封装对象的状态和行为,使得开发人员能够更加方便地进行代码复用和模块化开发。在Java中,Bean通常用于描述业务逻辑、数据模型、服务组件等,它们可以被其他代码引用和调用。

在Java的开发框架中,比如Spring框架,Bean是指由Spring容器管理的对象,它们通过配置文件或注解的方式进行声明和创建。Spring Bean具有生命周期管理、依赖注入等特性,可以被动态地创建、初始化、销毁,并通过容器进行管理和协调。Spring框架通过Bean的管理和依赖注入,实现了松耦合、可测试性和可扩展性,提供了强大的面向对象编程支持。

什么是松耦合

松耦合(Loose coupling)是软件设计中的一个原则,用于描述模块或组件之间的关系。它指的是模块或组件之间的依赖关系应该尽量减少或降低,使得它们能够独立地进行修改、扩展和测试,而不会对其他模块或组件造成过大的影响。

松耦合的设计具有以下特点:

  1. 低依赖性:模块之间的依赖应该尽量减少,避免直接依赖于具体的实现细节。模块之间的通信应该基于抽象接口或协议,而不是依赖具体的类或对象。

  2. 接口隔离:模块之间的接口应该是精简的,只包含必要的方法和属性,避免暴露不必要的细节。每个模块都应该有清晰的职责和接口定义,避免对其他模块的实现细节产生依赖。

  3. 解耦合:模块之间应该尽量解耦,即减少相互之间的直接关联性。通过中间层、消息传递、事件驱动等方式,实现模块之间的解耦,使得模块能够独立地进行修改和演化。

  4. 可扩展性:松耦合的设计能够更好地支持系统的扩展和修改。当需要添加新的功能或模块时,可以通过新增模块、替换组件等方式进行扩展,而不会对其他模块产生较大的影响。

通过实现松耦合的设计,可以提高系统的可维护性、可测试性和可扩展性。松耦合的模块更易于理解、修改和重用,同时也降低了代码之间的耦合度,使得系统更加灵活和可靠。

举个例子

假设我们有一个简单的订单管理系统,包含订单模块和库存模块。下面是一个例子来说明松耦合的概念:

在紧耦合的设计中,订单模块和库存模块可能直接依赖于彼此的实现细节,相互之间存在强耦合关系。例如,订单模块可能直接调用库存模块的方法来查询库存情况,并根据库存数量进行一些处理。

public class OrderModule {
private InventoryModule inventory;

public OrderModule() {
inventory = new InventoryModule();
}

public void placeOrder(Order order) {
// 获取库存数量
int quantity = inventory.getQuantity(order.getProduct());

// 根据库存情况进行处理
if (quantity > 0) {
// 下单逻辑...
} else {
// 库存不足逻辑...
}
}
}

在这个例子中,订单模块直接依赖于库存模块的实现,并且直接调用了库存模块的方法。这种紧耦合的设计存在以下问题:

  • 当库存模块的实现发生变化时,订单模块也需要相应地进行修改。
  • 难以对订单模块和库存模块进行单独的测试,因为它们之间的关联性很高。

相反,在松耦合的设计中,订单模块和库存模块之间通过接口进行通信,彼此之间不存在直接的依赖关系。

public interface InventoryService {
int getQuantity(String product);
}

public class OrderModule {
private InventoryService inventory;

public OrderModule(InventoryService inventory) {
this.inventory = inventory;
}

public void placeOrder(Order order) {
// 获取库存数量
int quantity = inventory.getQuantity(order.getProduct());

// 根据库存情况进行处理
if (quantity > 0) {
// 下单逻辑...
} else {
// 库存不足逻辑...
}
}
}

在这个例子中,订单模块通过依赖注入的方式获取库存模块的实现,而不是直接实例化库存模块。这种松耦合的设计带来了以下好处:

  • 订单模块和库存模块之间解耦,它们可以独立地进行修改和扩展。
  • 可以通过实现不同的InventoryService接口来切换不同的库存服务,而不会对订单模块的实现造成影响。
  • 可以更方便地对订单模块和库存模块进行单独的测试,因为它们之间的关联性降低。

这个例子展示了松耦合设计的思想,通过接口抽象和依赖注入等方式,实现了模块之间的解耦,提高了系统的可维护性和扩展性。

什么是事务

事务(Transaction)是指一组数据库操作组成的逻辑单位,它要么全部执行成功,要么全部失败回滚。事务具有以下特性,通常被称为 ACID 特性:

  1. 原子性(Atomicity):事务中的操作要么全部执行成功,要么全部失败回滚,不存在部分执行的情况。如果事务中的任意操作失败,系统将撤销已经执行的操作,回滚到事务开始前的状态。

  2. 一致性(Consistency):事务执行前和执行后,数据库的完整性约束没有被破坏。即使事务中的操作可能改变数据库中的数据状态,但是这些变化必须满足预定义的规则,确保数据库的一致性。

  3. 隔离性(Isolation):并发执行的事务之间相互隔离,一个事务的执行不应该影响其他事务的结果。每个事务都应该感觉到自己是在独占地使用数据库,不会被其他事务干扰。

  4. 持久性(Durability):一旦事务提交成功,其结果应该持久保存在数据库中,并且对于系统故障或崩溃的情况下也能够恢复。持久性保证了事务的结果不会因为系统故障而丢失。

事务通常通过以下四个关键操作来管理:

  1. 开始事务(BEGIN):标记事务的开始,事务中的操作将被记录并可以进行回滚。

  2. 提交事务(COMMIT):将事务中的操作应用到数据库,并使其成为永久性的更改。

  3. 回滚事务(ROLLBACK):撤销事务中的操作,将数据库恢复到事务开始前的状态。

  4. 设置保存点(SAVEPOINT):在事务中设置一个保存点,可以在回滚时将事务回滚到该保存点之前的状态。

事务的使用可以确保数据库操作的一致性和完整性,特别是在并发环境下,可以避免数据的混乱和冲突。常见的数据库管理系统如 MySQL、Oracle、PostgreSQL 等都支持事务的概念和相关操作。在应用开发中,事务的正确使用可以保证数据的可靠性和一致性。

什么是分布式

分布式(Distributed)是指在多台计算机或多个节点上协同工作的系统或应用程序。在分布式系统中,多个计算节点通过网络相互连接,共同完成任务或提供服务,形成一个整体。

分布式系统的设计目标通常包括以下几个方面:

  1. 可扩展性:能够方便地增加或减少计算节点,以应对负载的变化,实现水平扩展。

  2. 高可用性:通过冗余和故障转移机制,保证系统在部分节点或组件故障的情况下仍然可用。

  3. 容错性:分布式系统能够容忍节点或组件的故障,并且能够自动进行故障恢复。

  4. 性能:利用分布式计算资源,提高系统的计算、存储和网络性能。

  5. 数据一致性:在分布式环境下,保证数据在不同节点之间的一致性,避免数据的冲突和不一致。

分布式系统面临的挑战和问题包括:

  1. 通信和网络延迟:分布式系统依赖于网络进行节点之间的通信,网络延迟和不可靠性可能会影响系统的性能和可靠性。

  2. 数据一致性:在分布式环境下,多个节点可能并发地读写数据,需要采取合适的机制来保证数据的一致性。

  3. 并发控制:多个节点并发地进行计算和操作,需要解决并发访问共享资源的冲突问题。

  4. 故障处理:分布式系统中的节点可能会发生故障,需要采取故障检测和故障转移机制来保证系统的可用性。

常见的分布式系统包括分布式数据库系统、分布式文件系统、分布式缓存系统等。分布式系统的设计和实现涉及到分布式算法、一致性协议、负载均衡、故障恢复等技术。分布式系统的优势在于提供高性能、可伸缩性和可靠性,但也增加了系统设计和管理的复杂性。

分布式数据库系统、分布式文件系统、分布式缓存系统

分布式数据库系统(Distributed Database System)是一种将数据存储和处理分布在多个计算节点上的数据库系统。它允许数据在多个节点之间进行分片存储,并提供分布式查询和事务处理能力。

分布式数据库系统的设计目标是提高数据的可扩展性、可用性和性能。它通常具有以下特点:

  1. 数据分片:将数据划分为多个分片,每个分片存储在不同的节点上,实现数据的分布存储。分片可以按照不同的策略进行,如范围分片、哈希分片等。

  2. 数据复制:为了提高数据的可用性和容错性,分布式数据库系统通常会对数据进行复制,并将副本存储在多个节点上。数据的复制可以采用同步复制或异步复制的方式。

  3. 数据一致性:在分布式环境下,确保数据的一致性是一个重要的挑战。分布式数据库系统通常使用一致性协议和机制来解决数据一致性的问题,如基于副本的一致性协议、分布式事务协议等。

  4. 查询优化和路由:分布式数据库系统需要考虑将查询请求路由到合适的节点上进行处理,并进行查询优化,以提高查询性能。

分布式文件系统(Distributed File System)是一种将文件数据存储在多个计算节点上的文件系统。它将文件划分为多个块(Block),并将块分布存储在不同的节点上,实现文件的分布式存储和访问。

分布式文件系统的设计目标是提供高可用性、可扩展性和容错性。它通常具有以下特点:

  1. 数据分布和复制:文件数据被划分为多个块,并将块分布存储在多个节点上。为了提高可用性和容错性,文件块通常会进行多次复制,并存储在不同的节点上。

  2. 文件访问和路由:分布式文件系统提供文件访问接口,可以通过文件名或文件路径进行文件的读取和写入操作。文件系统需要考虑将文件访问请求路由到存储了相应块的节点上。

  3. 一致性和一致性协议:分布式文件系统需要考虑多个节点之间的数据一致性问题。通常使用一致性协议和副本管理策略来保证数据的一致性。

分布式缓存系统(Distributed Cache System)是一种将数据缓存分布在多个计算节点上的缓存系统。它通过在节点上存储一部分热点数据,以提高数据访问的性能和响应速度。

分布式缓存系统的设计目标是提供高速的数据访问和低延迟。它通常具有以下特点:

  1. 数据分片和路由:缓存数据被分片存储在多个节点上,根据数据的键(Key)进行路由和访问。这样可以将缓存数据均匀分布在多个节点上,避免单个节点的负载过高。

  2. 缓存更新和失效:分布式缓存系统需要考虑缓存数据的更新和失效问题。当数据发生变化时,需要及时更新缓存数据;当数据过期或失效时,需要从后端数据源重新加载数据。

  3. 缓存一致性:在分布式缓存系统中,多个节点上的缓存数据需要保持一致。通常使用缓存一致性协议和缓存失效策略来解决数据一致性的问题。

这些分布式系统在大规模、高并发的应用场景中发挥着重要作用,提供了可扩展性、高可用性和性能等方面的优势。它们的设计和实现涉及到分布式算法、一致性协议、数据分片和复制、负载均衡等技术。

希尔排序,c++

下面是一个使用C++实现的希尔排序算法示例:

#include <iostream>
using namespace std;

// 希尔排序函数
void shellSort(int arr[], int n) {
// 定义步长序列
int gap = n / 2;

// 对每个步长进行插入排序
while (gap > 0) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;

// 在当前步长下进行插入排序
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}

arr[j] = temp;
}

// 更新步长
gap /= 2;
}
}

// 测试示例
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);

cout << "排序前的数组: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;

shellSort(arr, n);

cout << "排序后的数组: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;

return 0;
}

CPU三级缓存的作用如下:

  • 第一级缓存(L1 Cache):位于CPU内部,与CPU核心紧密相连,速度最快。用于存储最常用的数据和指令。
  • 第二级缓存(L2 Cache):位于CPU内部,较大容量,速度较快。用于存储CPU频繁访问但不在L1 Cache中的数据和指令。
  • 第三级缓存(L3 Cache):位于CPU外部,容量更大,速度较慢。用于存储CPU访问次数较少但仍然需要快速访问的数据和指令。

CPU缓存的目的是提高CPU的数据访问速度,通过将常用的数据和指令存储在高速缓存中,减少了对主内存的访问次数,从而加快程序的执行速度。

进程和线程的区别:

  • 进程是程序的执行实例,是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的内存空间,包括代码、数据和堆栈等。
  • 线程是进程的执行单元,一个进程可以包含多个线程。线程共享进程的内存空间,包括代码、数据和堆栈等,因此线程之间的数据共享较为方便。

进程间通信的方式包括管道、信号量、共享内存、消息队列等。

HTTP长连接的实现是通过在一个TCP连接上进行多次HTTP请求和响应,而不是每次请求都建立和关闭一次TCP连接。在HTTP请求头中使用Connection: keep-alive来表示使用长连接。

GET和POST是HTTP协议中的两种常见请求方法:

  • GET请求用于获取资源,参数通过URL的查询字符串传递,有长度限制,一般用于查询数据。
  • POST请求用于提交数据,参数通过请求体传递,没有长度限制,一般用于提交表单数据或上传文件。

如果GET请求的参数过长,可以将参数放在请求体中进行POST请求,或者使用分段请求等方式将参数拆分为多个请求。

用户态和内核态是CPU执行指令时的两种工作模式:

  • 用户态:程序运行在用户空间,只能访问受限资源,无法直接访问硬件和操作系统内核。
  • 内核态:操作系统运行在内核空间,可以访问系统的所有资源和硬件。

Java线程属于用户态,因为Java虚拟机(JVM)是运行在用户空间的,Java线程的调度和管理是由JVM负责的。

读写并发可以通过使用锁机制(如读写锁)或使用线程安全的数据结构来实现。读操作可以并发执行,写操作需要互斥进行。

  1. Java常见的集合类包括ArrayList、LinkedList、HashMap、HashSet等。线程安全的Map有ConcurrentHashMap。

  2. 锁的实现方式包括悲观锁和乐观锁。公平锁和非公平锁是悲观锁的两种实现方式:

    • 公平锁:按照申请锁的顺序来获取锁,遵循先来先得的原则。
    • 非公平锁:不保证按照申请锁的顺序来获取锁,允许插队。

    公平锁的优点是保证了资源的公平性,缺点是可能导致线程频繁切换。非公平锁的优点是效率较高,缺点是可能会导致某些线程长时间无法获取锁。

  3. CAS(Compare and Swap)是一种无锁算法,用于实现多线程环境下的原子操作。CAS操作包括读取一个内存位置的值、比较该值与预期值、根据比较结果来更新内存位置的值。

    CAS的优点是无锁、高效,可以避免线程的切换和阻塞等开销。缺点是当多个线程同时执行CAS操作时,只有一个线程能成功,其他线程需要重新尝试,可能导致性能下降。

    CAS的缺点可以通过使用自旋锁或者将CAS操作转化为有锁的操作来解决。

  4. 线程池的核心参数包括线程池大小、核心线程数、最大线程数、线程空闲时间等。线程池的大小决定了可以同时执行的任务数量,核心线程数是线程池中保持活动状态的线程数,最大线程数是线程池中允许存在的最大线程数,线程空闲时间是线程在无任务可执行时保持活动状态的时间。

  5. JVM的类加载过程包括加载、验证、准备、解析和初始化五个阶段。加载阶段将类的字节码加载到内存中,验证阶段验证字节码的正确性,准备阶段为类的静态变量分配内存并初始化默认值,解析阶段将符号引用转换为直接引用,初始化阶段执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。

  6. 聚簇索引是将数据行存储在磁盘上物理上相邻的位置,非聚簇索引是将索引项存储在磁盘上物理上相邻的位置。为了避免

回表查找,可以将需要的列都包含在索引中,这样就可以通过索引直接获取所需数据,而不需要再次访问表。

  1. 最左匹配指的是在查询条件中使用多列索引时,索引列的顺序对查询的效率有影响,查询条件中使用的索引列的顺序要与索引定义的列的顺序一致,才能发挥最佳的索引效果。行锁是数据库中的锁机制,用于保证并发操作的一致性和数据的完整性。

  2. 常见的事务隔离级别有读未提交、读已提交、可重复读和串行化。在生产环境中一般使用读已提交(Read Committed)隔离级别。事务隔离级别的选择会影响并发性能和数据的一致性。

  3. Redis常用的数据结构有字符串(String)、哈希表(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。排行榜可以使用有序集合来实现,将每个成员作为排名对象,分数作为排名标准。

  4. 缓存穿透是指缓存和数据库中都没有需要的数据,导致每次请求都穿透缓存访问数据库,解决方式可以使用布隆过滤器过滤不存在的数据。缓存击穿是指某个热点数据失效,导致大量请求同时访问数据库,解决方式可以使用互斥锁或者设置短暂的缓存过期时间。缓存雪崩是指大量缓存同时失效,导致所有请求都直接访问数据库,解决方式可以设置随机的缓存过期时间或使用分布式缓存方案。

  5. 在项目中最有收获的地方可以是技术的成长、解决复杂问题的经验、与团队合作的能力提升等。

  6. 订单多次创建可能导致幂等性问题,即多次创建的结果不一致。可以通过在创建订单时使用唯一标识符(如订单号)来确保幂等性,当重复创建时,检查该标识符是否已存在,如果已存在则不执行重复创建操作。

什么是mysql的索引

MySQL的索引是一种数据结构,用于提高数据库查询操作的效率。索引是在表中某一列或多列上创建的,它们包含了对数据行的引用,以便可以快速定位到满足特定条件的数据行。

MySQL的索引可以分为以下几种类型:

  1. 主键索引(Primary Key Index):用于唯一标识表中的每一行数据。主键索引在创建表时可以指定,如果没有显式指定,则会自动创建一个隐藏的主键索引。

  2. 唯一索引(Unique Index):用于保证索引列的值在表中是唯一的。唯一索引可以包含空值,但每个非空值只能出现一次。

  3. 普通索引(Normal Index):也称为非唯一索引,用于加快查询速度。多个行可以具有相同的索引值。

  4. 全文索引(Full-text Index):用于在文本数据上执行全文搜索。全文索引适用于大量文本内容的查询,支持关键词搜索和自然语言搜索。

  5. 组合索引(Composite Index):由多个列组合而成的索引,用于加快多列条件查询的速度。组合索引的顺序很重要,根据查询的列顺序来决定是否能够有效使用索引。

索引可以提高查询的速度,因为它们可以减少需要扫描的数据量。然而,索引也会增加数据插入、更新和删除的成本,因为每次修改数据时都需要更新索引。因此,在设计数据库时,需要根据查询的频率和更新的频率来合理地选择索引,以平衡查询性能和数据维护的开销。

有序集合(Sorted Set)

有序集合(Sorted Set)是Redis中的一种数据结构,它类似于集合(Set),但每个元素都关联着一个”分数”(score),通过分数的排序可以将元素按照一定顺序进行存储和访问。

有序集合的特点包括:

  1. 元素的唯一性:每个元素在有序集合中是唯一的,不会存在重复元素。

  2. 元素的排序:每个元素都有一个关联的分数,通过分数进行排序。分数可以是浮点数或整数,根据分数的值来决定元素的顺序。

  3. 快速的插入、删除和查找:有序集合使用了内部的数据结构(跳表和哈希表)来实现高效的插入、删除和查找操作。

有序集合在实际应用中有广泛的用途,例如:

  • 排行榜:可以使用有序集合来存储用户的得分,并根据得分进行排名和排序。

  • 计数器:可以使用有序集合来存储计数器的值,每次增减操作都可以更新对应元素的分数。

  • 范围查找:可以根据分数的范围进行查找操作,快速找到满足条件的元素。

  • 任务调度:可以使用有序集合来存储任务的执行时间,按照时间顺序获取需要执行的任务。

有序集合提供了一种灵活且高效的数据结构,能够满足多种场景下的排序和查找需求。在Redis中,可以使用一系列的命令对有序集合进行操作,如添加元素、删除元素、按照分数范围查找等。

底层是什么

在Redis中,有序集合(Sorted Set)的底层数据结构是一个叫做”跳跃表”(Skip List)的数据结构。跳跃表是一种有序的数据结构,通过在普通链表的基础上添加多级索引,提供了快速的查找和插入操作。

跳跃表的特点包括:

  1. 有序性:跳跃表中的元素按照一定的顺序进行排列,可以根据元素的分数进行快速的有序查找。

  2. 多级索引:跳跃表通过添加多级索引来加速查找操作,索引层数越高,查找速度越快。每一层索引都是一个有序链表,其中每个节点保存了下一层索引的指针。

  3. 空间效率:跳跃表在维持有序性的同时,也要尽量减少额外的空间消耗。相比于平衡树等其他数据结构,跳跃表具有较好的空间效率。

跳跃表是一种高效的数据结构,适用于需要有序性和快速查找的场景。在Redis中,有序集合使用跳跃表作为底层数据结构,使得有序集合具备了快速插入、删除和按分数范围查找的能力。

怎么解决死锁

死锁是多线程或多进程程序中常见的一个问题,它发生在两个或多个线程或进程相互等待对方释放资源的情况下,导致程序无法继续执行。解决死锁问题需要采取一些策略和技术,以下是一些常见的方法:

  1. 避免死锁:通过合理地设计系统和资源分配策略,避免进程或线程陷入死锁的状态。这可以通过合理的资源分配顺序、资源申请的限制和超时机制等来实现。

  2. 使用资源分配图:通过绘制资源分配图,可以分析出哪些资源被哪些进程或线程占用,从而更好地了解可能导致死锁的情况。根据图中的信息,可以调整资源分配策略以避免死锁。

  3. 引入超时机制:在等待资源时,引入超时机制,如果等待时间过长,可以放弃当前请求,释放已占用的资源,避免进程或线程一直等待而导致死锁。

  4. 按序申请资源:确保进程或线程按照特定的顺序申请资源,避免出现循环等待的情况。

  5. 使用资源分级:将资源划分为不同的等级,确保进程或线程只能按照一定顺序申请资源,避免相互等待。

  6. 采用死锁检测和恢复机制:实现死锁检测算法,及时检测出死锁的发生,然后采取恢复措施,比如回退资源分配、终止部分进程等。

  7. 精心规划资源释放策略:在申请资源后,规划好资源的释放策略,避免长时间持有资源而阻塞其他进程。

  8. 使用同步原语:合理使用锁、信号量等同步原语,确保资源的访问是有序的,从而避免死锁。

  9. 引入资源预约:资源预约机制可以使进程在申请资源之前提前预约所需资源,从而降低死锁的可能性。

  10. 监控和报警系统:实现监控系统,及时发现死锁问题并触发警报,便于及时处理。

解决死锁是一个复杂的问题,需要根据具体情况采取不同的策略和方法。在编写多线程或多进程程序时,需要充分考虑资源分配和释放的顺序,以及合理地设计同步机制,从而避免死锁问题的发生。

MySQL的binlog和redolog如何保证一致性?

  • MySQL的binlog和redolog都是用于事务的持久化和恢复,但它们服务于不同的目的。Redo log是在事务提交前记录,以保证事务的持久性。Binlog记录的是逻辑日志,可以用于复制和恢复。
  • 一致性是通过在事务提交前将操作记录到redo log,并在事务提交后再记录到binlog,来保证的。如果在提交前出现问题,MySQL可以通过redo log进行恢复。如果在提交后出现问题,binlog可以用于恢复。

类加载时class文件加载到JVM的什么区域?

  • 类加载时的class文件会加载到JVM的方法区(Metaspace)中。方法区存储类的结构信息、静态变量、常量池等。

父类加载器能访问子类加载器吗?反过来呢?

  • 父类加载器可以访问子类加载器加载的类,但反过来是不行的。这是由类加载器的双亲委派模型所决定的。

父类怎么访问子类?

  • 父类无法直接访问子类,因为子类可能在不同的类加载器命名空间中。如果需要进行类的转型操作,应该在具体使用的位置进行。

抽奖系统设计题:怎么保证中奖者一定能收到奖品?考虑发奖失败的情况?高概率的热门奖品怎么处理?库存hotkey遇到超高并发怎么办?

  • 这是一个比较复杂的设计问题,需要考虑分布式事务、消息队列等方案,以确保中奖者能够收到奖品,同时保证系统的可用性和一致性。

synchronized实现底层?

  • synchronized 的实现底层是通过对象头中的标志位来实现的,当线程进入同步块时会尝试获取锁,如果锁已被其他线程占用,线程会被阻塞,直到锁被释放。

什么叫死锁?写出SQL模拟一个死锁。

  • 死锁是指两个或多个进程(线程)在互相等待对方持有的资源,从而造成的一种僵局。例如:
    -- Session 1
    UPDATE table1 SET column1 = 1 WHERE id = 1;

    -- Session 2
    UPDATE table2 SET column2 = 2 WHERE id = 2;

    -- Session 1
    UPDATE table2 SET column2 = 2 WHERE id = 2; -- 等待 table2 的锁
    -- Session 2
    UPDATE table1 SET column1 = 1 WHERE id = 1; -- 等待 table1 的锁

保证分布式一致性的所有方法?

  • 一致性算法如 Paxos、Raft,分布式锁,分布式事务,消息队列,分布式数据存储等。

一条Java代码执行会发生什么,说得越底层越好。

  • 一条Java代码的执行包括:编译为字节码、类加载、字节码解释、即时编译(JIT)等阶段。字节码解释和JIT编译会生成本地机器码,进而被CPU执行。

觉得设计模式的意义是什么,真的必要吗?

  • 设计模式是为了解决软件设计中的常见问题而提出的一些通用解决方案。在合适的情况下,设计模式可以使代码更具可读性、可维护性和扩展性,但并不是所有情况都需要使用设计模式。

容器部分面试题

Java 容器都有哪些

  1. Collection 的子类 List、Set

  2. List 的子类 ArrayList、LinkedList等

  3. Set 的子类 HashSet、TreeSet等

  4. Map 的子类 HashMap、TreeMao等

Collecion 和 Collections 有什么区别

java.util.Collection 是一个集合的顶级接口,它提供了对集合对象进行基本操作的通用接口方法,Collection 接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口的有 List 和 Set

java.util.Collections 是一个包装类(工具类),它包含了各种有关集合操作的静态多态方法。此类不能被实例化,用于对集合中元素进行排序、搜索以及线程安全等各种操作,服务于 Java 的 Collection 框架

List、Set、Map 之间的区别

  1. List、Set 都继承 Collection 接口,而 Map 则不是
  2. List 是一个有序集合,元素可重复,可有多个NULL值。可以使用各种循环遍历集合,因为它是有序的
  3. Set 是一个无序集合,元素不可重复,重复元素会被覆盖掉(注意:元素虽然无放入顺序,但元素在 Set 中的位置是由该元素的 HashCode 决定的,其位置是固定的,加入 Set 的 Object 必须定义 equals()方法),Set 只能用迭代,因为它是无序的
  4. Map 是由一系列键值对组成的集合,提供了 key 和 value 的映射。在 Map 中保证 key 与 value 一 一对应的关系。一个 key 对应一个 value ,不能存在相同的 key,但 value 可以相同

Set 与 List 相比较

  1. Set 检索元素效率较低,删除和插入效率高,因为删除和插入不会引起元素的位置变化
  2. List 可动态增长,查找元素效率高,但是删除和插入效率低,因为删除或插入一条元素,会引起其他元素位置变化
  3. Map 适合存储键值对的数据

HashMap 和 HashTable 的区别

  • 继承关系:

    • HashTable 继承自 Dictionary 类。
    • HashMap 继承自 AbstractMap 类。
  • 线程安全性:

    • HashMap 在缺省的情况下是非Synchronize的。
    • HashTable 的方法是Synchronize的。
    • 在多线程下直接使用 HashTable 不需要自己为它的方法实现同步。但使用 HashMap 时需要手动增加同步处理,例如 Map m = Collections.synchronizeMap(hashMap);
  • contains 方法:

    • HashMap 把 HashTable 的 contains() 方法去掉了,改成了 containsValue() 和 containsKey()。
    • HashTable 则保留了 contains()、containsValue()、containsKey() 三个方法,其中 contains() 与 containsValue() 功能相同。
  • null 值:

    • HashTable 中,key 和 value 都不能为 null 值。
    • HashMap 中,null 可以作为键,但这样的键只有一个。可以有多个 value 为 null 值的键。当 get() 方法返回 null 有可能是 HashMap 中没有该键,也有可能返回的 value 为 null。因此,HashMap 使用 containsKey() 方法判断是否存在键。
  • 遍历方式:

    • HashTable 与 HashMap 都是使用 Iterator 迭代器遍历。
    • HashTable 还支持 Enumeration 的方式。
  • 哈希值计算:

    • HashTable 直接使用对象的 HashCode。
    • HashMap 重新计算哈希值,先用 hash & 0x7FFFFFFF,再对 length 取模。其中 &0x7FFFFFFF 目的是为了将负的 hash 值转化为正值,因为 hash 值有可能为负数。
  • 数组初始化和扩容方式:

    • HashTable 在不指定容量的情况下默认是 11。
    • HashMap 为默认容量 16。
    • HashTable 不要求底层数组的容量一定要是2的整数次幂,而 HashMap 底层数组则一定为 2 的整数次幂。
    • HashTable 扩容时,将容量变成原来的 2倍+1 (old * 2 + 1)。
    • HashMap 扩容时直接将容量改为原来的 2倍 (old * 2)。

如何决定使用 HashMap 还是 TreeMap

如果需要得到一个有序的 Map 集合,就应该使用 TreeMap,因为 HashMap 的排序顺序不是固定的。除此之外,在不需要使用排序的情况下,由于 HashMap 有比 TreeMap 更好的性能,使用 HashMap 会更好。

HashMap 的实现原理

HashMap 的实现原理基于以下过程:

  1. 利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标。
  2. 存储时,如果出现 hash 值相同的 key,有两种情况:
    • 如果 key 相同,则覆盖原始值。
    • 如果 key 不同(出现冲突),则将当前的 key-value 放入链表中。
  3. 获取时,直接找到 hash 值对应的下标,进一步判断 key 是否相同,从而找到对应值。

HashMap 解决 hash 冲突的核心在于使用了数组的存储方式,将冲突的 key 的对象放入链表中。一旦发现冲突,就在链表中进行进一步的对比。

HashSet 的实现原理

HashSet 实际上是一个 HashMap 实例,都是一个存放链表的数组。它不保证存储元素的迭代顺序,允许使用 null 元素。HashSet 中不允许有重复元素。

HashSet 中的 add() 方法调用的是底层 HashMap 中的 put() 方法。HashSet 判断元素是否存在的依据是 HashSet 中的元素都存放在 HashMap 的 key 上面,而 value 中的值都是统一的一个固定对象。在 HashSet 中,由于 value 值没有用,不存在修改 value 值的说法。因此,往 HashSet 中添加元素时,首先判断元素(也就是 key)是否存在,如果不存在则插入,如果存在则不插入。

判断 key 是否存在要重写元素的类的 equals() 和 hashCode() 方法。向 Set 中添加对象时,首先调用此对象所在类的 hashCode() 方法,计算此对象的哈希值,决定了此对象在 Set 中存放的位置。如果此位置没有被存储对象,则直接存储;如果已有对象,则通过对象所在类的 equals() 比较两个对象是否相同,相同则不能被添加。

ArrayList 与 LinkedList 的区别

  • ArrayList是由动态数组构成,而LinkedList是由链表构成。
  • ArrayList适用于经常查询的集合,因为LinkedList是线性存储方式,需要移动指针从前往后查找。
  • LinkedList适用于新增和删除的集合,因为ArrayList是数组构成,删除某个值会对下标产生影响,需要进行数据的移动。
  • ArrayList的自由度较低,需要手动设置固定的大小。操作方便,直接创建、添加对象、根据下标使用。
  • LinkedList的自由度较高,能够动态随数组的数据量而变化。
  • ArrayList的主要开销在List需要预留一定空间。
  • LinkedList的主要开销在需要存储节点信息以及节点指针信息。

如何实现数组与 List 之间的转换

  • List to Array: 使用List的toArray()方法,传入一个数组的类型,例如String[] strs = strList.toArray(new String[strList.size()])
  • Array to List: 使用java.util.ArraysasList()方法,例如List<String> strList = Arrays.asList(strs)

ArrayList 与 Vector 的区别

  • ArrayList是非线程安全的,而Vector使用了Synchronized来实现线程同步。
  • ArrayList在性能方面优于Vector
  • ArrayListVector都会根据实际情况动态扩容,不同的是ArrayList扩容到原大小的1.5倍,而Vector扩容到原大小的2倍。

Array 与 ArrayList 的区别

  • Array是数组,定义时必须指定数据类型和数组长度。
  • ArrayList是动态数组,长度可以动态改变,会自动扩容。在不使用泛型的情况下,可以添加不同类型的元素。

在 Queue 中 poll() 与 remove() 有什么区别

  • poll()remove()都是从队列头删除一个元素。
  • 如果队列为空,remove()方法会抛出NoSuchElementException异常,而poll()方法只会返回null

哪些集合类是线程安全的

  • Vector: 比ArrayList多了同步化机制,线程安全。
  • HashTable: 比HashMap多了线程安全。
  • ConcurrentHashMap: 高效且线程安全的集合。
  • Stack: 栈,继承于Vector,也是线程安全。

迭代器 Iterator 是什么

  • Iterator是集合专用的遍历方式。
  • Iterator<E> iterator(): 返回此集合中元素的迭代器,通过集合的iterator()方法得到,所以Iterator是依赖于集合而存在的。

Iterator 怎么使用?有什么特点

Iterator 的使用方法

  • java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象。
  • next() 方法获取集合中下一个元素。
  • hasNext() 方法检查集合中是否还有元素。
  • remove() 方法将迭代器新返回的元素删除。

Iterator 的特点

  • Iterator 遍历集合过程中不允许线程对集合元素进行修改。
  • Iterator 遍历集合过程中可以用remove()方法来移除元素,移除的元素是上一次Iterator.next()返回的元素。
  • Iteratornext() 方法是通过游标指向的形式返回Iterator下一个元素。

Iterator 与 ListIterator 有什么区别

  • 使用范围不同:
    • Iterator 适用于所有集合,SetListMap 以及这些集合的子类型,而 ListIterator 只适用于 List 及其子类型。
  • ListIteratoradd() 方法,可以向 List 中添加元素,而 Iterator 不能。
  • ListIteratorIterator 都有 hasNext()next() 方法,来实现顺序向后遍历。而 ListIteratorhasPrevious()previous() 方法,可以实现逆向遍历,但是 Iterator 不能。
  • ListIterator 可以使用 nextIndex()previousIndex() 方法定位到当前索引位置,而 Iterator 不能。
  • 它们都可以实现 remove() 删除操作,但是 ListIterator 可以使用 set() 方法实现对象修改,而 Iterator 不能。

怎么确保一个集合不能被修改

  • 可以采用 java.util.Collections 工具类:
    • Collections.unmodifiableMap(map)
    • Collections.unmodifiableList(list)
    • Collections.unmodifiableSet(set)
  • 如若修改则会报错java.lang.UnsupportedOperationException.

多线程部分面试题

并发和并行有什么区别

  • 并发: 不同的代码块交替执行。
  • 并行: 不同的代码块同时执行。

个人理解

  • 并发: 放下手头的任务A去执行另外一个任务B,执行完任务B后,再回来执行任务A。例如,吃饭时来电话了,去接电话,打完电话后又回来吃饭。
  • 并行: 执行A的同时,接受到任务B,然后一起执行。例如,吃饭时来电话了,一边吃饭一边打电话。

线程和进程的区别

  • 根本区别: 进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
  • 在操作系统中能同时运行多个进程,进程中会执行多个线程。
  • 线程是操作系统能够进行运算调度的最小单位。

守护线程是什么

  • 守护线程是 JVM 内部的实现,如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。因此,守护线程是作为辅助线程存在的。

创建线程有哪几种方式

  1. 继承 Thread 类创建线程类:

    • 定义 Thread 类的子类,并重写该类的 run() 方法。
    • 创建 Thread 子类的实例,即创建了线程对象。
    • 调用线程对象的 start() 方法来启动该线程。
  2. 实现 Runnable 接口创建线程类:

    • 创建 Runnable 接口的实现类,并重写该接口的 run() 方法。
    • 创建 Runnable 实现类的实例,并依此实例作为 Threadtarget 来创建 Thread 对象。
    • 调用线程对象的 start() 方法来启动该线程。
  3. 通过 CallableFuture 创建线程:

    • 创建 Callable 接口的实现类,并重写 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
    • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
    • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
    • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

runnable 和 callable 有什么区别

相同点:

  • 都是接口。
  • 都可以编写多线程程序。
  • 都是采用 Thread.start() 启动线程。

不同点:

  • Runnable 没有返回值,Callable 可以返回执行结果,是个泛型,和 FutureFutureTask 配合可以用来获取异步执行的结果。
  • Callable 接口的 call() 方法允许抛出异常,Runnablerun() 方法异常只能在内部消化,不能往上继续抛。
  • 注意,Callable 接口支持返回执行结果,需要调用 FutureTask.get() 得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

线程有哪些状态

线程的生命周期包括以下状态:

  1. 新建(New): 线程被创建但还未启动。
  2. 就绪(Runnable): 线程已经被创建,等待被系统调度执行。
  3. 运行(Running): 线程正在执行任务。
  4. 阻塞(Blocked): 线程被挂起,暂时停止执行。
  5. 死亡(Dead): 线程执行完任务或因异常退出。

sleep() 和 wait() 的区别

  1. sleep() 和 wait() 区别:

    • 最大区别是 sleep() 休眠时不释放同步对象锁,其他线程无法访问;而 wait() 休眠时,释放同步对象锁,其他线程可以访问。
    • sleep() 执行完后会自动恢复运行,不需要其他线程唤起;而 wait() 执行后会放弃对象的使用权,其他线程可以访问,需要其他线程调用 notify()notifyAll() 来唤醒或通知等待队列。
  2. 线程调度:

    • sleep()Thread 类的静态方法,属于线程类;而 wait()Object 类的方法,属于对象。
    • sleep() 可以在任何地方使用,而 wait() 必须在同步块或同步方法中使用。

线程的 run() 和 start() 的区别

  • start() 是用来启动线程的,调用后会执行线程的 run() 方法。
  • run() 方法只是线程类中的一个普通方法,在主线程中执行。如果直接调用 run(),并不会启动新线程,而是在当前线程中执行。

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

  • notify() 方法随机唤醒对象的等待池中的一个线程,让其进入锁池。
  • notifyAll() 唤醒对象的等待池中的所有线程,让它们进入锁池。

创建线程池有哪几种方式

  1. 利用 Executors 创建线程池:
    • newCachedThreadPool():用于处理大量短时间工作任务的线程池。
    • newFixedThreadPool(int nThreads):重用指定数目 nThreads 的线程。
    • newSingleThreadExecutor():特点是工作线程数目限制为1。
    • newSingleThreadScheduledExecutor()newScheduledThreadPool(int corePoolSize):创建 ScheduledExecutorService,可进行定时或周期性的工作调度。
    • newWorkStealingPool(int parallelism):使用 Work-Stealing 算法,并行地处理任务,不保证处理顺序。

线程池的状态

线程池的状态包括:

  • 运行(RUNNING): 线程池一旦被创建,就处于 RUNNING 状态,可以接收新任务,对已排队的任务进行处理。
  • 关闭(SHUTDOWN): 不接收新任务,但能处理已排队的任务。通过调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
  • 停止(STOP): 不接收新任务,不处理已排队的任务,并中断正在处理的任务。通过调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN) 转变为 STOP 状态。
  • 整理(TIDYING): 在 SHUTDOWN 状态下,任务数为 0,其他所有任务已终止,线程池会变为 TIDYING 状态,执行 terminated() 方法。
  • 终止(TERMINATED): 线程池彻底终止,TIDYING 状态执行完 terminated() 方法后,会由 TIDYING 转变为 TERMINATED 状态。

线程池中的 submit() 和 execute() 有什么区别

  • 这两个方法都可以向线程池提交任务。
  • execute() 方法的返回类型是

void,定义在 Executor 接口中。

  • submit() 方法可以返回持有计算结果的 Future 对象,定义在 ExecutorService 接口中。

在 Java 程序中怎么确保多线程运行安全

确保多线程运行安全可以采用以下方式:

  1. 使用 synchronized 关键字。
  2. 使用 volatile 关键字,防止指令重排,保证变量的可见性。
  3. 使用 lock 锁机制,lock()unlock()
  4. 使用线程安全的类,如 StringBufferHashTableVector 等。
  5. 线程安全问题主要涉及原子性、可见性、有序性。

synchronizedvolatile 的作用是什么?有什么区别

  • 作用:

    • synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
    • volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性,禁止指令重排序。
  • 区别:

    • synchronized 可以作用于变量、方法、对象。volatile 只能作用于变量。
    • synchronized 可以保证线程间的有序性、原子性和可见性。volatile 只保证了可见性和有序性,无法保证原子性。
    • synchronized 线程阻塞,volatile 线程不阻塞。

synchronizedLock 有什么区别

  • synchronized 是一个 Java 关键字,在 JVM 层面上,而 Lock 是一个类。
  • synchronized 以获取锁的线程执行完同步代码,释放锁;如果线程中发生异常,JVM 会让线程释放锁。而 Lock 必须在 finally 中释放锁,否则容易造成线程死锁。
  • Lock 可以查看锁的状态,而 synchronized 不能。
  • 在性能上,如果竞争资源不激烈,两者性能差不多;而当竞争资源非常激烈时,Lock 的性能要优于 synchronized。同时,Lock 是可公平锁,而 synchronized 是非公平锁。

Spring 自动装配 bean 有哪些方式

  • default:默认方式和‘no’效果一样
  • no:不自动配置,需要使用 节点或参数
  • byName:根据名称进行装配
  • byType:根据类型进行装配
  • constructor:根据构造函数进行装配

Spring 事务实现方式有哪些

  1. 编程式事务管理:对基于 POJO 的应用来说是唯一选择,需要在代码中调用 beginTransaction()commit()rollback()等事务管理相关的方法。
  2. 基于 TransactionProxyFactoryBean 的声明式事务管理。
  3. 基于 @Transactional 的声明式事务管理。
  4. 基于 Aspectj AOP 配置事务。

Spring 的事务隔离是什么

Spring 的事务隔离是指多个事务并发执行时,事务之间的隔离程度。隔离级别包括:

  • READ_UNCOMMITTED:最低的隔离级别,允许一个事务读取另一个事务未提交的数据。
  • READ_COMMITTED:保证一个事务提交后才能被另一个事务读取。
  • REPEATABLE_READ:确保一个事务在多次读取同一数据时,能够得到一致的结果,即不会出现脏读、不可重复读,但可能会出现幻读。
  • SERIALIZABLE:最高的隔离级别,完全禁止其他事务对当前事务的操作,保证事务的完整性。

Spring Mvc 的运行流程

  1. 用户发送请求至前端控制器 DispatcherServlet。
  2. DispatcherServlet 收到请求调用处理器映射器 HandlerMapping。
  3. HandlerMapping 根据请求 URL 找到具体的处理器,生成处理器执行链 HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给 DispatcherServlet。
  4. DispatcherServlet 根据处理器 Handler 获取处理器适配器 HandlerAdapter 执行 HandlerAdapter 处理一系列的操作。
  5. 执行处理器 Handler(Controller,也叫页面控制器)。
  6. Handler 执行完成返回 ModelAndView 到 HandlerAdapter。
  7. HandlerAdapter 将 Handler 执行结果 ModelAndView 返回到 DispatcherServlet。
  8. DispatcherServlet 将 ModelAndView 传给 ViewResolver 视图解析器。
  9. ViewResolver 解析后返回具体 View。
  10. DispatcherServlet 对 View 进行渲染视图,即将模型数据 model 填充至视图中。
  11. DispatcherServlet 响应用户。

Spring Mvc 有哪些组件

  1. DispatcherServlet:前端控制器
  2. HandlerMapping:处理器映射器
  3. HandlerAdapter:处理器适配器
  4. HandlerInterceptor:拦截器
  5. ViewResolver:视图解析器
  6. MultipartResolver:文件上传处理器
  7. HandlerExceptionResolver:异常处理器
  8. DataBinder:数据转换
  9. HttpMessageConverter:消息转换器
  10. FlashMapManager:页面跳转参数管理器
  11. HandlerExecutionChain:处理程序执行链
  12. RequestToViewNameTranslator:请求转视图翻译器
  13. ThemeResolver
  14. LocaleResolver:语言环境处理器

@RequestMapping 的作用是什么

@RequestMapping 是一个注解,用来标识 HTTP 请求地址与 Controller 类的方法之间的映射。它可以指定 HTTP 请求的类型,包括 GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS、TRACE。

@Autowired 与 @Resource 的区别

  • @Autowired 为 Spring 提供的注解,需要导入包 org.springframework.beans.factory.annotation.Autowired,采取的策略为按照类型注入。
  • @Resource 注解由 J2EE 提供,需要导入包 javax.annotation.Resource,默认按照名称自动注入。

Mybatis 中 #{} 和 ${} 的区别

  • #{} 表示一个占位符,可以有效防止 SQL 注入。
  • ${} 表示拼接 SQL 串,可以用于动态判断字段。

Mybatis 有几种分页方式

  1. 原始分页:取出数据后,进行手动分割。
  2. LIMIT 关键字:修改执行 SQL 语句。
  3. RowBounds 实现分页:将 PageInfo 信息封装成 RowBounds,调用 DAO 层方法。
  4. Mybatis 的 Interceptor 实现:在 SQL 语句前加上 limit 关键字,不用手动添加。

Mybatis 一级缓存和二级缓存

  • 一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其生命周期与 SQLSession 一致。存在多个 SQLSession 或分布式环境中,可能会出现脏数据。
  • 二级缓存:也是基于 PerpetualCache 的 HashMap 本地缓存,存储作用域为 Mapper 级别。可以由多个 SQLSession 共享缓存。

Mybatis 插件的实现原理

插件的基本原理是使用 MyBatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 SQL,然后重写 SQL,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

数据库的三范式是什么

  1. 第一范式(1NF):数据表中的每一列都是不可再分的原子数据项。
  2. 第二范式(2NF):数据表中的非主键字段完全依赖于主键字段。
  3. 第三范式(3NF):数据表中的非主键字段之间不存在函数依赖。

如何获取当前数据库的版本

  • 进入 MySQL 输入 select version();
  • 进入 cmd 输入 mysql -V

以下是重新排版后的内容:

ACID 是什么

ACID 是指在可靠数据库管理系统(DBMS)中,事务(transaction)应具有的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

  • 原子性: 数据库中的事务执行是作为原子,要么执行整个语句,要么不执行。
  • 一致性: 在事务开始和结束后,数据库的完整性约束没有被破坏。
  • 隔离性: 事务的执行是互不干扰的,一个事务不可能看到其他事务运行时的中间某一时刻的数据。
  • 持久性: 在事务完成后,该事务对数据库所做的更改持久保存在数据库中,并不会被回滚。

char 和 varchar 的区别

  • 长度不同:
    • char:长度设置后不可变。
    • varchar:长度可动态改变。
  • 效率不同:
    • char:每次修改的数据长度相同,效率更高。
    • varchar:每次修改的数据长度不同,效率更低。
  • 存储不同:
    • char:存储时初始预计字符串再加上一个记录字符串长度的字节,占用空间较大。
    • varchar:存储时是实际字符串再加上一个记录字符串长度的字节,占用空间较小。

float 和 double 的区别

  • 在数据库中的所有计算都使用双精度完成,使用 float(单精度)会有误差,可能导致意想不到的结果。
  • MySQL浮点型和定点型可以用类型名称后加(M,D)来表示,其中 M 表示该值的总长度,D 表示小数点后面的长度。
  • FLOATDOUBLE 在不指定精度时,默认按照实际的精度来显示,而 DECIMAL 在不指定精度时,默认整数为 10,小数为 0。

MySQL 内连接、左连接、右连接有什么区别

  • 内连接 (INNER JOIN ... ON): 只显示两表中有关联的数据。
  • 左连接 (LEFT JOIN ... ON): 显示左表所有数据,右表没有对应的数据用 NULL 补齐,多了的数据删除。
  • 右连接 (RIGHT JOIN ... ON): 显示右表所有数据,左表没有对应的数据用 NULL 对齐,多了的数据删除。

MySQL 的索引是怎么实现的

  • 由于B+Tree数据结构的优势,目前 MySQL 基本都采用 B+Tree 方式实现索引。
  • MySQL 索引实现的数据结构:两种存储引擎都使用 B+Tree(B-Tree的变种)作为索引结构。
  • 在 MyISAM 中,索引叶子节点存放的是数据的地址;在 InnoDB 中,主键索引叶子节点存放的是数据本身,辅助索引叶子节点上存放的是主键值。

MySQL 索引设计原则

  • 适合索引的列是出现在 WHERE 子句中的列,或者连接子句中指定的列。
  • 基数较小的列,索引效果较差,没有必要在此列建立索引。
  • 使用短索引,对长字符串列进行索引时,应指定一个前缀长度以节省索引空间。
  • 不要过度索引,因为索引会增加磁盘空间需求,并降低写操作性能。

如何验证 MySQL 的索引是否满足需求

  • 使用 EXPLAIN 函数验证索引是否有效。

事务的隔离级别

  • Read Uncommitted(读未提交): 最低级别。
  • Read Committed(读已提交): 读已提交,可避免脏读情况发生。
  • Repeatable Read(可重复读): 确保事务可以多次从一个字段中读取相同的值,可以避免脏读和不可重复读,但仍会出现幻读问题。
  • Serializable(串行化): 最严格的事务隔离级别,要求所有事务被串行执行,可避免脏读、不可重复读、幻读情况的发生。

MySQL 常用的引擎

  • InnoDBMyISAM 都使用 B+Tree 存储数据。
  • InnoDB 支持事务,且支持四种隔离级别(读未提交、读已提交、可重复读、串行化),默认的为可重复读。
  • MyISAM 只支持表锁,不支持事务,但由于有单独的索引文件,在读取数据方面性能较高。

MySQL 的行锁、表锁、页锁

  • 行级锁: 锁定粒度最细,针对当前操作的行进行加锁,分为共享锁和排他锁。开销大,加锁慢,但锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 表级锁: 锁定粒度最大,对整张表加锁,分为表共享读锁和表独占写锁。开销小,加锁快,但锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 页级锁: 锁定粒度介于行级锁和表级锁之间,一次锁定相邻的一组记录。开销和加锁时间界于表锁和行锁之间,会出现死锁,锁定粒度一般。 MyISAM 和 MEMORY 采用表级锁,BDB 采用页面锁或表级锁(默认为页面锁),InnoDB 支持行级锁和表级锁,默认为行级锁。

以下是重新排版后的内容:

乐观锁和悲观锁

乐观锁:

  • 认为数据不会经常冲突,检测冲突在数据提交时进行。
  • 常见实现方式有版本号控制和时间戳控制。

悲观锁:

  • 对数据被外界修改持保守态度,整个数据处理过程中将数据处于锁定状态。
  • 实现通常依赖数据库提供的锁机制,例如 SELECT ... FOR UPDATE

MySQL 问题排查手段

  1. 使用 SHOW PROCESSLIST 命令查看当前所有连接信息。
  2. 使用 EXPLAIN 命令查询 SQL 语句执行计划。
  3. 开启慢查询日志,查看慢查询的 SQL。

MySQL 性能优化

  1. 创建索引,避免全盘扫描,尤其在 WHEREORDER BY 涉及的列上创建索引。
  2. 避免在索引上使用计算,注意 IN 关键字不走索引。
  3. 使用预编译防止 SQL 注入。
  4. 将多条 SQL 语句压缩到一条中,避免使用 *,注意不要在 WHERE 条件中判断 NULL
  5. 尽量不使用前置百分比的 LIKE
  6. 对于连续的数值,使用 BETWEEN 而不是 IN
  7. 选择正确的存储引擎。
  8. 考虑垂直/水平分割、分库分表、读写分离。

Redis 面试题

Redis 是什么?

  • Redis是一个完全开源、用C语言编写的高性能(key/value)分布式内存数据库。
  • 支持持久化,提供多种数据结构的存储。

Redis 的优点和使用场景

优点:

  • 支持数据持久化。
  • 提供多种数据结构的存储。
  • 支持数据备份,如master-slave模式。

使用场景:

  • 内存存储和持久化。
  • 取最新N个数据的操作。
  • 发布、订阅消息系统。
  • 定时器、计数器等。

为什么 Redis 是单线程的?

  • Redis基于内存操作,CPU不是瓶颈。
  • 单线程实现简单,且不会使CPU成为瓶颈。

Redis 的缓存预热是什么,如何解决缓存雪崩和缓存穿透?

缓存预热:

  • 在项目启动前访问可能的数据,加载到缓存中。

缓存雪崩解决方案:

  • Redis高可用,搭建集群。
  • 限流降级,每个key只能一个线程查询,其他线程等待。
  • 数据预热,提前加载缓存。

缓存穿透解决方案:

  • 缓存空对象,设置过期时间。
  • 布隆过滤器。
  • 数据预热。

Redis 支持的数据类型和Java客户端

  • 数据类型:String、List、Set、Hash、ZSet。
  • Java客户端:Redisson、Jedis、Lettuce。

Jedis 与 Redisson 的区别

  • Jedis是简单的Redis客户端,Redisson封装了更多数据结构和功能。
  • Redisson相对更大更灵活,Jedis更原生和灵活。

如何保证缓存与数据库数据一致性?

  • 对删除缓存进行重试。
  • 定期全量更新缓存。
  • 给所有缓存设置失效期。

Redis 持久化方式

  1. 快照方式(RDB):

    • 内存数据定期写入磁盘。
  2. 文件追加方式(AOF):

    • 记录所有操作命令,以文本形式追加到文件。
  3. 混合持久化方式:

    • 结合了RDB和AOF,先写入RDB形式,再追加AOF形式。提高Redis重启速度,减小数据丢失风险。

以下是重新排版后的内容:

Redis 怎么实现分布式锁

SET key value [EX seconds] [PX milliseconds] [NX|XX]
# EX second: 设置键的过期时间为second秒
# PX millisecond: 设置键的过期时间为millisecond毫秒
# NX: 只在键不存在时,才对键进行设置操作
# XX: 只在键已经存在时,才对键进行设置操作
# SET操作成功完成时,返回OK,否则返回nil

Redis 分布式锁的缺陷

  1. 死锁:

    • 解决:设置锁的过期时间,保证setNx和设置过期时间的操作原子性。
  2. 错位解锁:

    • 解决:加锁时记录当前线程ID,解锁时判断ID是否一致,使用Lua脚本保持原子性。
  3. 业务并发执行问题:

    • 解决:加锁成功后开启守护线程,临近过期时续时,重复此步骤直到业务完成。

Redis 如何做内存优化

  1. 缩减键值对象:

    • 保持业务要求下,使key越短越好,对value适当压缩。
  2. 共享对象池:

    • Redis内部维护[0-9999]的整数对象池,尽量使用整数对象以节省内存。
  3. 尽可能使用散列表(hashes)。

  4. 编码优化,控制编码类型。

  5. 控制key的数量。

Redis 淘汰策略有哪些

  1. noeviction: 不删除策略,达到最大内存限制时,如果需要更多内存,直接返回错误信息。
  2. allkeys-lru: 所有key通用;优先删除最近最少使用(LRU)的key。
  3. volatile-lru: 只限于设置了expire的部分;优先删除LRU的key。
  4. allkeys-random: 所有key通用;随机删除一部分key。
  5. volatile-random: 只限于设置了expire的部分;随机删除一部分key。
  6. volatile-ttl: 只限于设置了expire的部分;优先删除剩余时间(TTL)短的key。

Redis 常见问题及解决

缓存和数据库双写一致性问题

  • 解决:采取正确的更新策略,先更新数据库,再删缓存,并提供补偿措施,如利用消息队列。

缓存穿透问题

  • 解决:利用互斥锁,在缓存失效时先获取锁,请求数据库。采用异步更新策略,进行缓存预热。

缓存雪崩问题

  • 解决:给缓存失效时间加上随机值,使用互斥锁,或者双缓存策略。

缓存的并发竞争问题

  • 解决:根据需求使用分布式锁,保持业务的灵活变通。

RabbitMQ 部分面试题

RabbitMQ 使用场景

  • 多个应用之间的耦合。
  • 跨系统的异步通信。
  • 流量削峰,如注册用户、发送激活邮件、订单下单等。

RabbitMQ 的重要角色

  1. 生产者: 消息的创建者,负责创建和推送数据到消息服务器。
  2. 消费者: 消息的接收方,负责处理数据和确认消息。
  3. 代理: RabbitMQ本身,不生产不消费,只是消息的快递。

RabbitMQ 的重要组件

  1. ConnectionFactory(连接管理器): 应用程序与Rabbit之间建立连接的管理器。
  2. Channel(信道): 消息推送使用的通道。
  3. Exchange(交换器): 用于接受、分配消息。
  4. Queue(队列): 用于存储生产者的消息。
  5. RoutingKey(路由键): 将生成者的数据分配到交换器上。
  6. BindingKey(绑定键): 将交换器的消息绑定到队列上。

RabbitMQ 的消息存储方式

RabbitMQ对于queue中的message的保存方式有两种:discramdisc方式将消息存储到磁盘,而ram方式存储在内存中。

RabbitMQ 中 vhost 的作用

  • vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的队列、绑定、交换器和权限控制。
  • 在全局角度,vhost可以作为不同权限隔离的手段,例如不同应用可以跑在不同的vhost中。

RabbitMQ 的消息发送过程

  1. 生产者将消息通过channel发送到Exchange。
  2. Exchange通过绑定的routing key选择Queue。
  3. 消费者监听到Queue上有新的消息,消费该消息。

RabbitMQ 如何保证消息的稳定性

  • 提供了事务的功能,通过将channel设置为confirm(确认模式)。

RabbitMQ 如何避免丢失消息

  1. 消息持久化。
  2. 消费端的ack签收机制。
  3. 设置集群镜像模式。
  4. 消息补偿机制。

RabbitMQ 持久化的缺点

  • 持久化的缺点是降低了服务器的吞吐量,因为使用磁盘而非内存存储,可以使用SSD硬盘来缓解。

RabbitMQ 的广播方式

  • fanout广播模式:所有bind到此exchange的queue都可以接收消息。
  • direct直接交换:通过routingKey和exchange决定的那个唯一的queue可以接收消息。
  • topic通配符模式:所有符合routingKey的routingKey所bind的queue可以接收消息。

RabbitMQ 如何实现延迟消息队列

  • 通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能。
  • 使用RabbitMQ-delayed-message-exchange插件实现延迟功能。

RabbitMQ 集群的作用

  • 高可用:某个服务器出现问题,整个RabbitMQ仍可以继续使用。
  • 高容量:集群可以承载更多的消息量。

RabbitMQ 节点的类型

  1. 磁盘节点: 消息存储到磁盘,重启后消息仍在。
  2. 内存节点: 消息存储在内存中,重启服务器消息丢失,性能高于磁盘类型。

RabbitMQ 集群搭建注意事项

  1. 各节点之间使用–link连接。
  2. 各节点使用相同的erlang cookie值,用于节点认证。
  3. 必须包含一个磁盘节点。

RabbitMQ 每个节点是否是其他节点的完整拷贝

  • 不是。每个节点并不拥有所有队列的完全拷贝,避免增加冗余数据。

RabbitMQ 唯一一个磁盘节点崩溃的影响

  • 集群可以保持运行,但无法更改任何配置、添加用户、添加和删除集群节点等。

RabbitMQ 集群停止顺序要求

  • 先关闭内存节点,最后关闭磁盘节点,确保顺序正确以防消息丢失。

JVM 部分面试题

JVM 主要组成部分及作用

  1. 类加载器(Class Loader): 加载类文件到内存。
  2. 执行引擎(Execution Engine): 解释命令,交由操作系统执行。
  3. 本地库接口(Native Interface): 融合不同语言为Java所用。
  4. 运行时数据区(Runtime Data Area): 包括方法区、堆、栈等。

JVM 常见问题及解决

  1. 内存溢出(OutOfMemoryError):

    • 解决:增大堆内存、优化代码、检查是否存在内存泄漏。
  2. 栈溢出(StackOverflowError):

    • 解决:增大栈内存、优化递归算法。
  3. 方法区溢出:

    • 解决:增大方法区内存、减少类的加载。
  4. 死锁:

    • 解决:分析死锁原因,合理设计锁的获取顺序。
  5. 高CPU占用:

    • 解决:优化代码、检查是否存在无限循环。
  6. Full GC频繁:

    • 解决:调整堆大小、优化GC算法。
  7. 类加载问题:

    • 解决:检查类路径、冲突等问题。
  8. JVM调优:

    • 解决:调整堆内存大小、设置垃圾回收器、分析内存使用情况。
  9. PermGen空间溢出:

    • 解决:升级JVM版本、使用元空间替代。
  10. 线程问题:

    • 解决:分析线程堆栈信息、合理设计线程池。

JVM 运行时数据区是什么

  • :Java对象的存储区域。任何使用new分配的Java对象实例和数组都在堆上分配。Java堆可通过-Xms-Xmx进行内存控制。

  • 常量池:运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放。JDK 1.7以后,运行时常量池从方法区移到了堆上。

  • 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码等数据。

  • 虚拟机栈:虚拟机栈中执行每个方法时,都会创建一个栈桢用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 本地方法栈:与虚拟机栈发挥的作用相似。相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务。执行每个本地方法时,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  • 程序计数器:指示Java虚拟机下一条需要执行的字节码指令。

堆和栈的区别

  • 栈内存用来存储局部变量和方法调用。

  • 堆内存用来存储Java中的对象。无论是成员变量、局部变量还是类变量,它们指向的对象都存储在堆内存中。

  • 栈内存归属单个线程,一个栈对应一个线程。储存的变量只能在该线程中访问,也可以理解为私有内存。

  • 堆内存中的对象所有线程均可见,堆内存中对象可以被所有线程访问。

  • 栈内存要远小于堆内存。

队列和栈是什么?有什么区别

  • 队列和栈是两种不同的数据结构。

  • 队列:插入称为入队,删除称为出队。在队尾入队、队头出队。两边都可操作。先进先出(FIFO)。

  • :插入称为进栈,删除称为出栈。在栈顶进行操作。后进先出(LIFO)。

类加载器有哪些?什么是双亲委派模型

  • 启动类加载器(Bootstrap ClassLoader):加载<JAVA_HOME>\lib目录下核心库。

  • 扩展类加载器(Extension ClassLoader):加载<JAVA_HOME>\lib\ext目录下扩展包。

  • 应用程序类加载器(Application ClassLoader):加载用户路径(classpath)上指定的类库。

双亲委派的意思是,如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成。每一层都是如此,一直递归到顶层。当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实指的是父类,没有mother。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。

类加载的执行过程

  1. 加载:指将class字节码文件从各个来源通过类加载器装载入内存中。

    • 字节码来源:从本地路径下编译生成的.class文件、从JAR包中的.class文件、从远程网络以及动态代理实时编

译类加载器。包括启动类加载器、扩展类加载器、应用类加载器以及用户的自定义类加载器。

  1. 链接

    • 验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误,包括对文件格式的验证。

    • 准备:为类变量(注意,不是实例变量)分配内存,并赋予初值。初值不是代码中具体写的初始化值,而是Java虚拟机根据不同变量类型的默认初始值:8种基本类型的初值默认为0,引用类型的初值为null。

    • 解析:将常量池内的符号引用替换为直接引用。在解析阶段,虚拟机会把所有的类名、方法名、字段名这些符号引用替换为具体的内存地址或偏移量,即直接引用。

  2. 初始化:对类变量初始化,执行类构造器的过程。

如何判断对象是否可以收回

  • 引用计数算法

    • 判断对象的引用数量。

    • 通过判断对象的引用数量来决定对象是否可以被回收。

    • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。

    • 任何引用计数为0的对象实例可以被当作垃圾收集。

    • 优点:执行效率高,程序执行受影响较小;缺点:无法检测出循环引用的情况,导致内存泄漏。

  • 可达性分析算法

    • 通过判断对象的引用链是否可达来决定对象是否可以被回收。

    • 可以作为GC Root对象的对象有:虚拟机栈中引用的对象(栈帧中的本地变量表)、方法区中的常量引用对象、方法区中类静态属性引用对象、本地方法栈中JNI(Native方法)的引用对象、活跃线程中的引用对象。

Java中有哪些引用类型

  • 强引用(strong reference):在程序代码中普遍存在,类似Object obj = new Object()这类的引用。只要强引用还存在,垃圾收集器永远不会回收被引用的对象实例。

  • 软引用(soft reference):用来描述一些还有用但非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

  • 弱引用(weak reference):用来描述非必需对象,强度比软引用更弱。被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

  • 虚引用(phantom reference):也称为幽灵引用或幻影引用,是最弱的引用关系。一个对象实例是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

JVM 中垃圾回收算法

标记-清除算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记。标记完毕后,再扫描整个空间中未被标记的对象,进行回收。该算法不需要进行对象的移动,只需对不存活的对象进行处理。在存活对象比较多的情况下,极为高效。然而,由于直接回收不存活的对象,可能导致内存碎片。

复制算法

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它将堆分成一个对象面和多个空闲面。程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集合中扫描活动对象,将每个活动对象复制到空闲面,使得活动对象所占的内存之间没有空闲洞。这样空闲面变成了对象面,原来的对象面变成了空闲面。

标记-整理(压缩)算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同。在回收不存活的对象占用的空间后,该算法将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但解决了内存碎片的问题。

JVM 有哪些垃圾回收器

新生代收集器

  • Serial
  • ParNew
  • Parallel Scavenge

老年代收集器

  • CMS
  • Serial Old
  • Parallel Old

整堆收集器

  • G1

介绍一下 CMS 垃圾回收器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它基于标记-清除算法实现,并发收集,具有低停顿的特点。适用于注重服务的响应速度,希望系统停顿时间最短的场景,如web程序、b/s服务。

CMS收集器的运行过程分为以下4步:

  1. 初始标记:标记GC Roots能直接到达的对象,速度很快但仍存在Stop The World问题。
  2. 并发标记:进行GC Roots Tracing的过程,找出存活对象且用户线程可并发执行。
  3. 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
  4. 并发清除:对标记的对象进行清除回收。

CMS收集器的内存回收过程是与用户线程一起并发执行的。

缺点:

  • 对CPU资源敏感。
  • 无法处理浮动垃圾,可能导致Concurrent Model Failure失败而导致另一次Full GC的产生。
  • 存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。

新生代垃圾回收器和老生代垃圾回收器有哪些?有什么区别

新生代收集器

  • Serial
  • ParNew
  • Parallel Scavenge

老年代收集器

  • CMS
  • Serial Old
  • Parallel Old

区别:

  • 新生代垃圾回收器一般采用复制算法,效率高,但内存利用率低。
  • 老年代垃圾回收器一般采用标记-整理的算法,用于垃圾回收。

简述分代垃圾回收器是怎么工作的

分代回收器有两个分区:老生代和新生代。新生代使用复制算法,分为Eden、To Survivor、From Survivor三个分区。对象在Eden分区分配,当对象满了后,基于copying算法将活动对象复制到To Survivor区,清空Eden和From Survivor区,然后交换From Survivor和To Survivor分区。每次移动都使对象年龄加1,当年龄达到一定值(默认15)时,升级为老生代。

老生代当空间占用达到某个值时,触发全局垃圾收回,使用标记-整理算法进行垃圾回收。整个过程循环往复,构成分代垃圾回收的整体执行流程。

JVM调优的工具有哪些

  • jconsole:基于JMX的GUI性能监测工具。
  • VisualVM:提供可视界面,用于查看Java虚拟机上运行的Java应用程序的详细信息。
  • MAT(Memory Analyzer Tool):基于Eclipse的内存分析工具,用于查找内存泄漏和减少内存消耗。
  • GChisto:分析gc日志的工具,通过gc日志分析Minor GC、Full GC的时间、频率等。
  • gcviewer:可视化查看由Sun/Oracle、IBM、HP和BEA Java虚拟机产生的垃圾收集器的日志。

JVM调优的参数有哪些

(待完善,因篇幅限制)

Api接口如何实现?

在类中使用implements关键字实现Api接口。

MySQL链接数据库常用的几

种方式

  1. Mybatis框架
  2. Hibernate框架
  3. JDBC技术
  4. c3p0连接池
  5. dbcp连接池

SpringBoot如何集成Redis

pom.xml文件引入Redis依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application配置文件中书写Redis配置:

spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
# spring.redis.password=

SpringCloud的优点

  1. 服务之间采用轻量级通讯,如Restful。
  2. 精准的制定优化服务方案,提高系统的可维护性。
  3. 服务之间拆分细致,资源可重复利用,提高开发效率。

SpringCloud用了哪些组件

  1. Netflix Eureka 注册中心
  2. Netflix Ribbon 负载均衡
  3. Netflix Zuul 网关
  4. Netflix Hystrix 熔断器
  5. Feign 服务调用

List和Set的区别

  • List允许有多个重复对象,而Set不允许有重复对象。
  • List允许有多个NULL值,而Set只允许有一个NULL值。
  • List是一个有序的容器,输出顺序即是输入顺序。
  • Set是一个无序的容器,无法保证每个元素的顺序,但可以用TreeSet通过Comparator或Comparable维护排序顺序。
  • List的实现类有ArrayList、LinkedList、Vector。
  • Set的实现类有HashSet、TreeSet、LinkedHashSet。

Java中static的作用

static表示全局或静态,用来修饰成员变量和成员方法。它可以形成静态代码块,实现不用实例化就可以使用被public static修饰的变量或方法。

什么是单例模式?

单例模式保证整个项目中一个类只有一个对象的实例。它的好处包括节省公共资源和方便控制。要保证是单例模式,需要构造私有化、以静态方法返回实例,确保对象实例只有一个。

单例模式有哪几种?

  1. 饿汉模式:在类加载时就创建好对象,需要时直接使用。
  2. 懒汉模式:等需要时再创建对象,后续不再创建。

SpringBoot常用的几个注解

  1. @SpringBootApplication:SpringBoot的核心注解,用于标识启动类。
  2. @EnableAutoConfiguration:开启SpringBoot自动配置。
  3. @RestController:在控制层,是@ResponseBody@Controller注解的合集。
  4. @RequestMapping:处理请求地址映射的注解。
  5. @RequestParam:获取URL上传过来的参数。
  6. @Configuration:声明配置类。
  7. @Component:通用注解。
  8. @Service:业务逻辑层。

Java八大数据类型

  1. char:字符型

  2. byte:字节型

  3. boolean:布尔型

  4. float:单浮点型

  5. double:双浮点型

  6. int:整数型

  7. short:短整数型

  8. long:长整数型

MySQL分页和升序降序如何实现

分页使用limit,升序使用order by xx asc,降序使用order by xx desc

例子:

-- 分页
select name, age, sex from t_student limit 0, 5;

-- 升序
select name, age, sex from t_student order by age asc;

-- 降序
select name, age, sex from t_student order by age desc;

Maven是干什么的,它有什么好处?

Maven是专门构建和管理Java项目的工具。它的好处在于可以将项目过程规范化、自动化、高效化以及具有强大的可扩展性。

MySQL如何添加索引?

  1. 主键索引:PRIMARY KEY
  2. 普通索引:INDEX,使用ALTER TABLE table_name ADD INDEX index_name ( column )
  3. 唯一索引:UNIQUE INDEX,使用ALTER TABLE table_name ADD UNIQUE index_name ( column )
  4. 全文索引:FULLTEXT INDEX,使用ALTER TABLE table_name ADD FULLTEXT ( column)
  5. 多列索引:INDEX,使用ALTER TABLE table_name ADD INDEX index_name ( column1, column2, column3 )

MySQL索引的实现方式

MySQL索引底层的实现方式是B+Tree,具体可查看B+Tree的实现方式。

Vue的数据双向绑定原理

Vue的数据双向绑定原理是使用v-model属性,利用了Object.defineProperty()方法重新定义了对象获取属性值(get)和设置属性值(set)的操作。

ActiveMQ的消息存储方式

ActiveMQ采取先进先出模式,同一时间,消息只会发送给某一个消费者,只有当该消息被消费并告知已收到时,它才能在代理的存储中被删除。对于持久性订阅,每个消费者都会获取消息的拷贝。为了节约空间,代理的存储介质中只存储了一份消息,存储介质的持久订阅对象为其以后的被存储的消息维护了一个指针,消费者消费时,从存储介质中复制一个消息。消息被所有订阅者获取后才能删除。

KahaDB消息存储

基于文件的消息存储机制,为了提高消息存储的可靠性和可恢复性,它整合了一个事务日志.KahaDB拥有高性能和可扩展性等特点.由于KahaDB使用的是基于文件的存储,所以不需要使用第三方数据库