第七节 实战面试题集锦

亮子 2025-09-08 21:01:41 65 0 0 0

1、如何处理缓存预热和秒杀活动中的缓存问题?

缓存预热指在系统上线或活动开始前,提前将热点数据加载到缓存中,避免瞬间的数据库请求压垮后端。

如何处理缓存预热:

(1) 识别热点数据
- 静态热点:通过业务分析提前预知。如:秒杀活动的商品、热门新闻、首页推荐列表。
- 动态热点:通过实时数据分析系统(如 Flink、Spark Streaming)实时计算得出。如:突然爆火的视频、微博热搜。

(2) 预热时机与策略
- 系统启动时:服务启动后,自动从数据库或离线文件加载热点数据到缓存。
- 定时任务:在活动开始前(如秒杀前5分钟),通过定时任务(如 Spring Scheduler, XXL-Job)提前加载。
- 手动触发:运营人员通过管理后台手动触发预热操作。

(3) 预热的正确姿势
- 避免缓存击穿:预热不仅要加载数据,更要**设置合理的过期时间**。建议采用基础过期时间 + 随机扰动值的策略,避免大量key同时失效。
- 分片加载:如果预热的数据量巨大(如全量商品信息),不要一次性加载。应对数据**进行分片**,由多个节点分批缓慢加载,避免占满带宽和缓存服务资源。
- 冷热分离:对极热点的数据(如秒杀商品),可以**单独使用一个缓存集群**,与普通缓存隔离,避免相互影响。

如何缓存秒杀活动数据:

专门针对**秒杀活动中的缓存设计**,这是一个非常经典的高并发系统设计问题。核心思路是:**将绝大部分请求拦截在数据库之前,让缓存承担最大的压力**。

以下是详细的设计方案和最佳实践。

(1) 库存缓存 (Stock Cache) - 最关键的Key
- 目的:承担所有库存查询和扣减的请求,保护数据库。
- 设计
- Key: seckill:stock:{skuId} (e.g., seckill:stock:12345)
- Value: 库存数量(Integer)。**注意: value 设置为 String 类型,方便使用原子命令**。
- 命令:使用 DECRDECRBY 进行原子性扣减。

(2) 已售罄缓存 (Sold-out Cache)
- 目的:快速过滤无效请求。一旦库存为0,将商品标记为售罄,后续请求直接返回,无需访问库存Key。
- 设计
- Key: seckill:sold_out:{skuId}
- Value: 1 (表示售罄) 或 0 (表示未售罄)。
- 流程:请求先判断此标志位,若为1,直接返回“已售罄”。

(3) 用户购买记录缓存 (User Purchase Cache)
- 目的:防止同一用户重复购买,实现“一人一单”的逻辑。
- 设计
- Key: seckill:user:{userId}:{skuId} 或使用 Set 类型 seckill:success_users:{skuId} 存储所有成功用户的ID。
- Value: 订单ID或简单的成功标志(如1)。
- 流程:用户预扣库存成功后,将其用户ID写入此缓存。下次请求时先检查该用户是否已购买。

2、在项目中,使用 Redis 和 MQ 时遇到了哪些问题,是如何解决的?

在使用 Redis 和 MQ(如 RabbitMQ, Kafka, RocketMQ)时,确实会遇到各种问题。下面我将分别梳理常见问题及解决方案。


**Redis **

1. 缓存穿透

  • 问题:大量请求查询一个**数据库中根本不存在**的数据(如不存在的用户ID)。导致请求绕过缓存,直接打到数据库,可能压垮数据库。
  • 解决
    • 缓存空值:即使查询不到数据,也将 null 或一个特殊值(如 "NULL")写入缓存,并设置一个较短的过期时间(如 30-60秒)。
    • 布隆过滤器 (Bloom Filter):将所有可能存在的key哈希到一个巨大的bitmap中。请求来时,先经过布隆过滤器校验:
      • 如果判断**可能存在**,则继续后续流程(查缓存/数据库)。
      • 如果判断**肯定不存在**,则直接返回,保护后端。

