" />
文章

面试随记:随机问题三——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 自动装配到底怎么跑

可以按启动流程回答:

  1. 启动类上的 @SpringBootApplication 组合了 @EnableAutoConfiguration。
  2. @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 导入自动配置选择器。
  3. 选择器读取候选自动配置类。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。
  4. 候选配置类不是全部生效,而是经过 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等条件过滤。
  5. 条件满足的自动配置类被注册,里面的 @Bean 方法把对象放进 IOC 容器。
  6. 用户自己声明的 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 理解的好办法。一般可以按下面几步做:

  1. 模块划分:常见做法是 xxx-spring-boot-starter 做依赖聚合,xxx-spring-boot-autoconfigure 放自动配置代码。小项目也可以合在一个模块里,但正式 Starter 分开更清晰。
  2. 定义配置属性:用 @ConfigurationProperties(prefix = "xxx") 把 endpoint、timeout、enabled 等配置绑定成对象。
  3. 写自动配置类:用 @AutoConfiguration、@ConditionalOnClass、@EnableConfigurationProperties,再配合 @Bean 和 @ConditionalOnMissingBean 提供默认 Bean。
  4. 注册自动配置:在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中写入自动配置类名。旧版本则写 spring.factories。
  5. 测试条件生效:可以用 `ApplicationContextRunner
License:  CC BY 4.0