侧边栏壁纸
  • 累计撰写 30 篇文章
  • 累计创建 6 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

2025年Java最新面试题总结之第二章、深入技术栈

Administrator
2025-02-21 / 0 评论 / 0 点赞 / 35 阅读 / 25239 字 / 正在检测是否收录...
温馨提示:
本文最后更新于2025-02-21,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

第二章、深入技术栈

2.1)JVM 运行机制等问题

1、JVM是什么 包含哪些模块

①JVM是Java虚拟机,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言非常重要的特点跨平台性就是通过jvm实现,Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

整个JVM 框架由加载器加载文件,然后执行器在内存中处理数据,通过本地接口与异构系统交互

②JVM包括

1、类加载器(Class Loader )类加载器的作用是加载类文件到内存

2、执行器(Execution Engine) 执行引擎执行引擎也叫做解释器,负责解释命令,提交操作系统执行。

3、本地接口(Native Interface )作用是融合不同的编程语言为Java 所用

4、运行时数据区(Runtime data area) 我们所有写的程序都被加载到这里之后才开始运行。包括:

(1)线程共享的堆(含有字符串常量池),元空间(元空间在本地内存中,包含了加载的类信息和运行时常量池)

(2)线程私有的虚拟机栈、本地方法栈以及程序计数器

2、JVM 内存区域(运行时数据区)

①栈是运行时的单位 , 是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。所有的 Java 方法调用都是通过栈来实现的,方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

②Java 堆是所有线程共享的最大的一块内存区域,唯一目的就是存放对象实例。里面包含字符串常量池(针对字符串专门开辟的一块区域,主要目的是为了避免字符串的重复创建。)。因为Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆。通过垃圾回收算法 Java 堆还可以细分为:新生代和老年代。

③方法区(元空间)

方法区是被所有线程共享,方法区会存储已被虚拟机加载的 类信息、字段信息等。JDK1.8 最大的变化就是方法区的位置转移到了本地内存中被称为元空间。元空间中还包含运行时常量池:各种字面量和符号引用的常量池表 。

④程序计数器

线程私有的,是一块很小的内存空间,每个线程都有一个程序计数器,代码的流程控制,如分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

⑤本地方法栈

本地方法栈为JVM调用native方法时服务,一个Native方法就是一个java调用非java代码的接口。

PS:直接内存在本地内存中,不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现

3、垃圾收集器之G1 收集器

①垃圾回收器通常是作为一个单独的低级别的线程运行,通过垃圾回收算法对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

②JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。使用标记整理算法主要有初始标记,并发标记,最终标记,筛选回收四个过程。G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率。

4.1、内存中死亡对象判断方法

①引用计数法

每当有一个地方引用它,计数器就加 1;

当引用失效,计数器就减 1;

任何时候计数器为 0 的对象就是不可能再被使用的。

②可达性分析算法

以 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

引用的概念:

引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

1、强引用:最普遍的引用如String s = new String(“aaa”)。如果一个对象具有强引用,垃圾回收器绝不会回收它

2、软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

3、弱引用:的对象拥有更短暂的生命周期。一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

4、虚引用:虚引用并不会决定对象的生命周期。是一种形同虚设的引用,随时会被回收。

4.2、新老年代的含义和晋升逻辑

①长期存活的对象将进入老年代

对象首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC (新生代GC)后仍然能够存活年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。

②大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

③新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,也比较快。

老年代 GC(Major GC/Full GC):指发生在老年代的 GC,Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

4.3、垃圾回收算法

①标记清除算法

标记阶段,标记所有的可访问对象。

收集阶段,垃圾收集算法扫描堆并回收所有的未标记对象。

② 标记整理算法

标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

③复制算法

将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除。特点:不会产生空间碎片;内存使用率极低

④分代收集算法

不同的对象的生命周期是不一样的。分代回收把不同生命周期的对象 放在不同代上,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,老年代中因为对象的存活率极高,所以采用标记清理或者标记整理算法进行回收。

5、常用 JVM 调优手段

1、内存分配:根据应用程序的需求调整JVM的内存分配。如果应用程序需要处理大量数据,则可以增加JVM的堆内存和非堆内存。

2、垃圾回收:JVM的垃圾回收是一项重要的任务,它可以释放无用的对象并回收内存。通过选择不同的垃圾回收器、调整垃圾回收器的参数和设置合适的内存阈值等,可以提高垃圾回收的效率。

3、线程管理:JVM中的线程是Java应用程序的核心组成部分。通过调整线程的数量、设置线程的优先级和使用线程池等方式,可以提高Java应用程序的并发性能。

4、类加载:Java应用程序需要在运行时动态加载类。通过优化类的加载过程,可以提高应用程序的启动速度和响应性能。

5、编译优化:JIT编译器可以将Java代码编译为本机代码,从而提高应用程序的执行速度。通过设置JIT编译器的参数和选择适当的编译器,可以提高编译器的性能和效率。

6、I/O优化:I/O操作是Java应用程序中常见的性能瓶颈之一。通过使用缓冲区、选择合适的I/O库、减少I/O操作次数等方式,可以提高I/O操作的效率。

6、类的加载

JVM中类的装载是由类加载器和它的子类来实现的,类加载器负责在运行时查找和装入类文件中的类。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。

1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;

2)如果类中存在初始化语句,就依次执行这些初始化语句。

7、主内存和本地内存

主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)

本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

8、类什么时候被初始化?

1)创建类的实例,也就是 new 一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4) 反 射 (Class.forName(“com.lyj.load”))

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM 启动时标明的启动类,即文件名和类名相同的那个类只有这 6 中情况才会导致类的类的初始化。

类的初始化步骤:

1)如果这个类还没有被加载和链接,那先进行加载和链接

2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)

3)加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句。

9、内存溢出内存泄露的区别

在 Java 中,内存溢出(Out of Memory,OOM)和内存泄露(Memory Leak)是两个不同但又相互关联的概念,下面为你详细介绍它们的区别:

定义

  • 内存溢出

    • 指程序在申请内存时,没有足够的内存空间供其使用,从而导致程序抛出内存溢出异常。简单来说,就是程序所需要的内存超出了系统所能提供的最大内存限制。

  • 内存泄露

    • 是指程序中一些对象不再被使用,但由于某些原因,垃圾回收器无法将它们从内存中回收,这些无用对象一直占用着内存,随着时间的推移,可用内存会逐渐减少,最终可能导致内存溢出。

