面试题总结(仅供学习,无分组无排序)希望可以帮助到别人

文章目录

一、SpringCloud 组件

Nacos

1. 介绍:

​ nacos是springcloud alibaba的一个组件,它是用来实现服务注册中心,配置中心的。针对微服务做服务发现、配置管理,监控各个服务是否健康(正常运行)。

2. 经常使用的配置参数:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # IP地址端口号
        username: nacos # 用户名
        password: nacos	# 密码
        namespace: public # 选择命名空间(配置空间)

3. 使用的pom依赖:

<!-- nacos 服务注册发现(客户端)依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 配置中心做依赖管理 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

4. 核心功能

  • 服务注册: Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
  • 服务心跳: 在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
  • 服务同步: Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
  • 服务发现: 服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
  • 服务健康检查: Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳 的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)

5. Nacos和Eureka的区别:

  1. CAP理论: C 一致性,A 高可用,P 分区容错性

    • eureka支持 AP(高可用、分区容错性)
    • nacos支持 CP(一致性、分区容错性) 和 AP(高可用、分区容错性)
  2. 连接方式:

    • nacos使用的是netty和服务直接进行连接,属于长连接
    • eureka是使用定时发送和服务进行联系,属于短连接
  3. 服务异常剔除:

    • Eureka client在默认情况每隔30s想Eureka Server发送一次心跳,当 Eureka Server 在默认连续90s秒的情况下没有收到心跳, 会把 Eureka client 从注册表中剔除,在由 Eureka-Server 60秒的清除间隔,把 Eureka client 给下线
    • nacos client 通过心跳上报方式告诉 nacos注册中心健康状态,默认心跳间隔5秒,nacos会在超过15秒未收到心跳后将实例设置为不健康状态,可以正常接收到请求,超过30秒nacos将实例删除,不会再接收请求
  4. 操作方式实例:

    • nacos 提供了nacos console可视化控制话界面,可以对实例列表进行监听,对实例进行上下线,权重的配置,并且config server提供了对服务实例提供配置中心,且可以对配置进行CRUD,版本管理
    • eureka 仅提供了实例列表,实例的状态,错误信息,相比于nacos过于简单
  5. 自我保护机制:

    • 相同点: 保护阈值都是个比例,0-1 范围,表示健康的 instance 占全部 instance 的比例。
    • 不同点:
      • 保护方式不同
        • Eureka 保护方式:当在短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下,Eureka Server 不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enable-self-preservation: false)
        • Nacos保护方式:当域名健康实例 (Instance) 占总服务实例(Instance) 的比例小于阈值时,无论实例 (Instance) 是否健康,都会将这个实例 (Instance) 返回给客户端。这样做虽然损失了一部分流量,但是保证了集群的剩余健康实例 (Instance) 能正常工作。
      • 范围不同
        • Nacos 的阈值是针对某个具体 Service 的,而不是针对所有服务的。
        • Eureka的自我保护阈值是针对所有服务的。

Ribbon

Sentinel

Seata

Gateway

1. 介绍:

网关是在前端浏览器和后端微服务中间的服务,用于转发请求,这样微服务的端口不在暴露给前端页面,前端只需了解网关的地址和端口号。

2. 如果前端发起请求怎么知道访问哪个服务:

  1. 设置若干个路由,路由中包括 断言URI
  2. 请求过来的时候判断每个路由的 断言 如果为true,则表示该路由生效
  3. 该路由中的过滤器再进行生效

注意: 如果断言重复,只会匹配第一个

二、Spring事务失效的场景

  1. 事务方法的访问修饰符为非public
  2. 数据库表的存储引擎是MyISAM,事务会失效,因为MyISAM不支持事务
  3. 事务所在类没被spring管理
  4. try catch 捕获到异常被处理掉了,或者不是spring支持的异常
  5. 本类非事务方法调用本类事务方法,导致事务失效
  6. 数据源没有配置事务管理器,导致事务失效
  7. 传播类型不支持事务,导致事务失效
  8. 多线程调用导致两个事务方法不在同一个线程中,获得数据库链接不一样,事务也就不是同一个

三、Spring事务

1. @Transactional 注解的参数

参数意义
isolation事务隔离级别,默认为DEFAULT
propagation事务传播机制,默认为REQUIRED
readOnly事务读写性,默认为false
noRollbackFor一组异常类,遇到时不回滚,默认为{}
noRollbackForClassName一组异常类名,遇到时不回滚,默认为{}
rollbackFor一组异常类,遇到时回滚,默认为{}
rollbackForClassName一组异常类名,遇到时回滚,默认为{}
timeout超时时间,以秒为单位
value可选的限定描述符,指定使用的事务管理器,默认为“”

2. propagation 传播机制参数值

传播机制参数值意义
REQUIRED(spring默认的事务传播类型)如果当前没有事务,则自己新建一个事务,如果当前存在事务则加入这个事务
SUPPORTS当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
REQUIRES_NEW创建一个新事务,如果存在当前事务,则挂起该事务
NOT_SUPPORTED以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER如果当前没有事务存在,就以非事务方式执行;如果有,就抛出异常
NESTED如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

3. isolation 隔离级别

隔离级别参数值意义
DEFAULT使用各个数据库默认的隔离级别【默认】
READ_UNCOMMITTED读取未提交数据(会出现脏读, 不可重复读)(基本不使用)
READ_COMMITTED读取已提交数据(会出现不可重复读和幻读)
REPEATABLE_READ可重复读(会出现幻读)
SERIALIZABLE串行化

四、线程池的参数

  1. corePoolSize:核心线程数。
  2. maximumPoolSize:最大线程数。
  3. keepAliveTime:空闲线程存活时间。
  4. TimeUnit:时间单位。
  5. BlockingQueue:线程池任务队列。
  6. ThreadFactory:创建线程的工厂。
  7. RejectedExecutionHandler:拒绝策略

五、SpringCloud服务之间的请求怎么保证绕过权限控制

  1. 禁用OpenFeign的Hystrix熔断器功能:在FeignClient注解中通过@FeignClient(name = "serviceName", fallback = ServiceFallback.class, primary = false) 设置primary为false来禁用Hystrix,这样就可以绕过Hystrix的权限验证。
  2. 在调用方法中设置不需要验证权限:可以在调用其他服务的方法上添加@IgnoreSecurity注解,表示该方法不需要进行权限验证。
  3. 配置拦截器:可以通过配置拦截器,在请求被发送前对请求进行修改或者添加自定义的请求头,从而绕过权限验证。

需要注意的是,绕过权限验证可能会带来安全风险,应该根据实际情况进行权衡和处理。如果确实需要绕过权限验证,应该采取一些安全措施,例如在请求头中添加安全标识,或者在调用其他服务时采用加密传输等方式来保障数据的安全性。

六、SpringBoot核心注解

  1. @EnableAutoConfiguration
    1. 该注解用于启用Spring Boot的自动配置功能,它会根据应用程序的依赖和配置,自动配置一些Bean和组件
  2. @ComponentScan
    1. 扫描的包路径,它会自动扫描该路径下的所有组件,并将它们装配到Spring容器中
  3. @SpringbootConfiguration

七、定时任务怎么实现

  1. 通过在类上 @EnableScheduling 注解开启定时任务
  2. 在方法上使用 @Scheduled 注解开启一个定时任务
  3. @Scheduled 的参数有 cron 和 fixedRate 两种
    1. cron 表达式参数分别表示:
      • 秒(0~59) 例如0/5表示每5秒
      • 分(0~59)
      • 时(0~23)
      • 日(0~31)的某天,需计算
      • 月(0~11)
      • 周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)
    2. cron 表达式生成器 可以去看看
    3. fixedRate 固定时间
  4. 如果要开启多线程定时任务的话要使用 @EnableAsync 注解开启异步,在方法上使用 @Async 注解,可以使用 value 参数指定线程池

八、Mybatis的XML文件中SQL怎么复用

使用标签 sql 定义公共SQL语句,通过 include 标签进行引用

<sql id="Base_Column_List">
    id, name, age
</sql>
<sql id="demo1">
    SELECT * FROM tableA
    <where>
        tableA.code = #{code}
        <if test="city != null and city!= ''">
            AND tableA.city = #{city}
        </if>
    </where>