2. 缓存击穿

  • 问题:某个**热点key**(如爆款商品)在缓存过期的瞬间,大量请求同时到来,全部无法命中缓存,集体涌向数据库。
  • 解决
    • 永不过期:对极热点key,不设置过期时间,通过程序逻辑或定时任务在后台更新。
    • 互斥锁 (Mutex Lock):当缓存失效时,不是所有请求都去查数据库,而是**只有一个请求**能获得锁(如 SET lock_key unique_value NX PX 3000)。这个请求去查询数据库并重建缓存,其他请求则重试或等待缓存被填充。
    • 逻辑过期:缓存值不设置TTL,而是将过期时间存在value里。发现逻辑过期后,获取互斥锁,异步更新缓存。

3. 缓存雪崩

  • 问题:大量缓存key在**同一时间**或一个**短时间段内**集中失效,导致所有请求都涌向数据库。
  • 解决
    • 差异化过期时间:给缓存key的TTL加上一个**随机值**(如 基础时间 + random(0, 300)s),避免同时失效。
    • 缓存预热:系统上线或活动开始前,提前加载热点数据到缓存。
    • 构建高可用缓存集群:如 Redis Sentinel 或 Redis Cluster,防止因一台Redis宕机导致所有缓存不可用。
    • 服务降级与熔断:使用 Hystrix 或 Sentinel 等组件,当数据库压力过大时,对非核心服务进行降级,甚至直接熔断。

4. 数据一致性

  • 问题:更新数据库后,如何保证缓存与数据库的数据一致?(先更新数据库还是先删除缓存?)
  • 解决
    • 常用策略:Cache-Aside Pattern
      1. 读请求:先读缓存,命中则返回;未命中则读数据库,写入缓存。
      2. 写请求:**先更新数据库,再删除缓存(而非更新缓存)**。
    • 延迟双删:对于极高并发场景,在更新数据库后,先删一次缓存,然后延迟几百毫秒再删一次,以清除期间可能被其他线程写入的脏数据。
    • 通过 Canal 订阅数据库binlog:异步淘汰缓存,实现最终一致性,对业务代码无侵入。

5. 大Key与热Key

  • 大Key问题:一个key存储的value很大(如一个很大的hash或list),导致网络阻塞、查询慢、迁移困难。
    • 解决:将大key拆分成多个小key(如 user:info:1:base, user:info:1:detail)。或者使用序列化工具压缩value。
  • 热Key问题:某个key的访问频率极高,超过单台Redis服务器的处理能力。
    • 解决
      • 本地缓存:在应用层使用JVM本地缓存(如Caffeine)存储热Key,并设置短的过期时间。
      • Key复制:将热Key复制到多个Redis实例(如 key:1, key:2),请求通过客户端负载均衡到不同实例。

**消息队列 (MQ) **

1. 消息丢失

  • 问题:消息在生产者、MQ Broker、消费者这三个环节都可能丢失。
  • 解决
    • 生产者端
      • 开启confirm机制(RabbitMQ)或 ACK机制(Kafka):确保消息成功发送到Broker。如果失败,进行重试或记录日志。
    • Broker端
      • 持久化:将消息和队列都设置为持久化的,防止MQ宕机后消息丢失。
    • 消费者端
      • 关闭自动ACK,改为**手动ACK**。确保业务逻辑处理成功后,再通知Broker删除消息。如果处理失败,则进行重试或放入死信队列。

2. 消息重复消费

  • 问题:网络抖动、消费者重启等原因,可能导致消费者已处理消息但ACK未成功发送,Broker会重新投递消息,导致重复消费。
  • 解决
    • 保证消费逻辑的幂等性。这是最重要的解决方案。常见方法:
      • 数据库唯一键:利用数据库主键或唯一索引约束,防止重复插入(如订单ID)。
      • Redis SetNX:在处理前,将消息的唯一ID(如订单ID)写入Redis。如果已存在,则说明已处理过。
      • 乐观锁:更新数据时带上版本号(update table set version = version+1 where id=xxx and version=old_version)。

3. 消息积压

  • 问题:消费者消费速度远跟不上生产者发送速度,导致大量消息堆积在MQ中。
  • 解决
    • 紧急扩容:快速增加Consumer实例数量,提升整体消费能力。
    • 提升消费速度:优化消费者代码,避免耗时操作(如改为异步处理、批量处理)。
    • 降级非核心业务:如果积压的是非核心消息,可以将其路由到其他topic并记录,事后修复。
    • 源头限流:通知生产者暂时降低发送速率。