产生原因

  • 内存溢出

    • 对象创建过多:程序中创建了大量的对象,且这些对象长时间存活,导致堆内存被耗尽。例如,在一个循环中不断创建新的对象而不释放。

    • 内存泄漏积累:长期存在的内存泄漏会逐渐消耗可用内存,最终引发内存溢出。

    • 数据量过大:处理的数据量超过了系统内存的承受能力,如一次性读取大文件到内存中。

    • 配置不合理:JVM 的堆内存设置过小,无法满足程序运行的需求。

  • 内存泄露

    • 静态集合类持有对象引用:静态集合类(如 static Liststatic Map 等)中的对象引用不会被垃圾回收,即使这些对象已经不再使用。

    • 未关闭的资源:像数据库连接、网络连接、文件流等资源,如果在使用完后没有正确关闭,会导致这些资源对象一直被占用,无法被回收。

    • 内部类持有外部类引用:非静态内部类会隐式持有外部类的引用,如果内部类的生命周期过长,会导致外部类对象无法被回收。

    • 缓存使用不当:缓存中的对象如果没有合理的过期机制,会一直占用内存。

10、说一下双亲委派机制

双亲委派机制是 Java 类加载器的一种工作模式,它在 Java 的类加载过程中起着关键作用。

定义:双亲委派机制是指当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,直到请求最终到达顶层的启动类加载器。只有当父类加载器无法完成该加载请求(在它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载该类。

2.2)数据库事务、索引, 优化 SQL 等

1.1、隔离级别

读未提交 : 可以看到其他事务没有提交的结果。等于没有任何隔离性。

读已提交 :只能看到其他事务已经提交的改变。这种隔离级别会引起不可重复读。

可重复读: 它保证了可重复读取同样的数据,即同一个事务多次读取操作数据会看到同样的数据。解决了不可重复读但可能造成幻读。

可串行化 : 通过强制事务排序,解决了幻读的问题。它在每个读的数据行上面加上共享锁。(实际中基本不使用)

不同隔离级别可能导致的问题

1.2、脏读幻读,不可重复读和幻读的区别

脏读:当一个事务读取到另外一个事务修改但未提交的数据时,就可能发生脏读。

不可重复读:就是说,比如在A事务中进行多次相同的查询,B事务在A事务多次查询之间修改对应表中的数据,导致A事务多次读取的结果不一致。

幻读是’‘不可重复读’'的一种特殊场景:例子:事务一将表中性别列的值都更改为1,事务二又添加了一条"性别”的值为0记录,事务一再查询所有的记录时会发现有多了一条记录的“性别为0,这种情况就是所谓的幻读

不可重复读和幻读区别

不可重复读:一个事务中多次读取同一数据,但是由于其他事务的修改,导致两次读取的结果不一致。

幻读:一个事务中多次读取同一范围内的数据,但是由于其他事务的插入操作,导致两次读取的结果不一致。

2、事务的特性

原子性

事务要么全部提交成功,要么全部失败回滚,不能分割执行其中的操作。

一致性

事务的执行不会破坏数据关系的完整性和业务逻辑的完整性。

隔离性

一个事务不会被另一个事务影响,第一个事务执行完成后再执行另一个事务,但处于性能上的考虑,一般都需要事务并发执行,所以隔离程度有区别。

持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

3、数据库批处理有了解吗

批处理(batch) 操作数据库,批处理指的是一次操作中执行多条SQL语句。

第一步:打开mysql的批处理:

如: url=jdbc:mysql://127.0.0.1:3306/db5?characterEncoding=UTF-8&rewriteBatchedStatements=true

第二步:使用Statement接口或者Preparedstatement接口

保存将执行的SQL语句,执行保存的SQL语句

增加批处理:addBatch​(String sql) 将要执行的SQL先保存在当前命令列表中,先不执行

执行批处理:int[] executeBatch() 执行SQL语句,将批处理中所有SQL语句执行,

返回一个数组,这个数组是说明每条命令所影响的行数

清空批处理:clearBatch() : 清空当前批处理的语句

4.1、数据库优化

数据库参数配置优化

很多配置默认参数(如最大连接数、数据库占用的内存等),不是最佳的配置,例如最大连接数默认为100,当请求超过100时都要等待。

分库分表

后面第五题有讲

优化表结构的设计

包括字段数据类型,比如人的年龄用无符号的unsigned tinyint即可,没必要用integer。数据类型的长度比如用户的手机号11位长度,没必要用255个长度。有外键约束会影响插入和删除性能,如果程序能够保证数据的完整性,那在设计数据库时就去掉外键。

SQL语句的优化

其中最重要的方式就是使用索引,还有#{}防止sql注入

事务优化

避免大事务,操作的数据比较多的事务会带来风险:锁定太多的数据,造成大量的阻塞和锁超时,回滚时所需时间比较长,执行时间长容易造成主从延迟

服务器层面

主从复制,读写分离: 一台MySQL服务器同一时间点支持的并发数是有限的,所以增加MySQL服务器的数量也是一种增强数据库性能的方式。使用MySQL主从复制,增删改操作走Master主服务器,查询走Slaver从服务器,这样就减少了只有一台MySQL服务器的压力。

增加缓存层

增加缓存层,减少数据库连接也是一种优化手段,有些查询可以不用访问数据库,可以通过使用缓存服务器如redis、memcache、elasticsearch等增加缓存,减少数据库的连接

升级服务器硬件

更快的磁盘IO设备,更强的CPU,更大的内存,更大的网卡流量(带宽)等。

4.2、查询语句优化

①使用索引, 在SQL语句的WHERE和JOIN部分中用到的所有字段上,都应该加上索引。

②尽量防止索引失效,下面第七题有说索引失效原因

③分页查询优化 ,如果数据量较大,一次性获取所有数据会导致性能问题。应该通过使用LIMIT关键字,进行分页查询。

④where后面条件查询只在自己需要的范围查询

⑤select后面尽量不要使用 * 只选择你需要的字段

⑥OR改写成IN ,OR的效率是n级别,IN的效率是log(n)级别,IN的个数建议控制在200以内;

⑦能使用BETWEEN不用IN ,SELECT id FROM t WHERE num BETWEEN 1 AND 5;

⑧使用INNER JOIN代替子查询、使用EXISTS或NOT EXISTS代替IN和NOT IN等。

4.3、explain 执行计划

使用explain 执行计划进行优化,使用explain关键字可以知道MySQL是如何处理你的SQL语句以及表结构的性能瓶颈。使用方法,在select语句前加上EXPLAIN就可以了。如:

EXPLAIN select * from TableName;

这篇文章写的很好,看这个就行SQL优化之EXPLAIN执行计划

5、数据库分库分表

分库:就是一个数据库分成多个数据库,部署到不同机器。

业务量剧增,MySQL单机磁盘容量会撑爆,在高并发的场景下大量请求访问数据库,MySQL单机是扛不住的!分库可以增加磁盘容量,应对高并发,。

垂直分库:比如原来的数据库有用户表,订单表、积分表、商品表把他们拆分成用户库、订单库、积分库、商品库。

水平分库:将表的数据量切分到不同的数据库服务器上,每个服务器具有相同的库和表,只是表中的数据集合不一样。

