那些年遇到的问题

122 阅读21分钟

1. Stream 转化为map里的key为null会报错吗? 如果最后不collect 前面的处理还会执行吗?

在使用 Collectors.toMap 时,如果 key 为 null,会导致 NullPointerException。这是因为 HashMap 不允许 null 键(在某些实现中可能允许,但在 Collectors.toMap 中会有问题)。

如果 key 属性可能重复,Collectors.toMap 会抛出 IllegalStateException。可以通过提供一个合并函数来解决这个问题,例如 (existing, replacement) -> existing 保留现有值。

惰性求值的实现

  1. 中间操作的惰性求值

    • 中间操作(如 filtermap 等)不会立即对数据进行处理,而是将操作封装成一个 Pipeline 对象,并返回一个新的 Stream。
    • 每个 Pipeline 对象包含一个对数据进行处理的函数(如 Predicate 或 Function),以及对下一个 Pipeline 对象的引用。
  2. 操作链的构建

    • 每次调用中间操作时,都会创建一个新的 Pipeline 对象,并将其链接到前一个 Pipeline 对象上,形成一个操作链。
  3. 终端操作的触发

    • 终端操作(如 collectforEach 等)会触发整个操作链的执行。

    • 终端操作会遍历数据源,并依次调用每个 Pipeline 对象中的处理函数,对数据进行处理。

2. Spring AOP里代理类调用自己方法会是失效,有什么方法可以实现这个吗?

  • AspectJ:使用 AspectJ的@Configurable 注解 进行编译时织入或加载时织入,可以解决自调用问题。
  • 应用上下文:通过 ApplicationContext 获取代理对象,适用于 Spring AOP 的动态代理。
  • AopContext:使用 AopContext 获取当前代理对象,适用于 CGLIB 代理。
public void callA() {
        // 通过应用上下文获取代理对象
        A proxy = applicationContext.getBean(A.class);
        proxy.a(); // 这将触发 AOP 代理
    }
    
 
public void callA() {
        // 使用 AopContext 获取代理对象
        ((A) AopContext.currentProxy()).a(); // 这将触发 AOP 代理
    }

3. Springboot自动装配的原理?

javaguide.cn/system-desi…

SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。
自 Spring Boot 3.0 开始,自动配置包的路径从META-INF/spring.factories 修改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Spring Boot 自动装配的原理主要依赖于 @EnableAutoConfiguration 注解和 spring.factories 文件。

  1. 核心注解 @EnableAutoConfiguration

    • @EnableAutoConfiguration 是实现自动装配的核心注解。它通过引入 AutoConfigurationImportSelector 类,加载所有符合条件的自动配置类。
  2. AutoConfigurationImportSelector 类

    • AutoConfigurationImportSelector 实现了 ImportSelector 接口,其 selectImports 方法用于获取所有需要自动装配的类的全限定类名,并将这些类加载到 Spring IoC 容器中。
  3. spring.factories 文件

    • spring.factories 文件位于 META-INF 目录下,定义了自动配置类的全限定名。Spring Boot 启动时会扫描类路径中的所有 spring.factories 文件,并加载其中定义的自动配置类。
  4. 条件注解

    • Spring Boot 使用一系列条件注解(如 @ConditionalOnClass@ConditionalOnBean@ConditionalOnProperty 等)来控制自动配置类的加载。这些条件注解确保了自动配置的灵活性和按需加载。

总结:Spring Boot 自动装配通过 @EnableAutoConfiguration 注解和 spring.factories 文件,实现了按需加载和灵活配置,极大地简化了应用程序的配置和开发过程。

4. 分布式系统中2pc和3pc有何区别,2pc有什么缺点

1.两阶段提交

两阶段提交(Two-Phase Commit,简称 2PC)是一种分布式事务协议,确保所有参与者在提交或回滚事务时都处于一致的状态。2PC 协议包含以下两个阶段:

  1. 准备阶段(prepare phase):在这个阶段,事务协调者(Transaction Coordinator)向所有参与者(Transaction Participant)发出准备请求,询问它们是否准备好提交事务。参与者执行所有必要的操作,并回复协调者是否准备好提交事务。如果所有参与者都回复准备好提交事务,协调者将进入下一个阶段。如果任何参与者不能准备好提交事务,协调者将通知所有参与者回滚事务。
  2. 提交阶段(commit phase):在这个阶段,如果所有参与者都已准备好提交事务,则协调者向所有参与者发送提交请求。参与者执行所有必要的操作,并将其结果记录在持久性存储中。一旦所有参与者都已提交事务,协调者将向它们发送确认请求。如果任何参与者未能提交事务,则协调者将通知所有参与者回滚事务。