4. 顺序消息

  • 问题:默认情况下,消息可能不按顺序被消费(如一个订单的创建、付款、发货消息)。
  • 解决
    • RabbitMQ:保证顺序需要将需要保证顺序的一组消息发送到**同一个Queue**,并且只能有一个Consumer来消费这个Queue。
    • Kafka/RocketMQ:将需要保证顺序的消息发送到同一个 Partition 中,因为一个Partition只能被一个Consumer Group中的一个Consumer消费。

5. 死信队列 (DLQ) 的使用

  • 问题:有些消息因多次重试仍失败,一直占用资源,如何优雅地处理?
  • 解决
    • 配置MQ,当消息满足一定条件(如重试次数超限、被拒绝)时,将其自动投递到一个特殊的**死信交换机 (DLX)** 和 死信队列 (DLQ) 中。
    • 然后可以有独立的监控系统或人工程序来处理DLQ中的消息,分析失败原因,进行补偿或报警。

总结与项目经验描述

在面试中,你可以这样组织你的回答:

“在之前的XX项目中,我们大量使用了Redis和RabbitMQ/Kafka。确实遇到过一些典型问题:”

  1. “关于Redis,我们遇到过缓存击穿的问题。 当时一个热点商品缓存失效,瞬间DB请求量飙升。我们的解决方案是采用**互斥锁**:让一个请求去重建缓存,其他请求等待。同时,我们对关键数据进行了**缓存预热**,并给过期时间加上了**随机值**来避免雪崩。”
  2. “对于数据一致性问题, 我们遵循的是 ‘先更新数据库,再删除缓存’ 的策略。并且在一次秒杀活动中,因为并发极高,我们还采用了**延迟双删**的策略来确保万无一失。”
  3. “关于MQ,我们最担心的是消息丢失。 所以我们在生产者端开启了**confirm机制**,在Broker设置了**持久化**,在消费者端采用了**手动ACK**,只有业务成功处理后才确认消息,这样就构建了一个可靠的消息链路。”
  4. “另一个常见问题是消息重复, 我们的核心原则是**保证消费逻辑的幂等性**。比如在处理订单支付消息时,我们会先查一下Redis或数据库,判断这个订单是否已经处理过,避免重复入账。”

“通过这些实践,我们系统的高可用性和数据一致性得到了很大保障。”

这样的回答,既展示了你的技术深度,也体现了你从问题中发现、分析到解决的完整思维能力。

3、在微服务架构中,网关组件的作用是什么?

(1) 统一入口 (Unified Entry Point)

  • 功能:为所有客户端(Web、App、第三方)提供一个统一的访问地址(如 api.yourcompany.com),客户端无需感知内部有多少个微服务以及它们的地址。
  • 好处:实现了客户端与后端微服务的**解耦**。后端服务可以随时进行拆分、合并、升级或迁移,只要网关的路由规则随之更新,客户端无需任何改动。

(2)**身份认证与鉴权 (Authentication & Authorization)**

  • 功能:在网关层集中进行用户身份验证(如校验 JWT Token、API Key)和权限控制(如判断用户是否有权访问某个API)。
  • 好处
    • 安全:避免了每个微服务都需要重复实现认证逻辑,降低了安全漏洞的风险。
    • 性能:非法请求在网关层就被拦截,不会流入内部网络,减轻了后端服务的压力。

(3) 流量控制与熔断 (Rate Limiting & Circuit Breaking)

  • 功能
    • 限流:限制每个用户、每个IP或每个服务的请求频率,防止恶意攻击或某个服务被突发流量打垮。
    • 熔断降级:当某个目标服务响应慢或大量失败时,网关可以自动进行熔断,直接返回预设的降级响应(如“服务繁忙,请稍后重试”),防止故障蔓延导致系统雪崩。
  • 好处:保障系统的**高可用性**和**稳定性**。

