【面试实录】大厂Java面试:水货程序员谢飞机勇闯电商技术三面,面试官心态崩了(附12道真题详解)

🏢 场景:某互联网大厂 · Java后端开发岗(电商事业部) 👨‍💼 面试官:王总(技术总监,严肃脸,十年Java老兵) 🤡 候选人:谢飞机(简历写了8年经验,实际水分含量99%) 📋 面试轮次:共3轮,每轮4题,由浅入深


🎬 开场

会议室门被推开,谢飞机穿着一件印着 I ❤ Java 的T恤走了进来,胸前还别着一个"全栈工程师"的徽章。他自信满满地坐下,把简历往桌上一拍。

谢飞机:王总好!我是谢飞机,人如其名,写代码跟开飞机一样快!

王总(面无表情,翻开简历):嗯……简历上写你精通Java、Spring全家桶、微服务、分布式……那我们开始吧。今天主要围绕我们电商业务来聊,一共三轮。

谢飞机(搓手):放马过来!


📌 第一轮:Java基础与Spring Boot(热身局)

Q1:Java 8 Stream API 基础

王总:先来个简单的热热身。我们电商后台经常需要对商品列表做筛选和统计。比如有一个 List<Product>,我要筛选出价格大于100的商品,按价格降序排列,然后取出前5个商品名称。用 Java 8 Stream 怎么写?

谢飞机(眼睛一亮,这题会!):

List<String> result = productList.stream()
    .filter(p -> p.getPrice() > 100)
    .sorted(Comparator.comparing(Product::getPrice).reversed())
    .limit(5)
    .map(Product::getName)
    .collect(Collectors.toList());

这题我闭着眼都能写!filter 过滤、sorted 排序、limit 取前N个、map 转换、collect 收集,一条龙服务!

王总(微微点头):嗯,基础操作没问题。那我再追问一下,stream()parallelStream() 有什么区别?在什么场景下你会用并行流?

谢飞机parallelStream() 底层用的是 ForkJoinPool,会把任务拆分到多个线程并行执行。适合数据量大、计算密集、且元素之间没有依赖关系的场景。但如果是小数据量或者有共享可变状态,用并行流反而更慢,还有线程安全问题。

王总(难得露出一丝微笑):👍 不错,知道并行流的坑,比很多只会背八股的强。继续。


Q2:Spring Boot 自动装配原理

王总:你简历上写"精通Spring Boot"。那我问你,@SpringBootApplication 这个注解到底做了什么?Spring Boot 的自动装配(Auto-Configuration)原理是什么?

谢飞机(挺胸):这个我熟!@SpringBootApplication 其实是个组合注解,里面包含三个核心注解:

@SpringBootConfiguration  // 本质就是 @Configuration,标记为配置类
@EnableAutoConfiguration  // 开启自动装配,核心!
@ComponentScan            // 组件扫描,扫描当前包及子包

自动装配的核心在 @EnableAutoConfiguration,它通过 @Import(AutoConfigurationImportSelector.class) 加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)或老版本的 spring.factories 文件,里面列出了所有自动配置类。每个自动配置类上有 @Conditional 系列注解(比如 @ConditionalOnClass@ConditionalOnMissingBean),只有条件满足才会生效。

王总:那如果我想自定义一个 Starter,大致步骤是什么?

谢飞机:四步走:

  1. 创建一个 Maven 模块,引入 spring-boot-starter 依赖
  2. 写一个 XxxAutoConfiguration 配置类,用 @Configuration + @ConditionalOnClass 等条件注解
  3. 写一个 XxxProperties 类,用 @ConfigurationProperties 绑定配置
  4. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册你的自动配置类全限定名

王总(在笔记本上打了个勾):可以,这块你确实有准备。


Q3:Spring IoC 与依赖注入

王总:再问一个基础的。Spring 的 IoC 容器,@Autowired@Resource 有什么区别?如果同一个接口有两个实现类,Spring 怎么知道注入哪一个?

谢飞机

| 对比项 | @Autowired | @Resource | |--------|-------------|-------------| | 来源 | Spring 框架 | JSR-250(Java标准) | | 注入方式 | 默认按类型(byType) | 默认按名称(byName) | | 配合使用 | 可搭配 @Qualifier("beanName") 指定 | 可通过 name 属性指定 |

如果有两个实现类,我会用 @Qualifier 指定具体的 Bean 名称:

public interface PaymentService {
    void pay(BigDecimal amount);
}

@Service("alipayService")
public class AlipayService implements PaymentService { ... }

@Service("wechatPayService")  
public class WechatPayService implements PaymentService { ... }

// 注入时指定
@Autowired
@Qualifier("alipayService")
private PaymentService paymentService;

或者在实现类上用 @Primary 标记优先级。

王总(点头):OK,基础功还算扎实。第一轮你表现还行,但别高兴太早,第二轮开始上强度了。

谢飞机(小声嘀咕):上强度就上强度,我谢飞机什么场面没见过……


Q4:Maven 依赖冲突处理

王总:我们电商项目模块很多,经常遇到依赖冲突。你在 Maven 中遇到 jar 包冲突怎么排查和解决?