分表:就是一个表分成多个表。

数据量太大的话,一个查询SQL没命中索引,千百万数据量的表可能会拖垮这个数据库。

②高度为3的B+树可以存差不多千万的数据,单表数据量超过千万,就需要分表了否则磁盘IO次数会增多。

垂直分表:用户表包含id、name、age、email、desc,如果email、desc等字段不常用,我们把它拆分到另外一张表,命名为用户详细信息表。

水平分表:一个表的数据量太大,可以按照规则(如hash取模、时间范围等),把数据切分到多张表,将常用数据从表中独立出来。比如 在登录业务中只需要查询用户名和密码。

6、什么是索引,索引有几种

①索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。

②根据存储方式分为:

聚集索引(InnoDB引擎)

主键索引属于聚簇索引的叶子节点会存储指针的值和数据行,也就是说数据和索引是在一起,这就是聚簇索引,InnoDB中也只有主键索引才能是聚簇索引。

非聚集索引(MyISAM引擎)

二级索引(辅助索引)属于非聚簇索引,叶子节点只会存储数据行的指针,简单来说数据和索引不在一起,就是非聚聚簇索引;

③根据索引特点可以分为:主键索引,唯一索引,普通索引,多列索引,前缀索引,全文索引

6、索引原理

④索引就像目录,索引利用特定数据结构存储避免进行全表扫描。mysql默认索引结构B+树,特点是一个节点可以有多个分支节点,同时每个分支节点只存储key而不存储数据,只在叶子节点存储数据和key。这样大大降低了查找深度,减少了磁盘IO次数。

7、索引失效的原因/场景

①索引列作为计算的一部分或者使用函数,那么索引会失效。例如,下面这个查询无法使用 actor_ id 列的索引:

SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;

②不符合最左匹配原则,例如定义了 (a,b,c) 联合索引,相当于构造了 (a)、(a,b)、(a,b,c) 索引。如果要使 c 索引实际工作,那么必须在 WHERE 中同时加入 a、b 字段的条件,顺序无所谓。

③WHERE 子句的查询条件里使用了比较操作符 LIKE和正则,第一个字符不是通配符的情况下才能使用索引。比如查询条件是 LIKE ‘abc%’,MySQL 将使用索引,如果条件是 LIKE ‘%abc’,MYSQL 将不使用索引。

④查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。常见的范围查找有:LIKE ‘%abc’,和通过 <,>,<= ,\ >=,\ between,!=,或者 <> 操作符做比较。它们都是导致索引失效。

注意事项:IN() 和exists不是范围匹配,而是多个等值匹配,因此并不会导致索引失效。

⑤ 如果 WHERE 子句带有 or 且有字段不属于索引,那么即使其中带索引的字段也不会使用。

⑥使用了select*,大概率会查询非索引列的数据就无法使用覆盖索引了

⑦对查询结果排序时使用order by,不符合索引结构的顺序。

⑧索引列值为null,致COUNT(*)不能走索引

查询诸如SELECT COUNT(*) FROM Table 的时候,因为HASHSET中不能存储空值的,所以优化器不会走索引。

⑨not in 和 not exists使索引失效 而IN() 和exists不是范围匹配,而是多个等值匹配,因此并不会导致索引失效。

⑩字段类型不同 ,比如你的索引字段是varchar型,但是你搜索条件却是 userid=333,那这样索引不生效。

8、聚集索引和非聚集索引的区别

聚集索引(InnoDB引擎)

主键索引属于聚簇索引的叶子节点会存储指针的值和数据行,也就是说数据和索引是在一起,这就是聚簇索引,InnoDB中也只有主键索引才能是聚簇索引。

非聚集索引(MyISAM引擎)

二级索引(辅助索引)属于非聚簇索引,叶子节点只会存储数据行的指针(innoDB是主键),简单来说数据和索引不在一起,就是非聚聚簇索引;

9、什么字段不能做索引

应创建索引的字段

应创建索引的场景

1、作为主键必须有索引;(主键)

2、经常用在连接的列上,主要是一些外键,可以加快连接的速度(外键)

3、经常需要搜索的列上,加快搜索的速度; (需搜索)

4、经常需要排序的列上创建索引,利用索引已经排序,加快排序查询时间;(需排序)

5、 经常需要根据范围搜索的列上,因为索引已经排序,其指定的范围是连续的;(范围)

6、经常使用在WHERE子句中的列上,加快条件的判断速度。(where)

7、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;

不应创建索引的字段

1、查询中很少用到的列,有索引并不能提高查询速度。

2、取值很少的列,比如数据表中的性别列

3、text, image和bit数据类型的列,这些列的数据量要么相当大,要么取值很少。

4、被频繁更新的列,会提高搜索性能,但是会降低修改性能

10、什么情况下使用索引

要提升查询速度的时候,使用索引。索引出现的目的就是为了提升查询速度。使用索引的过程中我们应该避免索引失效。

11、MySQL数据库limit分页的使用

limit startIndex,length

startIndex是起始下标,length是长度,limit 0,5 表示从0开始,取长度为5的数据。

limit执行顺序在order by后面

12、说一下怎么做数据库的事务,除注解外如何使用事务

JDBC中进行事务处理

Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。

Spring 中进行事务处理

Spring事务包括编程式事务声明式事务

编程式事务需要在代码中手动控制事务的开始、提交和回滚等操作

首先通过transactionManager.getTransaction方法获取事务对象status,然后在try块中执行转账操作,最后通过transactionManager.commit(status)提交事务。如果在转账操作中发生了异常,则会通过transactionManager.rollback(status)回滚事务。

声明式事务则是通过在配置文件中声明事务的切入点和通知等信息来自动控制事务的行为。

声明式的事务管理是用Spring的AOP来实现的;在配置文件中声明事务管理器、事务通知等,然后在需要使用事务的方法上添加事务切面的注解即可。可以配置只读事务和回滚事务(传播行为为REQUIRED)当出现错误后进行回滚操作。在项目中通过aop切入事务到serivce层,这样做能使一次业务逻辑操作,包括几个数据库操作都控制在一个事务中。

下面代码中首先声明了事务管理器transactionManager,然后定义了事务通知tx:Advice,该通知会在transferMoney方法执行时进行事务管理。最后通过aop:config和aop:advisor来将txAdvice应用于transferPointcut定义的切入点上。

声明式事务注解方式

在需要使用事务的方法上添加@Transactional注解,并通过该注解的属性来指定事务的传播机制、隔离级别、超时时间等信息。

在下面的代码中,通过@Transactional注解来声明了事务的传播机制为Propagation.REQUIRED,隔离级别为Isolation.DEFAULT,超时时间为3600秒。在方法执行时,Spring会根据注解中的信息自动管理事务的行为。