</sql>
<select id="getList" parameterType="Map" resultType="Map">
    <include refid="demo1">
        <property name="code" value="${code}"/>
    </include>
</select>

九、Redis为什么存储快

  1. 完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。
  2. 采用单线程,省去了很多上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。(指的是网络请求模块使用一个线程来处理,即一个线程处理所有网络请求,其他模块仍用了多个线程)。
  3. 使用基于IO多路复用机制的线程模型,可以处理并发的链接。
  4. Redis 基于 Reactor 模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器 file event handler。由于这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型,但是它采用IO多路复用机制同时监听多个Socket,并根据Socket上的事件来选择对应的事件处理器进行处理。

十、Redis缓存击穿、穿透、雪崩

1. 缓存击穿

描述:

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

造成后果:

某一时刻数据库请求查询量过大,压力剧增,严重可能会引起数据库宕机。

解决方案:

  • 在第一个查询不到数据的请求上使用互斥锁,其他的线程抢不到锁就会等待,等第一个请求查询数据后,在放到redis中,后面的请求就可以从缓存中取值了。
  • 加长热点数据的过期时间(淘宝活动主打产品)。
  • 实时监控热点key,调整过期时间。
  • 启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失。

2. 缓存穿透

描述:

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

造成后果:

大量的无意义的查询落在DB上,明显会增加数据库的压力,严重的可能会引起数据库宕机。

解决方案:

  • 即使查询不到数据时(即返回null),也要将其塞入缓存中,设定短时限,例如30-60秒,最高5分钟。
  • 限流:
    • guava提供的限流组件PateLimiter,底层采用令牌桶算法。
    • Hystrix的SEMAPHORE的隔离策略。
    • Nginx客户端限流。
  • 设置黑/白名单(提集合set/bitmaps/布隆过滤器)

3. 缓存雪崩

描述:

当缓存服务器重启或者大量缓存集中在某一个时间段失效(较短的时间内,缓存中较多的key集中过期),这样在失效的时候,也会给后端系统(比如DB)带来很大压力。比如缓存服务器宕机了,会有大量的请求进来直接打到DB上面,结果就是DB扛不住,直接宕掉。

造成后果:

可能整服务全部崩掉。

解决方案:

  • 请求限流/降级。
  • Redis主从集群部署、设定持久化策略迅速恢复。
  • 缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
  • 缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

十一、Redis中数据更新,如何保证数据一致性

需求起因:

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

例:

  • 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
  • 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

解决方案:

  1. 采用延时缓存双删策略
    1. 先删除缓存
    2. 再写入数据库
    3. 休眠一定的时间(根据项目业务读取数据的耗时,可以确保请求结束即可)
    4. 再次删除缓存
  2. 异步更新缓存(基于订阅binlog的同步机制)
    1. MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
    2. 读Redis:热数据基本都在Redis
    3. 写MySQL:增删改都是操作MySQL
    4. 更新Redis数据:MySQ的数据操作binlog,来更新到Redis
  3. 通过消息中间件实时推送更新

十二、Redis在springboot中如何使用

导入依赖:

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

配置文件:

spring
	redis
		host: 127.0.0.1	# ip地址
		port: 6379 # Redis服务器连接端口
		password: # Redis服务器连接密码(默认为空)
		pool:
			max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
			max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
			max-idle: 8	# 连接池中的最大空闲连接
			min-idle: 0 # 连接池中的最小空闲连接
		timeout: 30000 # 连接超时时间(毫秒)

使用 RedisTemplate 类

十三、Java8 新特性中 map 和 flatMap 的区别

map:对集合中的元素逐个进行函数操作映射成另外一个

flatMap:接收一个函数作为参数,将流中的每个值都转换为另一个流,然后把所有的流都连接成一个流。各个数组并不是分别映射一个流,而是映射成流的内容,所有使用flatMap(Arrays::stream)时生成的单个流被合并起来,扁平化成了一个流。

十四、ResultMap里面的 collection association有什么区别

collection:

  • 用于处理一对多关联关系结果集映射
  • 比如一个学校有多个班级,每个班级有多个学生,这时候就可以使用collection来将一个学校对象关联到多个班级对象,一个班级对象关联到多个学生对象。在ResultMap中,collection通常需要指定子元素的ResultMap,以便将查询结果递归地映射到Java对象中。

association:

  • 用于处理一对一关联关系结果集映射
  • 比如一个学生只属于一个班级,这时候可以使用association来将一个学生对象关联到一个班级对象。在ResultMap中,association通常需要指定子元素的property或者ResultMap,以便将查询结果映射到Java对象中。

十五、Mybatis中#{} 和 ${}

#{}: 参数占位符(预编译),防止SQL注入

${}: 字符串替换符,用于SQL拼接

十六、介绍一下Redis是什么

redis是一个基于内存,内部采用的是单线程机制,支持多种数据类型的非关系型数据库。采用的是Key-Value的存储格式,它的数据类型有:string(字符串类型)、list(列表类型)、hash(散列类型)、set(集合类型)、sorted_set(有序集合类型);

十七、Redis数据类型介绍

Redis Key:

  • key不要太长,尽量不要超过1024字节,这不仅消耗内存,而且会降低查找的效率;
  • key也不要太短,太短的话,key的可读性会降低;
  • 在一个项目中,key最好使用统一的命名模式;

string(字符串类型):

  • 二进制安全;
  • 遇到数值操作的时候,redis会将字符串类型转换成数值;

lists(列表类型):

  • 底层实现不是数组,而是链表;
  • 对于一个具有上百万个元素的lists来说,在头部和尾部插入一个新元素,其时间复杂度是常数级别的,比如用LPUSH在10个元素的lists头部插入新元素,和在上千万元素的lists头部插入新元素的速度应该是相同的。
  • 查找数据慢
  • 我们可以利用lists来实现一个消息队列,而且可以确保先后顺序;
  • 利用LRANGE还可以很方便的实现分页的功能。
  • 评论也可以存进一个单独的lists中

hash(散列类型):

  • hash是一个 String 类型的 field 和 value 的映射表,特别适合用于存储对象,类似于 Java里面的 Map<String, Object>

set(集合类型):

  • 可以自动去重,底层是一个字符串类型的无序集合,底层是一个value为null的hash表,所以添加,删除,查找的复杂度都是 O(1)

sorted_set(有序集合类型):

  • 是一个有序的,不可重复的字符串集合。
  • 集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。
  • 集合中的成员是唯一的,但是评分是可以重复的。

十八、@RequestBody 和 @ResponseBody 注解

@RequestBody: 用在controller的方法参数上,作用是将请求体里的数据解析,然后填充到该注解标注的参数对象上,比如xml或者JSON格式的;请求方法通常是Post方法

@ResponseBody: 用在controller的类上或者方法上,作用是将controller层返回的对象转换成JSON或者xml格式的字符串,然后写在响应流中

十九、JVM有哪些核心指标?合理范围应该是多少?

TP999: 满足千分之九百九十九的网络请求所需要的最低耗时。

TP9999: 满足万分之九千九百九十九的网络请求所需要的最低耗时。

根据每个服务对AVG(平均)/TP999/TP9999等性能指标的要求是不同的,所以合理的范围也不同。

对于单台服务器来说:

  • jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤佳

  • jvm.gc.meantime:每次YGC耗时在100ms以内,50ms以内尤佳

  • jvm.fullgc.count:FGC最多几小时1次,1天不到1次尤佳

  • jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳

二十、JVM调优步骤

  1. 如果使用合理的JVM的参数配置,在大多数情况应该是不需要调优的,
  2. 其次说明还是存在少量场景需要调优,我们可以针对一些JVM核心指标配置监控警告,当出现波动的时候人为介入分析评估
  3. 最后举个例子

1. 分析和定位当前系统的瓶颈:

  1. CPU指标

    • 查看占用CPU最多的进程
    • 查看占用CPU最多的线程
    • 查看线程堆栈快照信息
    • 分析代码执行热点
    • 查看哪个代码占用CPU执行时间最长
    • 查看每个方法占用CPU时间比例

    常见命令:

    // 显示系统各个进程的资源使用情况
    top
    // 查看某个进程中的线程占用情况
    top -Hp pid
    // 查看当前 Java 进程的线程堆栈信息
    jstack pid
    