(4) 请求路由与负载均衡 (Routing & Load Balancing)

  • 功能:根据请求的路径(如 /user/**)、方法(GET/POST)或参数,将请求动态地路由到正确的后端服务实例上,并实现负载均衡(如轮询、随机、加权)。
  • 示例:将 /order/v1/** 的请求路由到 order-service 集群,将 /payment/v1/** 的请求路由到 payment-service 集群。
  • 好处:是微服务动态扩缩容的基础,也是实现灰度发布、蓝绿部署等高级特性的关键。

(5) 请求聚合与编排 (Aggregation & Orchestration)

  • 功能:针对特定场景(如移动端首页),网关可以将多个后端服务的调用合并为一个请求,统一返回给客户端。
  • 好处:极大减少了客户端与服务器之间的**网络往返次数(RTT)**,显著提升了前端性能,尤其适合网络环境不稳定的移动端。

(6) 跨域处理 (CORS)

  • 功能:集中处理浏览器跨域请求(OPTIONS 预检请求),无需每个服务单独配置。
  • 好处:简化了前端跨域访问的配置。

(7) 日志记录与监控 (Logging & Monitoring)

  • 功能:作为所有流量的必经之地,网关可以统一收集访问日志、 metrics(如QPS、响应时间、错误率)。
  • 好处:为系统性能监控、问题排查和数据分析提供了**全局视角**。

(8) 安全防护 (Security)

  • 功能:除了认证鉴权,还可以集成WAF(Web应用防火墙)功能,防御SQL注入、XSS等常见网络攻击。
  • 好处:为内部脆弱的微服务构筑了一道坚实的安全防线。

4、MySQL 在执行查询和更新语句时的底层流程是什么?

(1)查询语句流程:
- 连接层:建立连接、权限验证。
- 服务层:SQL解析、优化器生成执行计划。
- 存储引擎层:按执行计划操作数据,返回结果。

** 记忆法:**“接连优执”——连接验证、解析优化、执行返回。

(2)更新语句流程:
- 连接层:建立连接、权限验证。
- 服务层:SQL解析、优化器生成执行计划。
- 事务层:加锁、记录redo/undo日志。
- 存储引擎:更新内存数据,异步刷盘。

记忆法:“连解锁日志,刷盘保持久”——连接解析、加锁记日志、刷盘确保持久化。

5、请介绍一下 Spring Boot 和 Spring Cloud 的关系?

Spring Boot 是快速开发单个微服务的框架,简化配置和部署(“单体微服务加速器”);

Spring Cloud 是微服务架构的全家桶,基于 Spring Boot 实现服务注册发现、配置中心、网关、负载均衡、远程微服务调用、熔断降级等分布式功能( 微服务的“治理大师”)。

关系:Spring Cloud 依赖 Spring Boot 构建,Boot 是基础,Cloud 是扩展。

记忆法:“Boot 单快,Cloud 群管”——Boot 负责单个服务快速开发,Cloud 负责管理一群服务。

6、如何设计柔性事务?

设计柔性事务(**保证最终一致性**)的核心思路:

  1. 方案选择:
  • 本地消息表:业务表与消息表同事务,异步发送消息。
  • 事务消息:利用中间件(如RocketMQ)的事务消息机制。
  • TCC(Try-Confirm-Cancel):分阶段执行,预留资源、确认提交或取消释放。
  1. 关键设计:
  • 幂等处理:防止重复执行(如用唯一ID+状态判断)。
  • 状态追踪:记录事务阶段,便于回滚或重试。
  • 异步补偿:失败时通过定时任务触发补偿逻辑。

记忆法:“方幂状补”——选方案(本地/消息/TCC)、做幂等、追状态、补失败。

7、如何实现订单的延时处理?

实现订单延时处理的常见方式:

1. 定时任务:如Quartz,定期扫描待处理订单(适合低频场景)。
2. 消息队列:利用延迟队列(如RabbitMQ的TTL+死信队列、RocketMQ延迟消息),订单创建时发送延时消息,到期触发处理。
3. 时间轮算法:高效处理大量定时任务,如Netty的HashedWheelTimer。

记忆法:“定消时”——定时任务扫、消息队列推、时间轮算。

8、在项目中,如何优化数据库查询性能?

优化数据库优化数据库查询性能可从以下几方面入手:

  1. 索引优化:为查询频繁的字段建索引(主键、联合索引等),避免过度索引。
  2. SQL优化:避免select *、减少join、用exists代替in、分页优化(limit优化)。
  3. 表结构优化:分库分表(水平/垂直拆分)、合理设计字段类型。
  4. 缓存策略:热点数据缓存(如Redis),减少DB访问。
  5. 服务器配置:调整连接池、内存缓存(innodb_buffer_pool)等参数。

记忆法:“索SQL表缓配”——索引、SQL语句、表结构、缓存、配置。