1. Stream 转化为map里的key为null会报错吗? 如果最后不collect 前面的处理还会执行吗?
在使用 Collectors.toMap 时,如果 key 为 null,会导致 NullPointerException。这是因为 HashMap 不允许 null 键(在某些实现中可能允许,但在 Collectors.toMap 中会有问题)。
如果 key 属性可能重复,Collectors.toMap 会抛出 IllegalStateException。可以通过提供一个合并函数来解决这个问题,例如 (existing, replacement) -> existing 保留现有值。
惰性求值的实现
-
中间操作的惰性求值:
- 中间操作(如
filter,map等)不会立即对数据进行处理,而是将操作封装成一个Pipeline对象,并返回一个新的 Stream。 - 每个
Pipeline对象包含一个对数据进行处理的函数(如Predicate或Function),以及对下一个Pipeline对象的引用。
- 中间操作(如
-
操作链的构建:
- 每次调用中间操作时,都会创建一个新的
Pipeline对象,并将其链接到前一个Pipeline对象上,形成一个操作链。
- 每次调用中间操作时,都会创建一个新的
-
终端操作的触发:
-
终端操作(如
collect,forEach等)会触发整个操作链的执行。 -
终端操作会遍历数据源,并依次调用每个
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自动装配的原理?
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 文件。
-
核心注解
@EnableAutoConfiguration:@EnableAutoConfiguration是实现自动装配的核心注解。它通过引入AutoConfigurationImportSelector类,加载所有符合条件的自动配置类。
-
AutoConfigurationImportSelector类:AutoConfigurationImportSelector实现了ImportSelector接口,其selectImports方法用于获取所有需要自动装配的类的全限定类名,并将这些类加载到 Spring IoC 容器中。
-
spring.factories文件:spring.factories文件位于META-INF目录下,定义了自动配置类的全限定名。Spring Boot 启动时会扫描类路径中的所有spring.factories文件,并加载其中定义的自动配置类。
-
条件注解:
- Spring Boot 使用一系列条件注解(如
@ConditionalOnClass,@ConditionalOnBean,@ConditionalOnProperty等)来控制自动配置类的加载。这些条件注解确保了自动配置的灵活性和按需加载。
- Spring Boot 使用一系列条件注解(如
总结:Spring Boot 自动装配通过 @EnableAutoConfiguration 注解和 spring.factories 文件,实现了按需加载和灵活配置,极大地简化了应用程序的配置和开发过程。
4. 分布式系统中2pc和3pc有何区别,2pc有什么缺点
1.两阶段提交
两阶段提交(Two-Phase Commit,简称 2PC)是一种分布式事务协议,确保所有参与者在提交或回滚事务时都处于一致的状态。2PC 协议包含以下两个阶段:
- 准备阶段(prepare phase):在这个阶段,事务协调者(Transaction Coordinator)向所有参与者(Transaction Participant)发出准备请求,询问它们是否准备好提交事务。参与者执行所有必要的操作,并回复协调者是否准备好提交事务。如果所有参与者都回复准备好提交事务,协调者将进入下一个阶段。如果任何参与者不能准备好提交事务,协调者将通知所有参与者回滚事务。
- 提交阶段(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 它有两点改进:
- 引入了超时机制,同时在协调者和参与者中都引入超时机制(2PC 只有协调者有超时机制);
- 3PC 相比于 2PC 增加了 CanCommit 阶段,可以尽早的发现问题,从而避免了后续的阻塞和无效操作。
也就是说,3PC 相比于 2PC,因为引入了超时机制,所以发生阻塞的几率变小了;同时 3PC 把之前 2PC 的准备阶段一分为二,变成了两步,这样就多了一个缓冲阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
Full GC触发条件
-
老年代(Old Generation)空间不足:
- 当老年代的可用空间不足以容纳新对象或晋升对象时,会触发 Full GC。
- 例如,大量对象在年轻代经过多次 Minor GC 后晋升到老年代,导致老年代空间不足。
-
永久代(Permanent Generation)或元空间(Metaspace)空间不足:
- 在 JDK 7 及之前版本中,永久代用于存储类元数据。如果永久代空间不足,会触发 Full GC。
- 从 JDK 8 开始,永久代被移除,取而代之的是元空间(Metaspace)。如果元空间不足,也会触发 Full GC。
-
调用
System.gc()或Runtime.getRuntime().gc():- 手动调用
System.gc()或Runtime.getRuntime().gc()会建议 JVM 执行 Full GC。虽然这是一个建议,但 JVM 通常会执行 Full GC。
- 手动调用
-
Promotion Failure:
- 在 Minor GC 过程中,如果年轻代的存活对象无法晋升到老年代(因为老年代空间不足),会触发 Full GC。
Survivor区域对象晋升到老年代有两种情况:
- 一种是给每个对象定义一个对象计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置。
- 另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代,在这批对象在统计后发现可以晋升到老年代,但是发现老年代没有足够的空间来放置这些对象,这就会引起Full GC。
-
Concurrent Mode Failure:
- 在使用 CMS(Concurrent Mark-Sweep)垃圾收集器时,如果并发标记和清除阶段未能及时完成,导致老年代空间不足,会触发 Full GC。
-
堆碎片:
- 当堆内存中存在大量碎片,导致无法为大对象分配连续内存块时,可能会触发 Full GC 以尝试整理碎片。
MYSQL命中多个索引时如何选择
MySQL优化器一般会通过比较扫描行数、查询成本等因素,选择一个最优的索引来执行查询。
通过 EXPLAIN 可以看到 possible_keys 列中列出了可能使用的索引,而 key 列则表示实际使用的索引。rows显示了扫描的行数
在业务开发中,可以通过force index 显示的指定查询索引
Dubbo 的原理以及其核心组件的作用
核心组件及其作用
-
Provider(服务提供者)
-
作用:服务提供者是 Dubbo 中的一个核心组件,负责提供具体的服务实现。它在启动时会将自己提供的服务注册到注册中心,以便消费者能够发现和调用。
-
工作原理:
- 启动时,Provider 会将服务的元数据信息(如服务接口、版本、地址等)注册到注册中心。
- Provider 会监听注册中心的指令(如服务下线、配置变更等),以便动态调整服务状态。
- Provider 通过网络协议(如 Dubbo 协议、HTTP 协议等)接收消费者的远程调用请求,并返回执行结果。
-
-
Consumer(服务消费者)
-
作用:服务消费者是 Dubbo 中的另一个核心组件,负责调用远程服务。它通过注册中心获取服务提供者的地址列表,并通过负载均衡策略选择合适的服务提供者进行调用。
-
工作原理:
- 启动时,Consumer 会从注册中心订阅自己所需的服务列表,并缓存服务提供者的地址信息。
- Consumer 会根据负载均衡策略(如随机、轮询、一致性哈希等)选择合适的服务提供者进行调用。
- Consumer 通过网络协议发送远程调用请求,并接收服务提供者返回的结果。
-
-
Registry(注册中心)
-
作用:注册中心是 Dubbo 中的关键组件,负责服务的注册与发现。它维护着服务提供者和消费者的映射关系,确保服务消费者能够动态发现服务提供者。
-
工作原理:
- 注册中心接收服务提供者的注册请求,将服务的元数据信息存储在注册中心。
- 注册中心接收服务消费者的订阅请求,并返回相应的服务提供者列表。
- 注册中心监听服务提供者和消费者的状态变化(如上线、下线、配置变更等),并将变更通知给相关的服务消费者。
-
Dubbo 的调用过程
- 服务注册:服务提供者在启动时,将服务信息注册到注册中心。
- 服务订阅:服务消费者在启动时,从注册中心订阅所需的服务列表。
- 服务发现:消费者根据从注册中心获取的服务列表,选择合适的服务提供者。
- 远程调用:消费者通过网络协议调用服务提供者的服务,并接收返回结果。
- 动态调整:注册中心根据服务提供者和消费者的状态变化,动态调整服务的注册和订阅关系。
总结
- Provider:提供具体的服务实现,负责服务的注册和远程调用的处理。
- Consumer:调用远程服务,负责服务的订阅和负载均衡。
- Registry:维护服务的注册与发现,确保消费者能够动态发现提供者。
spring 中解决循环依赖为什么要用三级缓存,二级为什么不行呢?
A,B循环依赖,先初始化A,先暴露一个半成品A,再去初始化依赖的B,初始化B时如果发现B依赖A,也就是循环依赖,就注入半成品A,之后初始化完毕
B,再回到A的初始化过程时就解决了循环依赖,在这里只需要一个Map能缓存半成品A就行了,也就是二级缓存就够了,但是这个二级缓存存的是Bean对
象,如果这个对象存在代理,那应该注入的是代理,而不是Bean,此时二级缓存无法及缓存Bean,又缓存代理,因此三级缓存做到了缓存 工厂 ,也就是生成代理,这我的理解
:总结起来:二级缓存就能解决缓存依赖,三级缓存解决的是代理。
1. 三级缓存机制
Spring使用三级缓存来管理Bean的创建过程,以解决循环依赖问题。三级缓存包括:
-
一级缓存(singletonObjects):
- 存储完全初始化好的单例Bean实例。
- 这是我们通常获取Bean时访问的缓存。
-
二级缓存(earlySingletonObjects):
- 存储早期暴露的Bean实例,主要用于解决循环依赖。
- 在Bean创建过程中,将实例化但未完全初始化的Bean放入此缓存。
-
三级缓存(singletonFactories):
- 存储Bean工厂对象,用于创建Bean实例。
- 允许通过工厂方法获取Bean的早期引用。
2. 为什么需要三级缓存
使用三级缓存的主要目的是解决循环依赖问题,尤其是在代理对象的场景下。
- 循环依赖:
- 当两个或多个Bean相互依赖时,会出现循环依赖问题。Spring通过提前暴露未完全初始化的Bean来解决这一问题。
- 代理对象:
- 在某些情况下,Bean可能需要被代理(例如使用AOP),这意味着我们需要获取代理对象而不是原始对象。
- 三级缓存中的
singletonFactories允许在需要时创建代理对象,并将其放入二级缓存中。
3. 二级缓存为什么不够用
如果只使用二级缓存(earlySingletonObjects),在某些情况下会导致以下问题:
-
无法创建代理对象:
- 如果Bean需要被代理,二级缓存无法提供代理对象,因为它只存储早期的原始Bean实例。
- 三级缓存允许在获取早期引用时,通过工厂方法创建代理对象。
-
灵活性不足:
- 三级缓存提供了更大的灵活性,可以在Bean创建的不同阶段进行不同的处理。
- 通过三级缓存,可以在需要时动态决定是返回原始对象还是代理对象。
4. 三级缓存的工作流程
-
创建Bean:
- 当Spring创建一个Bean时,首先会实例化Bean,并将其放入三级缓存(
singletonFactories)。
- 当Spring创建一个Bean时,首先会实例化Bean,并将其放入三级缓存(
-
解决依赖:
- 在解决依赖关系时,如果检测到循环依赖,Spring会从三级缓存中获取Bean的早期引用。
- 如果需要代理对象,三级缓存中的工厂方法会返回代理对象,并将其放入二级缓存(
earlySingletonObjects)。
-
完成初始化:
- 完成Bean的初始化后,Spring会将完全初始化好的Bean放入一级缓存(
singletonObjects),并从二级缓存和三级缓存中移除。
- 完成Bean的初始化后,Spring会将完全初始化好的Bean放入一级缓存(
通过使用三级缓存,Spring能够灵活地处理Bean的创建和依赖关系,尤其是在涉及代理对象和循环依赖的复杂场景下。
单例模式里双重检查时检查什么,为什么需要?
(1) 为什么要两次检查?
- 第一次检查(无锁) :避免每次调用都进入同步块,减少性能损耗。
- 第二次检查(加锁后) :防止多个线程同时通过第一次检查后,重复创建实例。
(2) 为什么用 volatile?
-
禁止指令重排序:
对象的实例化(new Singleton())实际分为三个步骤:- 分配内存空间
- 初始化对象
- 将引用指向内存地址
若步骤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)。
- 第5位是0 → 新索引 = 原位置(
- 旧索引:
(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:countHash,字段为新闻ID,值为关注数- 操作:用户关注时
HINCRBY news:count {newsId} 1,取消关注时HINCRBY news:count {newsId} -1 - 优点:O(1)时间查询单个新闻的关注数(
HGET命令)
- 操作:用户关注时
1.3 Top10新闻排行榜
- Redis Sorted Set结构
使用news:topSorted Set,成员为新闻ID,分数为关注数-
更新策略:异步批量更新(避免频繁操作影响性能)
- 通过定时任务(如每5分钟)从
news:countHash同步数据到Sorted Set - 使用
ZADD news:top {score} {newsId}更新分数
- 通过定时任务(如每5分钟)从
-
查询Top10:
ZREVRANGE news:top 0 9 WITHSCORES(O(log N)时间)
-
2. 系统架构优化
2.1 分片与集群
- 用户数据分片:按用户ID哈希分片到多个Redis实例(如
user_shard_1,user_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) 续期流程
- 检查锁所有权:确认当前线程是否仍持有锁。
- 延长锁过期时间:若仍持有锁,则执行
pexpire命令将锁的过期时间重置为初始值(如 30 秒)。 - 循环执行:每隔 10 秒重复上述操作,直到锁被释放或线程终止。
(3) Watchdog 停止
- 主动释放锁:调用
lock.unlock()时,会停止续期并删除锁。 - 线程崩溃:若持有锁的线程崩溃,Watchdog 线程也会终止,锁最终会因过期而自动释放。