2PC 协议可以确保分布式事务的原子性和一致性,但是其效率较低,可能会出现阻塞等问题。因此,在实际应用中,可以使用其他分布式事务协议,如 3PC(Three-Phase Commit)或 Paxos 协议来代替。

两阶段提交问题

两阶段提交存在以下几个问题:

  • 同步阻塞问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。也就是说从投票阶段到提交阶段完成这段时间,资源是被锁住的。
  • 单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
  • 数据不一致问题:在 2PC 最后提交阶段中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了 commit 请求。而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交,于是整个分布式系统便出现了数据不一致性的现象。

2.三阶段提交

三阶段提交(Three-Phase Commit,简称3PC)是在 2PC 协议的基础上添加了一个额外的阶段来解决 2PC 协议可能出现的阻塞问题。3PC 协议包含三个阶段:

  • CanCommit 阶段(询问阶段):在这个阶段,事务协调者(Transaction Coordinator)向所有参与者(Transaction Participant)发出 CanCommit 请求,询问它们是否准备好提交事务。参与者执行所有必要的操作,并回复协调者它们是否可以提交事务。
  • PreCommit 阶段(准备阶段):如果所有参与者都回复可以提交事务,则协调者将向所有参与者发送PreCommit 请求,通知它们准备提交事务。参与者执行所有必要的操作,并回复协调者它们是否已经准备好提交事务。
  • DoCommit 阶段(提交阶段):如果所有参与者都已经准备好提交事务,则协调者将向所有参与者发送DoCommit 请求,通知它们提交事务。参与者执行所有必要的操作,并将其结果记录在持久性存储中。一旦所有参与者都已提交事务,协调者将向它们发送确认请求。如果任何参与者未能提交事务,则协调者将通知所有参与者回滚事务。

与 2PC 协议相比,3PC 协议将 CanCommit 阶段(询问阶段)添加到协议中,使参与者能够在 CanCommit 阶段发现并解决可能导致阻塞的问题。这样,3PC 协议能够更快地执行提交或回滚事务,并减少不必要的等待时间。需要注意的是,与 2PC 协议相比,3PC 协议仍然可能存在阻塞的问题。

3.两阶段提交 VS 三阶段提交

2PC 和 3PC 是分布式事务中两种常见的协议,3PC 可以看作是 2PC 协议的改进版本,相比于 2PC 它有两点改进:

  1. 引入了超时机制,同时在协调者和参与者中都引入超时机制(2PC 只有协调者有超时机制);
  2. 3PC 相比于 2PC 增加了 CanCommit 阶段,可以尽早的发现问题,从而避免了后续的阻塞和无效操作。

也就是说,3PC 相比于 2PC,因为引入了超时机制,所以发生阻塞的几率变小了;同时 3PC 把之前 2PC 的准备阶段一分为二,变成了两步,这样就多了一个缓冲阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

Full GC触发条件

  1. 老年代(Old Generation)空间不足

    • 当老年代的可用空间不足以容纳新对象或晋升对象时,会触发 Full GC。
    • 例如,大量对象在年轻代经过多次 Minor GC 后晋升到老年代,导致老年代空间不足。
  2. 永久代(Permanent Generation)或元空间(Metaspace)空间不足

    • 在 JDK 7 及之前版本中,永久代用于存储类元数据。如果永久代空间不足,会触发 Full GC。
    • 从 JDK 8 开始,永久代被移除,取而代之的是元空间(Metaspace)。如果元空间不足,也会触发 Full GC。
  3. 调用 System.gc() 或 Runtime.getRuntime().gc()

    • 手动调用 System.gc() 或 Runtime.getRuntime().gc() 会建议 JVM 执行 Full GC。虽然这是一个建议,但 JVM 通常会执行 Full GC。
  4. Promotion Failure

    • 在 Minor GC 过程中,如果年轻代的存活对象无法晋升到老年代(因为老年代空间不足),会触发 Full GC。

