月度记录-2025-3月

121 阅读8分钟

1. Spring解决循环引用

  1. 缓存机制:创建Bean时依次经过三级缓存。实例化后(未初始化)的Bean存入三级缓存(singletonFactories),提前暴露引用;依赖注入时若触发循环依赖,从三级缓存获取早期对象并升级到二级缓存(earlySingletonObjects),避免重复创建。
  2. 过程拆解:当A依赖B,B又依赖A时,Spring先创建A的半成品(仅实例化),存入三级缓存;创建B时从缓存拿到A的引用,完成B的初始化后,再回溯注入A,最终两者均初始化完成存入一级缓存(singletonObjects)。
  3. 限制条件:仅支持单例模式且使用Setter/字段注入的场景,原型(prototype)作用域或纯构造器注入的循环依赖会直接抛异常。仅仅单例(singleton)可以解决。

其实就是先暴露个半成品出去,用到的地方就先用半成品,等完全初始化好了,再换。

2. 为何三级缓存?二级不行吗?

三级缓存的本质:通过ObjectFactory将代理对象的生成时机推迟到依赖注入阶段,确保AOP与循环依赖协同工作。

  1. 代理时机问题:AOP代理通常在Bean初始化后生成,但循环依赖需在注入时(初始化前)暴露引用。三级缓存的工厂机制允许提前生成代理对象,保证注入的早期引用即为代理,而非原始对象。
  2. 避免重复代理:若仅用二级缓存,可能因重复执行代理逻辑导致性能损耗或对象不一致。三级缓存通过工厂的懒加载机制,确保代理仅生成一次。

3. 构造器注入、setter注入、字段注入,哪些注入spring可以解循环依赖?

构造器注入无法解决循环依赖。这是因为在使用构造器注入时,Spring容器需要在创建Bean时完成所有依赖注入操作。如果两个Bean之间互相依赖(即循环依赖),Spring无法在实例化Bean时解决它们的依赖关系,因此会导致BeanCurrentlyInCreationException异常,Spring容器会无法正常创建这两个Bean。

另外两个都可以。但是从设计的角度来看,尽量避免循环依赖是更好的选择。使用构造器注入可以保证我们在创建对象的时候所有依赖都已经满足。

4. 怎么理解ioc是DI(Dependency Injection,依赖注入)来实现的?ioc还有其他的实现方式?

IoC 是设计原则,核心思想是 将对象控制权转移给外部容器,而 DI 是实现这一原则的具体技术手段。二者的关系类似于"目标"与"实现方式"。

除了依赖注入,另一种经典实现是 依赖查找(Dependency Lookup, DL),两者对比:

特性

依赖注入(DI)

依赖查找(DL)

主动权

被动接收依赖

主动查找依赖

耦合度

完全解耦(无需知道容器API)

部分耦合(需知道查找接口)

典型实现

Spring @Autowired

EJB/JNDI 查找

代码侵入性

低(通过注解/配置)

高(需显式调用查找方法)

可测试性

高(易于模拟依赖)

较低(依赖具体环境)

通过JNDI查找依赖(传统EJB方式)
public class Car {
    public void build() {
        Context ctx = new InitialContext();
        Engine engine = (Engine) ctx.lookup("java:comp/env/Engine");
    }
}

本质理解

IoC 就像把"家电组装"的工作从消费者(对象自己)转移给了专业工厂(容器),DI 是这个工厂采用的"自动化流水线装配"技术。而DL则是让消费者自己按照说明书(API)去仓库找零件的半自助方式。现代软件开发选择DI,是因为它能实现更彻底的职责分离和架构解耦,容器可统一控制对象创建/销毁。

在智能化程度上,根本就不是一个时代的产物。

5. 如何自定义一个Bean的作用域(Scope)?举例说明实际应用场景。

6. 关于在线和离线业务

在线业务用RPC方式,实时查询。

像时效性不高的业务,直接让他去查表就行了。后续会演变成他去查从表,不会拖慢主表。

7. 为什么AopContext.currentProxy() 在异步线程中失效?

1. 依赖 ThreadLocal 的机制

  • AopContext.currentProxy() 的实现原理:Spring 的 AopContext 类通过 ThreadLocal 存储当前线程的代理对象。当在 同一个线程 中调用被代理的方法时,AopContext.currentProxy() 可以正确返回代理对象,确保 AOP 拦截器(如事务、日志)生效。
  • 异步线程的隔离性:异步任务由线程池中的新线程执行,而 ThreadLocal 的值是线程隔离的。父线程的 ThreadLocal 数据 不会自动传递 到子线程,导致异步线程中 AopContext.currentProxy() 返回 null 或原始对象(非代理)。

2. 典型错误场景

