面试随记:随机问题三——Spring Boot 条件装配、分布式锁与订单状态流转
面试随记:随机问题三——Spring Boot 条件装配、分布式锁与订单状态流转
面试指北 · 通用
题意
这篇是通用面试中“随机问题”的复盘,问题跨度比较大:Spring Boot 条件注解与自动装配、手写 Starter;分布式环境下商品数量增减该用什么锁、具体方案怎么落;订单 15 分钟未支付如何取消;订单完成后如何广播通知下游;以及订单场景下 Seata 分布式事务如何运作。
这类问题表面上是在问注解和中间件,实际是在考察能不能把“条件装配—自动配置—业务落地—一致性—幂等”串成一条线。回答时只背定义很容易显得干,最好能说清楚使用场景、底层机制和容易踩的坑。
结论
先给一个总览式回答:
@ConditionalOnProperty、@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnClass是 Spring Boot 自动装配的“开关”,核心作用是按条件决定某个配置类或 Bean 要不要生效。- Spring Boot 自动装配不是扫描所有类,而是通过
@EnableAutoConfiguration导入候选自动配置类,再根据条件注解过滤,最后注册 Bean。@ConditionalOnMissingBean让用户自定义 Bean 优先于默认配置。 - Starter 的本质是“依赖聚合 + 自动配置 + 配置属性 + 条件装配”,让引入方加一个依赖就能用。
- 商品库存增减不要只回答“加 Redis 分布式锁”。高并发扣减更稳的思路是数据库条件更新、Redis Lua 原子预扣、幂等流水、MQ 异步落库和最终对账;分布式锁可以降低冲突,但不能替代数据库约束和幂等。
- 订单 15 分钟未支付取消,常见方案是延迟消息为主、定时扫表兜底,取消时用状态机条件更新保证并发安全,消费端必须幂等。
- 订单完成广播通知下游,关键不是“发一条 MQ”,而是可靠事件、下游幂等、重试死信和广播语义的区分。
- Seata AT 模式通过全局事务、分支事务和
undo_log做提交与回滚补偿;它解决的是分布式事务一致性,不替代业务幂等、空回滚和悬挂处理。
要点展开
1. Spring Boot 条件注解
原文这里写的是 @ConidtionalOnProperty,正确拼写是 @ConditionalOnProperty。这几个注解都属于 Spring Boot 的 @Conditional 体系,通常用在自动配置类或 @Bean 方法上。
| 注解 | 作用 | 典型场景 | 注意点 |
|---|---|---|---|
@ConditionalOnProperty |
配置属性满足条件才装配 | 功能开关、不同环境启用不同实现 | 关注 prefix、name、havingValue、matchIfMissing |
@ConditionalOnBean |
IOC 容器中存在指定 Bean 才装配 | 自动配置依赖用户已声明的 Bean | 普通配置类里受解析顺序影响,推荐在自动配置类中使用 |
@ConditionalOnMissingBean |
IOC 容器中不存在指定 Bean 才装配 | 提供默认实现,允许用户覆盖 | 自动配置里非常常用,体现“用户配置优先” |
@ConditionalOnClass |
JVM/classpath 中存在指定类才装配 | 可选依赖、某中间件客户端存在时才生效 | 方法级使用要注意返回类型不要直接引用缺失类 |
@ConditionalOnProperty 不是简单看“有没有这个属性”,而是可以判断值。例如:
@Bean
@ConditionalOnProperty(
prefix = "demo.feature",
name = "enabled",
havingValue = "true",
matchIfMissing = false
)
public DemoService demoService() {
return new DemoService();
}
这里表示 demo.feature.enabled=true 时才注册 DemoService。matchIfMissing 表示配置缺失时是否也匹配,默认是 false,所以不写配置通常不会生效。实际做功能开关时,这个注解很好用,但要注意配置项命名和默认值,避免“以为开了其实没开”。
@ConditionalOnBean 和 @ConditionalOnMissingBean 是一对。前者是“容器里已有某个 Bean 才装配”,后者是“容器里没有某个 Bean 才装配”。自动配置里经常用 @ConditionalOnMissingBean 提供默认 Bean:用户自己声明了就用用户的,用户没声明才用框架默认的。这里有个容易错的点:条件评估依赖 Bean 定义的注册顺序,在普通 @Configuration 类里写 @ConditionalOnBean 可能因为顺序不确定而不生效。所以更推荐放在自动配置类中,必要时配合 @AutoConfigureAfter、@AutoConfigureBefore 控制顺序。
@ConditionalOnClass 是看 classpath 上有没有某个类。比如某个 Starter 依赖了可选客户端,只有客户端在 classpath 上才装配相关 Bean。方法级使用时要注意:如果方法返回类型直接写了那个可能不存在的类,类加载阶段就可能出问题。常见做法是把条件放在类级别,或者用 name 属性写类名,避免直接引用。
2. Spring Boot 自动装配到底怎么跑
可以按启动流程回答:
- 启动类上的
@SpringBootApplication组合了@EnableAutoConfiguration。 @EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)导入自动配置选择器。- 选择器读取候选自动配置类。Spring Boot 2.7 之前主要在
META-INF/spring.factories中配置org.springframework.boot.autoconfigure.EnableAutoConfiguration;Spring Boot 2.7 及 3.x 更推荐META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。 - 候选配置类不是全部生效,而是经过
@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等条件过滤。 - 条件满足的自动配置类被注册,里面的
@Bean方法把对象放进 IOC 容器。 - 用户自己声明的 Bean 通常优先,因为自动配置大量使用
@ConditionalOnMissingBean做兜底。
一个自动配置类的骨架大致是这样:
@AutoConfiguration
@ConditionalOnClass(DemoClient.class)
@EnableConfigurationProperties(DemoProperties.class)
public class DemoAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DemoClient demoClient(DemoProperties properties) {
return new DemoClient(properties.getEndpoint());
}
}
然后在 imports 文件里写上这个自动配置类的全限定名。这样引入方只要加依赖、配属性,就能拿到 DemoClient。如果用户自己声明了 DemoClient,默认的就不会再注册。
这里要强调一句:自动装配不是“把所有类都扫一遍”,而是“先给候选名单,再按条件筛选”。条件注解是自动装配的核心,不是可有可无的装饰。
3. 尝试写一个 Starter 的思路
写 Starter 是加深 Spring Boot 理解的好办法。一般可以按下面几步做:
- 模块划分:常见做法是
xxx-spring-boot-starter做依赖聚合,xxx-spring-boot-autoconfigure放自动配置代码。小项目也可以合在一个模块里,但正式 Starter 分开更清晰。 - 定义配置属性:用
@ConfigurationProperties(prefix = "xxx")把endpoint、timeout、enabled等配置绑定成对象。 - 写自动配置类:用
@AutoConfiguration、@ConditionalOnClass、@EnableConfigurationProperties,再配合@Bean和@ConditionalOnMissingBean提供默认 Bean。 - 注册自动配置:在
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中写入自动配置类名。旧版本则写spring.factories。 - 测试条件生效:可以用 `ApplicationContextRunner