​ 常用工具:JProfiler、JVM Profiler、Arthas等

  1. JVM内存指标

    • 查看当前 JVM 堆内存参数配置是否合理
    • 查看堆中对象的统计信息
    • 查看堆存储快照,分析内存的占用情况
    • 查看堆各区域的内存增长是否正常
    • 查看是哪个区域导致的GC
    • 查看GC后能否正常回收到内存

    常见的命令:

    // 查看当前的 JVM 参数配置
    ps -ef | grep java
    // 查看 Java 进程的配置信息,包括系统属性和JVM命令行标志
    jinfo pid
    // 输出 Java 进程当前的 gc 情况
    jstat -gc pid
    // 输出 Java 堆详细信息
    jmap -heap pid
    // 显示堆中对象的统计信息
    jmap -histo:live pid
    // 生成 Java 堆存储快照dump文件
    jmap -F -dump:format=b,file=dumpFile.phrof pid
    

    常见的工具:Eclipse MAT、JConsole等。

  2. JVM GC 指标

    • 查看每分钟GC时间是否正常
    • 查看每分钟YGC次数是否正常
    • 查看FGC次数是否正常
    • 查看单次FGC时间是否正常
    • 查看单次GC各阶段详细耗时,找到耗时严重的阶段
    • 查看对象的动态晋升年龄是否正常

    JVM 的 GC指标一般是从 GC 日志里面查看,默认的 GC 日志可能比较少,我们可以添加以下参数,来丰富我们的GC日志输出,方便我们定位问题。

    GC日志常用 JVM 参数:

    // 打印GC的详细信息
    -XX:+PrintGCDetails
    // 打印GC的时间戳
    -XX:+PrintGCDateStamps
    // 在GC前后打印堆信息
    -XX:+PrintHeapAtGC
    // 打印Survivor区中各个年龄段的对象的分布信息
    -XX:+PrintTenuringDistribution
    // JVM启动时输出所有参数值,方便查看参数是否被覆盖
    -XX:+PrintFlagsFinal
    // 打印GC时应用程序的停止时间
    -XX:+PrintGCApplicationStoppedTime
    // 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
    -XX:+PrintReferenceGC
    

2. 确定优化目标

定位出系统瓶颈后,在优化前先制定好优化的目标是什么,例如:

  • 将FGC次数从每小时1次,降低到1天1次
  • 将每分钟的GC耗时从3s降低到500ms
  • 将每次FGC耗时从5s降低到1s以内

3. 制订优化方案

针对定位出的系统瓶颈制定相应的优化方案,常见的有:

  • 代码bug:升级修复bug。典型的有:死循环、使用无界队列。
  • 不合理的JVM参数配置:优化 JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小。

4. 对比优化前后的指标,统计优化效果

5. 持续观察和跟踪优化效果

6. 如果还需要的话,重复以上步骤

二十一、什么是索引

索引是帮助MySQL高效检索数据的数据结构。

索引提高了数据检索的效率,降低了数据库IO成本,通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。

当然索引也有缺点,比如创建索引列也占用空间,不过可以忽略,优大于劣。索引虽然大大提高了查询效率,同时也降低了表更新的速度。

二十二、索引的结构

MySQL的索引是在存储引擎实现的,不同的存储引擎有不同的索引结构

  • B+tree索引
  • Hash索引
  • R-tree(空间索引)
  • Full-Text(全文索引)

各个引擎对索引结构的支持情况:

索引InnoDBMyISAMMemory
B+tree索引支持支持支持
Hash索引不支持不支持支持
R-tree(空间索引)不支持支持不支持
Full-Text(全文索引)5.6版本后支持支持不支持

二十三、为什么InnoDB存储引擎选择使用B+tree索引结构

  • 相对于二叉树,层级更少,搜索效率高;
  • 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页(一页默认是16KB)中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
  • B+树的底层是多路平衡查找树,对于每一次的查询的都是从根节点触发,到子叶结点才存放数据,根节点和非叶子结点都是存放的索引指针,查找叶子结点后,可以根据键值数据查询。
  • 相对Hash索引,B+tree支持范围匹配及排序操作;
  • 存储能力更强、三层B+树就能存储千万级别的数据。

二十四、索引类型

  • 主键索引
  • 唯一索引
  • 常规索引(普通索引)
  • 全文索引

从单列和多列 来区分:

  • 单列索引
  • 多列索引(联合索引)

从是否聚集索引区分:

  • 聚集索引(聚簇索引)(innodb 主键索引)
  • 非聚集索引 (innodb 非主键索引)

二十五、索引失效场景

where条件里:

  • 等号左边使用了函数,索引会失效
  • 使用不等号,索引会失效
  • 使用了or关键字,索引会失效
  • 使用like查询的时候,%在开头,索引会失效
  • 索引列上做计算
  • 使用 select *

联合索引中:

  • 违背了最左前缀原则,索引会失效

二十六、什么情况下适合建索引

  • 查询条件里的列,才适合建索引
  • 高频率查询语句,查询条件里有的列,优先创建索引
  • 在区分度比较低(比如性别,某某状态)的列,可以考虑和其他的字段组合,创建联合索引

二十七、如何进行SQL优化

在MySQL的my.ini配置文件中添加配置

slow-query-log=1   # 开启慢sql
long_query_time=5  # sql语句执行时间超过5秒的sql,就会被记录到慢sql日志文件中

开启慢查询日志,查看日志中需要优化的SQL语句,通过explain命令,来查询SQL执行计划,可以通过key查看索引命中情况。通过type查看查询类型,一般SQL至少达到range级别,最好是ref就可以了。

二十八、聚簇索引和非聚簇索引

聚簇索引:

  • 也叫聚集索引,在 InnoDB 存储引擎中,主键索引用的就是聚簇索引。它的索引和数据在同一个文件当中,查询不需要回表。

非聚簇索引:

  • 在 InnoDB 存储引擎中,辅助索引(主键索引以外的索引)就是非聚簇索引 ,先到索引文件中查询索引内容ID,如果还需要查询索引内容以外的数据,则需要回表。
  • 在 Myisam 存储引擎只有非聚集索引

二十九、什么是回表

先通过非聚集索引查询出数据所在行,当二级结构树中的数据不满足我们查询所需要的,在根据主键ID查询出数据,这种情况叫回表。

三十、什么是覆盖索引(索引覆盖)

当查询的时候,二级结构树中的数据满足我们查询所需要的,直接返回,不需要回表,这种情况叫索引覆盖也叫覆盖索引

三十一、Redis如何保证原子性

  • Redis中原子性操作命令包括:SETGETINCRDECRHSET等,其中INCRDECR操作的原子性是通过 Redis 服务器的单线程特性实现的,在 Redis 中,所有的命令都是排队执行的,因此对于同一个键的多个操作,Redis 会先处理第一个操作,然后再按照顺序处理其他的操作;
  • 除此之外,Redis 也提供了事务命令来保证数据的原子性。事务命令可以将多个命令打包成一个事务,然后一起执行。在事务执行期间,所有命令都不会被立即执行,而是加入到一个待执行的事务队列中。事务执行的结果要么全部成功,要么全部失败。而每个事务都有相应的 watch(监视) 属性,可以在事务执行之前监视一个或多个键,如果在事务执行期间有其他客户端修改了被监视的键,事务将不会被执行,并返回一个事务执行失败的消息,从而保证事务的原子性

三十二、Redis应用场景

  1. 将数据做缓存
  2. 取最新N个数据的操作(比如典型的取网站最新文章,可以将最新的5000条评论ID放在Redis的list数据类型中,并将超出集合部分从数据库获取)
  3. 排行榜(通过sorted set也叫zset数据类型,进行排序)
  4. 计数器(redis中命令是原子性的,可以使用INCRDECR构建计数器)
  5. 去重操作(通过set数据类型)

三十三、Redis持久化

Redis持久化可以使Redis在重启之后恢复数据,有两种方式:

  • RDB持久化: 将Redis在内存中的数据以快照的形式写入到磁盘中,以保证在系统崩溃或重启时数据的恢复。
  • AOF持久化: 将Redis执行的每一个写命令追加到一个文件中,以日志的形式并保存到磁盘中,以保证在数据出现问题时快速恢复。

Redis默认开启的数据持久化方式为RDB方式