Survivor区域对象晋升到老年代有两种情况:

  • 一种是给每个对象定义一个对象计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置。
  • 另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代,在这批对象在统计后发现可以晋升到老年代,但是发现老年代没有足够的空间来放置这些对象,这就会引起Full GC。
  1. Concurrent Mode Failure

    • 在使用 CMS(Concurrent Mark-Sweep)垃圾收集器时,如果并发标记和清除阶段未能及时完成,导致老年代空间不足,会触发 Full GC。
  2. 堆碎片

    • 当堆内存中存在大量碎片,导致无法为大对象分配连续内存块时,可能会触发 Full GC 以尝试整理碎片。

MYSQL命中多个索引时如何选择

MySQL优化器一般会通过比较扫描行数、查询成本等因素,选择一个最优的索引来执行查询。 通过 EXPLAIN 可以看到 possible_keys 列中列出了可能使用的索引,而 key 列则表示实际使用的索引。rows显示了扫描的行数 

DXScreenshot-20250210-113238.png

在业务开发中,可以通过force index 显示的指定查询索引

Dubbo 的原理以及其核心组件的作用

image.png

核心组件及其作用

  1. Provider(服务提供者)

    • 作用:服务提供者是 Dubbo 中的一个核心组件,负责提供具体的服务实现。它在启动时会将自己提供的服务注册到注册中心,以便消费者能够发现和调用。

    • 工作原理

      • 启动时,Provider 会将服务的元数据信息(如服务接口、版本、地址等)注册到注册中心。
      • Provider 会监听注册中心的指令(如服务下线、配置变更等),以便动态调整服务状态。
      • Provider 通过网络协议(如 Dubbo 协议、HTTP 协议等)接收消费者的远程调用请求,并返回执行结果。
  2. Consumer(服务消费者)

    • 作用:服务消费者是 Dubbo 中的另一个核心组件,负责调用远程服务。它通过注册中心获取服务提供者的地址列表,并通过负载均衡策略选择合适的服务提供者进行调用。

    • 工作原理

      • 启动时,Consumer 会从注册中心订阅自己所需的服务列表,并缓存服务提供者的地址信息。
      • Consumer 会根据负载均衡策略(如随机、轮询、一致性哈希等)选择合适的服务提供者进行调用。
      • Consumer 通过网络协议发送远程调用请求,并接收服务提供者返回的结果。
  3. Registry(注册中心)

    • 作用:注册中心是 Dubbo 中的关键组件,负责服务的注册与发现。它维护着服务提供者和消费者的映射关系,确保服务消费者能够动态发现服务提供者。

    • 工作原理

      • 注册中心接收服务提供者的注册请求,将服务的元数据信息存储在注册中心。
      • 注册中心接收服务消费者的订阅请求,并返回相应的服务提供者列表。
      • 注册中心监听服务提供者和消费者的状态变化(如上线、下线、配置变更等),并将变更通知给相关的服务消费者。

Dubbo 的调用过程

  1. 服务注册:服务提供者在启动时,将服务信息注册到注册中心。
  2. 服务订阅:服务消费者在启动时,从注册中心订阅所需的服务列表。
  3. 服务发现:消费者根据从注册中心获取的服务列表,选择合适的服务提供者。
  4. 远程调用:消费者通过网络协议调用服务提供者的服务,并接收返回结果。
  5. 动态调整:注册中心根据服务提供者和消费者的状态变化,动态调整服务的注册和订阅关系。

总结

  • Provider:提供具体的服务实现,负责服务的注册和远程调用的处理。
  • Consumer:调用远程服务,负责服务的订阅和负载均衡。
  • Registry:维护服务的注册与发现,确保消费者能够动态发现提供者。

spring 中解决循环依赖为什么要用三级缓存,二级为什么不行呢?

A,B循环依赖,先初始化A,先暴露一个半成品A,再去初始化依赖的B,初始化B时如果发现B依赖A,也就是循环依赖,就注入半成品A,之后初始化完毕

B,再回到A的初始化过程时就解决了循环依赖,在这里只需要一个Map能缓存半成品A就行了,也就是二级缓存就够了,但是这个二级缓存存的是Bean对

象,如果这个对象存在代理,那应该注入的是代理,而不是Bean,此时二级缓存无法及缓存Bean,又缓存代理,因此三级缓存做到了缓存 工厂 ,也就是生成代理,这我的理解

:总结起来:二级缓存就能解决缓存依赖,三级缓存解决的是代理。

1. 三级缓存机制

