外观
11 - 已知问题与优化方向
面试策略:主动提出项目中的问题并给出改进方案,展示代码审查能力和架构思维。这比"我的项目很完美"更有说服力。
Bug 清单
Bug 1: rollback.lua 用 String 命令操作 Hash 类型
严重程度: 致命 (运行时必报错)
文件: src/main/resources/rollback.lua (line 12)
lua
-- ❌ 当前代码: INCRBY 是 String 命令
redis.call("INCRBY", stockKey, buyNumber)
-- 但 seckill.lua 中 stockKey 是 Hash 类型:
-- seckill.lua line 74: redis.call('hincrby', stockKey, 'stock', -requestQuantity)后果: 执行 rollback.lua 时 Redis 报 WRONGTYPE Operation against a key holding the wrong kind of value,库存无法恢复。
修复:
lua
redis.call("HINCRBY", stockKey, "stock", buyNumber)Bug 2: rollback.lua 中 buyCount 字段名不一致
严重程度: 高 (静默失败)
文件: src/main/resources/rollback.lua (line 18)
lua
-- ❌ rollback.lua 用的是:
redis.call("HGET", buyCountKey, userId) -- field = "123"
-- ✅ seckill.lua 用的是:
redis.call('hget', buyCountKey, 'userId:' .. userId) -- field = "userId:123"后果: HGET 返回 nil,if 分支不执行,用户购买计数不回滚。用户后续购买时限购检查错误。
修复:
lua
local currentBuyCount = tonumber(redis.call("HGET", buyCountKey, "userId:" .. userId))
if currentBuyCount then
redis.call("HINCRBY", buyCountKey, "userId:" .. userId, -buyNumber)
endBug 3: CacheClient.unlock() 无 owner 校验
严重程度: 中 (缓存场景影响较小)
文件: src/main/java/com/hmdp/utils/CacheClient.java (line 174-176)
java
// ❌ 直接删除, 不检查锁的持有者
private void unlock(String key) {
stringRedisTemplate.delete(key);
}对比 SimpleRedisLock 使用 Lua 脚本做原子的 compare-and-delete。
影响场景:
- 线程 A 获取锁做缓存重建
- 重建耗时 > 10s, 锁过期
- 线程 B 获取锁
- 线程 A 完成, 调用 unlock → 删除了线程 B 的锁
- 线程 C 也获取锁 → 两个线程同时重建缓存(多查一次 DB, 不算严重但不正确)
Bug 4: seckill.lua 非幂等
严重程度: 高
文件: src/main/resources/seckill.lua
同一请求重复执行 seckill.lua 会:
HINCRBY stock -qty→ 库存被多次扣减HINCRBY buyCount +qty→ 购买计数被多次增加SADD orderId→ Set 天然去重, 不会重复
修复: 在扣减前检查 orderId 是否已存在:
lua
if redis.call('SISMEMBER', orderKey, orderId) == 1 then
return 0 -- 已处理, 直接返回成功
endBug 5: createVoucherOrder() 无 @Transactional
严重程度: 高
文件: src/main/java/com/hmdp/service/impl/VoucherOrderServiceImpl.java (line 81)
java
public void createVoucherOrder(VoucherOrder voucherOrder) {
// ...
// 步骤1: 扣 DB 库存 → 成功
seckillVoucherService.update()
.setSql("stock = stock - " + buyNumber)
.update();
// 步骤2: 保存订单 → 失败
if (!save(voucherOrder)) {
throw new Exception("保存订单失败");
}
}步骤 1 成功但步骤 2 失败时,DB 库存已扣但无对应订单 → 库存永久丢失。catch 块只回滚 Redis,不回滚 DB。
修复: 加 @Transactional。
Bug 6: KafkaOrderConsumer 未调用 acknowledge()
严重程度: 中
文件: src/main/java/com/hmdp/event/KafkaOrderConsumer.java (line 37-61)
配置了 ack-mode: manual,但 handleCreateOrder 方法签名中没有 Acknowledgment 参数,也没有调用 acknowledge()。
后果: offset 可能不被提交,消费者重启后重复消费所有消息。
Bug 7: LikeBehaviorServiceImpl.init() 返回值丢弃
严重程度: 低 (实际使用的是 Spring Bean)
文件: src/main/java/com/like/service/impl/LikeBehaviorServiceImpl.java (line 62-74)
java
@Resource
private LoadingCache<String, String> cache; // Spring 注入的 Bean
@PostConstruct
public LoadingCache<String, String> init() {
// 创建了新实例但返回值被 Spring 忽略
// cache 字段仍然指向 Spring 注入的 Bean
return Caffeine.newBuilder().maximumSize(100)...build(...);
}修复: this.cache = Caffeine.newBuilder()... 或删除此方法。
Bug 8: 验证码硬编码
严重程度: 安全隐患
文件: src/main/java/com/hmdp/service/impl/UserServiceImpl.java (line 87)
java
// String code = RandomUtil.randomNumbers(6);
String code = "123123"; // 为了方便测试,改成固定值Bug 9: Token TTL 单位不一致
严重程度: 低 (功能正常但非预期)
| 位置 | 单位 | 实际时长 |
|---|---|---|
UserServiceImpl.login() line 136 | TimeUnit.HOURS | 36000h ≈ 4年 |
RefreshTokenInterceptor line 44 | TimeUnit.MINUTES | 36000min ≈ 25天 |
创建时 TTL 极长,但每次访问都刷新为 25 天。实际效果是"25 天不活跃才过期"。
Bug 10: LikeBehaviorServiceImpl 中 articleLikeZsetKey 存错了值
严重程度: 中
文件: src/main/java/com/like/service/impl/LikeBehaviorServiceImpl.java (line 173)
java
// ❌ 应该存 userId, 但存了 articleId
if (!redisTemplate.opsForZSet().add(articleLikeZsetKey, articleId, score)) {
// ✅ 正确应该是:
if (!redisTemplate.opsForZSet().add(articleLikeZsetKey, userId, score)) {articleLikeZsetKey = likeZset:articleId:{articleId},这个 ZSet 的目的是记录"哪些用户赞了这篇文章",所以 member 应该是 userId 而不是 articleId。
架构优化建议
1. 消息幂等性
问题: Kafka 消费者可能重复消费,导致重复下单。
方案:
┌─────────────────────────────────────┐
│ 幂等表方案 │
│ │
│ 1. DB 创建 idempotent_key 表 │
│ (唯一索引: message_id) │
│ │
│ 2. 消费前: INSERT idempotent_key │
│ → 唯一索引冲突 = 重复消息, 跳过 │
│ → 插入成功 = 新消息, 继续处理 │
│ │
│ 3. 处理完毕: 更新状态为 DONE │
│ │
│ 4. 定时清理过期记录 │
└─────────────────────────────────────┘2. 分布式事务 — Lua + Kafka 的一致性
问题: seckill.lua 成功但 Kafka 发送失败 → Redis 扣了库存但没下单。
方案 A: 本地事务表
1. seckill.lua 成功
2. 在同一个 DB 事务中:
- 插入订单记录(状态=待处理)
- 插入消息记录到 outbox 表
3. 独立调度器扫描 outbox 表, 发送到 Kafka
4. Kafka 消费者更新订单状态方案 B: 手动回滚
java
try {
sendOrderMsgToKafka(orderId, voucherId, userId, buyNumber);
} catch (Exception e) {
// Kafka 发送失败 → 立即执行 rollback.lua
stringRedisTemplate.execute(ROLLBACK_SCRIPT, ...);
return Result.fail("系统繁忙, 请稍后重试");
}3. 热点数据预加载
问题: 秒杀开始瞬间,seckill:stock:{voucherId} Hash 可能未预热。
方案:
1. 后台创建秒杀券时, VoucherServiceImpl.addSeckillVoucher()
已经会写入 Redis Hash (当前实现)
2. 增加: 秒杀开始前 N 分钟, 定时任务检查 Redis 数据是否存在
如果不存在, 从 DB 重新加载
3. 增加: 秒杀库存缓存到本地 Caffeine
减少 Redis 网络往返 (适用于超高并发场景)4. 监控告警
缺失: 项目没有任何监控指标。
建议添加:
- Redis 连接池监控 (活跃连接数, 等待数)
- Kafka 消费者 lag 监控 (未消费消息积压量)
- 秒杀库存告警 (库存 < 阈值时告警)
- Lua 脚本执行耗时监控
- 线程池队列积压监控
- DB 连接池监控5. 限流升级
当前: 基于 Redis String 的固定窗口限流(有竞态条件)。
升级方案:
方案1: Redis + Lua 滑动窗口
ZRANGEBYSCORE rate:{key} (now-window) now → 窗口内请求数
ZADD rate:{key} now requestId → 记录请求
EXPIRE rate:{key} window → 设置过期
方案2: 令牌桶 (Guava RateLimiter)
本地限流, 无网络开销
适用于单机场景
方案3: Sentinel / Resilience4j
框架级限流 + 熔断 + 降级6. 缓存一致性增强
当前: Cache-Aside (先更新 DB 再删缓存)。
增强方案:
Canal + Redis 异步同步:
MySQL binlog → Canal → 解析变更 → 更新/删除 Redis 缓存
优点:
- 完全解耦: 业务代码不关心缓存
- 一致性更强: 基于 binlog, 不会遗漏
- 延迟可控: 通常 < 100ms面试 Q&A
Q1: 如果让你重新设计秒杀系统,你会怎么做?
- 入口限流:Nginx + 令牌桶,只放过 N 个请求到后端
- 资格校验:Redis Lua 原子扣减(保持当前方案)
- 异步下单:Kafka + 幂等消费者
- 补偿机制:Kafka 发送失败时立即回滚 Redis
- 监控告警:Prometheus + Grafana 监控全链路
- 数据一致性:定时对账任务,比对 Redis 库存与 DB 库存
Q2: 你怎么发现这些 Bug 的?
代码审查 + 逻辑推演:
rollback.lua的 Bug:对比seckill.lua和rollback.lua中相同 key 的操作命令,发现一个用 Hash 命令一个用 String 命令CacheClient.unlock:与SimpleRedisLock的实现对比,发现缺少 owner 校验@Transactional缺失:追踪异常路径,发现 DB 扣库存和保存订单不在一个事务中- Kafka ACK:检查配置与代码的一致性,发现配了 manual 但没调用 acknowledge
这些是跨文件一致性问题,单看一个文件很难发现,需要端到端追踪数据流。
Q3: 这些问题的优先级怎么排?
优先级 Bug 原因 P0 紧急 rollback.lua WRONGTYPE 回滚完全不工作, 库存丢失 P0 紧急 无 @Transactional DB 库存和订单不一致 P1 高 seckill.lua 非幂等 Kafka 重试时超卖 P1 高 Kafka ACK 未调用 重启后消息重复消费 P1 高 buyCount 前缀不一致 限购功能失效 P2 中 CacheClient.unlock 缓存场景影响较小 P2 中 articleLikeZsetKey 存错值 点赞列表数据错误 P3 低 验证码硬编码 仅开发环境 P3 低 Token TTL 单位不一致 功能正常 P3 低 @PostConstruct 返回值 实际使用 Spring Bean
加分回答
- 按照 Bug 影响范围和修复成本排优先级,展示工程判断力
- 对每个 Bug 不仅指出问题,还给出修复方案
- 提到"代码审查应该重点关注跨文件一致性",这是很多团队的痛点
- 将优化建议与面试中的"系统设计"问题关联(如"设计一个秒杀系统" → 直接引用改进方案)
- 提到可以用 SonarQube / SpotBugs 等静态分析工具自动发现部分问题
关联文档
- 03-秒杀系统 — Bug 1, 2, 4, 5 的详细上下文
- 01-Redis缓存设计 — Bug 3 的详细上下文
- 06-认证鉴权 — Bug 8, 9 的详细上下文
- 07-点赞子系统 — Bug 7, 10 的详细上下文
- 10-Kafka异步架构 — Bug 6 的详细上下文