区别:

  • RDB持久化的优点是占用空间小,数据恢复速度快,但是如果Redis因为某种原因在执行快照之前发生崩溃,那么数据将会丢失。
  • 而AOF持久化的优点是可靠性高,每个写操作都会被追加到文件中,恢复数据时只需要重新执行这些命令,所以可以保证数据不会丢失,但是由于需要重新执行文件中记录的所有写命令,所以相对来说占用磁盘空间相对较大、恢复数据速度相对较慢

命令触发RDB快照保存:

Redis可以通过SAVE或者BGSAVE命令手动触发RDB快照保存。SAVEBGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:

  • SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
  • BGSAVE 则 fork(复制) 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求。

如何开启AOF:

修改 redis.xxx.conf 配置文件

appendonly  yes 

三十四、Redis过期策略

惰性删除:

  • 介绍:指Redis在每次运行时不会主动检查数据是否已经过期,而是在Redis客户端获取某个键时才会检查此键是否已过期,如果过期则删除。
  • 优点:减轻Redis的负担,不会因为频繁地检查键是否过期而影响Redis的性能和响应时间。
  • 缺点:有可能会存在过期键未被及时删除的情况,从而导致Redis占用的内存越来越多。

定期删除:

  • 介绍:指Redis每隔一段时间主动检查key是否过期,如果某个key已经过期,则删除。定期删除默认情况下每秒执行 10 次,可以通过修改 Redis 配置文件中的 hz 参数来调整执行频率。
  • 优点:可以及时删除过期键,减少内存占用。
  • 缺点:会对Redis的性能造成一定的影响,因为每次检查都需要遍历一定数量的key,增加了 Redis 的 CPU 使用率。

Redis默认采用惰性删除策略。可以通过配置文件中的maxmemory-policy选项来开启定期删除策略。

三十五、Redis淘汰机制

介绍: 指当Redis的内存空间使用率达到最大值时,为了避免OOM(out of memory)错误,在新的写入操作时需要删除一些数据,以释放部分内存空间。

五种淘汰策略:

  1. volatile-lru:根据键的最近最少使用时间(LRU算法)删除已经过期的键
  2. volatile-ttl:根据键值对的过期时间,把最近即将过期的键删除掉
  3. volatile-random:随机删除一部分已过期键
  4. allkeys-lru:根据LRU算法删除键
  5. allkeys-random:随机删除一部分键

volatile: 表示只在过期键中选取要删除的

allkeys: 表示在所有键(包括已过期和未过期键)中选取要删除的。

在 Redis 的默认配置中,采用的是 volatile-lru 淘汰策略。

三十六、 Redis删除策略

  • DEL 命令: DEL 命令可以删除指定的 key。需要注意,DEL 命令不能删除所有键值对,其中包括正在被使用的键和已经设置过期时间但还没有过期的键。
  • EXPIRE 命令: EXPIRE 命令可以为指定的 key 设置过期时间。当过期时间到达后,Redis 会自动删除该键值对。如果需要取消键的过期时间,可以使用 PERSIST key 命令。
  • TTL 命令: TTL 命令可以查询指定 key 的剩余过期时间。使用 TTL key 命令可以查询 key 剩余的过期时间,如果返回结果为 -1,则表示该 key 没有设置过期时间;如果返回结果为 -2,则表示该 key 已经过期。
  • KEYS 命令: KEYS 命令可以查询符合给定模式的键值对,然后使用 DEL 命令删除这些键值对。需要注意的是,使用 KEYS 命令会比较消耗 Redis 的内存和 CPU 资源,因为 Redis 要扫描整个数据库来查找符合要求的键。在实际应用中,建议使用 SCAN 命令来逐步迭代数据库中的键值对,以减少资源消耗。

注意:Redis 是一个内存数据库,如果需要删除大量的数据,可能会占用大量的内存,导致 Redis 的性能出现问题。因此,在删除大量数据之前,需要评估数据量的大小、内存容量和 Redis 的性能等方面的因素,以便采用合适的策略进行数据删除。

三十七、CAS 和 AQS 的区别

CAS(Compare And Swap)和 AQS(AbstractQueuedSynchronizer) 都是用于实现线程安全的同步控制。

CAS: CAS 是一种基本的原子操作,用于实现无锁的并发控制,它使用比较和交换的方式来实现,当需要修改某个共享变量时,首先比较该变量的值是否等于预期的值,如果等于,则修改该变量的值为新值,并且返回操作成功。如果不等于,则说明其他线程已经修改了该变量,需要重新尝试操作,直至操作成功或达到尝试次数上限。CAS三个步骤(比较当前值、写入新值和返回旧值)。

AQS: AQS 则是 Java 提供的一种基于等待/通知机制的同步工具,它可以用于实现各种同步器(如 ReentrantLock、Semaphore 等),并提供了一些通用的基础实现,如独占锁、共享锁、条件变量等。它的实现基于一个双向链表,多个等待线程将组成一个阻塞队列,线程在等待时会进入阻塞状态,防止占用 CPU 资源,直到某个条件满足时,被唤醒再继续执行。

AQS 的实现使用了 CAS 操作,通过比较和交换的方式来保证线程的互斥性和可见性,从而实现了高效的无锁同步控制。

需要注意的是,CS 和 AQS 都是 Java 并发编程中的重要概念,它们可以通过实现 volatile、synchronized、ReentrantLock 等同步工具来保证线程安全和正确性。在使用时应该结合实际需求和性能特点,选择合适的同步控制方式,并注意避免出现死锁、竞争和性能瓶颈等问题。

三十八、如果Redis挂了的话,分布式锁怎么处理

  1. 设置锁超时时间:在设置 Redis 锁时,可以设置锁的超时时间,当 Redis 挂掉时,锁会在超时时间内自动释放。
  2. 添加守护进程:可以在 Redis 实例上添加一个守护进程,用于监测 Redis 状态,当 Redis 宕机时,守护进程会接管锁的功能,将锁转移到其他 Redis 节点上。
  3. 使用 Redis 集群:使用 Redis 集群方案,可以将锁和数据存储到多个 Redis 节点中,当其中一个节点出现问题时,其他节点会自动接管锁和数据的管理功能。
  4. 异常处理:在分布式锁中加入 Redis 挂掉的异常处理机制,在 Redis 节点挂掉时,可以自动将请求转发到其他可用节点,避免系统操作出现中断。
  5. 使用红锁机制实现分布式锁

三十九、Redis报内存满了怎么办

  1. 查找内存使用情况:首先,您可以使用 Redis 内置的 INFO 命令或其他监控工具来检查 Redis 实例当前的内存使用情况。 如果您使用的是 Redis 4.0 及更高版本,则可以使用 MEMORY USAGE 命令查看每个键使用的内存量。
  2. 确定内存问题的来源:如果 Redis 实例中存储的是大量数据,则您可能需要考虑增加服务器的内存,或者使用 Redis 的分片功能将数据分布到多个 Redis 实例上。
  3. 使用数据淘汰策略:Redis 提供了几种数据淘汰策略,以帮助您管理内存使用情况。例如,您可以使用 LRU(最近最少使用)策略来删除最近最少使用的键,或者使用 TTL(生存时间)策略来删除已过期的键。 您可以使用 CONFIG SET 命令配置 Redis 的淘汰策略。
  4. 增加最大内存限制:Redis 允许您通过 CONFIG SET 命令增加最大内存限制。 但是,这可能会导致 Redis 在达到内存限制时被强制终止。因此,您需要小心地设置最大内存限制,并确保您的 Redis 实例有足够的内存可以处理其当前的工作负载。
  5. 优化 Redis 配置:最后,您可以尝试优化 Redis 实例的配置,例如使用内存压缩或禁用持久性功能来减少内存使用。 但是,这可能会影响应用程序的性能或可靠性,因此需要小心权衡。

四十、数据库双写

介绍: 指的是在分布式环境中使用多个数据库进行数据同步的一种机制,通常用于提高系统的可靠性和容错能力。

实现方式:

  1. 读操作:客户端发起读请求时,主要查询主数据库,并从备份数据库中获取最新的数据作为补充。
  2. 写操作:写请求会同时发送给主数据库和备份数据库,主数据库先行确认成功后才视为成功,生效后再由主数据库将相关修改操作及其结果通知给备份数据库,确保所有数据的一致性。
  3. 数据更新:当主数据库更新数据时,需要同时将其同步到备份数据库中,以保持两边的数据一致。在不失去信号的情况下尽快完成同步,有的时侯可以采取异步或其他延迟处理来减少时间消耗。