谢飞机:这个实战中太常见了!我的套路是:

  1. 定位冲突:用 mvn dependency:tree -Dverbose -Dincludes=冲突的groupId 查看依赖树
  2. 分析原因:Maven 的依赖仲裁原则是"最短路径优先"和"先声明优先"
  3. 解决方案
    • <exclusions> 排除冲突依赖
    • <dependencyManagement> 统一版本号
    • 直接显式声明需要的版本
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.2</version>
        </dependency>
    </dependencies>
</dependencyManagement>

王总:嗯,实际干过活的人。

谢飞机(得意地往后一靠):那必须的!


📌 第二轮:Redis缓存与数据库(上强度)

Q5:Redis 在电商中的实战应用

王总:我们电商首页有个"热门商品"模块,QPS 峰值能到 5 万。这块你们怎么用 Redis 做缓存的?Redis 有哪些数据结构,分别适合什么电商场景?

谢飞机(开始有点紧张,但还能撑住):

Redis 五种基本数据结构在电商里都能用上:

| 数据结构 | 电商场景 | |---------|--------| | String | 商品详情缓存、库存计数、分布式锁 | | Hash | 购物车(field=商品ID,value=数量) | | List | 最新消息推送、秒杀排队队列 | | Set | 共同关注、抽奖去重 | | ZSet | 热门商品排行榜(score=浏览量/销量) |

热门商品排行榜我们用 ZSet:

// 用户浏览商品时,增加分值
redisTemplate.opsForZSet().incrementScore("hot:products", productId, 1);

// 查询Top10热门商品
Set<ZSetOperations.TypedTuple<String>> top10 = 
    redisTemplate.opsForZSet().reverseRangeWithScores("hot:products", 0, 9);

王总:嗯,那缓存和数据库的一致性你怎么保证?比如商品改了价格,缓存怎么更新?

谢飞机:我们用延迟双删策略:先删缓存 → 更新数据库 → 延迟一段时间再删一次缓存。同时配合 Canal 监听 Binlog 做兜底。

王总(追问):为什么是"延迟双删"而不是直接更新缓存?

谢飞机(顿了一下):因为……并发场景下,两个线程同时读,一个先读到旧数据,然后另一个更新了数据库和缓存,第一个线程又把旧数据写回缓存,就脏了。双删能降低这个概率。

王总:方向对,但说得不够精确。坐下继续。


Q6:缓存穿透、击穿、雪崩

王总:你刚才提到缓存。那我问你,缓存穿透、缓存击穿、缓存雪崩分别是什么?在电商大促场景下,你遇到过哪种?怎么解决的?

谢飞机(挠头):这个……我大概知道……

  • 缓存穿透:查一个根本不存在的数据,缓存里没有,每次都打到数据库。比如用户故意传一个负数的商品ID。
  • 缓存击穿:某个热点Key过期了,大量请求同时打到数据库。
  • 缓存雪崩:大量Key同时过期,或者Redis挂了,请求全部涌向数据库。

解决方案嘛……穿透用布隆过滤器,击穿用互斥锁或者逻辑过期,雪崩就是……过期时间加随机值,然后……然后搞个集群……

王总(皱眉):你这个回答太笼统了。布隆过滤器的误判率怎么控制?互斥锁具体用什么实现?逻辑过期的代码怎么写?这些你能说清楚吗?

谢飞机(开始冒汗):这个……布隆过滤器就是……设置几个哈希函数……误判率调低的话……就多设几个……具体参数我记不太清了……

王总(叹气):行,先跳过,后面再说。


Q7:HikariCP 连接池调优

王总:我们电商订单服务用的是 HikariCP 连接池。你之前项目里连接池参数怎么配的?maximumPoolSize 设多少?怎么确定的?

谢飞机(又有点虚了):我们一般……默认是10个连接,我有时候会调到20或者50……

王总(打断):依据是什么?为什么是20不是200?

谢飞机:因为……连接太多会……占用内存?而且数据库也扛不住?

王总(严肃):这不是"大概""可能"的问题。HikariCP 官方给了一个公式:

connections = (core_count * 2) + effective_spindle_count

一台4核机器,大概9-10个连接就够了。盲目调大连接数,上下文切换的开销反而会让性能下降。你连这个都不知道,怎么做的调优?

谢飞机(擦汗):我……我们都是运维配的,我主要写业务代码……

王总:做后端不懂连接池调优,那你的"精通"两个字是不是有点廉价了?

谢飞机:…………


Q8:MyBatis 动态SQL与分页

王总:来一个你擅长的。电商后台有个商品搜索功能,用户可以按名称、分类、价格区间、上架状态等任意组合筛选。用 MyBatis 动态 SQL 怎么写?

谢飞机(终于又逮到一个会的):用 <where> + <if> 标签:

<select id="searchProducts" resultType="Product">
    SELECT id, name, price, category_id, status
    FROM t_product
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="categoryId != null">
            AND category_id = #{categoryId}
        </if>
        <if test="minPrice != null">
            AND price >= #{minPrice}
        </if>
        <if test="maxPrice != null">
            AND price &lt;= #{maxPrice}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
    </where>
    ORDER BY create_time DESC
</select>

分页的话用 PageHelper 插件,在 Service 层 PageHelper.startPage(pageNum, pageSize) 就行了。

王总<where> 和直接写 WHERE 1=1 有什么区别?