指定只读事务的办法为

bean配置文件中,prop属性增加“read-Only”

或者用注解方式@Transactional(readOnly=true)

相当于将数据库设置成只读数据库,此时若要进行写 的操作,会出现错误。

13、事务的七种传播行为

什么是事务的传播行为:事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法时,事务如何传播。

14、缓存和数据库同步

1、定时同步:应用程序可以定期将MySQL中的数据同步到Redis中。这种方法的优点是实现简单,缺点是可能会导致数据不一致,因为数据在同步的过程中可能会被修改。

2、实时同步:可以使用消息队列实现MySQL和Redis之间的实时同步。当MySQL中的数据发生变化时,将变更的数据以消息的方式发送到消息队列。触发器或者消息队列会立即通知Redis进行更新。执行对应的数据同步操作。这种方法的优点是实时性高,缺点是实现复杂。

3、双写模式:可以将MySQL和Redis同时写入,确保数据的一致性。这种方法的优点是实现简单,缺点是可能会影响性能。

4、读写分离:可以将MySQL用于写操作,而将读操作从MySQL转移到Redis中。从缓存读取数据,如果读不到从数据库加载然后读入缓存。这种方法的优点是实现简单,提高系统的性能,缺点是可能会导致数据不一致,因为MySQL写操作的数据和Redis读操作的数据之间的同步延迟,与存在一定的延迟。。读写分离是指,只在MySQL中执行写操作。

5.异步写入:异步写入是指先将数据写入MySQL,然后异步将数据写入Redis。这种方法可以提高系统的性能,但是不能保证数据的实时性。

6.利用MySQL的binlog实时同步:通过解析MySQL的binlog,可以获取到数据库的更新操作日志,然后将变更的数据实时同步到Redis中。可以使用Canal等开源工具来实现binlog的解析。

7.利用MySQL的触发器实时同步:在MySQL中创建触发器,当数据发生变更时,触发器会将变更的数据发送到Redis中。这种方式需要在MySQL中编写触发器逻辑。

15、缓存和数据库同步的双删策略了解嘛

双删策略的执行步骤:

①首次删除缓存:在更新数据库之前,首先删除缓存中的数据。这一步是为了防止在更新数据库的过程中,有线程读取到旧的缓存数据。

②更新数据库:执行数据库的更新操作。

③再次删除缓存:在数据库更新完成后,再次删除缓存中的数据。这一步是为了确保在数据库更新后,缓存中不会保留旧的或不一致的数据。同时,由于此时数据库已经更新完成,新的数据将在下次查询时被加载到缓存中。

注意点:

缓存删除失败的处理:如果缓存删除失败,需要采取重试机制或其他补偿措施来确保缓存数据的最终一致性。

性能考虑:频繁的缓存删除操作可能会对缓存系统的性能产生影响,因此需要根据实际业务场景进行权衡和优化。

事务一致性:在数据库更新和缓存删除之间,需要确保事务的一致性,以避免出现部分更新或回滚的情况。

16、了解mvcc吗?

MVCC 即多版本并发控制(Multi-Version Concurrency Control),是一种用于数据库管理系统的并发控制机制,主要用于实现数据库的隔离性。

基本原理:MVCC 允许数据库系统在同一时间为不同的事务提供数据的不同版本,使得事务在读取数据时可以不受其他事务并发修改的影响,从而实现了一定程度的并发读取和写入操作,提高了数据库的并发性能。其核心原理是通过为数据库中的每个数据行维护多个版本,每个事务根据自己的时间戳或事务版本号来访问适合自己的数据版本,就好像每个事务都在一个独立的数据库快照上进行操作一样

优点

  • 提高并发性能:MVCC 允许事务在一定程度上并发地进行读写操作,减少了锁的争用,从而提高了数据库的并发处理能力。

  • 降低死锁风险:因为 MVCC 减少了对锁的依赖,所以相比基于锁的并发控制机制,死锁发生的概率更低。

局限性

  • 存储开销:MVCC 需要为每个数据行维护多个版本,这会增加数据库的存储开销,特别是在数据更新频繁的情况下,可能会导致大量的 undo 日志和数据版本的产生,占用较多的存储空间。

  • 性能影响:在读取数据时,需要根据版本号和 Read View 来判断应该读取哪个版本的数据,这会增加一定的查询处理时间。同时,在数据更新时,也需要维护数据版本和 undo 日志,这也会对更新性能产生一定的影响。

17、b树,b+树的区别

结构

  • B 树:节点存键和数据,叶子节点无顺序连接。

  • B + 树:非叶子节点只存键,数据全在叶子节点,且叶子节点有指针连成有序链表。

查询

  • B 树:单键查询逐层找,范围查询需多次从根找,效率低。

  • B + 树:单键查询类似但树可能更矮;范围查询从起始叶子节点沿链表找,效率高。

插入删除

  • B 树:插入删除可能导致节点分裂或合并,操作复杂影响多节点。

  • B + 树:操作主要在叶子节点,相对简单,对整体结构影响小。

应用

  • B 树:适合对单个数据随机访问要求高的场景,如文件系统索引。

  • B + 树:更适用于数据库索引,因范围查询和磁盘 I/O 优势明显。

2.3)多线程,锁,并发处理等

1、线程生命周期

线程的生命周期是指一个线程从创建到终止所经历的各个阶段。在Java中,线程的生命周期通常包括以下五种状态:

新建(New):线程被创建时处于新建状态。它还没有开始运行,没有执行任何代码。

就绪(Runnable):当线程被启动后,它进入就绪状态。此时,线程已经准备好运行,但是操作系统可能还没有分配到处理器资源。

运行(Running):当操作系统将处理器资源分配给线程时,它进入运行状态。此时,线程正在执行其代码。

阻塞(Blocked):当线程需要等待某个条件满足(例如等待I/O操作完成)时,它进入阻塞状态。在阻塞状态下,线程会暂时放弃处理器资源,直到等待的条件满足。

终止(Terminated):当线程执行完毕或因异常而终止时,它进入终止状态。此时,线程不再占用任何资源。

2.1、synchronized关键字

synchronized 关键字,代表加锁,保证被它修饰的方法或者代码块只能有一个线程执行。 synchronized是一个对象锁,也就是它锁的是一个对象。我们无论使用哪一种方法,synchronized都需要有一个锁对象,它可以修饰实例方法,修饰静态方法,修饰代码块

①synchronized修饰实例方法, 在方法上加上synchronized关键字即可。进入同步代码前要获得当前实例(调用这个方法的实例)的锁 。

public class SynchronizedTest1 {

public synchronized void test() {

System.out.println("synchronized 修饰 方法");

}

}

②synchronized修饰静态方法,加上synchronized关键字即可, 由于静态方法不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。给静态方法加synchronized锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前静态方法所在类的Class对象的锁。