优点: 数据库双写机制的好处是使得数据更加的安全和可靠

缺点: 引入了额外的网络通讯和数据存储开销。

建议: 为了避免冲突和矛盾,请确保异步的双写仅仅发生在写压力较小时,若系统的写压力大,则建议增加延迟确认策略;

四十一、SpringBoot的启动流程

  1. 寻找main函数所在类;
  2. 加载SpringApplication类;
  3. 分析应用配置,创建并刷新Spring应用上下文(ApplicationContext);
  4. 扫描应用中的@Bean注解的组件,并将其注册到Spring应用上下文中;
  5. Spring Boot会自动加载@EnableAutoConfiguration注解修饰的配置类,借助条件注解@Conditional按需加载相应的Bean;
  6. 在Spring应用上下文准备好之后,如果检测到Web环境,则创建嵌入式的Tomcat容器,在该容器中运行Spring Boot应用。

四十二、Mybatis的一级二级缓存

  • 一级缓存:

    • 介绍: 是指SqlSession级别的缓存,它的作用范围是SqlSession内部,因此它是SqlSession唯一的缓存。当SqlSession执行查询操作时,查询结果会被存储到一级缓存中,当再次执行相同的查询操作时,会优先从一级缓存中获取数据,而不用查询数据库。
    • 注意: 一级缓存的生命周期是与SqlSession的生命周期相同,也就是说,当SqlSession被关闭时,一级缓存也会被清空。
  • 二级缓存:

    • 介绍: 是指Mapper级别的缓存,它的作用范围是Mapper的namespace级别。当Mybatis执行查询操作时,查询结果会被存储到二级缓存中,当再次执行相同的查询操作时,会优先从二级缓存中获取数据,而不用查询数据库。

    • 注意: 二级缓存的生命周期是与Mapper的生命周期相同,也就是说,当Mybatis应用关闭时,二级缓存也会被清空。

  • 如何开启二级缓存:

  • 介绍: 二级缓存需要配置Mapper的cache属性来启用二级缓存。同时也需要在Mybatis的配置文件中配置二级缓存的相关属性,如cacheType、eviction、flushInterval等。

  • 注意:二级缓存虽然能够提高查询效率,但在应用高并发的情况下,可能会出现数据不一致的问题,因此在使用二级缓存时需要对并发问题进行规避和处理。

四十三、二级缓存介绍

注意: 在MyBatis中,启用二级缓存并不会自动关闭一级缓存。一级缓存和二级缓存是两个独立的缓存机制,它们可以同时使用。

禁用一级缓存: 可以在MyBatis的Mapper XML文件中的<select><update><delete>等元素上添加flushCache="true"属性来禁用一级缓存。这样,每次执行该SQL语句时,都会强制刷新一级缓存。

开启二级缓存

  1. 配置文件:

    mybatis:
      configuration:
        # cache-enabled 为true的话开启二级缓存
        cache-enabled: true
    
  2. 配置参数:在mapper,xml文件中

    <cache eviction="LRU" flushInterval="60000" size="1024" blocking="" readOnly="true" type=""/>
    

    参数介绍:

    1. eviction(缓存淘汰策略):指定缓存中对象的淘汰策略,默认值为LRU(最近最少使用)。其他可选值有FIFO(先进先出)和SOFT(基于软引用的缓存淘汰)。
    2. flushInterval(刷新间隔):指定缓存刷新的时间间隔,单位为毫秒。当设置为一个正整数时,表示定期刷新缓存,刷新间隔为指定的毫秒数。
    3. size(缓存最大容量):指定缓存中最多可以存储的对象数量。当缓存对象数量达到指定的最大容量时,会按照缓存策略进行淘汰。
    4. readOnly(只读缓存):指定缓存中的对象是否只读。如果设置为true,表示缓存中的对象不会被修改,可以提高性能。默认值为false
    5. type(自定义缓存实现):指定自定义的缓存实现类。MyBatis提供了默认的二级缓存实现,但你也可以根据需要自定义缓存实现。自定义缓存实现类必须实现org.apache.ibatis.cache.Cache接口。
    6. blocking(阻塞行为):指定了在获取缓存对象时是否使用阻塞。
      1. true 时,表示获取缓存对象的操作是阻塞的。如果一个线程正在从数据库中加载数据并将其放入缓存中,其他线程在获取相同数据的缓存对象时会被阻塞,直到数据加载完成。这可以确保并发环境下数据的一致性,但也可能导致性能下降。
      2. false 时,表示获取缓存对象的操作是非阻塞的。如果一个线程正在加载数据并将其放入缓存中,其他线程在获取相同数据的缓存对象时会立即返回缓存中的旧值,而不会等待数据加载完成。这可以提高性能,但也可能导致缓存数据的不一致。
      3. 默认情况下,blocking 属性的值为 true,即启用阻塞行为。你可以根据具体的需求和性能要求来决定是否启用阻塞。

开启二级缓存的同时关闭一级缓存:

mybatis:
  configuration:
    # 配置一级缓存作用域 SESSION STATEMENT
    # 设置为 STATEMENT 可以理解为关闭一级缓存
    local-cache-scope: statement

四十四、如何实现线程同步的 HashMap,如何避免 HashMap 死锁

实现线程同步的 HashMap,可以使用 Java 中提供的线程安全的 ConcurrentHashMap。它是一种强化版的 HashMap,支持并发读和高并发写入,能够保证多个线程并发访问时不会发生数据错乱。

避免 HashMap 死锁,需要避免在并行操作中产生环路依赖关系,导致阻塞进程。为了避免死锁,我们可以按照固定顺序获取锁,以避免不同线程获取锁的顺序不同而引起的死锁。也可以使用锁分离技术,将不同部分的数据放在不同的锁中,以减少锁的争用,从而提升并发性能。

总之,在设计并发程序时,需要考虑到操作的顺序、锁的粒度、共享资源的竞争等因素,尽可能地避免死锁和其他并发安全问题。具体实现方式还要结合具体业务场景进行综合折中和优化。

四十五、 Java类型擦除

Java 类型擦除也叫 **伪泛型 ** ,指的是在使用泛型时,该泛型在编译时期被擦除为其原始类型,即泛型类型信息在运行时丢失。这是由于 Java 编译器将泛型类型参数替换为限定该泛型的类型或其父类(如果未显式指定)。

四十六、双亲委派

Java双亲委派是一种类加载机制。它是指在Java虚拟机中,如果要加载一个类,会首先从当前类加载器的父类加载器中查找是否已经加载该类,如果没有,则交给父类加载器去完成类的加载。如果父类加载器还是找不到该类,则继续向上委托给其父类加载器进行查找,直到顶层的引导类加载器。如果顶层的引导类加载器仍然找不到该类,则返回ClassNotFoundException异常。

这样做的好处是可以避免重复加载同一个类,保证类的唯一性和一致性。同时也防止了JDK核心类被篡改的风险,增强了安全性。

总之,Java双亲委派机制可以保证Java应用程序的正常运行,避免出现类冲突和环境污染的问题。

四十七、Java中Set集合如何做到去重

  1. 通过hashCode()equals()方法来判断是否为相同的元素。
  2. 当一个新元素添加到Set集合中时,它会先调用该元素的hashCode()方法得到一个哈希值,然后根据这个哈希值决定该元素在内部存储结构中存储的位置。
  3. 如果两个元素的哈希值不同,则认为它们不相同;
  4. 如果哈希值相同,再比较equals()方法返回的结果,如果也相同,则认为这是同一个元素,此时新元素将不会被添加到Set集合中。

注意: 如果哈希值相同但equals()方法返回false的情况,称之为哈希冲突(Hash Collision)。Java中使用链表或者红黑树的形式解决哈希冲突的问题。