Spring使用三级缓存来管理Bean的创建过程,以解决循环依赖问题。三级缓存包括:

  1. 一级缓存(singletonObjects)

    • 存储完全初始化好的单例Bean实例。
    • 这是我们通常获取Bean时访问的缓存。
  2. 二级缓存(earlySingletonObjects)

    • 存储早期暴露的Bean实例,主要用于解决循环依赖。
    • 在Bean创建过程中,将实例化但未完全初始化的Bean放入此缓存。
  3. 三级缓存(singletonFactories)

    • 存储Bean工厂对象,用于创建Bean实例。
    • 允许通过工厂方法获取Bean的早期引用。

2. 为什么需要三级缓存

使用三级缓存的主要目的是解决循环依赖问题,尤其是在代理对象的场景下。

  • 循环依赖
    • 当两个或多个Bean相互依赖时,会出现循环依赖问题。Spring通过提前暴露未完全初始化的Bean来解决这一问题。
  • 代理对象
    • 在某些情况下,Bean可能需要被代理(例如使用AOP),这意味着我们需要获取代理对象而不是原始对象。
    • 三级缓存中的singletonFactories允许在需要时创建代理对象,并将其放入二级缓存中。

3. 二级缓存为什么不够用

如果只使用二级缓存(earlySingletonObjects),在某些情况下会导致以下问题:

  • 无法创建代理对象

    • 如果Bean需要被代理,二级缓存无法提供代理对象,因为它只存储早期的原始Bean实例。
    • 三级缓存允许在获取早期引用时,通过工厂方法创建代理对象。
  • 灵活性不足

    • 三级缓存提供了更大的灵活性,可以在Bean创建的不同阶段进行不同的处理。
    • 通过三级缓存,可以在需要时动态决定是返回原始对象还是代理对象。

4. 三级缓存的工作流程

  1. 创建Bean

    • 当Spring创建一个Bean时,首先会实例化Bean,并将其放入三级缓存(singletonFactories)。
  2. 解决依赖

    • 在解决依赖关系时,如果检测到循环依赖,Spring会从三级缓存中获取Bean的早期引用。
    • 如果需要代理对象,三级缓存中的工厂方法会返回代理对象,并将其放入二级缓存(earlySingletonObjects)。
  3. 完成初始化

    • 完成Bean的初始化后,Spring会将完全初始化好的Bean放入一级缓存(singletonObjects),并从二级缓存和三级缓存中移除。

通过使用三级缓存,Spring能够灵活地处理Bean的创建和依赖关系,尤其是在涉及代理对象和循环依赖的复杂场景下。

单例模式里双重检查时检查什么,为什么需要?

(1) 为什么要两次检查?

  • 第一次检查(无锁) :避免每次调用都进入同步块,减少性能损耗。
  • 第二次检查(加锁后) :防止多个线程同时通过第一次检查后,重复创建实例。

(2) 为什么用 volatile

  • 禁止指令重排序
    对象的实例化(new Singleton())实际分为三个步骤:

    1. 分配内存空间
    2. 初始化对象
    3. 将引用指向内存地址
      若步骤2和3被重排序,其他线程可能看到未完全初始化的对象。volatile 确保写操作不会重排序。
  • 保证可见性:确保所有线程看到最新的实例状态。

(3) 同步块的锁对象

  • 通常使用类的 Class 对象(如 Singleton.class)作为锁,保证全局唯一性。

HashMap resize 的过程

resize 的触发条件是:

  • 当 HashMap 中的元素数量超过 load factor(负载因子)乘以当前容量时,触发扩容。
  • 默认的负载因子是 0.75,默认初始容量是 16

 resize 的 扩容步骤

(1) 计算新容量

  • 新容量 = 旧容量 × 2(直到达到最大容量 230)。
  • 新阈值 = 新容量 × 负载因子。

(2) 创建新数组

  • 初始化一个容量翻倍的新数组。例如,旧容量16 → 新容量32。

(3) 元素迁移(核心步骤)

  • 遍历旧数组:逐个处理每个哈希桶(链表或红黑树)。

  • 重新计算索引:利用哈希值的高位优化计算新位置。

    • 旧索引:index = hash & (oldCap - 1)
    • 新索引:若哈希值的对应高位为0,则新索引 = 原位置;若为1,则新索引 = 原位置 + 旧容量。

    示例

    • 旧容量16(二进制掩码 00001111),新容量32(掩码 00011111)。

    • 哈希值二进制后5位为 0001 1010

      • 第5位是0 → 新索引 = 原位置(1010 → 10)。
      • 第5位是1 → 新索引 = 原位置 + 16(10 + 16 = 26)。