public static synchronized void test(){
i++;
}

补充:线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

③修饰代码块: synchronized修饰代码块需要传入一个对象。进入同步代码库前要获得给定对象的锁,这里的this 。

public class SynchronizedTest2 {

public void test() {

    synchronized (this) {
    
        System.out.println("synchronized 修饰 代码块");
    }
  }
}

补充;synchronized(object) ,表示进入同步代码库前要获得 给定对象的锁

synchronized(类.class) ,表示进入同步代码前要获得 给定 Class 的锁

注意:

①原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。

②可见性:保证共享变量的修改能够及时可见。当某线程进入synchronized代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本,执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。其他获取不到锁的线程会阻塞等待,所以变量的值一直都是最新的。

③有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。

④悲观锁:synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。

⑤独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。

⑥非公平锁:synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待。这样有利于提高性能,但是也可能会导致饥饿现象

⑦可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁,可一定程度避免死锁。

2.2、并发编程的三个概念

①原子性指的是一个不可以被分割的操作,即这个操作在执行过程中不能被中断,要么全部不执行,要么全部执行。且一旦开始执行,不会被其他线程打断。

②可见性 指的是一个线程修改了共享变量后,其他线程能立即感知这个变量被修改。

③有序性 指程序按照代码的先后顺序执行。在Java内存模型中,为了提升效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。

3.1、volatile关键字

volatile 是最轻量的同步机制。只能用于变量(类的成员变量、类的静态成员变量),volatile是为了保证多个线程间拿到的变量都是最新的同一个内容,用volatile修饰后使的能够实时同步;

被volatile修饰之后就保证了:

①可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。除了volatile,Java中的synchronized和final两个关键字 以及各种 Lock也可以实现可见性。

②顺序性:禁止进行指令重排序。

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量。

③volatile关键字无法保证原子性,更准确地说是volatile关键字只能保证单操作的原子性,比如 x=1,但是无法保证复合操作的原子性,比如x++

3.2、synchronized 和 volatile 的区别

1、volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。

2、volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

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

4、volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

5、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

4、ThreadLocal

ThreadLocal叫做线程本地变量,ThreadLocal想让线程间对某个变量不相互干扰,隔离开来;ThreadLocal中填充的变量属于当前线程独有的变量,对其他线程而言是隔离的。

①ThreadLocal是JDK包提供的,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题

②ThreadLocal相当于线程间的数据隔离。Synchronized则是为用于线程间的共享数据加锁。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

③ThreadLocal 适用于变量在线程间隔离而在方法或类间共享的场景。最常见的ThreadLocal使用场景为用来解决数据库连接、Session 管理等

threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值,不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的。

5.1、 线程池介绍与优势

①线程池介绍

线程池是一种多线程处理形式,可以理解为多个线程的集合。线程是需要时就创建,执行完任务就销毁,而线程池是取用一个创建好的线程,用完就放回去。

//线程池(JDK)

ExecutorService executorService = Executors.newFixedThreadPool(16);
//千人并发访问
for(int i=0;i<1000;i++){
  
  executorService.submit(new Runnable() {
  
    @Override
    public void run() {
    studentService.queryAllStudentCount();
    }
    
  });

}

②线程池优势

1、降低系统资源消耗,提高系统响应速度:通过重用已存在的线程,降低线程创建和销毁造成的消耗;无需等待新线程的创建便能立即执行;

2、线程并发数的管控,统一的分配,调优和监控。因为线程若是无限制的创建,可能会产生OOM(内存溢出),并且会造成cpu过度切换。

3、提供更强大的功能,延时定时线程池。

5.2、 线程池参数

核心线程数量、最大线程数、等待队列数、每一个线程的执行时间、线程的名称等参数的线程。

5.3、创建线程池的方式, 线程池有几种

创建线程池的方式 (使用Executors顶层容器静态类实现创建)

Executory.newFixedThreadPool(int);//创建固定容量大小的线程池

线程池有几种

5.4、线程池的启动策略?

①线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。

②当调用 execute() 方法添加一个任务时,线程池会做如下判断:

1、如果正在运行的线程数量小于核心线程数,那么马上创建线程运行这个任务;

2、如何大于或等于核心线程数,那么将这个任务放入队列。

3、如果队列满了,而且正在运行的线程数量小于最大线程数,那么还是要创建线程运行这个任务;

4、如果队列满了,而且正在运行的线程数量大于或等于 最大线程数,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

③当一个线程完成任务时,它会从队列中取下一个任务来执行。

④当一个线程超过一定的时间(keepAliveTime)没事做,并且当前运行的线程数大于核心线程数,那么这个线程就被停掉。

6、 AQS

①AQS是什么?

AQS定义了一套多线程访问共享资源的同步器框架。 来实现同步类(Lock、Semaphore、ReentrantLock等)。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

7、异步是线程池的还是spring自带的

Spring的异步处理。如果是使用Spring Boot项目那只需要2个注解就能搞定了。如下:

第一步:基于Springboot启动类启用加@EnableAsync注解

@EnableAsync
@SpringBootApplication
public class AsyncApplication {

public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}

第二步在要使用的方法上加@Async注解, Spring应用默认的线程池为SimpleAsyncTaskExecutor,指在@Async注解在使用时,不指定线程池的名称。

@Slf4j
@Service
public class BusinessServiceImpl implements BusinessService {
/**

* 方法4:没有指定线程池,验证默认线程池也ok(不推荐:规避资源耗尽的风险)

*/

@Async
public void asyncDemo4() {
log.info("asyncDemo4:" + Thread.currentThread().getName() + " 正在执行 ----------");
try {
Thread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("asyncDemo4:" + Thread.currentThread().getName() + " 执行结束!!");
}
}

也可以自己配置一个线程池 ThreadPoolTaskExecutor详细内容看这篇文章

8、ReentrantLock可重入锁

意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。

// **************************Synchronized的使用方式**************************

// 1.用于代码块

synchronized (this) {}

// 2.用于对象

synchronized (object) {}

// 3.用于方法

public synchronized void test () {}

// 4.可重入

for (int i = 0; i < 100; i++) {

synchronized (this) {}

}

// **************************ReentrantLock的使用方式**************************

public void test () throw Exception {

// 1.初始化选择公平锁、非公平锁

ReentrantLock lock = new ReentrantLock(true);

// 2.可用于代码块

lock.lock();

try {

try {

// 3.支持多种加锁方式,比较灵活; 具有可重入特性

if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }

} finally {

// 4.手动释放锁

lock.unlock()

}

} finally {

lock.unlock();

}

}

9、怎么创建一个多线程,有哪些方法实现,优劣是什么

实现多线程可以通过继承Thread类和实现Runnable接口。

(1)继承Thread