四十八、HashMap和HashTable的区别

  • 线程安全:
    • HashTable是线程安全的,其所有方法都是synchronized的。这意味着在多线程情况下,HashTable的操作是同步的,但是也使得其在性能上有所损失。而HashMap不是线程安全的,需要进行额外的同步操作处理线程安全问题。
  • 键空值:
    • HashTable不允许null键和null值,否则会抛出NullPointerException。而HashMap可以存储null键和null值,但要注意避免在键上出现空指针异常。
  • 迭代器:
    • 迭代HashTable时,其元素的顺序是不确定的,而迭代HashMap时,其元素的顺序是可以被预测的。如有需要,可以使用LinkedHashMap来保留元素的插入顺序或者使用TreeMap来保持元素的自然排序顺序。
  • 容量:
    • HashTable默认的初始大小为11 每次扩充为2n+1
    • HashMap默认的初始化大小是16 每次扩充为2倍

四十九、ArrayList 和 LinkedList 的区别

  • ArrayList底层基于数组实现,它的每个元素占用相同的内存空间,可以根据索引直接访问元素,插入或删除元素时需要移动其他元素的位置,因此,在频繁插入或删除元素时效率较低,但查找元素时效率较高。
  • LinkedList底层是基于双向链表实现的。每个元素存储下一个元素的地址,插入或删除元素时只需要修改相邻元素的地址,因此,在频繁进行插入或删除操作时效率较高,但随机访问元素时效率较低。

五十、MySQL隔离级别?

MySQL的四种隔离级别分别为:

  • 读未提交:
    • 介绍: 在数据库并发控制中存在的一种问题,也称为"脏读"。即一个事务读取到另一个事务未提交的数据,可能导致数据不一致性的问题。为了避免"读未提交"的问题,通常采用锁机制或MVCC(多版本并发控制)等技术来保证数据的一致性。
  • 读已提交:
    • 介绍: 指当一个事务对数据完成更新操作并提交后,其他事务才可以读取到这些更新后的数据。该隔离级别能够避免脏读、不可重复读等问题,但无法避免幻读问题。在该隔离级别下,不同事务之间的操作仍然是并发执行的,且每个事务看到的数据可能是最新的,也可能是过期的,因此需要在编写应用程序时谨慎考虑相关问题。
  • 可重复读:
    • 介绍: 保证了在一个事务中多次查询同一数据时,能够得到同样的结果。也就是说,在可重复读隔离级别下,如果在一个事务中多次执行同一个查询语句,不会出现数据被其他事务修改的情况。
    • 实现: 为了实现可重复读隔离级别,MySQL在每个事务开始时会创建一个视图(View),用于记录当时数据库中的数据状态,当事务结束后,这个视图也会被销毁。因此,即使其他事务对同一个表做了修改,也不会影响当前事务的视图。
    • **问题:**虽然可重复读可以解决幻读问题(即在一个事务内多次查询同一个范围的数据,但结果却不一致),但是它也存在一些问题,例如会导致长时间占用事务和锁资源,增加系统负担。因此,在使用可重复读隔离级别时,需要根据具体场景进行权衡。
  • 串行化:
    • 介绍: 将多个并发事务按照一定的顺序执行,使得它们的执行结果与以串行的方式执行相同,保证了数据的一致性和正确性。在MySQL中,可以通过设置不同的隔离级别来控制事务的并发情况,从而实现事务的串行化。
    • 问题: 但需要注意,在高并发的环境下,严格的串行化可能会带来较大的性能开销,需要根据具体场景进行权衡和选择。

您可以使用SET TRANSACTION命令来设置隔离级别。如果不指定任何隔离级别,则默认为可重复读。

五十一、MySQL的锁机制

MySQL中的锁机制是为了保证多个会话对同一数据进行访问时的安全性和并发性能。

根据使用方式划分:

  • 悲观锁: 也称为独占锁,是指事务在对数据进行操作之前,先获取该数据上的排他锁,确保其他事务不能修改该数据。在事务完成操作后会主动释放锁。
    • 优点:可以有效地避免数据出现脏读、不可重复度等问题,能够保证系统的数据一致性。适合于处理高并发系统,在并发量比较大的情况下依然能够保证数据的唯一性,在保证数据一致性方面有更好的效果。
    • 缺点:锁的粒度较大,容易降低并发性能,阻塞其他事务对相同资源的访问,从而导致死锁问题。锁定时间过长,会阻塞其他事务的执行,从而降低系统的吞吐量和性能。受限于数据库管理系统实现方式的不同,可能出现各种锁规则的差异,应用程序需要针对不同的数据库进行针对性的优化。
  • 乐观锁: 是指事务在对数据进行操作之前,先检查这些数据没有被其他事务修改过,然后再执行操作。常用的乐观锁实现方式包括版本号机制CAS算法等。
    • 优点:是能够提高并发性能和系统吞吐量,减少锁等待的时间和死锁风险。
    • 缺点:是容易出现ABA问题,需要额外维护版本号或者使用高并发下的线程安全的数据结构。
    • 注意:乐观锁适用于并发冲突较小的场景,在高并发、大量写操作的情况下可能会导致效率降低,因此需要根据具体场景进行选择。

根据粒度划分:

  • 行锁:

    • 介绍: 是MySQL的一种粒度更细的锁,它可以将锁定范围限制在数据库表中的具体行上。

    • 优点: 相对于表级锁来说,行锁的并发能力更强,可以有效地避免死锁问题,并且可以提高系统的吞吐量和性能。

    • 缺点: 使用行锁的同时也有可能会产生死锁问题。

    • 注意:在进行数据操作时应尽量减少锁的持有时间,避免事务嵌套等操作,从而有效地降低死锁风险。同时,也可以通过设置ISOLATION LEVEL参数来调整锁定级别,以满足不同场景下的需求。

    • 实现方式:

      • 表共享读锁:允许多个事务同时读取同一张表,但防止其他事务同时对该表进行写操作。

        • 注意:如果某个事务想要修改一张表上的数据,则必须先等待该表的所有共享读锁都被释放后,再以排它锁的方式进行操作。否则,该事务会被阻塞在锁等待队列中,直到其他事务释放共享读锁。
      • 表独占写锁:对整个表进行加锁,防止其他事务对该表进行任何修改或添加数据,直到当前锁被释放后才可正常操作。使用LOCK TABLES语句指定表名和访问模式

        • 注意:一旦获取了表独占写锁,其他用户就无法对该表进行任何修改操作,包括SELECT查询语句也不能执行,直到该锁被显式地释放或者当前事务提交后才可以

        •   # 获取mytable表的排他写锁
            LOCK TABLES mytable WRITE;
          
  • 表锁:

    • 介绍: 是针对整张数据表进行加锁。当一个事务在对一张表进行操作时,会自动获得该表的表级锁,在锁定期间其他事务只能对该表进行读取操作。
    • 优点: 简单易用,适用于对整个表进行操作的场景。
    • 缺点: 锁定范围较大,容易造成并发性能瓶颈,并且可能会出现死锁问题,降低系统的吞吐量和性能。
    • 实现方式:
      • 共享锁: 又称为读锁,其他事务可以同时获取同一份数据的共享锁,但不能获取排它锁,也不能在已经加共享锁的数据上再加排它锁。只有当共享锁全部释放后,才能进行排它锁的获取与更新操作。共享锁的目的是为了预防并发查询期间数据修改,提高并发性能。
      • 排它锁: 又称为写锁,其他事务无法获取相应数据的锁,假如一个事务已经对某份数据获取了排它锁,则其他事务无法对该份数据进行任何形式的锁定和访问。排它锁的目的是为了防止数据并发问题,在读写交替进行时提供数据一致性保障。
      • 意向共享锁和意向排他锁: 是InnoDB存储引擎中的一种特殊锁机制。主要用于辅助InnoDB存储引擎在未获得行级锁时进行加锁优化,降低死锁风险。
        • 当一个事务需要给某行数据加行级锁时,会先向上获取该表和库级别的意向排它锁。这样做可以避免当前事务与其他会话获取该表和库的排它锁产生冲突。
        • 如果表上已经存在排它锁,则该事务加锁失败;如果不存在排它锁,则该事务再向下获取该行数据的锁,如果锁定成功,则将其释放之前添加的意向锁。同理,如果当前事务想给一张表加一个共享锁,它会先试图取得一个表级别的 IS 意向锁,如果插入操作则会自动升级为 IX 排它锁以保证互斥性。
        • 因此,意向锁只有在表格整体加锁的时候才能发挥效用,通过意向锁,MySQL依据类型管理了系统资源的申请和释放,从而使空间索引并发控制更加高效和稳定。
  • 页级锁:

    • 介绍: 数据库将整张表划分为一个个固定大小的数据页,对每个数据页进行加锁处理。当事务需要修改某个数据时,会首先申请数据所在页的独占锁,并在修改操作完成后释放该锁,防止其他事务同时访问同一页中的数据。而当事务只需要读取数据时,则可以使用共享锁,多个事务可以同时读取同一页中的数据。
    • 优点:
      • 与行锁相比:页级锁锁的粒度更大,可以避免过多的锁竞争,降低锁冲突的概率,从而提高系统并发能力;
      • 与表锁相比:页级锁锁的粒度更小,能够避免表锁存在的同步等待问题,同时也不会让系统出现死锁问题。
    • 缺点: 在使用页锁锁定大量连续页的情况下,会造成锁冲突的概率增大、锁等待时间增加,从而影响数据库的并发执行效率和性能。此外,页级锁还容易产生锁粒度过程大或过小的问题,影响系统的性能表现。
  • 元数据锁:

    • 介绍: 用于保护数据库内部的元数据信息,如表结构、字段属性、索引信息等。这些元数据在执行DDL语句(如ALTER TABLE, DROP INDEX等)时需要进行修改,因此需要使用元数据锁保证其一致性、准确性和安全性。元数据锁可以被理解为一个读写锁,当有事务对元数据进行读操作时会加上共享锁,允许其他事务继续进行读取操作;而当有事务对元数据进行写操作时,则会加上排他锁,阻止其他事务对该元数据的读写处理,直到当前事务提交或回滚后才释放锁
    • 实现方式: 元数据锁的具体实现方式涉及到InnoDB存储引擎底层的事务管理机制,通过如下语句查看当前系统所有的元数据锁信息:SHOW ENGINE INNODB STATUS
    • 注意:过度的元数据锁可能会带来性能问题,特别是在高并发的情况下。因此,尽量避免长时间占用元数据锁,以提高系统的性能和并发能力
  • 全局锁:

    • 介绍: 也称为全局只读锁或全局排它锁。当应用程序需要对整个MySQL实例进行维护或备份时,会使用全局锁来实现数据的一致性和安全性。在加上全局锁后,系统内任何数据修改操作都将无法进行,直到当前进程释放该锁为止。因此,在加锁期间系统是只读的,除了当前进程以外的其他线程和进程都无法做任何更新操作,包括对表和数据的查询和修改。
    • 实现方式: 获取全局锁 FLUSH TABLES WITH READ LOCK;
    • 注意: 加上全局锁会导致MySQL实例处于只读状态,所有更新操作都被阻塞,因此必须谨慎使用。另外,在释放全局锁之前,需要确保所有的数据备份、同步等维护工作已经完成,否则可能会导致数据不一致或流量损失等问题。

