Skip to content

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)
end

Bug 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。

影响场景:

  1. 线程 A 获取锁做缓存重建
  2. 重建耗时 > 10s, 锁过期
  3. 线程 B 获取锁
  4. 线程 A 完成, 调用 unlock → 删除了线程 B 的锁
  5. 线程 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  -- 已处理, 直接返回成功
end

Bug 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 136TimeUnit.HOURS36000h ≈ 4年
RefreshTokenInterceptor line 44TimeUnit.MINUTES36000min ≈ 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: 如果让你重新设计秒杀系统,你会怎么做?

  1. 入口限流:Nginx + 令牌桶,只放过 N 个请求到后端
  2. 资格校验:Redis Lua 原子扣减(保持当前方案)
  3. 异步下单:Kafka + 幂等消费者
  4. 补偿机制:Kafka 发送失败时立即回滚 Redis
  5. 监控告警:Prometheus + Grafana 监控全链路
  6. 数据一致性:定时对账任务,比对 Redis 库存与 DB 库存

Q2: 你怎么发现这些 Bug 的?

代码审查 + 逻辑推演

  1. rollback.lua 的 Bug:对比 seckill.luarollback.lua 中相同 key 的操作命令,发现一个用 Hash 命令一个用 String 命令
  2. CacheClient.unlock:与 SimpleRedisLock 的实现对比,发现缺少 owner 校验
  3. @Transactional 缺失:追踪异常路径,发现 DB 扣库存和保存订单不在一个事务中
  4. Kafka ACK:检查配置与代码的一致性,发现配了 manual 但没调用 acknowledge

这些是跨文件一致性问题,单看一个文件很难发现,需要端到端追踪数据流。

Q3: 这些问题的优先级怎么排?

优先级Bug原因
P0 紧急rollback.lua WRONGTYPE回滚完全不工作, 库存丢失
P0 紧急无 @TransactionalDB 库存和订单不一致
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 等静态分析工具自动发现部分问题

关联文档