定义一个类继承Thread类,复写Thread类中的public void run()方法,将线程的任务代码封装到run方法中,直接创建Thread的子类对象,创建线程。调用start()方法,开启线程(调用线程的任务run方法)

(2)实现Runnable接口;

定义一个类,实现Runnable接口;覆盖接口的public void run()的方法,将线程的任务代码封装到run方法中;创建Runnable接口的子类对象,将Runnabl接口的子类对象作为参数传递给Thread类的构造函数,创建Thread类对象调用start()方法,启动线程。

区别:

(1)实现Runnable接口避免了单继承的局限性

(2)继承Thread类线程代码存放在Thread子类的run方法中,实现Runnable接口线程代码存放在接口的子类的run方法中;

10、项目中的多线程

那使用到多线程的时候就必须要先对线程场景进行一个解构,也就是对单体请求如何合理的拆分成多线程请求

1.多人订阅服务,防止超出数量限制

使用数据库锁或分布式锁控制并发问题,从而控制减库存的冲突,避免超卖问题

①数据库分布式锁可以采用乐观锁,但由于数据库本身的性能和并发处理能力并不理想,在高并发项目中,使用数据库锁也是不合适的;

②基于Redis实现的分布式锁,由于Redis是单线程的,不管有多少个并发请求,Redis会将请求排队进行处理,即一个一个地有先后顺序地处理,这样即不会有并发问题,即不会产生减库存的冲突,从而解决减库存的超卖问题;

2.页面上需要导出excel文件

异步。比如页面上需要导出excel文件,由于业务要求,需要导出全部数据需要10分钟左右。如果使用同步的方式,用户需要在这个导出页面等待10分钟,不能做其它操作,这样肯定是不行的。那么可以采用异步,用户点击导出,导出接口主线程往数据库当中插入一条导出记录,开个子线程获取数据,写入Excel文件,完成之后更新导出记录,当中,然后主线程直接给用户返回。这样用户点击导出会生成一个导出记录信息,不用在这里等待,等导出完成,之后可以在导出记录当中进行下载。

异步导出实现

11、如何测试并发量?

ApacheBench 是 Apache 服务器自带的一个web压力测试工具,简称ab。

ab又是一个命令行工具,对发起负载的本机要求很低,根据ab命令可以创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问,执行命令 ab -n -c url

12、Java线程间的通信

包含锁与同步,等待通知机制,信号量,管道

第五章线程间通讯

13、两种创建线程池方式的底层原理

1. 使用 Executors 工具类创建

  • 原理:本质是调用 ThreadPoolExecutor 构造函数,预设不同参数。如 newFixedThreadPool 创建固定大小线程池,核心和最大线程数相同,用无界队列。

  • 特点:创建简单,但可能有内存溢出等隐患。

2. 通过 ThreadPoolExecutor 构造函数创建

  • 原理

    • 任务提交:先看核心线程数够不够,不够就创建;够了放队列;队列满且线程数没到最大,创建非核心线程;都满了按拒绝策略处理。

    • 线程复用:线程执行完任务从队列取新任务,不立即销毁。

    • 空闲处理:超核心线程数的空闲线程,超过时间会销毁。

  • 特点:参数可灵活定制,避免潜在问题。

2.4)Redis 数据库,Linux服务器

1、redis默认端口号有几个库,有哪些数据类型

redis默认16个库,redus默认端口号6379

数据类型以及应用场景:

String:简单的key-value: 缓存、计数器、分布式锁等。

List列表: 链表、队列、微博关注人时间轴列表等。

Hash字典: 用户信息、Hash 表等。

Set集合: 去重、赞、踩、共同好友等。

Zset(sorted set)有序集合: 访问量排行榜、点击量排行榜等。

2、redis用在项目哪里,用来解决什么问题

①我们项目是一直迭代升级,一开始没有使用Redis的时候,我们所有数据都是直接到达数据库获取,导致我们后端的数据库经常出现cpu及io压力很大,后续我们将前端业务系统上一些不需要实时更新的数据,一些频繁查询的热点数据,进行了Redis缓存存储,来提升系统的能力。

②会话缓存(Session Cache)

用户登录Web应用时候,将会话数据存储于Redis,并将唯一的会话ID(Session ID)返回到客户端的Cookie中。当用户再向应用发送请求时,会将此会话ID包含在请求中。无状态的Web服务器,根据这个会话ID从Redis中搜索相关的会话数据来进一步请求处理。

③排行榜/计数器

Redis提供了排序集合(Sorted Sets)的功能,排序集合是唯一元素(比如:用户id)的集合,每个元素按分数排序,这样可以快速的按分数来检索元素

3、Redis的优缺点

优点

快速:Redis使用内存存储数据,读写速度非常快。

多种数据类型:Redis支持多种数据结构,可以适应不同的应用场景。

丰富的特性:Redis支持事务、Lua脚本、发布订阅模式等高级特性。

高可用高扩展性:Redis可以通过主从复制、哨兵模式和集群模式等方式实现高可用和横向扩展。

缺点

内存限制:由于Redis使用内存存储数据,因此受到内存容量的限制。

持久化问题:Redis默认不会将数据持久化到硬盘,需要使用持久化机制来解决数据丢失问题。

单线程模型:Redis采用单线程模型,虽然可以通过多实例和多线程方式解决并发问题,但是并发能力相对较弱。

4、缓存穿透是什么,如何避免

缓存穿透是什么

缓存穿透是指查询一个不存在的数据,由于缓存中没有数据,请求会直接穿透到数据库中,从而引起数据库的压力过大,严重影响系统的性能。

解决缓存穿透的常用方法有两种:①布隆过滤器

是一种高效的数据结构,可以判断一个元素是否存在于一个集合中,同时也可以减轻数据库的压力。在使用布隆过滤器的时候,首先将所有的数据hash到一个位图中,如果查询的数据在位图中不存在,那么直接返回不存在,从而避免了对数据库的查询操作。

在SpringBoot中,我们可以使用Guava提供的布隆过滤器实现缓存穿透的解决方案。例如:

@Bean

public BloomFilter bloomFilter() {

return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000, 0.001);

}

@Override
public User getUserById(String id) {

// 先从布隆过滤器中查询是否存在。布隆过滤器中没有直接返回null

if (!bloomFilter.mightContain(id)) {

return null;

}

// 如果存在,则查询Redis中的缓存数据

User user = redisTemplate.opsForValue().get(id);

if (user == null) {

// 如果Redis中不存在,则查询数据库

user = userDao.getUserById(id);

if (user != null) {

// 将数据缓存到Redis中

redisTemplate.opsForValue().set(id, user);

} else {

// 如果数据库中也不存在,则将该id加入到布隆过滤器中

bloomFilter.put(id);

}

}

return user;

}

②空对象缓存并设置过期时间

