Java 大厂一面模拟:从会员积分到账单对账的缓存与一致性连环拷问
开场说明
这是一场面向校招高阶及 1-3 年 Java 后端候选人的模拟大厂一面,时长约 30 分钟。面试以“会员积分系统”为业务主线,串联起缓存设计、消息可靠性、事务一致性及对账补偿等核心模块。问题覆盖 Redis、MySQL、MQ、Spring 事务与 JVM 基础,强调从原理到边界再到线上落地的完整链路。追问风格贴近真实大厂节奏:先问用法,再挖原理,接着压边界场景,最后落到项目取舍。
候选人画像:具备 Spring Boot + MySQL + Redis 基础开发经验,参与过简单业务模块开发,但对高并发、一致性保障和故障兜底机制理解较浅。
主问题部分
1. 你在项目中如何实现用户积分的增加?请描述核心流程。
参考回答:
用户完成下单或签到等行为后,后端接收事件,先查用户当前积分,然后加锁更新积分字段,最后返回新积分值。使用 MySQL 的 UPDATE user_points SET points = points + ? WHERE user_id = ? 保证原子性。
面试官追问点:是否考虑并发场景?如何避免超发?
2. 如果积分更新频繁,MySQL 压力大,你会怎么优化?
参考回答:
引入 Redis 缓存用户积分,行为触发后先写 Redis,再异步同步到 MySQL。Redis 使用 INCRBY 命令保证原子性,同时设置过期时间防止冷数据长期占用内存。
面试官追问点:Redis 宕机了怎么办?数据丢了怎么恢复?
3. 你说异步同步,具体怎么实现?用线程池还是消息队列?
参考回答: 用消息队列更可靠。积分变更后发一条消息到 Kafka,消费者监听并落库 MySQL。相比线程池,MQ 提供持久化、重试和削峰能力。
面试官追问点:消息丢了或重复消费怎么办?
4. 消息重复消费怎么处理?你做过幂等设计吗?
参考回答:
在消费者端做幂等。每条消息带唯一业务 ID(如 user_id + action_type + timestamp),消费前先查 MySQL 是否已处理过该 ID,若已存在则跳过。
面试官追问点:唯一 ID 冲突概率高吗?如何生成更可靠的唯一键?
5. 如果 Redis 和 MySQL 数据不一致,你怎么发现和修复?
参考回答: 通过定时对账任务扫描差异。每天凌晨跑批,对比 Redis 中积分总和与 MySQL 实际值,发现不一致则触发告警并尝试修复。修复策略可以是“以 MySQL 为准”或“以最新行为日志为准”。
面试官追问点:对账频率怎么定?实时性要求高的话怎么办?
6. 你说用 Kafka,那消息顺序性怎么保证?比如用户连续签到两次,必须按顺序处理。
参考回答:
Kafka 通过分区保证同一 key 的消息有序。将 user_id 作为消息 key,确保同一用户的积分变更消息进入同一个 partition,消费者单线程消费即可保序。
面试官追问点:分区数变了怎么办?扩容后顺序性还成立吗?
7. Spring 事务在积分更新中怎么用的?@Transactional 能保证 Redis 和 MySQL 同时成功吗?
参考回答: 不能。@Transactional 只管理数据库事务,Redis 操作不在事务范围内。如果先写 MySQL 成功,再写 Redis 失败,就会不一致。我们通常先写 MySQL,再写 Redis,并配合重试或补偿机制。
面试官追问点:那有没有办法做到“近似事务”?比如两阶段提交?
8. 你们系统出现过积分超发吗?怎么排查的?
参考回答:
出现过一次。原因是并发签到未加锁,多个请求同时读出旧值并叠加。后来加了分布式锁(Redisson),并在 MySQL 层用 SELECT FOR UPDATE 做二次校验。
面试官追问点:Redisson 锁过期了但业务还在执行,会怎么样?
追问部分
追问 1:Redis 做积分缓存,如果缓存击穿怎么办?
候选人可能回答:加互斥锁,只让一个请求回源查 DB。
面试官继续压:
- 互斥锁用本地锁还是分布式锁?为什么?
- 如果回源查询很慢(比如用户历史积分复杂计算),锁持有时间过长,其他请求全阻塞,怎么办?
- 有没有不阻塞的方案?比如缓存空值或异步预热?
正确答案方向: 必须用分布式锁(如 Redisson),因为多实例部署下本地锁无效。对于慢查询,可设置锁超时 + 异步刷新缓存,或采用“缓存空对象 + 延迟双删”策略。高并发场景建议预热点用户积分。
追问 2:Kafka 消费者宕机重启后,如何保证不丢消息?
候选人可能回答:开启自动提交 offset,消费者处理完再手动提交。
面试官继续压:
- 手动提交的时机怎么选?处理中提交还是处理完提交?
- 如果消费者处理消息时崩溃,offset 已提交但业务未落库,怎么办?
- 你们有没有做消费进度监控?如何快速定位积压?
正确答案方向: 必须“处理完再提交 offset”,否则会丢消息。建议结合业务日志记录处理状态,或使用事务型消息(如 RocketMQ)。监控方面可用 Kafka Manager 或自研 dashboard 跟踪 lag。
追问 3:Spring @Transactional 在嵌套调用时传播行为怎么选?
候选人可能回答:一般用 REQUIRED,支持嵌套事务。
面试官继续压:
- 如果外层事务回滚,内层事务一定回滚吗?
- 如果内层方法抛出异常但被 catch 了,外层事务还会回滚吗?
- 在积分系统中,发消息要不要放在事务内?为什么?
正确答案方向: REQUIRED 下内外层共享事务,任一回滚则整体回滚。但异常必须抛出才能触发回滚,catch 后需手动 setRollbackOnly。发消息应放在事务提交后(如 @TransactionalEventListener),避免事务未提交就发消息导致状态不一致。
追问 4:对账任务发现 Redis 积分比 MySQL 高,可能是什么原因?
候选人可能回答:Redis 未及时同步,或 MySQL 写入失败但 Redis 成功了。
面试官继续压:
- 如何区分是“同步延迟”还是“永久丢失”?
- 如果差异持续扩大,说明系统有漏洞,你如何设计自动修复策略?
- 修复时会不会引发新的不一致(比如修复过程中又有新行为)?
正确答案方向: 可通过行为日志时间戳判断是否为延迟。自动修复应基于“最终一致性”原则,采用“增量修复 + 版本号控制”,修复期间新行为仍正常处理,修复完成后再次校验。
追问 5:如果让你设计一个积分流水表,你会怎么建索引?
候选人可能回答:按 user_id 和 create_time 建联合索引。
面试官继续压:
- 查询某用户最近 10 条流水,SQL 怎么写?索引是否生效?
- 如果还要按 action_type 筛选,索引怎么调整?
- 数据量上亿后,分页查询
LIMIT 1000000, 10很慢,怎么优化?
正确答案方向: 索引应为
(user_id, create_time DESC),查询用WHERE user_id = ? ORDER BY create_time DESC LIMIT 10。加 action_type 可建(user_id, action_type, create_time)。大分页建议用游标分页(基于 last_id 或 last_time)。
面试点评
本场面试聚焦“会员积分系统”这一典型业务场景,覆盖了缓存一致性、消息可靠性、事务边界、对账补偿四大核心能力域。重点考察候选人是否具备从“功能实现”到“稳定性保障”的思维跃迁。
候选人易卡点:
- 混淆本地锁与分布式锁适用场景;
- 忽视消息消费的幂等与顺序保障;
- 对 Spring 事务传播理解停留在注解层面,缺乏异常处理与监听器配合意识;
- 对账设计缺乏自动化与版本控制思维。
高分候选人应能清晰区分“理想模型”与“工程落地”的差距,并给出可落地的兜底方案。
技术补丁包
-
Redis 缓存击穿防护 原理:当热点 key 失效时,大量请求同时击穿到 DB,需通过互斥锁限制回源并发。 设计动机:避免 DB 瞬时压力导致雪崩。 边界条件:锁超时设置需大于业务最大处理时间,否则可能重复回源。 落地建议:使用 Redisson 的
RLock,配合缓存空值 + 延迟双删策略。 -
Kafka 消息顺序性保障 原理:同一 key 的消息路由到同一 partition,单线程消费保证顺序。 设计动机:解决用户行为时序依赖问题(如签到、兑换)。 边界条件:partition 数量变更会导致 key 重新哈希,历史顺序可能被打乱。 落地建议:partition 数提前规划,或通过业务层 version 字段做最终一致性校验。
-
Spring 事务传播行为(REQUIRED) 原理:若当前存在事务则加入,否则新建事务。 设计动机:简化嵌套服务调用的事务管理。 边界条件:异常必须抛出才能触发回滚,catch 后需手动标记回滚。 落地建议:结合
@TransactionalEventListener在事务提交后发消息,避免状态不一致。 -
消息幂等消费设计 原理:通过唯一业务 ID 判断消息是否已处理。 设计动机:应对网络重试、消费者重启等导致的重复投递。 边界条件:唯一 ID 需全局唯一且稳定(避免时间戳精度不足)。 落地建议:使用
user_id + action_type + biz_id组合,配合数据库唯一索引或 Redis set 去重。 -
MySQL 联合索引最左前缀原则 原理:查询条件必须包含索引最左列才能生效。 设计动机:提升多条件查询性能。 边界条件:范围查询右边的列无法使用索引。 落地建议:高频查询字段放左侧,排序字段放右侧,避免
SELECT *。 -
分布式锁 Redisson 实现原理 原理:基于 Redis 的
SET resource_name random_value NX PX timeout命令实现。 设计动机:提供跨 JVM 的互斥访问控制。 边界条件:锁自动过期可能导致业务未完成就被释放,引发并发问题。 落地建议:使用看门狗机制自动续期,或业务层加 version 控制。 -
缓存与数据库双写一致性 原理:无法强一致,只能追求最终一致。 设计动机:提升读性能同时保障数据可靠性。 边界条件:先写 DB 还是先写缓存?删除缓存失败怎么办? 落地建议:采用“先写 DB,再删缓存” + 延迟双删 + 对账补偿组合策略。
-
Kafka 消费者 offset 管理 原理:消费者记录已消费位置,重启后从上次的 offset 继续。 设计动机:避免重复消费或丢失消息。 边界条件:自动提交可能导致消息未处理完就标记为已消费。 落地建议:手动提交 offset,配合业务处理状态日志。
-
对账任务设计要点 原理:定期比对源系统与目标系统数据差异。 设计动机:发现并修复异步系统的不一致问题。 边界条件:对账期间新数据持续写入,可能导致误判。 落地建议:基于时间窗口对账,配合 version 或 timestamp 做增量修复。
-
Spring AOP 代理机制 原理:基于 JDK 动态代理或 CGLIB 生成代理对象。 设计动机:实现声明式事务、日志等横切关注点。 边界条件:同类内部方法调用不走代理,@Transactional 失效。 落地建议:避免同类内自调用事务方法,或通过
AopContext.currentProxy()获取代理。 -
Redis INCRBY 原子性 原理:Redis 单线程执行命令,INCRBY 是原子操作。 设计动机:替代数据库自增,提升性能。 边界条件:仅适用于整数值,浮点需自行处理精度。 落地建议:配合 Lua 脚本实现复杂原子逻辑。
-
MySQL MVCC 与读提交隔离级别 原理:通过 undo log 构建数据快照,实现非阻塞读。 设计动机:提升并发读性能。 边界条件:不可重复读问题依然存在。 落地建议:积分系统通常用 READ_COMMITTED,避免幻读影响对账。
-
线程池拒绝策略 原理:当队列满且线程数达上限时触发拒绝。 设计动机:防止资源耗尽。 边界条件:CallerRunsPolicy 可能导致调用方阻塞。 落地建议:积分系统建议使用 DiscardPolicy 或自定义降级策略,避免影响主流程。
-
JVM OOM 排查流程 原理:通过堆转储分析对象占用。 设计动机:定位内存泄漏或大对象问题。 边界条件:Full GC 频繁但堆不大,可能是元空间溢出。 落地建议:结合 Arthas 在线诊断,关注静态集合、线程池未关闭等常见泄漏点。
-
最终一致性补偿机制 原理:通过定时任务或消息重试达成数据一致。 设计动机:弥补异步系统无法强一致的缺陷。 边界条件:补偿频率与业务容忍度需平衡。 落地建议:设计可重试、可幂等的补偿接口,配合告警监控。
更多推荐


所有评论(0)