谢飞机<where> 标签会自动去掉第一个条件前面多余的 ANDOR,而且如果所有条件都不满足,它不会生成 WHERE 关键字。WHERE 1=1 虽然也能用,但不够优雅,而且如果后面拼接条件时出了SQL注入问题就不好排查了。

王总:OK,这题算你过了。

谢飞机(小声):终于喘口气了……


📌 第三轮:分布式与高并发(终极拷问)

Q9:Kafka 在订单系统中的应用

王总:我们电商下单后,需要异步处理很多事情:扣减库存、发送短信通知、积分变更、推送给物流系统。这块我们用 Kafka 做消息解耦。你来说说,Kafka 怎么保证消息不丢失?

谢飞机(深吸一口气,开始支吾):这个……消息不丢失嘛……就是……

生产者那边……设置 acks=all……然后……Broker 那边……副本数设3……消费者那边……手动提交 offset……

王总:方向是对的,但你能展开说说吗?acks=all 具体是什么含义?ISR 机制了解吗?如果 Broker 挂了,消息怎么保证不丢?

谢飞机acks=all 就是……所有副本都写入成功才算发送成功……ISR 是……就是……跟得上 Leader 的那些副本……叫 In-Sync Replicas……

王总:那消费者端呢?如果消费到一半,服务重启了,怎么保证消息不重复消费也不丢失?

谢飞机(彻底卡壳):这个……我们一般就是……重试几次……然后……记个日志……

王总(摇头):你这回答,跟没说一样。幂等性怎么做?消息去重的方案有哪些?

谢飞机:…………我……这块确实不太熟……


Q10:Spring Cloud 微服务熔断降级

王总:我们电商系统拆了十几个微服务:商品服务、订单服务、库存服务、支付服务、用户服务……用 Spring Cloud 做服务治理。你了解熔断器模式吗?Resilience4j 和 Hystrix 有什么区别?

谢飞机(试探性地):熔断器就是……像家里的保险丝一样,电流太大就断了,保护电路……微服务里就是,下游服务挂了,上游就别一直调了,直接返回个兜底数据……

Hystrix 已经……不维护了,Resilience4j 是替代品……Resilience4j 更轻量,基于函数式编程,不需要继承什么类……

王总:那 Resilience4j 的熔断器有几种状态?状态转换条件是什么?滑动窗口怎么配的?

谢飞机:状态就是……关闭、打开、半开……CLOSED、OPEN、HALF_OPEN……转换条件就是……失败率超过阈值就打开……半开状态放几个请求试探……

王总:失败率阈值怎么设?慢调用比例怎么算?这些你在项目中实际调过吗?

谢飞机(声音越来越小):我们……我们用的默认配置……

王总(深呼吸):默认配置……行吧。


Q11:分布式事务

王总:最后一个大场景。用户下单,涉及三个服务:订单服务创建订单、库存服务扣减库存、积分服务增加积分。这三个操作要么全成功,要么全失败。你怎么保证分布式事务的一致性?

谢飞机(额头冒汗):分布式事务……我知道好几种方案……

可以用……2PC,两阶段提交……或者用 TCC……Try-Confirm-Cancel……还有本地消息表……还有 Seata 框架……

王总:你说了四种方案,那你觉得我们电商下单场景应该选哪种?为什么?

谢飞机:我觉得……用 Seata 的 AT 模式?因为它……对业务代码侵入小……

王总:Seata AT 模式的原理是什么?它的 undo_log 是干什么用的?全局锁怎么工作的?在什么场景下 AT 模式不适用?

谢飞机(彻底懵了):undo_log 就是……回滚日志……全局锁就是……锁住那行数据……不适用的场景……我……

王总:你连 Seata AT 模式的基本原理都说不清楚,就敢说"用 Seata"?

谢飞机(已经开始看门口了):王总,这个……分布式事务这块,我确实……项目里用得不多……我们之前都是……最终一致性……用消息队列补偿的……

王总:那消息队列补偿方案你详细说说?

谢飞机:就是……本地事务和消息发送绑定……用事务消息……RocketMQ 的事务消息……或者用本地消息表……定时任务扫描……

王总(看了看表):好了,你不用说了。


Q12:Elasticsearch 商品搜索

王总:最后一个问题。我们商品搜索用的是 Elasticsearch,支持分词、模糊搜索、多维度筛选。你了解 ES 的倒排索引原理吗?和 MySQL 的 LIKE 查询相比,优势在哪里?

谢飞机(最后的挣扎):倒排索引就是……把词拆开,建一个词到文档的映射……比如"红色连衣裙"拆成"红色""连衣""裙"……搜"红色"就能找到这个商品……

比 LIKE 快是因为……不用全表扫描……直接查索引……

王总:ES 的分词器你用过哪些?中文分词用什么?ik_max_word 和 ik_smart 有什么区别?

谢飞机:中文用 IK 分词器……ik_max_word 是……最细粒度切分……ik_smart 是……智能切分,粒度粗一点……

王总:那如果用户搜"苹果",既想匹配水果"苹果",又想匹配手机品牌"Apple",你怎么处理同义词?

谢飞机:这个……配一个……同义词词典?在 analyzer 里面加……

王总(合上笔记本,站起来):好了,今天的面试就到这里。

谢飞机(紧张):王总,您觉得我……