当查询的数据不存在时,将一个空对象缓存到Redis中。这样下次查询同样不存在的数据时,就可以直接从Redis中获取到一个空对象,从而避免了对数据库的查询操作。

在SpringBoot中,我们可以通过设置Redis缓存的过期时间来实现空对象缓存的解决方案。例如:

@Override

public User getUserById(String id) {

User user = redisTemplate.opsForValue().get(id);

if (user == null) {

// 如果Redis中不存在,则查询数据库

user = userDao.getUserById(id);

if (user != null) {

// 将数据缓存到Redis中

redisTemplate.opsForValue().set(id, user);

} else {

// 如果数据库中也不存在,则将一个空对象缓存到Redis中,设置五分钟过期时间防止缓存雪崩

redisTemplate.opsForValue().set(id, new User(), 5, TimeUnit.MINUTES);

}

}

return user;

}

5、缓存击穿是什么,如何避免

缓存击穿是什么

缓存击穿是指一个非常热点的数据在缓存中过期之后,正好在这个时间段内有大量的请求访问该数据,这些请求会直接穿透到数据库中,从而引起数据库的压力过大

两种解决方式①设置热点数据永不过期

缺点:热点数据可能会被修改,如果不及时更新缓存,可能会导致缓存中的数据与实际数据不一致

@Override

public User getHotUserById(String id) {

User user = redisTemplate.opsForValue().get(id);

if (user == null) {

// 如果Redis中不存在,则查询数据库

user = userDao.getHotUserById(id);

if (user != null) {

// 将数据缓存到Redis中,设置过期时间为1小时

redisTemplate.opsForValue().set(id, user, 1, TimeUnit.HOURS);

}

}

return user;

}

②双写策略

有大量请求在缓存中查询数据时,1、先在缓存中写入一个空对象,2、然后让一个请求去查询数据库,然后写入缓存,其余请求都去查询缓存。

在SpringBoot中,我们可以通过设置Redis缓存的过期时间来实现延迟缓存双写策略的解决方案。例如:

@Override

public User getHotUserById(String id) {

User user = redisTemplate.opsForValue().get(id);

if (user == null) {

// 如果Redis中不存在,则写入一个空对象

redisTemplate.opsForValue().set(id, new User(), 5, TimeUnit.MINUTES);

// 去数据库中查询数据并更新缓存

user = userDao.getHotUserById(id);

if (user != null) {

redisTemplate.opsForValue().set(id, user, 1, TimeUnit.HOURS);

}

}

return user;

}

6、缓存雪崩是什么,如何避免

缓存雪崩是什么

缓存雪崩是指当缓存中的大量数据在同一时间失效,导致大量请求直接访问数据库,从而引起数据库的压力过大,严重影响系统的性能。

两种解决方式①缓存数据的随机过期时间

增加随机因素,从而避免大量数据在同一时间失效的情况。在SpringBoot中,我们可以通过设置Redis缓存的过期时间和一个随机值来实现这个解决方案。例如:

@Override

public List<User> getUserList() {

List<User> userList = redisTemplate.opsForValue().get("userList");

if (userList == null) {

// 如果Redis中不存在,则查询数据库

userList = userDao.getUserList();

if (userList != null && userList.size() > 0) {

// 将数据缓存到Redis中,并增加随机的过期时间

int random = new Random().nextInt(600) + 600;

redisTemplate.opsForValue().set("userList", userList, random, TimeUnit.SECONDS);

}

}

return userList;

}

②预热缓存

将系统中的热点数据提前加载到缓存中,从而避免了大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过编写一个启动时执行的方法,来实现预热缓存的解决方案。例如:

@Component

public class CacheInit implements CommandLineRunner {

@Autowired

private UserDao userDao;

@Autowired

private RedisTemplate<String, Object> redisTemplate;

@Override

public void run(String... args) throws Exception {

List<User> userList = userDao.getUserList();

if (userList != null && userList.size() > 0) {

// 将数据缓存到Redis中,并设置过期时间为1小时

for (User user : userList) {

redisTemplate.opsForValue().set(user.getId(), user, 1, TimeUnit.HOURS);

}

}

}

}

③分布式锁

使用分布式锁,从而避免大量请求同时访问数据库的情况。在SpringBoot中,我们可以通过Redisson来实现分布式锁的解决方案。例如:

@Override

public List<User> getUserList() {

List<User> userList = redisTemplate.opsForValue().get("userList");

if (userList == null) {

// 如果Redis中不存在,则尝试获取分布式锁

RLock lock = redissonClient.getLock("userListLock");

try {

// 尝试加锁,并设置锁的过期时间为5秒

boolean success = lock.tryLock(5, TimeUnit.SECONDS);

if (success) {

// 如果获取到了锁,则查询数据库并将数据缓存到Redis中

userList = userDao.getUserList();

if (userList != null && userList.size() > 0) {

redisTemplate.opsForValue().set("userList", userList, 1, TimeUnit.HOURS);

}

}

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

// 释放锁

lock.unlock

}

} return userList;

}

7、Redis有哪几种持久化方式?优缺点是什么?

持久化方式

RDB 持久化:该机制可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。

AOF 持久化:记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。

同时应用 AOF 和 RDB:当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。

RDB优缺点

RDB 优点:RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份,RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

RDB 缺点:你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。

AOF优缺点

优点:

1、AOF 文件是一个只进行追加操作的日志文件。AOF 的默认策略为每秒钟 fsync(文件同步) 一次,并且就算发生故障停机,也最多只会丢失一秒钟的数据。Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。

缺点:

对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。 AOF在恢复大数据集时的速度比 RDB 的恢复速度要慢。

8.1、Redis有哪几种数据淘汰策略

①对于设置了过期时间的

volatile-ttl 越早过期的越先被删除。

volatile-random 设置了过期时间的键值对中,进行随机删除。

volatile-lru(Least Recently Used) 设置了过期时间的键值对中,选择最长时间没有使用的键删除。

volatile-lfu((Least Frequently Used) ) 设置了过期时间的键值对中,选择最少使用次数的键进行删除。

②对于所有的

allkeys-lru:从所有的键中,选择最长时间没有使用的键进行删除。

allkeys-lfu,从所有的键中,选择最少使用次数的键进行删除。

allkeys-random:从所有的键中,随机选择一个进行删除。

noeviction:默认是禁止淘汰,如果数据达到了最大内存限制,写入数据时会报错。

8.2、Redis不同的缓存淘汰策略会导致什么问题

1. noeviction

  • 问题:缓存满时写入操作报错,新数据无法存入,可能影响业务功能。

2. allkeys-lru

  • 问题:可能误删常用但最近未访问的热数据,导致频繁回源到数据库加载。

3. allkeys-random

  • 问题:随机性强,可能删除重要数据,使缓存命中率下降。

