Java场景题面试合集
1. 如何保证支付成功后 “更新订单状态、扣减库存、发放优惠券” 的事务一致性?
这三个操作涉及跨服务 / 跨数据库交互,需通过分布式事务方案保证一致性,核心是 “要么全成,要么全败”。具体可采用以下方案:
优先选 TCC 模式:适合业务逻辑复杂、需强一致性的场景。
- Try 阶段:检查资源合法性(如库存是否充足、优惠券是否可发放),冻结资源(如预扣库存、标记优惠券为 “待发放”)。
- Confirm 阶段:若 Try 成功,执行实际操作(更新订单为 “已支付”、扣减冻结库存、发放优惠券到用户账户)。
- Cancel 阶段:若任一操作失败,回滚资源(解冻库存、取消优惠券冻结状态、订单状态回滚为 “未支付”)。
次选事务消息(最终一致性):若业务允许短暂不一致(如 1 分钟内),可通过 RocketMQ 等中间件实现。
- 支付成功后,发送 “待确认” 事务消息,本地先更新订单状态(标记为 “支付中”)。
- 消息确认后,库存服务、优惠券服务消费消息执行扣减和发放;若消费失败,通过重试机制补执行,确保最终一致。
2. 订单表分库分表策略及跨分页、ID 唯一性问题解决
订单表数据量大时,需通过水平分片(按行拆分) 分散压力,核心是合理选择分片键。
分库分表策略:
- 分片键选
user_id
:用户的所有订单会落在同一分片,查询 “我的订单” 时无需跨库,效率高;缺点是查询 “全局订单(如平台所有订单)” 需跨分片。 - 分片键选
order_id
:若订单 ID 含时间戳(如前 8 位为日期),可按时间范围分片(如按月份分表),适合 “按时间查订单” 场景;需保证 ID 生成规则包含分片信息(如嵌入用户 ID 哈希值)。 - 中间件:用 Sharding-JDBC 配置分片规则(如按
user_id % 8
分 8 个库,每个库分 16 张表)。
- 分片键选
跨分页查询解决:
- 若按
user_id
分片,用户订单在单分片内,分页查询(如 “我的订单第 2 页”)直接在分片内执行,无问题。 - 若查全局分页(如 “平台今日前 100 条订单”):通过冗余表(存订单 ID、用户 ID、创建时间等核心字段)或 Elasticsearch 预聚合,避免跨库全量扫描。
- 若按
订单 ID 全局唯一性:
- 用雪花算法(Snowflake) 生成 ID:64 位 ID 包含时间戳(确保有序)、机器 ID(避免分布式冲突)、序列号(同一毫秒内去重),既保证唯一性,又便于按时间范围分片。
3. 高并发下用 Redis 实现优惠券防重发放
核心是通过 Redis 的原子操作,在发放前拦截重复请求,避免同一用户重复领取。
防重方案步骤:
- 定义唯一标识键:以
coupon:user:{user_id}:{coupon_id}
为 key(用户 ID + 优惠券 ID 唯一确定一次领取)。 - 原子性检查与标记:发放前,用
SET key 1 NX EX 86400
(NX:不存在才设置,EX:24 小时过期)执行操作。- 若返回 OK:表示首次领取,继续执行发放逻辑(写入数据库)。
- 若返回 null:表示已领取,直接返回 “已发放” 提示,拦截重复请求。
- 异常处理:若 Redis 设置成功但数据库写入失败,需删除 Redis 键(
DEL key
),避免误拦截。
- 定义唯一标识键:以
优势:Redis 单线程原子操作保证并发安全,性能高(支持每秒 10 万 + 请求),适合高并发场景。
4. 商品价格叠加多优惠的灵活计算策略
需设计可扩展的规则引擎,支持新增优惠类型时无需修改核心代码,核心是 “策略模式 + 配置化”。
设计方案:
- 抽象优惠策略:定义统一接口
DiscountStrategy
,包含calculate(PriceContext context)
方法(入参为商品基础价、用户信息等)。 - 实现具体策略:为每种优惠实现接口,如:
MemberDiscount
:按会员等级计算折扣(如 VIP 打 9 折)。FullReduction
:满减计算(如满 200 减 30)。CouponDiscount
:优惠券抵扣(如固定减 50 元)。
- 定义优惠优先级:通过配置表设置执行顺序(如先会员折扣→再满减→最后用优惠券),避免因顺序导致的结果差异。
- 动态加载策略:从数据库读取用户可享受的优惠规则(如用户有哪些优惠券、是否满足满减条件),动态选择对应的策略类执行计算。
- 抽象优惠策略:定义统一接口
优势:新增优惠类型时,只需新增策略类并配置规则,符合 “开闭原则”,灵活支持业务迭代
5. 订单支付成功后通知物流系统,如何保证消息不丢失?
消息不丢失需覆盖发送、存储、消费三个环节,核心是 “每个环节都有确认机制”,具体方案如下:
发送环节:确保消息能成功投递到中间件
- 用事务消息(如 RocketMQ 的 TransactionMQ)绑定本地事务与消息发送:支付成功后,先执行本地 “更新订单为已支付” 事务,再发送 “通知物流” 的事务消息;若本地事务失败,消息不会被投递,避免消息超前。
- 发送端加重试机制:若消息发送超时 / 失败,通过定时任务(如每隔 10s)重试,上限 3 次,避免网络波动导致的瞬时失败。
存储环节:确保中间件能持久化消息
- 中间件开启持久化(如 RocketMQ 将消息写入 CommitLog 磁盘文件,Kafka 落盘到分区日志),即使中间件宕机,重启后可恢复消息。
- 配置消息过期时间(如 24 小时),避免无效消息长期占用存储。
消费环节:确保物流系统能正确处理消息
- 消费端采用手动 ACK 机制:物流系统处理完 “创建物流单” 后,再向中间件发送确认(ACK);若处理失败(如接口报错),不发送 ACK,中间件会将消息重新放入队列,等待重试。
- 消费端加幂等处理:物流系统用 “订单 ID” 作为唯一键,处理前检查是否已创建物流单,避免重复处理(因重试导致的消息重复)。
6. 用户下单预占库存,15 分钟未支付自动释放,如何实现?
核心是 “定时触发库存释放”,需兼顾实时性与性能,推荐延迟队列 + 定时任务兜底方案:
主方案:基于中间件的延迟队列
- 下单预占库存时,同时向延迟队列(如 RabbitMQ 的死信队列)发送一条 “释放库存” 消息,设置消息 TTL(存活时间)为 15 分钟。
- 消息过期后,自动进入死信交换机绑定的死信队列,库存服务监听该队列,消费消息时检查订单状态:若订单仍为 “未支付”,则释放预占库存(恢复可用库存),并更新订单为 “已取消”。
- 优势:无需主动轮询,实时性高(过期即处理),对系统压力小。
兜底方案:定时任务补漏
- 因中间件可能存在消息丢失风险,需加定时任务(如 Quartz)每小时执行一次:扫描 “未支付且创建时间超过 15 分钟” 的订单,批量释放其预占库存。
- 扫描时用索引优化(如按 “订单状态 + 创建时间” 建联合索引),避免全表扫描,减少 DB 压力。
7. 每日对账时发现订单金额与支付记录不一致,如何设计核对系统?
核对系统需实现 “自动比对、差异定位、异常处理”,核心是 “全量比对 + 分层校验”:
第一步:明确数据源与比对维度
- 数据源:订单表(订单 ID、应付金额、实付金额、订单状态)、支付记录表(支付 ID、关联订单 ID、支付金额、支付状态、支付时间)。
- 比对维度:按 “订单 ID” 关联,核对 “实付金额是否相等”“订单状态与支付状态是否匹配”(如订单 “已支付” 对应支付 “成功”)。
第二步:自动比对流程
- 全量同步:每日凌晨(如 2 点),通过 ETL 工具(如 Flink)将前一天的订单数据与支付数据同步到核对库(独立于业务库,避免影响业务)。
- 关联比对:用订单 ID 左连接支付记录,生成差异表:
- 金额差异:订单实付≠支付金额(如订单 100 元,支付 99 元)。
- 状态差异:订单 “已支付” 但无支付记录,或支付 “成功” 但订单 “未支付”。
- 冗余差异:支付记录对应订单不存在(可能支付错单)。
- 分级处理:
- 可自动修复:如状态差异(支付成功但订单未更新),调用订单服务接口补更新状态。
- 需人工介入:如金额差异(需财务核查是否有退款、优惠计算错误),生成差异报表推送给运营 / 财务。
第三步:保障机制
- 比对过程日志全量记录(谁、何时、处理了什么差异),支持追溯。
- 重试机制:若同步或比对失败,自动重试 3 次,避免临时网络问题导致的遗漏。
8. 异步处理订单的线程池任务堆积,如何定位和调整参数?
核心是 “先定位堆积原因,再针对性调优”,分两步处理:
第一步:定位任务堆积原因
- 监控核心指标(通过 Spring Boot Actuator 或 Prometheus):
- 线程池状态:活跃线程数(是否达最大值)、队列剩余容量(是否已满)、拒绝次数(是否激增)。
- 任务执行耗时:平均 / 最大执行时间(若耗时过长,会导致线程被占用,新任务只能入队列)。
- 任务提交速率:单位时间提交的任务数(是否超过线程池处理能力)。
- 常见原因:
- 任务执行慢:如处理时调用了慢接口(第三方物流、支付回调),或 DB 查询未走索引。
- 线程资源不足:核心线程数 / 最大线程数设置过小,无法及时处理任务。
- 队列容量不合理:队列过大导致任务积压,过小则频繁触发拒绝策略。
第二步:调整参数与优化
- 针对任务执行慢:
- 优化任务逻辑:异步任务拆分(如 “更新订单状态” 与 “通知用户” 拆分为两个任务),避免单任务做太多事;慢接口加缓存或异步化。
- 增加超时控制:用
Future.get(timeout)
设置任务超时时间,避免线程被无限阻塞。
- 针对线程资源不足:
- 调整线程池参数(结合任务类型):
- IO 密集型任务(如调用外部接口):最大线程数可设为
CPU核心数*2
,允许更多线程并行等待 IO。 - CPU 密集型任务(如复杂计算):最大线程数设为
CPU核心数+1
,避免线程切换开销。
- IO 密集型任务(如调用外部接口):最大线程数可设为
- 动态调整:用动态线程池(如 Hippo4j),支持运行时修改核心线程数、队列容量,无需重启服务。
- 调整线程池参数(结合任务类型):
- 针对队列问题:
- 队列容量适中:若任务允许短暂积压,队列容量可设为
最大线程数*10
;若需快速响应,队列设小些(如 100),并配置合理的拒绝策略(如记录日志 + 异步重试,而非直接丢弃)。
- 队列容量适中:若任务允许短暂积压,队列容量可设为
9. 频繁查询不存在的商品 ID 导致 DB 压力,如何防御?
核心是 “提前拦截无效请求,减少 DB 访问”,采用 “多级缓存 + 过滤” 方案:
第一级:布隆过滤器(Bloom Filter)前置过滤
- 启动时加载所有有效商品 ID 到布隆过滤器(如 Guava 的 BloomFilter),存储在内存中。
- 收到商品查询请求时,先通过布隆过滤器判断:
- 若过滤器判定 “不存在”:直接返回 “商品不存在”,不查 DB。
- 若判定 “可能存在”(允许小概率误判):继续后续查询(因布隆过滤器有极小假阳性)。
- 优势:内存占用小(100 万商品 ID 约占 120KB),查询耗时微秒级,适合高并发场景。
第二级:缓存空结果
- 对布隆过滤器误判的 “不存在商品 ID”,查询 DB 后发现不存在,将其写入 Redis(键为
product:{id}
,值为null
),设置短期过期时间(如 5 分钟)。 - 下次同一 ID 查询时,直接从 Redis 获取空结果,避免再次访问 DB。
- 对布隆过滤器误判的 “不存在商品 ID”,查询 DB 后发现不存在,将其写入 Redis(键为
第三级:限流与监控
- 对高频查询同一不存在 ID 的请求(可能是恶意攻击),用 Sentinel 限流(如单 ID 每秒最多 5 次请求),防止刷库。
- 监控异常 ID:统计 “查询不存在商品 ID 的次数”,超过阈值(如单 ID 日查 1000 次)告警,人工排查是否为恶意请求或 ID 生成逻辑错误。
10. 促销期间如何防止系统被流量打挂?实现一个基于 Sentinel 的限流方案
核心是 “按需限流 + 熔断降级”,基于 Sentinel 实现 “流量控制 + 过载保护”:
方案设计步骤
明确限流维度(按业务优先级划分):
- 核心接口:下单接口(
/order/create
)、支付接口(/pay/submit
)—— 允许较高阈值,保证核心流程可用。 - 非核心接口:商品详情(
/product/detail
)、评论列表(/comment/list
)—— 阈值可设低,优先保障下单。 - 用户维度:对普通用户限流,对 VIP 用户放宽限制(通过
userId
白名单)。
- 核心接口:下单接口(
配置限流规则(通过 Sentinel 控制台或 Nacos 持久化):
- 限流模式:QPS 模式(限制每秒请求数),如核心接口
/order/create
设 QPS=10000,非核心/product/detail
设 QPS=5000。 - 流控效果:快速失败(超出阈值直接返回 “系统繁忙”),或匀速排队(如秒杀场景,控制请求匀速进入,避免瞬间峰值)。
- 限流模式:QPS 模式(限制每秒请求数),如核心接口
熔断降级兜底:
- 对依赖的下游服务(如库存服务、支付服务)配置熔断规则:若接口成功率低于 90% 或响应时间超过 500ms,自动熔断(暂停调用),避免级联失败。
- 降级策略:返回缓存数据(如商品详情用本地缓存兜底)或默认值(如 “库存查询失败,请稍后再试”)。
监控与动态调整:
- 通过 Sentinel Dashboard 实时监控流量曲线、通过 / 拒绝数,发现阈值不合理时动态调整(无需重启)。
- 结合压测结果:促销前通过压测确定各接口最大承载 QPS,限流阈值设为压测值的 80%(留缓冲)。