(4) 链表拆分优化(Java 8+)

  • 低位链表:保留在原位置。
  • 高位链表:迁移到新位置(原索引 + 旧容量)。
  • 红黑树处理:若树节点数 ≤ 6,退化为链表。

针对1千万用户和100万新闻的场景,实现用户关注新闻、新闻关注数统计及Top10新闻

1.1 用户-新闻关注关系

  • Redis Set结构
    使用 user:{userId}:news 存储每个用户关注的新闻ID集合

    • 优点:快速查询用户关注的新闻列表(SMEMBERS命令,O(1)时间)
    • 内存优化:启用Redis的ziplist编码(小集合内存压缩)

1.2 新闻关注计数器

  • Redis Hash结构
    使用 news:count Hash,字段为新闻ID,值为关注数

    • 操作:用户关注时 HINCRBY news:count {newsId} 1,取消关注时 HINCRBY news:count {newsId} -1
    • 优点:O(1)时间查询单个新闻的关注数(HGET命令)

1.3 Top10新闻排行榜

  • Redis Sorted Set结构
    使用 news:top Sorted Set,成员为新闻ID,分数为关注数
    • 更新策略:异步批量更新(避免频繁操作影响性能)

      • 通过定时任务(如每5分钟)从news:count Hash同步数据到Sorted Set
      • 使用 ZADD news:top {score} {newsId} 更新分数
    • 查询Top10ZREVRANGE news:top 0 9 WITHSCORES(O(log N)时间)

2. 系统架构优化

2.1 分片与集群

  • 用户数据分片:按用户ID哈希分片到多个Redis实例(如 user_shard_1user_shard_2
  • 新闻数据分片:按新闻ID范围分片(如 news_shard_1 存储ID 1-500k,news_shard_2 存储500k+)
  • 使用Redis Cluster:自动管理分片和故障转移

2.2 冷热数据分离

  • 热数据:近期活跃用户和热门新闻数据保留在Redis
  • 冷数据:长期未访问数据归档到数据库(如MySQL),通过LRU策略自动淘汰

2.3 异步处理

  • 关注操作队列:用户关注/取消关注操作写入Kafka队列,由消费者批量更新计数器和排行榜
  • 最终一致性:通过消息队列保证数据最终一致,降低实时操作压力

Redisson 的 Watchdog 机制

Redisson 的 Watchdog 机制(看门狗)  是其分布式锁(如 RLock)的核心组件之一,主要用于解决锁的自动续期问题,防止因业务执行时间过长导致锁过期释放,从而引发数据不一致的问题

1. Watchdog 的作用

  • 问题背景
    当使用 Redis 分布式锁时,通常会设置一个锁的过期时间(避免死锁)。但如果业务执行时间超过锁的过期时间,锁会被自动释放,其他线程可能获取到锁,导致并发问题。
  • 解决方案
    Watchdog 通过后台线程定期检查并延长锁的持有时间(续期),确保业务执行期间锁不会过期。

2. 工作机制

(1) 锁获取与 Watchdog 启动

  • 当通过 Redisson 获取锁(如 lock.lock())时,如果未显式指定锁的超时时间,Watchdog 会自动启动。
  • 手动指定超时时间
    当通过 lock.lock(leaseTime, TimeUnit) 或类似方法显式设置锁的过期时间时,Watchdog 会被禁用。
  • 未指定超时时间
    若未显式设置过期时间(如直接调用 lock.lock()),Watchdog 才会启动并自动续期。
  • 默认锁超时:30 秒(可通过 lockWatchdogTimeout 参数调整)。
  • 续期间隔:超时时间的 1/3(默认 10 秒),即每 10 秒检查一次锁状态并续期。

(2) 续期流程

  1. 检查锁所有权:确认当前线程是否仍持有锁。
  2. 延长锁过期时间:若仍持有锁,则执行 pexpire 命令将锁的过期时间重置为初始值(如 30 秒)。
  3. 循环执行:每隔 10 秒重复上述操作,直到锁被释放或线程终止。

(3) Watchdog 停止

  • 主动释放锁:调用 lock.unlock() 时,会停止续期并删除锁。
  • 线程崩溃:若持有锁的线程崩溃,Watchdog 线程也会终止,锁最终会因过期而自动释放。