王总(面无表情):谢飞机是吧,你的情况我了解了。基础部分还行,但一到深入的原理和实战调优,就明显功底不够。简历上的"精通",建议以后改成"了解"。

谢飞机:那……结果什么时候出?

王总(已经在看下一个候选人的简历了):回去等通知吧。

谢飞机(站起来,把椅子推回去,小声说):等通知……就是没戏了呗……

王总(头也不抬):下一位!

谢飞机灰溜溜地走出会议室,T恤上的 I ❤ Java 在走廊的灯光下显得格外刺眼。他掏出手机,打开了某招聘App,把简历上的"精通"默默改成了"熟悉"……



📚 全部12道面试题 · 详细解析(小白学习版)

以下是每道题的完整答案,包含业务场景、技术原理、代码案例,适合Java初学者系统学习。


✅ Q1:Java 8 Stream API 基础

📋 业务场景

电商后台商品管理页面,运营需要按条件筛选商品并排序展示。

🔑 核心知识点

Stream 是 Java 8 引入的函数式编程API,支持对集合进行声明式操作,核心操作分为:

中间操作(返回Stream,惰性求值):

  • filter() - 过滤
  • map() - 映射转换
  • sorted() - 排序
  • limit() / skip() - 截取/跳过
  • distinct() - 去重

终端操作(触发计算,返回结果):

  • collect() - 收集为集合
  • forEach() - 遍历
  • reduce() - 归约
  • count() - 计数

💻 完整代码

@Data
@AllArgsConstructor
class Product {
    private Long id;
    private String name;
    private BigDecimal price;
    private String category;
}

public class StreamDemo {
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
            new Product(1L, "iPhone 15", new BigDecimal("7999"), "手机"),
            new Product(2L, "MacBook Pro", new BigDecimal("14999"), "电脑"),
            new Product(3L, "AirPods", new BigDecimal("1299"), "耳机"),
            new Product(4L, "iPad", new BigDecimal("3999"), "平板"),
            new Product(5L, "Apple Watch", new BigDecimal("2999"), "手表")
        );

        // 筛选价格>2000,按价格降序,取前3个名称
        List<String> result = products.stream()
            .filter(p -> p.getPrice().compareTo(new BigDecimal("2000")) > 0)
            .sorted(Comparator.comparing(Product::getPrice).reversed())
            .limit(3)
            .map(Product::getName)
            .collect(Collectors.toList());

        System.out.println(result); // [MacBook Pro, iPhone 15, iPad]

        // 并行流:适合大数据量、CPU密集型、无共享状态
        long count = products.parallelStream()
            .filter(p -> p.getPrice().compareTo(new BigDecimal("3000")) > 0)
            .count();
    }
}

⚠️ 注意事项

  • parallelStream() 使用 ForkJoinPool.commonPool(),默认线程数 = CPU核心数-1
  • 有共享可变变量时,禁止使用并行流
  • 数据量 < 1000 时,并行流通常比串行流更慢(线程调度开销)

✅ Q2:Spring Boot 自动装配原理

📋 业务场景

理解 Spring Boot 为什么引入一个依赖就能直接用,不需要手动配置一堆Bean。

🔑 核心原理

@SpringBootApplication
  ├── @SpringBootConfiguration → 标记为配置类
  ├── @EnableAutoConfiguration → 开启自动装配
  │     └── @Import(AutoConfigurationImportSelector.class)
  │           └── 读取 META-INF/spring/...AutoConfiguration.imports
  │                 └── 加载所有自动配置类
  │                       └── @Conditional 条件判断是否生效
  └── @ComponentScan → 扫描当前包

💻 自定义Starter代码

// 1. 配置属性类
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
    private String accessKey;
    private String secretKey;
    private String signName;
    // getter/setter
}

// 2. 自动配置类
@Configuration
@ConditionalOnClass(SmsClient.class)
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public SmsClient smsClient(SmsProperties properties) {
        return new SmsClient(
            properties.getAccessKey(),
            properties.getSecretKey()
        );
    }
}

// 3. 注册文件
// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
// 内容:com.example.sms.SmsAutoConfiguration

📌 @Conditional 常用注解

| 注解 | 含义 | |------|------| | @ConditionalOnClass | classpath中存在指定类时生效 | | @ConditionalOnMissingBean | 容器中不存在指定Bean时生效 | | @ConditionalOnProperty | 配置文件中存在指定属性时生效 | | @ConditionalOnWebApplication | 是Web应用时生效 |


✅ Q3:Spring IoC 与依赖注入

📋 业务场景

电商系统中,支付接口有多个实现(支付宝、微信、银联),需要根据业务场景注入不同实现。

💻 代码示例

public interface PaymentService {
    String pay(BigDecimal amount, String orderId);
}

@Service("alipay")
public class AlipayServiceImpl implements PaymentService {
    @Override
    public String pay(BigDecimal amount, String orderId) {
        return "支付宝支付成功: " + amount;
    }
}

@Service("wechat")
public class WechatPayServiceImpl implements PaymentService {
    @Override
    public String pay(BigDecimal amount, String orderId) {
        return "微信支付成功: " + amount;
    }
}

@RestController
public class OrderController {

    // 方式1:@Autowired + @Qualifier 指定名称
    @Autowired
    @Qualifier("alipay")
    private PaymentService paymentService;