// 主线程中提交异步任务
threadPoolExecutor.submit(() -> {
    // 异步线程中调用 AopContext.currentProxy(),返回 null!
    OrderInvoiceCommandApi proxy = (OrderInvoiceCommandApi) AopContext.currentProxy();
    proxy.doExecuteInvoiceMethod(logId); // 事务失效!
});

3. 解决方法

先在主线程获得proxy,然后再传入异步线程。

8. Spring Data Redis保证原子性

Redis 的事务操作(MULTI/EXEC)可以保证命令的原子性提交,但不是“数据库式事务”。它不支持回滚,也不能保证中间命令失败就回退之前的操作。

Redis 的 Lua 脚本是真正的原子操作,所有命令在同一个脚本里执行,要么都成功,要么都失败(不会被并发打断)。

特性

Redis MULTI/EXEC

数据库事务

回滚(rollback)

❌ 不支持

✅ 支持

失败中止

❌ 不中止

✅ 会中止

并发隔离

⚠️ 基于 WATCH

✅ 支持

错误恢复

❌ 不支持

✅ 支持

9. redis需要多字段管理用 Hash,简单键值对用 String

场景

数据结构

原因

用户购物车(多商品)

Hash

需要管理多个字段(商品ID、数量),支持独立更新,整体过期。

短信验证码

String

简单键值对,单次写入和读取,直接关联过期时间。

根据需求选择合适的数据结构,可以简化代码并提高性能。

10. spring 单例,策略模式

写策略模式,生搬硬套,搞了个环境类,去set各种不同的策略,结果我的策略注入到spring了,默认单例。导致线程不安全,还好领导及时发现,让我有机会在上线之前改正,否则闯祸了要。

11. redis直接可以执行increment吗?

是的,Redis 允许对不存在的 Key直接执行 INCR,如果 Key 不存在,INCR 会自动创建 Key 并初始化为 1,这使得 INCR 具备天然的原子性,可以直接使用,而无需 setIfAbsent 进行预先初始化。

  1. 如果 Key 存在:值递增 1。
  2. 如果 Key 不存在:Redis 自动创建 Key,并将值设为 1。

12. 同一接口多个实现类,注入Map

Spring的自动装配机制:Spring会自动将同一接口的所有实现类收集到Map中,无需手动配置,简化了多态行为的动态分发。

当需要注入多个同一接口的实现类时,可以通过注入Map的方式,其中键(Key)为Bean的名称,值(Value)为对应的实现类实例。以下是详细的步骤说明:

  1. 接口定义:首先定义一个接口(如PopupBehaviorLimitService),不同的业务逻辑由其实现类完成。
  2. 实现类与Bean命名:每个实现类通过@Component或@Service等注解注册为Spring Bean,并明确指定Bean的名称(如@Service("TYPE1")),确保名称与业务类型标识(如枚举的name())一致。
  3. 自动注入Map:在需要使用的类中,通过@Autowired或构造函数注入Map。Spring会自动收集所有实现该接口的Bean,并以Bean名称为键、实例为值形成Map。
  4. 动态获取实例:根据业务逻辑中的类型标识(如filter.getBehaviorLimitType().name())从Map中获取对应的实现类实例,执行相应方法。

13. Micrometer

尽量低基数标签,也就是取值种类(cardinality)非常少 的标签,即该标签只可能有少数几种取值。例如枚举值。

指标类型

作用

示例场景

Counter

记录累计值(只增不减)

HTTP 请求总数、错误次数

Timer

测量耗时和速率(如接口延迟)

API 响应时间、数据库查询耗时

Gauge

跟踪瞬时值(可增可减)

内存使用量、队列积压任务数

DistributionSummary

统计分布(如请求体大小)

文件上传大小、批处理耗时分布

14. 批量in查询优化

批量in取值,提前想好索引和后续如果加cache怎么搞。

本地cache或者redis(mget)。

如果是cluster模式的redis 用{hashKey}方式来把一些key放到一个slot。

15. 为什么需要 {hashKey}?

Redis Cluster 采用一致性哈希,将 key 分配到 16384 个 slot 之一。在 集群模式下,默认不同 key 可能分布在不同的 slot,这会导致:

👉 解决方案:使用 {hashKey} 让多个 key 被分配到相同 slot。

16. {hashKey} 机制

  • Redis 只会 对 {} 内的内容做哈希计算,其他部分不会影响 slot 分配。
  • 示例:

mykey1 -> SLOT 1234 mykey2 -> SLOT 5678

不同 slot,MGET mykey1 mykey2 会报错

但如果用 {} 包裹共同的 hashKey:

user:{123}:name -> SLOT 5000 user:{123}:age -> SLOT 5000 user:{123}:email -> SLOT 5000

同一个 slot,可以批量操作

MGET user:{123}:name user:{123}:age user:{123}:email