两阶段锁定协议: MySQL的锁机制采用两阶段锁定协议(Two-Phase Locking Protocol),这种协议将事务分为两个阶段:加锁阶段和释放锁阶段;

  • 加锁阶段: 从事务开始到执行操作前,所有涉及的数据对象都必须被加上适当的锁,MySQL自动完成该过程。
  • 释放锁阶段: 事务进行到一定程度后,需要将之前获得的锁释放掉,让其他等待锁的线程继续进行操作。MySQL同样自动完成该过程。

五十二、ConcurrentHashMap和HashMap的区别

  • 线程安全性:
    • ConcurrentHashMap是线程安全的,而HashMap是非线程安全的。ConcurrentHashMap的实现利用了分段锁技术,将整个数据结构分成多个段(Segment),每个线程只能锁住其中一个段进行访问,从而实现了并发的对象更新。
    • HashMap的实现没有进行分段,因此在多线程环境下需要开发者自己实现线程同步控制。
  • 性能:
    • 在多线程的情况下,ConcurrentHashMap的性能要比HashMap好很多。因为ConcurrentHashMap的分段锁技术可以使不同段的数据被不同的线程锁住,从而大大减小了锁竞争的概率。而HashMap在多线程访问时会存在锁竞争问题,性能会受到严重的影响。
  • 键值对的顺序:
    • HashMap不保证键值对的顺序,而ConcurrentHashMap在不涉及扩容和重新哈希(rehashing)的情况下可以预测到键值对的顺序。事实上,ConcurrentHashMap的某些迭代器可以按照预测的顺序进行遍历。
  • 初始化容量和加载因子:
    • ConcurrentHashMap则比HashMap多了两个参数:初始容量和加载因子,其中初始容量既可以指定,也可以使用默认值;而加载因子则比HashMap大,默认值为0.75。

五十三、ConcurrentHashMap

  • 介绍: ConcurrentHashMap是Java的一个线程安全的哈希表实现类,它与普通的HashMap的区别在于ConcurrentHashMap支持并发访问,多个线程可以同时读取和写入它的数据,而不需要额外的同步措施。
  • 主要实现原理:
    • 分段锁: ConcurrentHashMap内部维护了一个Segment数组。每个Segment本质上就是一个小型的HashMap,它们之间相互独立,因此支持多个线程同时读写不同的Segment,从而实现了更高的并发性。
    • CAS操作: ConcurrentHashMap中对于同一个Segment内的数据读写操作仍然需要同步,但是使用了比传统的锁机制更轻量级的CAS(Compare And Swap)操作,从而提高了并发性能。
    • 重新分配: 当ConcurrentHashMap中的某一个Segment的容量达到了一个阈值时,这个Segment中的数据会被重新分配到一个新的更大的Segment中,从而避免了整个ConcurrentHashMap需要重新调整大小和复制数据的代价。

五十四、HashMap什么时候进行二次哈希

当HashMap产生Hash冲突的时候,并且当前位置对应的链表长度大于8的时候,就会触发二次哈希,以寻找一个新的槽位来存储当前元素。

五十五、何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?

只要两个元素的key计算的hashcode值相同就会发生hash碰撞,jdk8之前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞

五十六、ArrayList如何进行扩容

ArrayList内部是一个数组,初始化长度为10。添加元素的时候,会检查数组的长度,如果数组满了就会对数组进行扩容。

new一个新的数组,长度为原来数组长度的2倍。然后将原数组的元素拷贝到新数组中,在将新数据放到数组指定位置。

五十七、HashMap为什么线程不安全

  • 多线程下扩容死循环。JDK1.7中的HashMap使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题
  • 多线程的put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同的,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。此问题在JDK1.7和JDK1.8中都存在
  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题,此问题在JDK1.7和JDK1.8中都存在权协议,转载请附上原文出处链接及本声明。

五十八、使用select * 查询一定慢吗

在数据库中执行 SELECT * 查询并不一定会慢。查询的速度取决于多个因素,包括表的大小索引的使用查询条件的复杂性以及数据库服务器的性能等。

然而,使用 SELECT * 查询可能会导致一些潜在的性能问题和不必要的开销:

  1. 返回大量数据:如果表中包含大量的列或者有大量的行,执行 SELECT * 查询将返回大量的数据。在网络传输和结果集处理方面,这可能导致额外的开销和延迟。此外,如果应用程序只需要一部分列的数据,选择性地指定所需的列可能会更高效。
  2. 未使用索引:如果没有适当的索引支持 SELECT * 查询,数据库服务器可能需要执行全表扫描,这将导致查询速度变慢。在这种情况下,创建适当的索引可以显著提高查询性能。
  3. 冗余或不必要的列:SELECT * 查询会返回表中的所有列,包括可能是冗余或不必要的列。如果查询结果中包含了不需要的列,这将浪费数据库服务器的资源和带宽。

因此,在实际应用中,最好根据需要选择特定的列,并使用适当的索引来优化查询性能。只选择所需的列可以减少数据传输和处理的开销,并且可以更好地利用数据库服务器的资源。

五十九、线程之前如何共享数据

  • 共享内存: 多个线程之间可以通过共享内存区域来访问和修改数据,线程可以使用指针或者引用来直接读取和写入共享内存中的数据。在使用共享内存的时候,需要采取适当的同步机制,比如使用互斥锁来确保线程之间共享数据是同步和有序的
  • 消息传递: 线程可以通过消息传递机制来共享数据。每个线程都有自己的消息队列,线程可以将数据打包成消息发送到其他线程的队列中,其他线程可以从队列中接收并处理消息。消息传递可以使用同步机制来确保消息的正确交换和处理。
  • 全局变量: 线程可以通过访问共享的全局变量来共享数据。全局变量是在所有线程中可见的变量,线程可以读取和修改这些变量的值。为了避免竞争条件,需要使用互斥锁等同步机制来保护全局变量的访问。
  • 数据结构: 线程可以通过共享数据结构来共享数据。多个线程可以同时访问和修改数据结构中的数据。为了确保线程安全,需要使用适当的同步机制来保护对数据结构的访问。