    // 方式2:@Resource 按名称注入
    @Resource(name = "wechat")
    private PaymentService wechatPayService;

    // 方式3:注入所有实现类(策略模式)
    @Autowired
    private Map<String, PaymentService> paymentServiceMap;

    public void pay(String channel, BigDecimal amount) {
        PaymentService service = paymentServiceMap.get(channel);
        service.pay(amount, "ORDER_001");
    }
}

⚠️ 关键区别

  • @Autowired:Spring注解,默认byType,可搭配@Qualifier
  • @Resource:JSR-250标准注解,默认byName,找不到再byType
  • @Primary:当同一类型有多个Bean时,标记优先注入的那个

✅ Q4:Maven 依赖冲突处理

📋 业务场景

电商项目模块众多(商品、订单、支付、物流),不同模块引入了不同版本的同一依赖,导致冲突。

🔑 排查步骤

# 1. 查看完整依赖树
mvn dependency:tree

# 2. 查看特定依赖的引入路径
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind

# 3. 分析冲突(显示被忽略的版本)
mvn dependency:tree -Dverbose

💻 解决方案

<!-- 方案1:exclusions 排除 -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>old-module</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 方案2:dependencyManagement 统一版本 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.1.2-jre</version>
        </dependency>
    </dependencies>
</dependencyManagement>

📌 Maven 依赖仲裁原则

  1. 最短路径优先:A→B→C(1.0) vs A→C(2.0),选2.0(路径更短)
  2. 先声明优先:路径长度相同时,pom中先声明的优先

✅ Q5:Redis 在电商中的实战应用

📋 业务场景

电商首页热门商品、购物车、库存扣减、秒杀等核心场景都依赖Redis。

🔑 五大数据结构实战

@Service
public class EcommerceRedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // ===== String:商品详情缓存 =====
    public Product getProduct(Long id) {
        String key = "product:" + id;
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            return JSON.parseObject(json, Product.class);
        }
        // 缓存未命中,查数据库
        Product product = productMapper.selectById(id);
        redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        return product;
    }

    // ===== Hash:购物车 =====
    public void addToCart(Long userId, Long productId, int quantity) {
        String key = "cart:" + userId;
        redisTemplate.opsForHash().increment(key, String.valueOf(productId), quantity);
    }

    public Map<Object, Object> getCart(Long userId) {
        return redisTemplate.opsForHash().entries("cart:" + userId);
    }

    // ===== ZSet:热门商品排行榜 =====
    public void viewProduct(Long productId) {
        redisTemplate.opsForZSet().incrementScore("hot:rank", String.valueOf(productId), 1);
    }

    public List<String> getTop10() {
        Set<String> top = redisTemplate.opsForZSet()
            .reverseRange("hot:rank", 0, 9);
        return new ArrayList<>(top);
    }

    // ===== String + Lua:库存扣减(原子操作)=====
    public boolean deductStock(Long productId, int quantity) {
        String key = "stock:" + productId;
        String luaScript =
            "local stock = tonumber(redis.call('get', KEYS[1]))\n" +
            "if stock >= tonumber(ARGV[1]) then\n" +
            "    redis.call('decrby', KEYS[1], ARGV[1])\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            String.valueOf(quantity)
        );
        return result != null && result == 1;
    }
}

📌 缓存与数据库一致性方案

| 策略 | 做法 | 优缺点 | |------|------|--------| | Cache Aside | 读:先缓存→miss查DB→写缓存;写:先更新DB→删缓存 | 最常用,简单可靠 | | 延迟双删 | 删缓存→更新DB→延迟N ms→再删缓存 | 降低并发不一致概率 | | 订阅Binlog | 用Canal监听MySQL Binlog,异步更新/删除缓存 | 最终一致性,解耦 |


✅ Q6:缓存穿透、击穿、雪崩

📋 业务场景

电商大促(双11)期间,恶意攻击或流量突增导致缓存失效,数据库被打垮。

🔑 三大问题对比

| | 缓存穿透 | 缓存击穿 | 缓存雪崩 | |--|---------|---------|--------| | 原因 | 查询不存在的数据 | 热点Key过期 | 大量Key同时过期/Redis宕机 | | 表现 | 每次请求都打到DB | 瞬间大量请求打到DB | 所有请求都打到DB | | 解决 | 布隆过滤器/缓存空值 | 互斥锁/逻辑过期 | 随机过期时间/集群/限流 |

💻 布隆过滤器解决穿透

// 使用 Guava BloomFilter
BloomFilter<Long> productFilter = BloomFilter.create(
    Funnels.longFunnel(),
    1000000,   // 预期插入量
    0.01       // 误判率1%
);

// 初始化:将所有商品ID加入布隆过滤器
productMapper.selectAllIds().forEach(productFilter::put);

// 查询时先过滤
public Product getProduct(Long id) {
    if (!productFilter.mightContain(id)) {
        return null; // 一定不存在,直接返回
    }
    // 可能存在,继续查缓存/DB
    ...
}

💻 互斥锁解决击穿

