场景题
一、电商库存系统设计面试问答
库存系统整体怎么设计?
面试官:电商业务中的库存系统应该怎么设计?
答:
我理解库存系统不能只看成一个简单的“数量加减”,它核心要解决三个问题:第一是不能超卖,第二是下单未支付时要能锁库存,第三是要能支撑 Redis、MySQL、订单系统之间的数据一致性。
如果是高并发场景,比如秒杀或大促,我会采用 Redis + MySQL + MQ 的设计。Redis 扛住瞬时流量,MySQL 做最终数据落库,MQ 用来削峰和异步创建订单。
1. 库存数据怎么存?
MySQL 作为最终数据源,库存表里一般不会只放一个 stock 字段,而是拆成几个字段:
| 字段 | 含义 |
|---|---|
available_stock | 可售库存,用户当前能下单的数量 |
locked_stock | 锁定库存,用户已下单但还没支付的数量 |
actual_stock / total_stock | 实际库存,方便商家对账 |
version | 版本号,用于乐观锁,防止并发更新冲突 |
Redis 这边可以用 Hash 结构,比如:
| Redis Key | Field |
|---|---|
stock:{sku_id} | available、locked、version |
2. 高并发下怎么防止超卖?
高并发场景下,我不会让每个请求都直接打 MySQL,而是先在 Redis 里做预扣库存。
用户下单时,通过 Redis Lua 脚本做原子操作:先判断 available 是否足够,如果足够,就扣减 available,同时增加 locked。因为 Lua 脚本在 Redis 中是原子执行的,所以可以避免多个请求同时扣减导致超卖。
Redis 扣减成功后,再发送 MQ 消息,由订单服务异步创建订单,并把库存变更落到 MySQL。MySQL 层也不会完全信任前面的流程,还会用乐观锁或者 available_stock >= quantity 这种条件做最后兜底。如果数据库更新失败,就走补偿逻辑,比如回滚 Redis 库存或取消订单。
Redis 里的可用库存和锁定库存从哪里来?
面试官:Redis Hash 里的可用库存和锁定库存,是从数据库拿的吗?怎么拿?
答:
是的,Redis 里的库存数据初始来源一定是 MySQL。Redis 只是为了提高读取和扣减性能,真正可靠的数据源还是数据库。
一般有两种加载方式:一种是活动前预热,另一种是运行时懒加载。
1. 活动前预热
比如明天 10 点有一场秒杀,库存是 100 台。系统会在活动开始前,比如 9:55,从 MySQL 查询这个 SKU 的库存,然后写入 Redis Hash。
写入后的结构大概是:
| Field | Value |
|---|---|
available | 100 |
locked | 0 |
version | 当前数据库版本号,可选 |
这样用户开抢时,Redis 里已经有库存数据,请求不需要再查数据库。
2. 运行时懒加载
如果是普通商品,或者 Redis 里刚好没有这个库存缓存,请求进来会先查 Redis。如果发现为空,就不能直接让所有请求都去查 MySQL,否则会造成缓存击穿。
比较稳的做法是:先加分布式锁,只让一个线程去 MySQL 查询库存,然后回写 Redis,并设置过期时间。其他请求等缓存回填后,直接读 Redis。
这个流程可以概括为:
先查缓存,缓存没有就加锁,拿到锁后查数据库,写回 Redis,再释放锁。
3. 商家修改库存时怎么同步?
如果商家后台补货或修改库存,一般以 MySQL 为准。流程是先更新数据库,再更新或删除 Redis 缓存。
我更倾向于删除 Redis Key,让下一次访问自动触发懒加载,这样可以减少直接更新缓存带来的不一致风险。
普通下单场景怎么设计?
面试官:如果只是普通下单购买商品,没有那么高的并发,库存系统应该怎么设计?
答:
普通下单场景下,我不会把系统设计得像秒杀那么重。因为并发不高时,最重要的不是极限性能,而是数据准确、流程简单、后期好维护。
我会采用 MySQL 为核心,Redis 做读取加速 的方案。
1. 数据模型
普通电商库存表也建议拆成三类库存:
| 字段 | 含义 |
|---|---|
total_stock | 总库存,代表仓库真实库存 |
available_stock | 可用库存,代表当前还能卖多少 |
locked_stock | 锁定库存,代表已下单但未支付的数量 |
version | 乐观锁版本号 |
这样设计的原因是,用户下单后不一定马上支付。如果只减一个 stock 字段,就很难处理待支付、取消订单、超时释放这些场景。
2. 下单时怎么扣库存?
普通场景下,我会直接扣 MySQL,用数据库事务保证强一致性。
用户点击下单时,系统先判断库存是否足够,然后执行库存锁定:
把 available_stock 减少,把 locked_stock 增加,同时用 version 或 available_stock >= quantity 防止并发超卖。
如果 SQL 影响行数是 1,说明锁库存成功;如果是 0,说明库存不足或者发生并发冲突,就返回失败或重试。
锁库存成功后,再创建一条订单记录,订单状态是“待支付”。
3. 支付成功怎么处理?
用户支付成功后,订单状态改为“已支付”。库存这边再把 total_stock 减少,同时把 locked_stock 减少。
这里不需要再减 available_stock,因为下单阶段已经扣过可售库存了。
4. 取消订单或超时未支付怎么办?
如果用户主动取消订单,或者 30 分钟内没有支付,就要释放库存。
释放时把 locked_stock 减少,把 available_stock 加回来,同时把订单状态改成“已取消”。
5. Redis 在普通场景里做什么?
普通场景下,Redis 不作为核心扣减入口,只做库存展示的缓存。比如商品详情页显示“有货”“库存紧张”,可以直接读 Redis,减少数据库压力。
如果 Redis 和 MySQL 不一致,我会以 MySQL 为准,再通过定时任务做库存对账,比如每小时或每天凌晨对比一次,把 Redis 里的库存修正过来。
普通下单和秒杀库存设计有什么区别?
面试官:普通下单系统和秒杀系统的库存设计有什么区别?
答:
主要区别在于系统目标不一样。普通下单更重视一致性和可维护性,秒杀更重视抗高并发和削峰。
| 维度 | 普通下单 | 秒杀场景 |
|---|---|---|
| 核心目标 | 数据准确、逻辑简单 | 抗瞬时高并发 |
| 扣减位置 | MySQL 直接扣减 | Redis Lua 预扣 |
| Redis 作用 | 主要做读取缓存 | 参与核心扣减 |
| MQ 作用 | 可以不用 | 用于削峰和异步下单 |
| 一致性策略 | 数据库事务 + 乐观锁 | Redis 原子扣减 + MQ + 数据库兜底 |
| 系统复杂度 | 较低 | 较高 |
面试里我会这样总结:
对于普通商城项目,我会优先保证库存数据的一致性和系统可维护性,所以采用 MySQL 为核心的库存设计。通过 available_stock 和 locked_stock 处理下单未支付场景,用乐观锁和库存条件判断解决并发扣减问题。Redis 主要用于库存展示缓存,再配合定时对账任务兜底。这样既能避免超卖,也不会把普通业务设计得过于复杂。
乐观锁的版本号怎么生成?
面试官:上面说到乐观锁,那个版本号是怎么生成的?
答:
乐观锁的版本号一般不是代码单独生成的,也不是用 UUID 这种方式生成的,而是数据库在每次更新成功时自动递增。
1. 初始版本号怎么来?
商品库存记录刚创建时,version 字段一般默认是 0 或 1。这个默认值可以在建表时设置,也可以由代码插入时初始化。
2. 更新时版本号怎么变化?
每次更新库存时,SQL 会同时做两件事:
一是用旧版本号做校验,比如 where version = oldVersion;
二是更新成功后,把版本号加 1,比如 set version = version + 1。
也就是说,新版本号不是提前算好的,而是在数据库更新成功的那一刻,由数据库基于旧版本号递增出来的。
3. 为什么要用版本号?
版本号的作用是判断“我现在要修改的数据,还是不是我刚才查到的那一份”。
比如线程 A 查到库存是 10,版本号是 1。在线程 A 更新之前,线程 B 已经改过库存,并把版本号改成 2。那线程 A 再拿版本号 1 去更新时,就会失败,避免覆盖别人已经提交的修改。
4. 为什么不只判断库存值?
只判断库存值可能会有 ABA 问题。比如库存从 10 变成 9,又被改回 10。线程 A 如果只看库存值,会以为数据没变过,但其实中间已经被修改过。
版本号就能解决这个问题。哪怕库存值又回到了 10,版本号也已经从 1 变成了 3,线程 A 拿旧版本号更新就会失败。
5. 如果用 MyBatis-Plus 怎么做?
如果项目里用 MyBatis-Plus,可以在实体类的 version 字段上加 @Version 注解。更新时框架会自动帮我们把旧版本号加到 where 条件里,并在更新成功后执行版本号加 1。
面试里我会这样说:
乐观锁的版本号通常是数据库维护的。库存记录初始化时给一个默认版本号,比如 0。每次更新库存时,SQL 会带上旧版本号作为条件,只有版本号匹配才允许更新,更新成功后再执行 version = version + 1。这样可以判断数据有没有被别人改过,避免并发更新覆盖,也能解决只判断库存值可能出现的 ABA 问题。
二、如何设计一个高并发系统的方案
回答思路
设计高并发系统不能一上来就堆技术组件,而是先明确业务目标、流量规模和瓶颈位置,再从入口、应用、缓存、数据库、异步化、稳定性几个层面逐步设计。
可以按照“先抗住流量,再保证正确性,最后保证可观测和可恢复”的思路来回答。
1. 明确并发目标和容量评估
首先要确认业务峰值,例如 QPS、TPS、日活、峰值持续时间、核心接口响应时间、数据一致性要求等。
常见评估方式:
- 根据历史流量和活动预估峰值流量。
- 区分读多写少、写多读少、瞬时突发等不同场景。
- 明确核心链路和非核心链路,例如下单、支付、库存扣减属于核心链路,短信通知、积分发放、日志记录可以异步处理。
只有先确定容量目标,后面的限流、缓存、线程池、数据库拆分才有依据。
2. 入口层削峰和限流
高并发系统首先要保护入口,避免瞬时流量直接打穿后端服务。
常见方案:
- 使用 Nginx、网关或负载均衡把请求分发到多个服务节点。
- 对核心接口增加限流,例如令牌桶、漏桶、滑动窗口限流。
- 对恶意请求、重复请求、爬虫请求做风控和拦截。
- 对秒杀、抢购类场景增加排队、验证码、预约、分批放量等削峰手段。
入口层的目标不是让所有请求都进来,而是保证系统在可承受范围内稳定运行。
3. 应用层无状态和水平扩展
应用服务尽量设计成无状态,这样才能通过增加机器数量进行水平扩容。
关键点:
- 用户登录态放到 Redis 或统一认证中心,不依赖单机 Session。
- 文件、图片等静态资源放到对象存储或 CDN。
- 服务节点通过注册中心和负载均衡进行调用。
- 线程池、连接池、超时时间要根据机器资源和接口耗时合理配置。
应用层的核心目标是让服务可以快速扩容,并且单个节点故障不会影响整体系统。
4. 缓存设计降低数据库压力
高并发系统里,数据库通常是最容易成为瓶颈的地方,所以读请求要优先考虑缓存。
常见方案:
- 使用本地缓存缓存热点配置或字典数据。
- 使用 Redis 缓存商品详情、用户信息、库存预扣等高频访问数据。
- 对热点 Key 增加随机过期时间,避免缓存雪崩。
- 对空值进行缓存,避免缓存穿透。
- 对热点数据增加互斥锁或逻辑过期,避免缓存击穿。
缓存不是简单地加一层 Redis,还要考虑过期策略、数据一致性和热点 Key 问题。
5. 数据库层优化和拆分
当缓存无法完全解决写入压力或数据量压力时,需要从数据库层继续优化。
常见方案:
- SQL 优化:避免
SELECT *、避免索引失效、使用EXPLAIN分析执行计划。 - 索引优化:根据查询条件建立合适的联合索引,遵循最左前缀原则。
- 读写分离:主库负责写,从库负责读,降低主库压力。
- 分库分表:当单表数据量过大时,按用户 ID、订单 ID 等业务字段进行水平拆分。
- 冷热数据分离:近期热数据留在在线库,历史数据归档到冷库或数仓。
数据库层要重点关注慢查询、锁冲突、连接池耗尽和主从延迟。
6. 异步化和消息队列削峰
对于非实时、非核心的业务,可以通过 MQ 异步处理,降低主链路耗时。
典型场景:
- 下单成功后发送短信、推送通知、发放积分。
- 支付成功后异步更新统计报表。
- 秒杀请求先写入 MQ,再由消费者按能力处理。
使用 MQ 时要重点保证消息可靠性:
- 生产者发送确认。
- 消息持久化。
- 消费者幂等处理。
- 消费失败重试。
- 必要时使用死信队列兜底。
MQ 的作用是削峰填谷,但不能把一致性问题直接丢给 MQ,需要配合幂等、补偿和对账机制。
7. 数据一致性和幂等设计
高并发系统不能只追求快,还要保证数据不乱。
关键设计:
- 核心写操作要有幂等控制,例如使用业务唯一号、订单号、请求流水号。
- 库存扣减要使用乐观锁、Redis Lua 脚本或数据库条件更新,避免超卖。
- 涉及多个系统的数据变更,可以使用本地消息表、事务消息、TCC 或最终一致性方案。
- 对支付、库存、账户余额这类关键数据,要有定时对账和补偿任务。
面试中可以强调:能强一致就强一致,强一致成本太高时,用最终一致性加补偿机制。
8. 稳定性保护和可观测性
最后要考虑系统出问题时如何保护和定位。
稳定性方案:
- 设置合理的超时时间,避免线程长时间阻塞。
- 对下游服务做熔断、降级和隔离。
- 对核心接口做限流和排队。
- 通过灰度发布、滚动发布降低上线风险。
可观测性方案:
- 接入日志、指标、链路追踪。
- 监控 QPS、RT、错误率、CPU、内存、GC、线程池、连接池等指标。
- 对慢 SQL、MQ 积压、Redis 热点 Key 设置告警。
高并发系统一定要能快速发现问题、定位问题、恢复问题。
面试总结
可以这样总结:
高并发系统设计的核心不是单点技术,而是一整套体系。入口层做限流和削峰,应用层无状态水平扩展,缓存层承接高频读,数据库层做索引优化、读写分离和分库分表,非核心链路通过 MQ 异步化,同时通过幂等、补偿、对账保证数据一致性,最后配合熔断、降级、监控和链路追踪保证系统稳定性。
三、接口性能定位与优化
1. 如何系统性定位性能瓶颈?
在动手优化前,必须先找到“慢”在哪里。可以按照从宏观到微观的顺序逐层排查:
1. 确认问题范围(看监控) 首先打开监控面板,快速判断问题的性质:
- 是个别接口慢还是所有接口都慢? 如果只有个别慢,问题通常在业务代码或特定依赖;如果整体都慢,可能是服务器资源(CPU、内存、带宽)到了瓶颈。
- 是一直慢还是突然变慢? 突然变慢通常与外部依赖故障、流量突增或定时任务有关。
- 是所有请求都慢还是部分慢? 观察 P50(中位数)和 P99(尾部延迟)指标。如果 P50 正常但 P99 极高,说明有少量“坏请求”拖了后腿。
2. 分层排查,逐个击破 一个请求的生命周期通常为:用户请求 → 网关/代理 → 应用服务器 → 业务代码 → 数据库/缓存/第三方服务。建议从下往上或结合链路追踪工具进行排查:
基础设施层: 登录服务器使用
top、vmstat等命令。重点关注 CPU 的wa(IO等待)是否过高,以及系统负载(load average)是否持续超过 CPU 核数。应用代码层:
这是最常见的瓶颈区。强烈推荐使用
Arthas等在线诊断工具,无需重启应用即可定位。- 使用
trace命令追踪方法内部的调用链路和各环节耗时,一眼就能看出是哪个子方法占用了大部分时间。 - 使用
thread或jstack查看线程状态,排查是否存在死锁或大量线程处于BLOCKED状态(锁竞争)。
- 使用
数据库与依赖层:
- 检查数据库的慢查询日志,对可疑 SQL 执行
EXPLAIN分析执行计划,查看是否发生了全表扫描或索引失效。 - 检查 Redis 缓存命中率,以及第三方 RPC/HTTP 调用的超时情况。
- 检查数据库的慢查询日志,对可疑 SQL 执行
2. 常见优化手段
定位到具体瓶颈后,可以从以下几个维度进行针对性优化:
1. 数据库层优化(性价比最高的优化) 数据库查询慢通常是接口卡顿的头号杀手(约占40%)。
- 消灭 N+1 查询: 避免在循环中查数据库。例如获取用户列表及其订单时,应先查出所有用户,提取 ID 批量查询订单,再在内存中进行关联映射。
- 索引与 SQL 规范: 为高频查询条件添加合适的联合索引;严禁使用
SELECT *,只查询前端需要的字段,减少网络传输和 IO 开销;尽量避免LIKE '%xxx%'这种左模糊查询导致索引失效。
2. 引入多级缓存 对于读多写少、实时性要求不高的数据(如商品详情、字典配置),优先走缓存。
- 可以采用“本地缓存 + 分布式缓存(Redis)”的多级缓存策略。
- 注意: 必须为缓存设置合理的过期时间,防止内存溢出或数据长期不一致。
3. 异步处理与非核心逻辑剥离 将接口中的非核心流程剥离出去。例如用户注册成功后发送短信通知、记录操作日志等,这些动作完全不需要阻塞主接口的响应,可以通过消息队列(MQ)或线程池进行异步化处理。
4. 架构与外部依赖治理
- 设置超时与熔断: 任何对外部服务(包括内部其他微服务、第三方 API)的调用,都必须设置合理的超时时间。同时引入熔断机制(如 Sentinel、Resilience4j),当下游服务响应过慢或不可用时,快速失败并返回降级数据,防止自身服务的线程池被拖垮。
- Web 容器调优: 根据服务器配置合理调整 Tomcat 等容器的最大线程数和连接超时时间,开启 Gzip 压缩以减少网络传输量。
四、Redis 与 Lua 脚本
Redis 结合 Lua 脚本,相当于给 Redis 装上了一个“可编程的原子操作引擎”。它的核心作用主要体现在以下三个方面:
1. 保证复杂操作的原子性 Redis 本身是单线程的,Lua 脚本在 Redis 中执行时也是原子性的。这意味着脚本里的所有命令会作为一个整体被执行,中间不会被其他客户端的命令打断。
- 对比传统事务: Redis 原生的
MULTI/EXEC事务不支持回滚,且无法根据上一步的执行结果进行条件判断。而 Lua 脚本可以包含if-else、循环等复杂逻辑,真正实现了“要么全部成功,要么完全不执行”的强一致性。
2. 大幅减少网络开销,提升性能 在高并发场景下,客户端和 Redis 服务器之间的网络延迟往往是性能瓶颈。
- 合并请求: 原本需要客户端发送多次网络请求(比如先
GET获取数据,在客户端判断逻辑,再SET写回数据)才能完成的操作,通过 Lua 脚本可以合并成一次网络往返。脚本直接在 Redis 服务端运行,极大地降低了网络延迟,提升了系统的吞吐量。
3. 灵活实现复杂的业务逻辑 Lua 脚本让开发者可以在 Redis 服务端直接编写定制化的业务逻辑,而不仅仅局限于 Redis 提供的内置命令。这在处理复杂数据结构(如 ZSET、Hash)或需要多步计算的场景下非常有用。
经典应用场景
为了让你更直观地理解,以下是 Redis + Lua 脚本最常见的几个实战场景:
- 高并发库存扣减(如秒杀系统): 在秒杀时,需要“判断库存是否充足”和“扣减库存”两步操作绝对原子化,否则极易出现超卖。Lua 脚本可以完美解决这一问题,性能远超数据库锁或普通的 Redis 分布式锁。
- 精准的滑动窗口限流: 利用 Redis 的 ZSET(有序集合)配合 Lua 脚本,可以原子化地清理过期请求、统计当前窗口内的请求数,并判断是否放行,实现非常精准的 API 限流。
- 高可靠分布式锁: 普通的
SETNX加锁如果后续设置过期时间的命令执行失败,容易导致死锁。Lua 脚本可以保证“加锁”和“设置过期时间”的原子性;同样,在释放锁时,也能通过脚本原子化地“判断锁是否属于当前线程”并“删除锁”,防止误删他人的锁。 - 解决缓存击穿: 当热点缓存失效时,Lua 脚本可以原子化地执行“加锁 -> 查数据库 -> 重建缓存 -> 释放锁”的全流程,防止大量请求瞬间击穿到数据库。
五、秒杀与库存一致性专题
问题 1:秒杀场景下,Redis 和库存一致性怎么保证?
回答思路
秒杀库存使用 Redis 做预扣减。
具体流程如下:
- 将待秒杀商品的库存数量提前预热到 Redis 中。
- 秒杀请求到来时,使用 Lua 脚本在 Redis 中完成库存扣减。
- 用户下单并支付成功后,再扣减数据库中的真实库存。
- 数据库库存扣减和支付状态更新放在同一个事务中,保证数据库库存和支付状态的一致性。
关键点
- Redis 负责抗高并发和预扣减。
- 数据库负责最终真实库存。
- 只有支付成功后,才扣减真实库存。
- 通过事务保证数据库侧库存和订单支付状态一致。
问题 2:如果 Redis 挂了怎么办?
回答思路
Redis 不能使用单点部署,需要使用集群模式提升可用性。
可以从以下几个方面展开:
- Redis 集群部署,避免单点故障。
- 主从复制,主节点故障后从节点可以接管。
- 哨兵或集群模式实现故障转移。
- 根据业务需要增加降级策略,例如暂停秒杀入口或限制流量。
问题 3:Redis 扣减了库存,但因为网络原因订单没有创建成功怎么办?
回答思路
可以根据业务要求选择不同方案。
方案一:允许商品没有完全抢完
订单没有创建成功时,不扣减数据库中的真实库存。
原因是 Redis 只是预扣减,真实库存只有在用户下单并支付成功后才会扣减。因此,即使 Redis 预扣减失败或订单创建失败,也不会影响数据库真实库存。
适用场景:
- 业务允许秒杀商品没有全部售完。
- 更关注系统稳定性,而不是强制卖完所有库存。
方案二:使用 MQ 保证订单最终创建成功
下单操作可以通过 MQ 异步完成。
需要从以下方面保证消息可靠性:
- 生产者消息发送确认。
- MQ 消息持久化。
- 消费者幂等处理。
- 消费失败后的重试机制。
- 必要时使用死信队列兜底。
这样可以尽量保证 Redis 扣减成功后,订单最终能够创建成功。
方案三:通过补偿服务回滚库存
如果业务要求秒杀商品必须尽量全部售完,可以增加补偿服务。
补偿方式如下:
- 检查订单是否创建失败。
- 如果订单创建失败,调用补偿服务回滚 Redis 库存。
- 补偿服务可以通过定时任务或事件监听触发。
- 后台服务定期检查未成功创建的订单,并对对应商品库存进行回滚。
方案四:对账和人工干预
可以做库存扣减数量和订单数量的对账。
如果发现 Redis 扣减数量、订单数量、数据库库存之间不一致,可以在发货前进行人工干预。
问题 4:如果问 Redis 和数据库的一致性,怎么回答?
回答思路
可以按照一致性模型来回答:
- 强一致性。
- 最终一致性。
秒杀场景中通常不会追求 Redis 和数据库的强一致性,而是通过 Redis 预扣减、数据库最终扣减、MQ 异步处理、补偿任务和对账机制来保证最终一致性。
六、ES 与向量检索
问题 1:为什么不单独使用 ES,而是使用 ES + 向量?
回答思路
这里不是单独使用向量数据库,而是使用 ES + 向量化能力。
之前将数据作为文档存入 ES,主要用于全文检索。ES 的传统全文检索依赖 BM25 算法,适合关键词匹配,但不适合语义匹配。
例如:
- 用户输入:
求解 x² - 5x + 6 = 0 - 文档标题:
二次方程求解方法
如果只使用传统 ES 检索,因为用户输入和文档标题之间没有完全匹配的关键词,ES 可能无法返回这个文档。
如果将数据转换为向量存入 ES,就可以做语义搜索,找到语义相似的内容。
实现流程
- 将文档转换为向量。
- 将原始文档和向量一起存入 ES。
- 用户输入问题后,将用户输入也转换为向量。
- 使用用户输入向量和 ES 中存储的文档向量做相似度匹配。
- 根据匹配结果找到最合适的文档答案。
关键点
- ES 传统全文检索适合关键词匹配。
- 向量检索适合语义匹配。
- ES + 向量可以同时兼顾关键词搜索和语义搜索。
七、微服务与系统设计
问题 1:微服务循环依赖怎么设计?
回答思路
微服务之间如果出现循环依赖,可以通过增加业务编排层来解耦。
一种常见做法是:
- 网关负责统一入口。
- 各个 Service 只处理自己的核心业务能力。
- 在网关和 Service 中间增加一个业务层或编排层。
- 由业务层完成跨服务的数据组装和流程编排。
关键点
这个业务层相当于中转站,可以避免服务之间直接互相调用,从而减少循环依赖。
八、设计模式
问题 1:工厂模式 + 策略模式实现登录
参考资料
回答思路
不同登录方式可以抽象成不同策略,例如:
- 密码登录。
- 手机验证码登录。
- 微信登录。
- 第三方授权登录。
使用策略模式封装不同登录方式的具体实现,使用工厂模式根据登录类型选择对应的登录策略。
关键点
- 策略模式负责消除大量
if else。 - 工厂模式负责根据登录类型创建或获取对应策略。
- 后续新增登录方式时,只需要新增策略类,不需要大面积修改原有逻辑。
九、SQL 优化
问题 1:复杂 SQL 怎么优化?涉及多张表操作时怎么处理?
1. 索引优化
- 确保 Join 字段有索引:为所有用于连接的字段创建索引,尤其是外键字段。
- 使用覆盖索引:如果查询只需要索引中的字段,可以避免回表操作,提升性能。
- 使用复合索引:根据查询条件创建合适的复合索引,注意索引的最左前缀原则。
2. 查询语句优化
- 减少 Join 表数量:只连接必要的表,避免不必要的复杂查询。
- 使用
EXPLAIN分析执行计划:查看查询执行计划,定位慢查询原因。 - 避免
SELECT *:只查询需要的字段,减少数据传输量。 - 优化子查询:某些情况下,可以将子查询改写为 Join 操作。
3. 数据库设计优化
- 合理分表:对于大表,可以考虑垂直分表或水平分表,减少单表数据量。
- 反范式化设计:适当冗余数据以减少 Join 操作,但需要权衡数据一致性。
- 使用临时表或视图:复杂查询可以拆分为多个步骤,通过临时表或视图存储中间结果。
4. 缓存优化
对于高频查询数据,可以增加缓存,减少数据库压力。
需要注意:
- 缓存命中率。
- 缓存穿透、击穿、雪崩问题。
- 缓存和数据库的一致性。
5. 代码层优化
可以将复杂查询拆分成多个简单查询,再在代码层完成数据组装。
例如:
- 先查询主表数据。
- 再批量查询关联表数据。
- 使用 Stream 或 Map 在内存中完成关联组装。
这种方式可以减少复杂 Join,但需要注意数据量,避免一次性加载过多数据。
十、Redis Lua 秒杀专题
问题 1:Lua 脚本在秒杀中的作用是什么?
回答思路
Lua 脚本在秒杀系统中主要用于代替 Redis 的多次普通命令操作,保证库存扣减过程的原子性和高性能。
主要作用
- 保证原子性。
- 减少网络通信次数。
- 提升执行效率。
- 避免高并发下的超卖问题。
原子性
Lua 脚本在 Redis 中执行是原子的,执行过程中不会被其他命令打断。
这对秒杀系统非常重要,因为秒杀系统需要在极短时间内完成库存判断和库存扣减。如果拆成多个 Redis 命令执行,在高并发下可能出现并发安全问题。
减少网络开销
Lua 脚本在 Redis 服务端执行,可以把多次 Redis 操作合并成一次请求,减少客户端和 Redis 服务端之间的网络通信次数。
提升执行性能
Lua 脚本执行速度较快,因为它避免了多次网络往返和重复命令解析的开销,适合秒杀这种高并发场景。
面试总结
在秒杀系统中,Lua 脚本的核心价值是:在 Redis 层面原子化完成库存判断和扣减,既能防止超卖,又能减少网络开销,提高系统吞吐量。
十一、50 道最近面试高频问题
问题 1:你是否设置过JVM启动参数?例如堆内存大小、垃圾回收器选择等?这些参数如何影响线程的创建与运行效率?
有的,我在生产环境部署时,这几个参数是必调的。
设置堆的初始大小和最大大小,通常设置为相同的值。设置年轻代中Eden区和两个Survivor区的大小比例。年轻代和老年代默认比例为1:2。每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等。当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而避免full GC,使用-Xmn设置年轻代的大小。尝试使用大的内存分页。使用非占用的垃圾收集器。
特别是针对秒杀这种高并发场景,我会固定堆的初始和最大大小(-Xms/-Xmx)避免动态扩容的性能损耗,根据服务器配置(如4核8G)选择合适的垃圾回收器。针重点调优年轻代,适当调大年轻代比例(-Xmn),让大对象直接进入老年代,避免在新生代频繁复制,从而减少Young GC频率,保证订单处理的吞吐量。
关于对线程与效率的影响:
线程栈(-Xss,默认1M),这个参数的设定需要权衡:如果设置过大,内存会被快速耗尽,导致系统无法创建新线程而报错;如果设置过小,虽然能开启更多线程,但一旦业务代码中存在较深的递归调用,线程就会因栈空间不足而崩溃,所以通常要根据业务逻辑的调用深度来综合考量。
GC停顿时间:选择合适的回收器(如G1或ZGC)并合理分配新生代/老年代比例,能显著减少 Stop-The-World 的时间,从而保证业务线程的运行效率,避免接口超时。
问题 2:你们之前公司项目中有没有遇到过较大数据量的处理?比如千万级的数据量?在处理这类数据时,是否使用过事务型数据库或相关技术,如索引、锁、连接池、缓存等?
“有的,我在之前的电商项目中就遇到过类似的情况。当时订单表和交易流水表随着项目运营的时间和推广力度的影响,数据量开始不断增长,单表突破了千万,这时候普通的查询已经开始出现明显的延迟,甚至偶尔会拖垮数据库。针对这个问题,我主要从存储层、查询层、写入层三个维度进行了优化: 第一,在存储层,我实施了分库分表。 这是解决千万级数据最根本的手段。我使用了 ShardingSphere 中间件,分了2个库,4个表,以 user_id 作为分片键进行取模运算得出对应的库和表的关联关系。这个组件有个好处就是,只需要在配置文件中配置分片规则即可,插入数据时,会自动的存入对应的库表,查询时会自动去对应的库表中查询。聚合查询也会帮我们做。
关于事务:分库后涉及跨库事务的问题,对于强一致性场景,利用ShardingSphere自带的本地事务管理;对于跨服务的分布式事务,则采用了 TCC 或 RabbitMQ 事务消息来保证最终一致性。
第二,在查询层,深度优化了索引和 SQL。 数据量大时,索引失效是致命的。尽量使用覆盖索引查询,避免回表。比如在列表页,只查 id、status 等关键字段,详情页再根据 ID 去查全量信息。 深分页优化: 针对运营后台的翻页查询(比如 limit 1000000, 10),我使用 “游标法”(记录上一页最大的 ID,where id > max_id limit 10),因为 MySQL 在扫描大量数据后会丢弃,非常浪费资源。 连接池调优: 面对高并发,调整了 Druid 连接池 的参数,根据业务类型(CPU 密集型/IO 密集型)合理设置了 initialSize 和 maxActive,防止因连接等待导致的线程阻塞。 第三,在写入层和高频读取层,我们引入了缓存和异步削峰。 缓存抗读: 对于热点订单数据,我们利用 Redis 做二级缓存。为了防止缓存击穿,我们使用了互斥锁Redisson重建缓存;为了防止大 Key 问题,我们对 Value 较大的对象进行了拆分存储。 异步抗写: 在促销活动高峰期,下单请求会先发送到 RocketMQ,消费者再慢慢消费入库。这样既保护了数据库不被瞬间流量打死,又利用了消息队列的重试机制保证了数据的可靠写入。”
问题 3:你说你ai客服这块做了敏感词过滤,具体是怎么做的,如果用户问的问题中确实会涉及到敏感词怎么办?
我引入了开源的 sensitive-word 组件,它自带基础的敏感词库。但是开源组件自带的词库肯定不够,所以又做了一套数据库管理后台。运营人员可以在后台动态维护敏感词库。我在代码里做了逻辑,每次变更敏感词时,会自动同步更新到组件的内存中,实现了无需重启服务的热更新。这样能第一时间应对突发的舆情风险。
如果敏感词过滤器检测到用户情绪激烈,系统不会机械地回复‘你在说什么’,而是会触发Prompt工程中的预设角色。AI会根据RAG(检索增强)知识库,调用预设的安抚话术,比如‘非常理解您的心情,我马上为您升级处理’,同时直接触发Function Calling机制,自动帮用户创建工单或转接给人工客服,避免AI在那‘一本正经地胡说八道’。我们用的是 Spring AI Alibaba,它跟Spring Boot 3.3项目集成非常丝滑。接入的是阿里百炼平台的通义千问,为了支持上述的向量匹配和会话历史,我用了 Milvus 存储知识库向量,用 Qdrant 做FAQ的语义检索,会话记录则存在 MongoDB 里。
问题 4:在你的ai客服中用到了MCP或者Function Callinng吗? 你怎么理解?
“是的,我在项目中同时用到了 Function Calling 和 MCP 机制。
我对它们的理解是:大模型本身是一个‘只懂理论的书呆子’,它擅长处理语言和逻辑,但无法直接操作我们的业务系统(比如查数据库、调支付)。而 Function Calling 和 MCP 就是给这个书呆子配了一套‘手脚’,让它能调用工具去解决实际问题。
Function Calling主要是解决‘业务操作类’问题,这是大模型与我们 Java 后端交互的核心机制。当用户问出大模型无法直接回答的问题时(比如‘我的订单发货了吗?’、‘我要退货’),大模型不会瞎编,而是会识别出这是一个工具调用请求。
MCP是解决‘知识库扩展’问题,如果说 Function Calling 是让 AI 调用接口去‘做事’,那 MCP 就是让 AI 调用外部资源去‘学习’。在我们的项目中,MCP 主要用于配合 RAG(检索增强生成)**。当用户问一些非常专业的产品参数或冷门政策时,大模型本地知识库里没有,它就会通过 MCP 协议,去调用我们部署在向量数据库(Milvus)里的知识库,实时检索相关文档片段,然后基于这些片段生成答案。
问题 5:MySQL 的 LEFT JOIN 和 INNER JOIN 有什么区别?
INNER JOIN 只有当左表和右表都能根据关联字段匹配上时,这条数据才会被保留下来,其他不关联的数据删除。 LEFT JOIN 则是以左表为‘主表’,左表的数据全部保留,右表中匹配不到的数据,用 NULL 值来填充右表的字段。
问题 6:你项目中用过MongoDB吗?
用过的。在我们的AI智能客服项目中要保留以前的历史会话记录时,我就使用MongoDB来存储的。它是一个非关系型数据库,比较灵活。
会话主要是存会话ID、消息角色、消息内容、时间戳、消息句号、元数据这些核心字段,将来复现聊天记录时需要我们根据会话的sessionid和时间段进行查询。
问题 7:在处理大量数据时,是否遇到过消息积压导致消息顺序错乱的问题?
这个是可以解决的,把消息都存储同一个分区下就行了,有两种方式都可以进行设置,第一个是发送消息时指定分区号/编号,第二个是发送消息时按照相同的业务设置相同的key,因为默认情况下分区也是通过key的hashcode值来选择分区的,hash值如果一样的话,分区肯定也是一样的。
问题 8:你们当时是什么原因导致消息没有发出去?最后是如何解决的?最终的解决方式是什么?
我的项目中没遇到过,因为在保证消息的可靠性方面做得比较好。
我认为主要原因通常有两个:一是网络抖动导致生产者与MQ之间的连接超时;二是服务意外宕机,导致内存中的消息还没发出去就丢了。 我采用了‘本地消息表’方案来实现最终一致性: 第一步:在业务数据库中专门建了一张message_log表。当处理业务逻辑时,我不会直接把消息发给MQ,而是把要发送的消息内容,作为一条记录,和业务数据写在同一个本地数据库事务里。好处是只要业务提交成功,这条消息就一定持久化在数据库里了,哪怕服务立刻宕机,数据也不会丢。 第二步:事务提交后,我会通过一个异步线程去读取这张表,把消息真正发送给MQ。发送成功后,再更新这条记录的状态为‘已发送’。 第三步:我写了一个定时任务,每隔一段时间(比如1分钟)去扫描message_log表,查找那些状态还是‘未发送’且创建时间超过一定阈值(比如5分钟)的记录。一旦发现这种‘漏网之鱼’,定时任务就会重新尝试发送。如果重试几次还是失败,就会报警转人工处理。
问题 9:你说你们ai用的websocket做的前后端连接,那如果连接突然断了怎么办?
我的ai后端使用的是WebFlux流式处理,当连接断开时,WebFlux 能够感知到客户端的取消信号,会立即停止大模型的任务,从而节省计算资源。
WebSocket技术,它能建立一个“长连接”,和“心跳机制“一起搭配使用,就是每隔30秒,服务器和手机互相发个“你还活着吗?”的小消息。 如果连续两次没收到回复,就认为“这人掉线了”,赶紧把连接关掉,避免资源浪费。连接断了,我也想了办法去获取:消息存数据库并且标记好状态,服务端主动去查,系统就从数据库里把没推送成功的找出来,补发一遍。
问题 10:你提到使用过CodeX或其他类似工具(如Claude code、Qoder),请确认你是否真的使用过这些工具?如果使用过,请说明你在实际项目中如何应用它们,你是如何实现开发效率提升或代码生成的?同时,请结合你对Spring Boot、微服务架构的理解,说明这些工具在现代Java开发中的实际价值和使用场景
我使用的是Claude Code,是这样应用的:
①需求设计:辅助分析需求,生成需求说明书、数据库设计等文档;
②原型图:Claude Code+ Pencil 生成原型;
③开发流程:生成开发规范文档,按默认 /plan/openspec 模式开发,需求审查迭代;
④测试:编写测试用例,执行测试。
Spring Boot 是Java开发的‘约定优于配置’的典范。通过自动配置和起步依赖,简化了Spring应用的搭建和开发过程;微服务架构是将单体应用拆分为一组小服务,每个服务运行在独立的进程中,通过轻量级机制通信。它的核心优势在于解耦、独立部署和技术多样性,但同时也带来了分布式系统的复杂性。
问题 11:Kafka是如何保证消息顺序性的?全局有序和分区有序有什么区别?
kafka默认存储和消费消息,是不能保证顺序性的,因为一个topic数据可能存储在不同的分区中,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性。如果有这样的需求的话,我们是可以解决的,把消息都存储同一个分区下就行了,有两种方式都可以进行设置,第一个是发送消息时指定分区号,第二个是发送消息时按照相同的业务设置相同的key,因为默认情况下分区也是通过key的hashcode值来选择分区的,hash值如果一样的话,分区肯定也是一样的。
分区有序(局部有序):这是我们最常用的模式。比如订单系统,我只需要保证‘订单A’的创建、支付、发货是按顺序的,至于‘订单A’和‘订单B’谁先发生并不重要。这种模式下,我们可以利用多分区并行消费,吞吐量极高。
全局有序:这意味着整个 Topic 的所有消息必须严格按时间顺序处理。要实现这一点,唯一的办法是将 Topic 的分区数设置为 1。但这会导致消费者也只能单线程消费,彻底丧失了 Kafka 的水平扩展能力和高吞吐优势。因此,除非是金融流水等极端场景,否则我不会使用全局有序。
问题 12:你知道什么是向量化吗?你知道向量化里面存的是什么格式吗?
向量化就是把非结构化的信息(比如文字、图片)翻译成计算机能听懂的‘数字语言’。需要通过一个嵌入模型的工具,把一段文字压缩成一组多维向量(数组)。计算机就能通过计算向量之间的‘距离’,来判断两段文字在语义上是不是相似的。
向量数据库里存的是一个‘键值对’结构,用的 Embedding 模型会把每一段文字变成一个高维空间里的‘点’。语义相近的文字,在这个空间里的距离就会靠得很近。
问题 13:在引入MySQL、Redis和消息队列(MQ)的复杂业务场景中,如何设计一个保证数据写入、缓存更新与消息推送三者之间一致性的方案,以避免消息漏发、堆积等问题,并提升系统的健壮性与可控性?
我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
我们要保证消息的不丢失。第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据。第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化。第三个是开启消费者确认机制为auto,由spring确认消息处理成功后完成ack,当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理。
问题 14:判断对象是否未被使用有哪些方法?同时,请说明你对 Spring 的 AOP 的理解。
在 JVM 中,我不使用引用计数法,而是采用可达性分析算法。通过‘GC Roots’对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。如果一个对象到 GC Roots 没有任何引用链相连,则证明该对象是不可用的。二次标记:即使对象不可达,它也不会立刻被回收,而是会经历一次‘筛选’和‘二次标记’过程,只有当对象真正无法复活时,内存才会被回收。 Aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般比如可以做为公共日志保存,事务处理等。 切面(Aspect):就是具体的增强逻辑类。 切入点(Pointcut):定义在哪里执行增强,比如 execution(* com.example.service..(..))或者@Pointcut注解。 通知(Advice):具体执行的时机,分为前置、后置、返回、异常和环绕通知。其中环绕通知(@Around)最强大,可以控制目标方法是否执行。 在同一个类中,通过 this.method() 调用带有 @Transactional 或 @Async 的方法时,AOP 会失效。因为这是直接调用目标对象的方法,绕过了代理对象。解决方案通常是注入自身 Bean 或者使用 AopContext.currentProxy()。
问题 15:你们在远程调用中使用的是什么方式?
在我的 AI 客服微服务架构中,服务间的通信主要依赖于 Spring Cloud OpenFeign。它让我们像调用本地接口一样调用远程服务。我只需要定义一个 Interface,加上 @FeignClient 注解和 @RequestMapping 等 Spring MVC 注解,Feign 就会自动帮我们生成实现类。这比直接使用 RestTemplate 拼接 URL 字符串要优雅得多,也更容易维护。Feign 默认集成了 Ribbon(或 Spring Cloud LoadBalancer),可以直接通过服务名进行调用,自动实现客户端负载均衡。 我们可以方便地结合 Sentinel 或 Resilience4j 来实现熔断降级,防止雪崩。
问题 16:你在开发AI相关系统时,遇到了哪些技术难题?是如何利用多线程、线程池、并发映射、异步编程以及Spring Boot框架等技术手段来解决这些难题的?
AI回答慢:调用的模型反应慢,使用MCP和Function Calling去查询本地信息,反应会慢。
我将一些常用问题存入数据库和缓存中,针对用户的高频提问,优先匹配FAQ库。如果命中,直接返回结果,降低了响应时间。
我还开启了多线程并行执行这些独立的工具调用,保证了系统在高并发下的稳定性。
问题 17:你们在实现分布式事务时使用的是什么框架?除了AT模式,还有哪些模式可以详细说明?
我在项目中主要使用阿里巴巴开源的Seata框架来实现分布式事务。除了最常用的AT模式,XA模式也是一种重要的解决方案。 AT 模式是 Seata 的默认模式,对业务无侵入。它的核心逻辑是在第一阶段就提交本地事务并生成回滚日志,第二阶段异步清理日志或同步回滚。优点是开发极快,加个注解就行。 XA 模式则是基于数据库原生的 XA 协议。它的第一阶段只是“预提交”,不真正释放数据库锁,要等第二阶段协调者通知后才真正提交或回滚。 两者最大的区别就在于“锁的粒度”和“性能”。 AT 模式在第一阶段就释放了本地锁,并发性能高;而 XA 模式是刚性事务,锁要一直持有到全局事务结束,这会严重阻塞并发,性能较差。 所以我们优先选 AT 模式,就是因为它在保证数据一致性的同时,对业务的侵入性最小,且并发性能远好于 XA。 只有在对数据一致性要求极其严苛、且能接受性能损耗的极少数场景下,才会考虑 XA。
问题 18:AOP 是通过什么方式实现?有哪几种实现方式?目前有哪几种实现方式?
“Spring AOP的核心实现方式是动态代理。它允许我们在不修改原有业务代码的情况下,通过代理对象来增强方法的功能,比如添加日志、事务控制等。 主要有两种实现方式: JDK动态代理:这种方式要求目标对象必须实现至少一个接口。代理类会在运行时动态生成一个实现了相同接口的类,并将方法调用委托给InvocationHandler处理。 CGLIB动态代理:当目标对象没有实现任何接口时,Spring会使用CGLIB。它通过在运行时动态生成目标类的子类来实现代理,并重写父类的方法来织入增强逻辑。需要注意的是,被final修饰的类或方法无法被CGLIB代理。 在现代Spring Boot应用中,通常会根据目标类的情况自动在这两种方式之间切换,开发者一般无需手动干预。”
问题 19:线程池创建的核心参数有哪些?流程是怎么样的?
“创建线程池有七个核心参数,这七个参数分别是: corePoolSize(核心线程数):线程池中常驻的线程数量。 maximumPoolSize(最大线程数):线程池允许创建的最大线程数量。 keepAliveTime(空闲线程存活时间):非核心线程在空闲时的最大存活时间。 unit(时间单位):keepAliveTime的单位。 workQueue(工作队列):用于存放待执行任务的阻塞队列。 threadFactory(线程工厂):用于创建新线程,可以自定义线程名称等属性。 handler(拒绝策略):当队列满且线程数达到最大值时,处理新任务的策略。 其执行流程可以概括为:当一个新任务提交时,如果当前线程数小于corePoolSize,会优先创建新的核心线程来执行;如果核心线程已满,任务会被放入workQueue等待;如果队列也满了,但总线程数还未达到maximumPoolSize,则会创建非核心线程来处理任务;如果线程数和队列都已满,则会触发handler定义的拒绝策略。”
问题 20:Thread.sleep 和 Object.wait 两个线程等待方法的区别
这两个方法虽然都能让线程暂停,但它们的区别非常大。 首先,从归属上看,sleep是Thread类的静态方法,而wait是Object类的实例方法。 其次,也是最关键的一点,sleep方法不会释放当前持有的任何锁,它只是让当前线程暂停执行一段时间。而wait方法必须在synchronized同步块或方法中调用,它会立即释放当前持有的对象锁,并进入等待状态,直到其他线程调用notify()或notifyAll()来唤醒它。 最后,sleep可以在任何地方使用,而wait必须在同步上下文中使用,否则会抛出IllegalMonitorStateException异常。
问题 21:你了解过垃圾回收吗?具体来说,你是否了解其具体的实现逻辑和行为逻辑?
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
引用计数法:一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收。可达性分析算法:现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。存在一个根节点,判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收。
垃圾回收算法有标记清除算法、复制算法、标记整理算法、分代回收
在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS(并发)垃圾收集器、G1垃圾收集器
G1垃圾收集器是应用于新生代和老年代,划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,采用复制算法。
问题 22:请结合实际场景,说明分布式锁在高并发系统中的设计原理、实现方式及其与数据结构
分布式锁的核心目的是在分布式环境下,保证同一时刻只有一个客户端能访问共享资源,常用于优惠券的发放、定时任务重复执行等问题。 以Redis实现为例,其设计原理是利用Redis单线程处理命令的特性来保证原子性。最基本的实现是使用SET key value NX EX seconds命令,即只有当键不存在时才设置,并附带一个过期时间,防止死锁。但这还不够,为了保证锁只能由持有它的客户端释放,我们需要在value中存入一个唯一的标识(如UUID),并在解锁时使用Lua脚本原子性地判断value是否匹配,匹配成功再删除。使用Redisson框架,它内置了看门狗机制,可以自动为锁续期,避免了业务未执行完锁就过期的问题,极大地简化了开发。
问题 23:如果你在系统中发现核心模块的数据库查询性能较差,例如慢查询数量非常多,你会从哪些维度进行排查和优化?
我们当时做压测的时候有的接口非常的慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统Skywalking ,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个sql出了问题。如果,项目中没有这种运维的监控系统,其实在MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中,我记得上一个项目配置的是2秒,只要SQL执行的时间超过了2秒就会记录到日志文件中,我们就可以在日志文件找到执行比较慢的SQL了。
如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复。
问题 24:如果主节点宕机,哨兵会自动将一个健康的节点升级为新的主节点,这一过程在分布式系统中是如何实现的?
“这是Redis Sentinel(哨兵)机制的核心功能,其实现过程可以分为四个步骤: 监控:哨兵进程会持续向所有主节点和从节点发送心跳包,以监控它们的健康状况。 主观下线:当某个哨兵在指定时间内没有收到主节点的回复,它就会将该主节点标记为“主观下线”。 客观下线:当大多数哨兵都判断主节点为主观下线后,它们会通过投票机制达成一致,将该主节点标记为“客观下线”,确认其真的宕机了。 领导者选举与故障转移:哨兵集群会选举出一个领导者哨兵来执行故障转移。这个领导者会从下线的旧主节点的从节点中,根据优先级、复制进度等规则选择一个最合适的从节点,并向它发送SLAVEOF NO ONE命令,将其提升为新的主节点。最后,它会通知其他从节点和客户端新的主节点地址,完成整个切换过程。”
问题 25:请解释Java中的面向对象编程特性,包括封装、继承、多态的实现方式,并说明抽象类与接口的区别及其在实际开发中的应用场景。
“Java的三大面向对象特性是构建复杂软件系统的基石。 封装:通过将属性和方法设为private,并提供public的getter/setter方法来访问,从而隐藏内部实现细节,保护数据安全。 继承:使用extends关键字,子类可以复用父类的代码,并建立“is-a”的关系,是实现代码复用的重要手段。 多态:指同一个行为具有多种不同表现形式。它的实现需要三个条件:继承、方法重写、父类引用指向子类对象。这使得程序更加灵活和可扩展。
关于抽象类和接口的区别,我的理解是:抽象类是对‘事物本质’的抽象,解决的是‘它是什么’的问题;而接口是对‘行为能力’的抽象,解决的是‘它能做什么’的问题。 从语法和特性上说: 抽象类就像一个半成品的父类,它用 abstract 修饰,可以包含构造器、成员变量,以及抽象方法和普通方法。一个类只能单继承一个抽象类。 接口在以前只能定义常量和抽象方法,但从 JDK 1.8 开始,接口里也可以有默认方法(default)和静态方法了。接口里的方法默认都是 public 的,一个类可以实现多个接口。 在实际开发中,我通常这样区分它们: 优先用抽象类:当几个类有很强的‘血缘关系’,并且有很多共用的代码和属性时。比如‘电子萌宠’案例,猫和狗都是宠物,它们都有名字、年龄,都有吃东西的通用逻辑,这时候定义一个 Pet 抽象类来复用代码是最合适的。 优先用接口:给不相关的类赋予某种通用能力,或者需要实现‘多继承’时。比如 ‘防盗门’和‘电梯’,它们完全不是一个东西,但都需要‘报警’功能;或者像‘手机套餐’那样,需要灵活地组合‘通话’、‘上网’、‘短信’这些不同的能力。这时候用接口就能让代码更灵活,实现‘多实现’。"
问题 26:如何实现上万条数据的导入导出而不导致内存溢出?
在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出。整体流程就是通过CountDownLatch+线程池配合去执行。
问题 27:在什么情况下会使用 Thread Local?你觉得核心是什么?Thread Local的原理是什么?
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享。在ThreadLocal内部维护了一个 ThreadLocalMap 类型的成员变量,用来存储资源对象。当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中。当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值。当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值。
问题 28:请详细解释 AQS 的工作原理,CAS 的底层机制,以及 MySQL 中 MVCC 的实现原理,这些技术在高并发场景下的应用和设计思想是什么?
“这三个都是并发编程的基石,它们的设计思想都是通过巧妙的机制来避免或减少锁的竞争,从而提升性能。 AQS(AbstractQueuedSynchronizer):它是ReentrantLock、CountDownLatch等同步工具的底层框架。其核心是一个FIFO的等待队列和一个state状态变量。线程尝试获取锁失败后,会被封装成一个节点加入队列并挂起。当持有锁的线程释放锁时,会唤醒队列中的下一个线程。它的设计思想是将线程的阻塞、唤醒等操作统一管理。 CAS(Compare-And-Swap):它是一种乐观锁的实现,包含三个操作数:内存位置V、预期原值A和新值B。它的操作是原子的,会将V的值与A进行比较,如果相等则将V的值更新为B,否则不做任何操作。它的底层依赖于CPU的硬件指令(如x86的cmpxchg)来保证原子性。它的设计思想是假设没有冲突,直接进行操作,失败了再重试。 MVCC(Multi-Version Concurrency Control):它是InnoDB引擎实现高并发读写的关键。它通过为每行数据维护一个隐藏的事务ID和回滚指针,并结合Undo Log,使得读操作可以访问到数据的历史版本。这样,读操作不需要加锁,写操作也只针对最新版本,从而实现了读写不冲突。它的设计思想是通过保存数据的历史版本来提高并发性能。”
问题 29:请详细说明Java中多线程编程的实现方式,包括线程的创建、线程池的使用、线程安全的保障机制,以及如何通过并发映射(如ConcurrentHashMap)实现高并发下的数据访问,并结合线程池的配置与管理,说明其在实际应用中的性能优化策略。
“Java多线程的实现方式多样,核心在于合理创建、管理和同步线程。 线程创建:传统方式包括继承Thread类或实现Runnable接口,但更推荐使用实现Callable接口的方式,因为它允许任务有返回值并能抛出异常。 线程池使用:生产环境中严禁使用Executors工具类创建线程池,因为它可能导致OOM。我们应该手动创建ThreadPoolExecutor,并根据业务类型(IO密集型或CPU密集型)合理配置核心线程数、最大线程数、队列容量等参数。 线程安全保障:基础手段是使用synchronized关键字或ReentrantLock。对于简单的变量,可以使用volatile保证可见性。此外,大量使用java.util.concurrent包下的并发工具类,如ConcurrentHashMap。 并发映射应用:ConcurrentHashMap在JDK 1.8之后放弃了分段锁,转而采用synchronized + CAS + Node的方式。它只对链表或红黑树的头节点加锁,锁粒度更细,大大提升了并发度。 性能优化策略:优化的关键在于使线程池的配置与业务特征相匹配。例如,对于耗时的IO操作,可以配置较大的队列和较多的线程;而对于计算密集型任务,线程数应接近CPU核数。同时,通过监控线程池的运行状态(如活跃线程数、队列大小),可以动态调整参数,以达到最佳性能。”
问题 30:请说明你在实际项目中使用过的分布式事务解决方案,重点分析AT模式与TCC模式的特性差异,并指出当前方案可能存在的性能瓶颈或技术挑战。
在项目中主要使用Seata框架,其中AT模式因其低侵入性而被广泛应用。 AT模式与TCC模式的特性差异主要体现在以下几个方面: 侵入性:AT模式几乎对业务代码无侵入,开发者只需关注本地事务。而TCC模式需要为每个分支手动实现Try、Confirm、Cancel三个方法,侵入性很强。 性能:AT模式在第一阶段就会提交本地事务,但会持有数据库层面的全局锁,这可能成为并发瓶颈。TCC模式在Try阶段只是预留资源,真正的资源锁定在Confirm阶段,理论上并发性能更好。 一致性:AT模式依赖数据库的本地ACID特性,而TCC的一致性完全由业务逻辑保证。 当前的性能瓶颈和技术挑战: AT模式的全局锁:在高并发更新同一行数据时,全局锁会导致事务串行化执行,严重影响吞吐量。 TCC的空回滚与幂等:网络抖动可能导致Try请求未到达,但Cancel请求先到,造成空回滚。或者Confirm/Cancel请求重复到达。这些问题都需要在业务代码中妥善处理,增加了开发复杂度。
问题 31:在Spring Cloud微服务架构中,限流和熔断的规则分别是什么?它们分别使用了哪些技术工具?特别是基于线程池隔离方式的熔断机制,其具体规则和实现原理是什么?
限流的规则是控制单位时间内的请求数量,防止突发流量打垮服务。常用的工具有Sentinel和Resilience4j,它们支持QPS、并发线程数等多种限流模式。 熔断的规则是当下游服务响应过慢或错误率过高时,主动切断调用关系,快速失败,避免级联故障。Hystrix和Sentinel都提供了熔断功能。 基于线程池隔离的熔断机制:这是Hystrix的核心原理,主要通过“资源隔离”来实现。它为每个依赖服务分配一个独立的线程池。比如调用“订单服务”就用专门的线程池,不占用主线程或其他服务的资源。如果这个独立线程池满了,或者该服务的错误率超过了设定阈值,熔断器就会“打开”。此时,后续请求不再排队等待,而是直接拒绝并快速执行降级逻辑(Fallback),从而避免因为一个下游服务的故障把整个系统的线程资源耗尽。
问题 32:你提到使用RAG实现知识库问答,请问这部分技术是如何应用的?
在I客服项目中,为了解决大模型知识滞后和产生“幻觉”的问题,我们引入了RAG(检索增强生成)技术。 其应用流程分为三步: 知识库构建:我们将产品手册、FAQ文档等非结构化数据进行清洗、切片,然后通过Embedding模型将这些文本片段转化为向量,并存入向量数据库(如Milvus)中。 意图检索:当用户提出问题时,我们首先将这个问题也转化为向量,然后在向量数据库中进行相似度搜索,找出与问题最相关的几个知识片段。 增强生成:我们将检索到的知识片段作为上下文,和用户的问题一起组装成一个新的Prompt,发送给大模型。这样,大模型就能基于我们提供的准确信息来生成回答,而不是仅仅依赖其训练数据,从而保证了答案的准确性和时效性。
问题 33:你了解雪花算法吧?详细说说,你项目中用到了吗?
“是的,我非常了解。雪花算法是一种能在分布式系统中生成全局唯一、趋势递增的Long类型ID的算法。它的核心思想是将一个64位的long整数划分为几个部分。 在我们的订单系统中就用到了雪花算法。因为订单ID不仅要全局唯一,还要大致有序。有序的ID对数据库索引非常友好,可以避免因随机ID(如UUID)导致的页分裂和频繁的页合并,从而提升插入性能。”
问题 34:如果使用Redis生成单号,一旦Redis数据丢失,如何保证单号生成的可靠性?在Redis哨兵或集群架构下,虽然可用性较高,但如果仍出现数据丢失,应如何应对?
这是可靠性和一致性的问题。单纯依赖Redis确实存在数据丢失的风险,尤其是在主从切换的瞬间,可能存在数据未同步的问题。为了应对这种情况,我的设计方案是结合数据库来保证最终可靠性。首先,开启Redis的AOF持久化,确保每次自增操作都被记录到日志中。将Redis生成的单号定期同步到MySQL的一个专用表中。这个表记录了当前已分配的最大单号。一旦Redis数据丢失,我们可以从MySQL中读取最新的单号值,用它来重新初始化Redis的计数器。虽然在故障恢复期间可能会产生少量重复ID,但可以通过应用层的去重机制或数据库的唯一约束来避免问题。更稳健的方案是采用“Redis + 数据库号段”模式。Redis负责高性能地生成号段内的ID,当号段快用完时,再从数据库中申请下一个号段。这样即使Redis挂了,也只是损失一个号段,影响范围可控。
问题 35:你提到做过支付相关的工作,请详细说明你在支付系统中使用的技术栈,包括如何保证交易的原子性与一致性,如何设计高并发下的支付流程,以及你如何处理支付失败或超时的情况?
原子性与一致性保证:核心的交易流水记录、账户余额扣减等操作,都在同一个数据库事务中完成,保证原子性。对于跨服务的操作(如支付成功后通知订单服务),我们使用消息队列(RabbitMQ)的事务消息或Seata的TCC模式来保证最终一致性。 幂等性:这是支付系统的生命线。我们会为每笔支付请求生成一个唯一的业务流水号,无论是支付网关的回调还是用户的重试,服务端都会先校验这个流水号是否已处理过,防止重复扣款。
支付失败或超时处理: 明确失败:如果支付网关明确返回失败,我们会更新订单状态,并通知用户。 超时关单:对于长时间处于“支付中”状态的订单,我们会启动一个延迟任务(如使用Redis Key过期监听或RabbitMQ延迟消息)。当任务触发时,如果订单仍未支付成功,则自动关闭订单,并释放占用的库存。 掉单处理:为了防止支付成功但回调丢失的情况,会有一个定时任务,主动去支付网关查询那些状态不明的订单,进行对账和状态修复。
问题 36:Redis和数据库的一致性怎么去实现?(两种方式都讲讲)
我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
问题 37:hashmap的底层原理是什么?
1,底层使用hash表数据结构,即数组+(链表 | 红黑树)
2,添加数据时,计算key的值确定元素在数组中的下标
key相同则替换
不同则存入链表或红黑树中
3,获取数据通过key的hash计算数组下标获取元素
问题 38:oom异常,线程死锁,cpu飙升分别怎么排查呢?
oom异常排查:
①启动 jar 包的时候加上 JVM 参数,这样程序一旦出现 OOM 内存溢出,
就会自动生成堆快照文件。之后我们用 IDEA 或者 Eclipse 打开这个堆转内存快照文件,就能很直观地看到是哪个对象占内存太大、是哪里的代码导致内存溢出,快速定位问题。
②还可以用 jconsole 或者可视化工具 VisualVM 来实时监控、查看并定
位问题。
③我们也可以使用 Arthas 工具来生成堆转储快照,通过它的 heapdump
命令导出内存快照文件,之后再用 VisualVM 打开分析,就能清晰定
位是哪些对象占用内存过多,快速排查问题。
线程死锁排查:
①一种是用 JDK 自带的工具,出现线程死锁时,我们可以直接使用 jstack
命令打印出当前所有线程的堆栈信息,在里面找到死锁相关的线程和
具体代码位置,再结合业务日志一起分析,就能定位出是哪几行代码、
哪几个锁导致了死锁。
②另一种是用 Arthas 这款诊断工具,它排查起来更简单直观,直接执行
thread -b 命令,就能自动检测出发生死锁、相互阻塞的线程,直接显示出持有锁和等待锁的信息,不用自己一点点翻堆栈,快速就能找到问题线程和对应的代码。
CPU飙升排查:
①可以使用使用top命令查看占用cpu的情况,通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id,通过ps 查看当前进程中的线程信息,看看哪个线程的cpu占用较高、使用jstack命令打印进行的id,找到这个线程,就可以进一步定位问题代码的行号。
②使用 Arthas,直接执行 thread 命令就能看到所有线程的 CPU 占比,锁定 CPU 最高的线程 ID,再输入 thread 线程ID 就能直接跳到出问题的代码位置。另外,如果遇到方法参数或者返回值异常,直接使用 Arthas 的 watch 命令,就能实时监控方法的入参、返回结果以及异常信息。
问题 39:Redis在用户量大、内存已写满的情况下,如何处理新数据的写入?其过期策略有哪些?
当Redis内存达到配置的上限(maxmemory)时,它会触发内存淘汰机制。我们可以配置maxmemory-policy来决定策略,常见的有: volatile-lru:只针对设置了过期时间的键,使用LRU算法淘汰。 allkeys-lru:在所有键中使用LRU算法淘汰(最常用)。 allkeys-random:随机淘汰。 Redis的过期策略主要包含两个方面:第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key 时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。 第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key 。
定期清理的两种模式: SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数 。FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
问题 40:在使用消息队列(如RabbitMQ、Kafka)时,如何保证消息的幂等性以避免重复消费?如果消费完成后未提交偏移量,如何防止消息丢失?
幂等性保证: 核心思想是唯一标识去重。 数据库唯一键:利用数据库的主键或唯一索引。比如插入订单流水,如果主键冲突,第二次插入会失败,从而避免重复。 Redis原子操作:在处理业务前,先将消息的唯一ID存入Redis(SETNX),如果返回成功才处理业务,否则视为重复消息。 状态机机制:比如更新订单状态,SQL写成UPDATE order SET status = 2 WHERE id = 1 AND status = 1。如果状态已经是2,更新行数为0,天然幂等。
防止消息丢失(Offset管理): 必须遵循“先处理业务,再提交Offset”的原则,但要注意原子性。 手动提交:关闭自动提交(enable.auto.commit=false)。 同步提交:在业务逻辑执行成功后,调用commitSync()。 最佳实践:将业务数据和Offset保存在同一个本地事务中(类似本地消息表),或者确保业务执行成功后立即提交。如果提交失败,下次重启消费者会从上一次成功的Offset重试,导致重复消费,所以这就回到了上面的“幂等性”保障。
问题 41:当数据量较大导致消息队列出现积压时,除了扩容外,还有哪些应急措施可以保证数据不丢失、不积压?如果扩容后下游消费能力仍跟不上,如何通过技术手段缓解或解决这一问题?
“当MQ积压严重时,说明消费速度远小于生产速度。除了增加消费者实例(扩容),还可以采取以下紧急手段: ①降级非核心业务:暂时停止消费者中对实时性要求不高的逻辑(如发送短信、记录日志),优先处理核心业务(如扣减库存、落库)。 ②临时扩容转发:编写一个临时的消费者程序,它不做任何业务处理,只负责把积压的消息快速取出来,转发到一个新的、拥有更多分区的临时Topic中。然后部署大量的真实消费者去消费这个新Topic。这是一种“空间换时间”的策略。 ③调整批次大小:如果消费者支持批量拉取,可以适当增大批次大小,减少网络交互次数,提高吞吐量。 ④排查下游依赖:很多时候积压是因为下游数据库慢或第三方接口超时。此时应检查是否有慢SQL或外部服务故障。”
问题 42:在物联网项目中,同时使用了RabbitMQ和MQTT,它们都是消息队列,存在一定的功能重叠,那么具体在项目中这两个技术分别承担什么角色?为什么选择同时使用两者而不是只用其中一个?
“虽然都涉及消息传递,但它们的定位完全不同: MQTT:它是一个传输协议,专为低带宽、高延迟的不稳定网络设计(如传感器、移动设备)。在我们的IoT项目中,MQTT主要用于设备与服务器之间的通信(南北向流量)。设备通过MQTT协议上报数据到服务端(Broker),因为它非常轻量且省电。 RabbitMQ:它是一个标准的消息代理中间件,运行在服务端。在我们的架构中,后端服务接收到MQTT传来的数据后,会将其封装成标准消息发送到RabbitMQ中。RabbitMQ负责服务端内部的业务解耦(东西向流量),比如削峰填谷、异步处理数据存储、报警分析等。”
问题 43:在使用UUID作为请求标识时,如何保证在高并发场景下实现有效的分布式锁?如果每次请求的UUID都不同,如何确保锁的正确性和唯一性?是否可以使用其他机制(如Redis的原子操作)来替代UUID实现锁的控制?
“直接用UUID做锁的Key是有风险的,因为UUID太长且无序,会影响Redis性能。 改进方案:如果必须用UUID,建议配合Redis的SET key value NX EX命令。Key可以是业务标识(如lock:order:123),Value设为UUID。 释放锁:释放时必须校验Value是否是当前的UUID,这需要用到Lua脚本保证原子性,防止误删其他线程的锁。 更好的替代:在高并发下,更推荐使用雪花算法生成的Long型ID作为Key,或者直接使用Redisson框架,它会自动处理锁的命名、看门狗续期和原子释放,无需手动管理UUID。”
问题 44:请详细介绍一下设计模式,并说说工厂加策略模式,以及对应的使用场景
“设计模式是解决特定问题的通用模板。我重点讲讲工厂模式 + 策略模式的组合,策略模式负责封装行为。它将不同的业务算法或规则抽象成独立的类,使它们可以互相替换。工厂模式负责管理创建。它根据客户端的请求,统一创建并返回合适的策略对象,将客户端与具体实现解耦。工厂模式负责“选策略”,策略模式负责“跑逻辑”。这在消除复杂的if-else或switch语句时非常有用。 场景举例:支付系统。用户可以选择支付宝、微信、银联等多种支付方式。 传统做法:在Service层写一堆if (type == "ALI") ... else if (type == "WECHAT") ...,代码臃肿且难以维护。 优化方案: ①策略模式:定义一个统一的支付接口PaymentStrategy,包含pay()方法。然后为每种支付方式创建一个实现类(AlipayStrategy, WechatStrategy)。 ②工厂模式:创建一个PaymentFactory,它内部维护一个Map,Key是支付类型,Value是对应的策略对象。 ③调用:客户端只需要告诉工厂我要哪种支付,工厂返回对应的策略对象,然后调用pay()。 优点:符合开闭原则,新增一种支付方式只需新增一个类,无需修改原有代码。”
问题 45:说说Redis的主从同步流程
“主从同步分为了两个阶段,一个是全量同步,一个是增量同步 。
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是
这样的: 第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id
和offset偏移量。第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节
点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息 保持一致。第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致 。
当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命
令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送
给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时
候,都是依赖于这个日志文件,这个就是全量同步 。
增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时
候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是
第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后
的数据,发送给从节点进行数据同步”
问题 46:能解释一下I/O多路复用模型吗?
“I/O多路复用允许一个线程同时监控多个文件描述符(Socket连接)的状态。 核心比喻:就像一个餐厅服务员(线程),他不需要一直守着一个客人(连接),而是拿个小本本(Selector),记下哪桌客人在点菜、哪桌吃完了。他可以轮询这个小本本,或者直接由客人叫他(事件驱动),一旦某桌有动静,他再去服务。 技术实现:在Linux中体现为select、poll和epoll。Netty等高性能框架就是基于epoll实现的,这使得单线程能处理成千上万个并发连接,极大地节省了系统资源。”
问题 47:你们ai智能客服的提示词工程是如何设计和实现的?特别是在自动判断逻辑中,是如何通过技术手段实现智能判断的?请详细说明技术实现细节
“我们的提示词工程采用了结构化Prompt + 动态变量注入的方式。 结构设计:我们将Prompt拆分为:[角色设定] + [任务目标] + [约束条件] + [少样本示例] + [用户输入]。 智能判断实现: 为了判断用户意图(是闲聊、投诉还是咨询业务),我们在Prompt中加入了一段思维链指令:“请先分析用户的意图类别,如果是‘投诉’,请调用安抚话术;如果是‘业务咨询’,请检索知识库。” 技术手段:我们通过LangChain框架,将用户的上下文历史、检索到的知识片段动态拼接到Prompt的占位符中,发送给大模型。模型根据预设的逻辑规则输出特定的标记(如INTENT:COMPLAINT),后端解析这个标记来路由到不同的业务流程。”
问题 48:请解释B+树结构在数据库中的应用,以及日志(如redo log或binlog)在事务执行过程中的具体执行流程和作用
”B+树应用:MySQL InnoDB引擎使用B+树作为索引结构。 优势:所有数据都存储在叶子节点,且叶子节点之间通过双向链表连接。这使得范围查询(BETWEEN, >)非常快,只需要遍历链表即可,而不用像B树那样进行中序遍历。 事务执行流程(WAL机制): 当事务更新数据时,InnoDB遵循预写式日志策略: 内存修改:先在Buffer Pool中修改数据页。 写Redo Log:将修改操作记录到Redo Log Buffer,并持久化到磁盘的Redo Log文件(Write Ahead Log)。这是为了保证崩溃恢复。 写Binlog:Server层将SQL语句记录到Binlog。 两阶段提交:协调Redo Log和Binlog的一致性,确保要么都写成功,要么都失败。 刷盘:后台线程择机将脏页刷新到磁盘数据文件中。“
问题 49:在百万级并发的秒杀场景下,如何保证系统响应速度稳定并避免超卖?同时,在一个分布式下单场景中(包括下单、库存扣减、优惠券发放),如何在不使用C++框架的前提下保证最终一致性,并处理极端异常情况?
”秒杀的核心是“层层过滤,最终落地”。 前端静态化:将秒杀页面CDN加速,甚至做成静态HTML,减少请求打到后端。 Nginx限流:在网关层拦截恶意刷单和超出系统承载的请求。 Redis预减库存:这是关键。活动开始前将库存预热到Redis。请求来了先在Redis中通过Lua脚本原子性地扣减库存(DECR)。只有扣减成功的请求才放行进入下一步。 MQ异步下单:扣减库存成功后,发送消息到MQ,立即返回给用户“排队中”。后端服务慢慢消费MQ进行数据库下单。 数据库乐观锁:最后的数据库更新SQL加上版本号或库存条件:UPDATE stock SET num = num - 1 WHERE id = 1 AND num > 0,作为最后一道防线防止超卖。“
问题 50:针对用户订单表中一亿条数据,如何优化查询七天内支付订单并进行统计分析的SQL查询性能?
”面对亿级数据,单表查询肯定不行。
分库分表:如果单表过大,按照user_id取模分表,或者按照create_time进行按月/按周归档分表。
索引优化:针对“支付时间”和“订单状态”这两个查询条件,建立联合索引。这样数据库可以直接在索引树上完成过滤,避免回表,速度最快。 读写分离:统计分析属于重查询,绝对不能走主库。应该走专门的从库或数据分析库。 异构索引表/ES:对于复杂的统计(如“统计过去7天每天的交易额”),直接查MySQL太慢。可以使用Canal同步数据到Elasticsearch,利用ES的聚合能力进行快速统计;或者建立一个专门的分析报表库(如ClickHouse),定时将清洗后的数据导入其中进行分析。“