在进行线程间数据共享时,需要注意以下几点:

  • 同步: 为了避免竞争条件和数据不一致性,需要使用适当的同步机制来保护共享数据的访问。常见的同步机制包括互斥锁、信号量、条件变量等。
  • 原子操作: 对于共享数据的更新操作,应该使用原子操作来确保操作的原子性。原子操作是不可分割的操作,要么完全执行,要么不执行。
  • 数据一致性: 在线程间共享数据时,需要确保数据的一致性。如果多个线程修改同一个数据,需要考虑并发访问的顺序和操作的正确性,以避免数据不一致的情况发生。
  • 死锁: 在使用同步机制时,需要注意避免死锁的发生。死锁是指两个或多个线程相互等待对方释放资源而无法继续执行的情况。

六十、Java内存模型(共享内存)

介绍: Java内存模型定义了线程和主内存之间的关系,在多线程中,每个线程都有一个副本(也称为工作内存或者本地内存),其存储了该线程读/写共享变量的副本,线程操作的都是副本数据,副本和主内存的变量之间可能会存在短暂的不一致。

方式: 线程之间通信一般用两种方式:消息传递和共享内存

共享内存主要有三个关注点: 可见性、有序性、原子性。Java内存模型(JVM)解决了可见性有序性的问题,而锁解决了原子性的问题。

可能会存在的问题: 一个线程修改了变量,另一个线程再去读取这个变量,有可能读不到修改后的值

解决: 使用volatile修饰变量,保证变量的可见性和有序性。

可见性: 一个线程修改了值后,另一个线程去读取这个变量的值,能读到最新值

有序性: 实际代码执行的过程,并不一定完全按照源代码的顺序去执行。这个叫做重排序,对volatile修饰的变量进行读和写时,编译器会在读和写的地方添加内存屏障,避免重排序

六十一、Springboot除了crud还可以做什么

  • Web应用程序开发: Spring Boot可以用于构建强大的Web应用程序RESTful API。它集成了Spring MVC框架,提供了简化的方式来处理HTTP请求和响应、路由URL、处理表单数据、编写验证逻辑等。同时,它还支持使用各种模板引擎(如Thymeleaf、FreeMarker)进行动态页面渲染。
  • 安全性和身份验证: Spring Boot提供了Spring Security框架的集成,可以轻松实现身份验证、授权和安全保护。它支持各种身份验证方式(如基于表单的身份验证、基于令牌的身份验证等),并提供了对角色和权限的细粒度控制。
  • 数据访问和持久化: Spring Boot集成了Spring Data JPA、Spring Data JDBC等模块,可以与各种数据库进行交互。它简化了数据访问层的开发,提供了强大的查询和事务管理功能。
  • 缓存: Spring Boot提供了对多种缓存解决方案的支持,如Ehcache、Redis等。通过简单的配置,可以使用缓存来提高应用程序的性能和响应速度。
  • 消息队列和异步处理: Spring Boot整合了Spring AMQP、Spring Kafka等模块,可以轻松地与消息队列(如RabbitMQ、Apache Kafka)进行集成。这使得应用程序能够实现异步处理、事件驱动架构等。
  • 批处理: Spring Boot提供了Spring Batch框架的支持,可以方便地处理大量的批处理任务,如数据导入、报表生成等。它提供了高度可配置的作业定义和执行,支持事务管理、故障处理和监控等功能。
  • 定时任务: Spring Boot内置了对定时任务的支持,可以轻松地创建和管理定时任务。通过使用注解或XML配置,可以定期执行特定的任务或方法。
  • 微服务开发: Spring Boot与Spring Cloud等框架的集成使得构建和管理微服务架构变得更加简单。它提供了服务发现负载均衡熔断降级等功能,可以实现分布式系统的构建和管理。

六十二、如何判断哪些对象可以作为垃圾被回收?

1. 引用计数法

这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器加一;当引用失效时,计数器减一。任何时刻计数器为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。

2. 可达性分析法

这个算法的基本思想是通过一系列称为"GC Roots"的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,如果不在引用链上(即GC Roots到对象不可达时),则证明此对象是不可用的。

那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:

  • 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象。

  • 方法区中的类静态变量属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI(Native方法)引用的对象。

六十三、垃圾收集算法

  • 标记-清除(Mark-Sweep)算法:
    • 介绍: 首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
    • 不足: 这种算法的不足主要体现在效率和空间,从效率的角度讲,标记和清除两个过程的的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收动作。
  • 复制(coping)算法:
    • 介绍: 复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次性清理掉。这样每次只需要对整个半区进行内存回收,
    • 不足: 复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。
  • 标记-整理(Mark-Compact)算法:
    • 介绍: 过程与标记-清除算法一样,不过不是直接对可回收对象进行整理,而是让所有存活对象都向一端移动,然后清理掉边界以外的内存。
  • 分代收集算法:
    • 介绍: 目前虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法结合了以上的内容,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担当的(老年代),采用标记-清理算法或者标记-整理算法。

六十四、Mybatis什么时候需要开启或关闭一二级缓存

开启一级缓存的场景:

  • 需要提高相同 SqlSession 内多次相同查询的性能: 如果在同一个 SglSession 中多次执行相同的查询语句,开启一级缓存可以避免重复查询数据库,提高性能。
  • 查询操作频繁,而且数据不经常变动: 如果应用场景中的查询操作频繁,而数据变动较少,可以开启一级缓存以减少数据库查询的次数,提高系统的响应速度。
  • 多个操作依赖同一查询结果: 如果多个操作依赖于同一个查询结果,开启一级缓存可以确保这些操作共享相同的结果,;避免重复查询,提高效率

关闭一级缓存的场景:

  • 数据更新频繁: 如果应用中的数据更新频繁,开启一级缓存可能导致缓存中的数据过期1从而出现脏数据的问题。在这种情况下,可以考虑关闭一级缓存,以确保查询结果的准确性。
  • 对数据强一致性要求较高: 如果应用对数据的强一致性要求较高,即使查询的是同一条数据,也需要确保获取到的是最新的数据,可以关闭一级缓存。

开启二级缓存的场景:

  • 多个SqlSession 之间需要共享查询结果: 如果应用中有多个 SlSession 实例,且这些实例之间需要共享查询结果,可以开启二级缓存,以减少重复查询数据库的开销,提高性能。
  • 数据更新较少,且需要跨 SqlSession 共享查询结果: 如果应用中的数据更新较少,而且多个 SqlSession 之间需要共享查询结果,开启二级缓存可以提高查询性能。

关闭二级缓存的场景:

  • 数据更新频繁: 如果应用中的数据更新频繁,开启二级缓存可能导致缓存中的数据过期从而出现脏数据的问题。在这种情况下,可以考虑关闭二级缓存,以确保查询结果的准确性。
  • **对数据强一致性要求较高:**如果应用对数据的强一致性要求较高,即使查询的是同一条数据,也需要确保获取到的是最新的数据,可以关闭二级缓存。

六十五、定时任务有几种类型

  • 周期性定时任务 (Fixed-Rate) : 这种类型的定时任务按照固定的时间间隔执行,不考虑1任务的执行时间。无论上一次任务是否完成,下一次任务都会按照指定的时间间隔触发执行。
  • 固定延迟定时任务 (Fixed-Delay): 这种类型的定时任务在上一次任务执行完成后,等待固定的时间间隔后再次触发执行。任务的执行时间不影响下一次任务的触发时间
  • Cron 表达式定时任务: Cron 表达式是一种灵活而强大的定时任务表达方式,可以精确控3.制任务的执行时间。Cron 表达式由多个时间字段组成,通过指定秒、分钟、小时、日期.月份和星期等信息来定义任务的触发时间。
  • 单次定时任务: 这种类型的定时任务只会执行一次,任务在指定的时间点触发执行后就结束,不会再重复执行。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

桂秋拾貳.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值