public Product getProductWithLock(Long id) {
    String key = "product:" + id;
    String json = redisTemplate.opsForValue().get(key);
    if (json != null) return JSON.parseObject(json, Product.class);

    // 缓存miss,加分布式锁
    String lockKey = "lock:product:" + id;
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

    if (Boolean.TRUE.equals(locked)) {
        try {
            // 双重检查
            json = redisTemplate.opsForValue().get(key);
            if (json != null) return JSON.parseObject(json, Product.class);

            Product product = productMapper.selectById(id);
            redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
            return product;
        } finally {
            redisTemplate.delete(lockKey);
        }
    } else {
        // 没拿到锁,短暂等待后重试
        Thread.sleep(50);
        return getProductWithLock(id);
    }
}

💻 逻辑过期解决击穿

// 缓存的value中包含逻辑过期时间
public class CacheData<T> {
    private T data;
    private long expireTime; // 逻辑过期时间戳
}

// 查询时判断逻辑过期时间
// 如果逻辑过期了,异步开一个线程去更新缓存
// 当前请求仍然返回旧数据(保证可用性)

💻 雪崩预防

// 过期时间加随机值,避免同时过期
int randomTtl = 30 * 60 + new Random().nextInt(300); // 30分钟 + 0~5分钟随机
redisTemplate.opsForValue().set(key, value, randomTtl, TimeUnit.SECONDS);

✅ Q7:HikariCP 连接池调优

📋 业务场景

电商订单服务高峰期,数据库连接不够用,请求排队等待,导致响应变慢。

🔑 核心配置

spring:
  datasource:
    hikari:
      # 最大连接数(核心公式:CPU核数*2 + 磁盘数)
      maximum-pool-size: 10
      # 最小空闲连接
      minimum-idle: 5
      # 连接超时时间(获取连接的最大等待时间)
      connection-timeout: 30000    # 30秒
      # 空闲连接存活时间
      idle-timeout: 600000         # 10分钟
      # 连接最大存活时间(防止连接老化)
      max-lifetime: 1800000        # 30分钟
      # 连接测试查询
      connection-test-query: SELECT 1

📌 为什么不能盲目调大连接数?

连接数过大的问题:
1. 数据库端:每个连接占用内存(MySQL约10MB/连接),连接太多OOM
2. 应用端:线程上下文切换开销增大
3. 锁竞争:更多连接意味着更多的锁等待

经验公式:
connections = (core_count * 2) + effective_spindle_count
4核机器 ≈ 4*2+1 = 9个连接

📌 HikariCP vs 其他连接池

| 特性 | HikariCP | Druid | C3P0 | |------|----------|-------|------| | 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | | 监控 | 简单 | 丰富(自带监控面板) | 一般 | | 维护 | Spring Boot默认 | 阿里维护 | 基本停更 |


✅ Q8:MyBatis 动态SQL与分页

📋 业务场景

电商后台商品搜索,支持多条件任意组合查询。

💻 完整代码

<!-- ProductMapper.xml -->
<select id="searchProducts" resultType="Product">
    SELECT id, name, price, category_id, status, create_time
    FROM t_product
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="categoryId != null">
            AND category_id = #{categoryId}
        </if>
        <if test="minPrice != null">
            AND price >= #{minPrice}
        </if>
        <if test="maxPrice != null">
            AND price &lt;= #{maxPrice}
        </if>
        <if test="status != null">
            AND status = #{status}
        </if>
        <if test="createTimeStart != null">
            AND create_time >= #{createTimeStart}
        </if>
    </where>
    ORDER BY create_time DESC
</select>

📌 <where> vs WHERE 1=1

<!-- ❌ 不推荐:WHERE 1=1 -->
SELECT * FROM t_product WHERE 1=1
<if test="name != null"> AND name = #{name} </if>

<!-- ✅ 推荐:<where> 标签 -->
<!-- 自动去除第一个AND/OR,条件全空时不生成WHERE -->

💻 分页(PageHelper)

@Service
public class ProductService {
    public PageInfo<Product> search(ProductQuery query, int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<Product> list = productMapper.searchProducts(query);
        return new PageInfo<>(list);
    }
}

✅ Q9:Kafka 在订单系统中的应用

📋 业务场景

用户下单后,需要异步通知库存、积分、物流、短信等多个下游系统。

🔑 架构图

用户下单 → 订单服务(写入DB + 发送Kafka消息)
                    ↓
              Kafka Topic: order-created
              ↓     ↓     ↓     ↓
           库存服务 积分服务 物流服务 短信服务

🔑 消息不丢失的三道防线

① 生产者端:

# acks=all:所有ISR副本确认才算发送成功
acks=all
# 发送失败重试
retries=3
# 开启幂等生产者(防重复)
enable.idempotence=true

② Broker端:

# 副本因子>=3
replication.factor=3
# ISR中最少确认副本数
min.insync.replicas=2
# 关闭不干净选举
unclean.leader.election.enable=false

③ 消费者端:

// 手动提交offset,消费成功后再提交
@Component
public class OrderKafkaListener {