4. volatile-lru

  • 问题:若设置过期的键少,仍可能因无合适淘汰对象写入失败;也可能误删近期少用的重要数据。

5. volatile-random

  • 问题:随机淘汰有过期时间的键,可能删除即将使用的数据,增加数据库查询压力。

6. volatile-ttl

  • 问题:只考虑剩余时间,可能删除即将被频繁访问但快过期的数据。

9、使用redis防止重复提交

用户点击后我们会将用户的提交在redis中存放一个标志,如果用户重复提交,我们会检查redis的标志,来拒绝用户的第二次提交,只处理第一次提交请求;

10、具体怎么使用redis缓存

@Service

public class StudentServiceImpl implements StudentService {

@Autowired

StudentMapper studentMapper;

@Autowired(required = false)

RedisTemplate redisTemplate;

@Override

//public synchronized Long queryAllStudentCount() {

public Long queryAllStudentCount() {

//key序列化,对数据没有任何影响

redisTemplate.setKeySerializer(new StringRedisSerializer());

//查询缓存

Long allStudentCount = (Long)redisTemplate.opsForValue().get("allStudentCount");

if(allStudentCount==null){

synchronized (this){

//查询缓存

allStudentCount = (Long)redisTemplate.opsForValue().get("allStudentCount");

if(allStudentCount==null){

System.out.println("---查询数据库---");

//查询数据库

allStudentCount=studentMapper.selectAllStudentCount();

//放入缓存

redisTemplate.opsForValue().set("allStudentCount", allStudentCount, 20, TimeUnit.SECONDS);

}else{

System.out.println("---缓存命中---");

}

}

}else{

System.out.println("---缓存命中---");

}

return allStudentCount;

}

}

11、redis主从同步和哨兵机制

主从复制: Redis 支持主从复制机制,其中一个 Redis 实例作为主节点,负责写操作,而其他实例作为从节点,负责复制主节点的数据。这种方式可以实现数据的备份和负载均衡,从而提高可靠性和性能

1.数据复制:主节点将写操作记录在本地的操作日志(AOF文件或RDB文件)中,并定期将这些操作发送给从节点。从节点接收到操作后,对其进行重放,从而保持与主节点的数据一致性。

2.异步复制:主节点将写操作发送给从节点时,是异步的,即主节点不会等待从节点的确认。这样可以提高性能,但在故障发生时可能导致部分数据丢失。

3.读写分离:主节点负责写操作,而从节点负责读操作。这样可以分担主节点的负载,并提供更好的读性能。

Sentinel 哨兵: Redis Sentinel 是一个监控和自动故障恢复系统

Sentinel 系统有三个主要任务:

①监控:Sentinel 不断的检查主服务和从服务器是否按照预期正常工作。

②提醒:被监控的 Redis 出现问题时,Sentinel 会通知管理员或其他应用程序。

③自动故障转移:监控的主 Redis 不能正常工作,Sentinel 会开始进行故障迁移操作。将一个从服务器升级新的主服务器。 让其他从服务器挂到新的主服务器。同时向客户端提供新的主服务器地址。

12、Redis采用单线程设计为什么还那么快

①redis是内存数据库,纯内存访问避免了磁盘的 I/O 等耗时操作

②redis 为单线程不会因为线程创建导致的性能消耗,没有多线程切换的开销,减少了锁竞争,以及频繁创建线程和销毁线程的代价。Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。

③采用了 I/O 多路复用机制,非阻塞I/O大大提升了并发效率

④高效的数据结构:这里所说的数据结构并不是 Redis 提供给我们使用的 5 种数据类型:String、List、Hash、Set、SortedSet。而是这些数据类型底层的数据结构

13、讲一下Linux服务器部署项目

①先需要在Linux服务器上部署环境,安装jdk,安装MySQL,redis服务器和tomact服务器,nginx服务器。

②用maven打包项目,将项目打成war包。

③使用xshell工具,把war包上传到linux服务器,将项目传到tomcat的webapp文件夹里面。

④启动tomcat,启动MySQL服务,就可以访问系统了

14、Linux常用命令

l文件和目录操作

  • ls:列出目录内容。ls -l 以长格式显示,ls -a 显示包括隐藏文件在内的所有文件。

  • cd:切换工作目录。如 cd /home/user 进入指定目录,cd .. 返回上一级目录。

  • mkdir:创建新目录。例如 mkdir new_folder 创建名为 new_folder 的目录。

  • rm:删除文件或目录。rm file.txt 删除文件,rm -r directory 递归删除目录。

  • cp:复制文件或目录。cp source.txt destination.txt 复制文件,cp -r source_dir dest_dir 复制目录。

  • mv:移动或重命名文件和目录。mv old.txt new.txt 重命名,mv file.txt /new/path 移动文件。

  • touch:创建空文件或更新文件的访问和修改时间。如 touch test.txt 创建 test.txt 文件。

文件内容查看

  • cat:一次性显示整个文件内容。cat file.txt 查看 file.txt 文件内容。

  • more:分页显示文件内容,按空格键翻页。如 more large_file.txt

  • less:与 more 类似,但功能更强大,可前后翻页等。less big_file.log

  • head:显示文件开头几行,默认 10 行。head -n 5 file.txt 显示前 5 行。

  • tail:显示文件末尾几行,默认 10 行。tail -f log.txt 实时跟踪文件新增内容。

权限管理

  • chmod:改变文件或目录的权限。chmod +x script.sh 为 script.sh 文件添加可执行权限。

  • chown:更改文件或目录的所有者。chown user:group file.txt 将文件所有者改为 user,所属组改为 group。

进程管理

  • ps:查看当前运行的进程。ps -ef 显示所有进程的详细信息。

  • top:实时显示系统中各个进程的资源占用情况。

  • kill:终止进程。kill -9 PID 强制终止指定进程 ID(PID)的进程。

网络操作

  • ping:测试与目标主机的网络连通性。ping www.example.com

  • ifconfig(或 ip addr):查看和配置网络接口信息。ifconfig 显示网络接口详细信息,ip addr 是较新的替代命令。

  • netstat(或 ss):查看网络连接、路由表等信息。netstat -tuln 显示所有监听的 TCP 和 UDP 端口,ss -tuln 是更高效的替代。

  • ssh:远程登录到其他 Linux 主机。ssh user@remote_host 以 user 身份登录到远程主机。

搜索查找

  • grep:在文件中搜索指定模式的文本。grep "keyword" file.txt 在 file.txt 中查找包含 keyword 的行。

  • find:在指定目录下查找文件。find /home -name "*.txt" 在 /home 目录下查找所有扩展名为 .txt 的文件

2025年Java最新面试题总结之第一章、基础技术栈

2025年Java最新面试题总结之第二章、深入技术栈

2025年Java最新面试题总结之第三章、框架,服务器,中间件等问题

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区

出错了!