    @KafkaListener(topics = "order-created", groupId = "stock-group")
    public void onMessage(ConsumerRecord<String, String> record, Acknowledgment ack) {
        try {
            OrderEvent event = JSON.parseObject(record.value(), OrderEvent.class);
            // 1. 幂等性检查(用唯一ID去重)
            if (idempotentService.isProcessed(event.getEventId())) {
                ack.acknowledge();
                return;
            }
            // 2. 业务处理
            stockService.deduct(event.getProductId(), event.getQuantity());
            // 3. 标记已处理
            idempotentService.markProcessed(event.getEventId());
            // 4. 手动确认
            ack.acknowledge();
        } catch (Exception e) {
            log.error("消费失败", e);
            // 不ack,Kafka会重新投递
        }
    }
}

📌 幂等性方案

| 方案 | 实现 | |------|------| | 唯一ID + Redis | SETNX eventId 判断是否已处理 | | 数据库唯一索引 | 插入记录,重复则忽略 | | 状态机 | 订单状态只能单向流转,重复操作无效 |


✅ Q10:Spring Cloud 微服务熔断降级

📋 业务场景

电商大促时,积分服务挂了,不能因为积分服务不可用就导致整个下单流程失败。

🔑 熔断器三种状态

CLOSED(关闭)→ 失败率超阈值 → OPEN(打开)→ 等待超时 → HALF_OPEN(半开)
     ↑                                                        ↓
     ←←←←←←←←← 试探请求成功 ←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
                                                       试探失败 → 回到OPEN

💻 Resilience4j 配置

resilience4j:
  circuitbreaker:
    instances:
      orderService:
        sliding-window-type: COUNT_BASED
        sliding-window-size: 10          # 滑动窗口10次请求
        failure-rate-threshold: 50       # 失败率50%触发熔断
        wait-duration-in-open-state: 10s # OPEN状态持续10秒
        permitted-number-of-calls-in-half-open-state: 3 # 半开状态试探3次
        slow-call-duration-threshold: 2s # 超过2秒算慢调用
        slow-call-rate-threshold: 80     # 慢调用比例80%触发

💻 代码示例

@Service
public class OrderService {

    @Autowired
    private PointsServiceClient pointsClient;

    @CircuitBreaker(name = "orderService", fallbackMethod = "addPointsFallback")
    public void createOrder(OrderDTO order) {
        // 创建订单
        orderMapper.insert(order);
        // 调用积分服务(可能失败)
        pointsClient.addPoints(order.getUserId(), order.getAmount());
    }

    // 降级方法:积分服务挂了,先记日志,后续补偿
    public void addPointsFallback(OrderDTO order, Throwable t) {
        log.warn("积分服务不可用,延迟补偿: orderId={}", order.getId());
        // 写入补偿表,定时任务重试
        compensationMapper.save(new CompensationRecord(order.getId(), "ADD_POINTS"));
    }
}

📌 Resilience4j vs Hystrix

| | Resilience4j | Hystrix | |--|-------------|--------| | 维护状态 | 活跃 | 已停更 | | 编程模型 | 函数式,轻量 | 需继承HystrixCommand | | 依赖 | 仅依赖Vavr | 依赖较多 | | 功能 | 熔断、限流、重试、隔舱 | 熔断、隔舱 |


✅ Q11:分布式事务

📋 业务场景

用户下单 = 创建订单 + 扣减库存 + 增加积分,三个操作分布在不同服务,必须保证一致性。

🔑 四种主流方案对比

| 方案 | 一致性 | 性能 | 侵入性 | 适用场景 | |------|--------|------|--------|--------| | 2PC(两阶段提交) | 强一致 | 低 | 低 | 数据库层面(XA) | | TCC | 强一致 | 中 | 高(写3个接口) | 资金类 | | 本地消息表 | 最终一致 | 高 | 中 | 异步场景 | | Seata AT | 最终一致 | 中 | 低 | 通用CRUD |

💻 Seata AT 模式

// 订单服务(发起方)
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
    // 1. 创建订单(本地事务)
    orderMapper.insert(dto);
    // 2. 扣减库存(远程调用,Seata自动管理)
    stockFeignClient.deduct(dto.getProductId(), dto.getQuantity());
    // 3. 增加积分(远程调用)
    pointsFeignClient.add(dto.getUserId(), dto.getPoints());
    // 任何一步失败,Seata自动回滚所有操作
}

Seata AT 原理:

  1. 一阶段:拦截SQL,记录 before image(undo_log),执行业务SQL,注册分支事务
  2. 二阶段提交:异步删除 undo_log
  3. 二阶段回滚:根据 undo_log 反向生成SQL,恢复数据

AT模式不适用场景:

  • 跨数据库的复杂查询
  • 需要强一致性的金融场景(用TCC)
  • 涉及外部系统调用(如第三方支付回调)

💻 本地消息表方案(最终一致性)

// 订单服务
@Transactional
public void createOrder(OrderDTO dto) {
    // 1. 创建订单
    orderMapper.insert(dto);
    // 2. 同一事务中,写入本地消息表
    messageMapper.insert(new LocalMessage(
        dto.getOrderId(),
        "DEDUCT_STOCK",
        JSON.toJSONString(dto),
        "PENDING"
    ));
}

// 定时任务:扫描本地消息表,发送MQ
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
    List<LocalMessage> messages = messageMapper.findByStatus("PENDING");
    for (LocalMessage msg : messages) {
        kafkaTemplate.send("stock-deduct", msg.getPayload());
        messageMapper.updateStatus(msg.getId(), "SENT");
    }
}

// 库存服务:消费消息,幂等处理
@KafkaListener(topics = "stock-deduct")
public void onDeductStock(String payload) {
    StockMessage msg = JSON.parseObject(payload, StockMessage.class);
    // 幂等:先查是否已处理
    if (deductLogMapper.exists(msg.getOrderId())) return;
    stockMapper.deduct(msg.getProductId(), msg.getQuantity());
    deductLogMapper.insert(msg.getOrderId());
}

✅ Q12:Elasticsearch 商品搜索

📋 业务场景

用户在电商App搜索框输入关键词,需要毫秒级返回相关商品,支持分词、模糊匹配、多维度筛选。

🔑 倒排索引原理

正排索引(MySQL):文档ID → 内容
  doc1: "红色连衣裙"
  doc2: "蓝色牛仔裤"
  doc3: "红色T恤"

倒排索引(ES):词 → 文档ID列表
  "红色" → [doc1, doc3]
  "连衣" → [doc1]
  "裙"   → [doc1]
  "蓝色" → [doc2]
  "牛仔" → [doc2]
  "T恤"  → [doc3]

搜索"红色" → 直接命中 [doc1, doc3],O(1)复杂度
MySQL LIKE '%红色%' → 全表扫描,O(n)复杂度

📌 IK分词器

// ik_max_word(最细粒度)
"中华人民共和国" → ["中华人民共和国", "中华人民", "中华", "华人", "人民共和国", "人民", "共和国", "共和", "国"]

// ik_smart(智能切分)
"中华人民共和国" → ["中华人民共和国"]

💻 Spring Data Elasticsearch 代码

@Document(indexName = "products")
public class ProductDocument {
    @Id
    private Long id;

    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String name;

    @Field(type = FieldType.Keyword)
    private String category;

    @Field(type = FieldType.Double)
    private Double price;

    @Field(type = FieldType.Integer)
    private Integer sales;
}

@Service
public class ProductSearchService {

    @Autowired
    private ElasticsearchRestTemplate esTemplate;

    public List<ProductDocument> search(String keyword, String category,
                                         Double minPrice, Double maxPrice,
                                         int page, int size) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 关键词搜索(分词匹配)
        if (StringUtils.hasText(keyword)) {
            boolQuery.must(QueryBuilders.matchQuery("name", keyword));
        }
        // 分类精确匹配
        if (StringUtils.hasText(category)) {
            boolQuery.filter(QueryBuilders.termQuery("category", category));
        }
        // 价格区间
        RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
        if (minPrice != null) rangeQuery.gte(minPrice);
        if (maxPrice != null) rangeQuery.lte(maxPrice);
        boolQuery.filter(rangeQuery);

        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(boolQuery)
            .withSort(SortBuilders.scoreSort().order(SortOrder.DESC))
            .withSort(SortBuilders.fieldSort("sales").order(SortOrder.DESC))
            .withPageable(PageRequest.of(page, size))
            .build();

        SearchHits<ProductDocument> hits = esTemplate.search(searchQuery, ProductDocument.class);
        return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
}

📌 同义词配置

// config/analysis/synonym.txt
"苹果,Apple,apple,iPhone"
"手机,移动电话,smartphone"

// 在mapping中引用
"analyzer": {
    "ik_synonym": {
        "type": "custom",
        "tokenizer": "ik_max_word",
        "filter": ["my_synonym_filter"]
    }
},
"filter": {
    "my_synonym_filter": {
        "type": "synonym",
        "synonyms_path": "analysis/synonym.txt"
    }
}

📌 ES vs MySQL LIKE 对比

| | MySQL LIKE | Elasticsearch | |--|-----------|---------------| | 原理 | 全表扫描 | 倒排索引 | | 速度 | 数据量大时极慢 | 毫秒级 | | 分词 | ❌ 不支持 | ✅ 支持中文分词 | | 相关性排序 | ❌ | ✅ TF-IDF/BM25 | | 适用场景 | 小数据量精确查询 | 大数据量全文搜索 |


🎯 总结:谢飞机的面试复盘

| 轮次 | 表现 | 评分 | |------|------|------| | 第一轮(基础) | Stream、Spring Boot、IoC、Maven 都答得不错 | ⭐⭐⭐⭐ | | 第二轮(进阶) | Redis实战OK,但缓存三大问题含糊,连接池完全不会 | ⭐⭐⭐ | | 第三轮(高级) | Kafka、分布式事务、熔断降级全部卡壳 | ⭐⭐ |

给所有Java同学的建议:

  1. 基础要扎实:Stream、Spring IoC、Maven 是地基,必须滚瓜烂熟
  2. 原理要深入:不要只停留在"会用",要理解"为什么"
  3. 实战要真实:面试官一问细节就能分辨你是真做过还是背的八股
  4. 简历别写"精通":除非你真的能讲清楚底层原理和调优经验
  5. 分布式是必修课:微服务、消息队列、分布式事务,大厂必问

谢飞机最后把简历上的"精通"改成了"熟悉",但他没有放弃。三个月后,他带着真正的项目经验和扎实的技术功底,再次走进了大厂的大门。

这一次,他没有说"回去等通知"。

面试官说的是:"你什么时候能来上班?"


如果这篇文章对你有帮助,欢迎点赞、收藏、转发!你的支持是我创作的最大动力!🚀

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