一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍

2026年4月5日,某电商平台在备战618大促前夕,技术团队召开了一场关于秒杀系统架构升级的评审会。当前系统在高并发场景下频繁出现超卖问题,QPS峰值突破8000时,库存扣减错误率高达3.7%。业务方明确要求:在30天内完成架构改造,保证库存强一致性,同时将系统吞吐量提升至15000 QPS以上,且不允许引入新的中间件依赖(如ZooKeeper)。

团队最初提出两套方案:方案A采用本地锁 + 数据库乐观锁,方案B采用Redis分布式锁 + Lua脚本原子扣减。评审会上,双方围绕性能、一致性、运维成本和故障恢复能力展开激烈讨论。最终,团队在充分评估后选择了一条折中路径:基于Redisson实现分布式锁,结合本地缓存预热与异步日志补偿机制,构建最终一致性的高可用秒杀架构。

问题背景:超卖频发,系统濒临崩溃

当前秒杀系统采用Spring Boot + MySQL架构,核心扣减逻辑如下:

java 复制代码
@Transactional
public boolean deductStock(Long itemId, int quantity) {
    Product product = productMapper.selectById(itemId);
    if (product.getStock() < quantity) {
        return false;
    }
    product.setStock(product.getStock() - quantity);
    return productMapper.updateById(product) > 0;
}

在高并发场景下,多个线程同时读取到相同库存值,导致超卖。尽管已添加synchronized关键字,但由于服务部署在4台机器上,本地锁无法跨JVM生效。团队尝试引入数据库悲观锁(SELECT FOR UPDATE),但压测显示TPS骤降至1200,无法满足业务需求。

错误直觉:本地锁 + 乐观锁就能解决问题?

方案A主张:"既然分布式锁复杂,不如回归本地锁 + 数据库乐观锁"。其核心逻辑是:

  1. 使用synchronized保证单节点内线程安全;
  2. 在更新时增加版本号校验(UPDATE product SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ?);
  3. 若更新失败,重试3次。

表面看似乎合理:乐观锁避免了行锁竞争,重试机制可应对冲突。但评审会上,资深架构师指出三大致命缺陷:

  • 跨节点失效:4台机器各自持锁,无法阻止并发写入;
  • 重试风暴:高并发下大量请求重试,数据库连接池被打满;
  • 版本号竞争:即使库存充足,因版本号冲突导致大量请求失败,用户体验差。

压测结果验证了担忧:在5000并发下,成功扣减率仅68%,平均响应时间飙升至1.2秒,MySQL CPU使用率持续超过90%。

正确方案:Redisson分布式锁 + 异步补偿机制

方案B提出使用Redis分布式锁,但直接使用SETNX存在锁过期、误删等问题。团队最终选择Redisson框架,其内置看门狗机制可自动续期,避免业务未执行完锁已释放。

核心实现如下:

java 复制代码
public boolean deductStockWithLock(Long itemId, int quantity) {
    RLock lock = redissonClient.getLock("stock:lock:" + itemId);
    try {
        boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
        if (!locked) {
            log.warn("获取锁失败,itemId={}", itemId);
            return false;
        }
        // 再次查询库存,防止锁等待期间库存变化
        Product product = productMapper.selectById(itemId);
        if (product.getStock() < quantity) {
            return false;
        }
        product.setStock(product.getStock() - quantity);
        int updated = productMapper.updateById(product);
        if (updated > 0) {
            // 异步记录日志,用于后续对账
            stockLogService.asyncLog(itemId, quantity, "DEDUCT");
        }
        return updated > 0;
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

为进一步提升性能,团队引入以下优化:

  • 本地缓存预热:启动时加载热门商品库存至Caffeine缓存,减少数据库查询;
  • 锁粒度细化 :按商品ID分段加锁(如stock:lock:1001),避免全局锁竞争;
  • 异步日志补偿:扣减成功后异步写入日志表,定时任务对账修复异常数据;
  • 熔断降级:当Redis不可用时,自动降级为本地锁 + 限流,保障系统可用性。

压测结果显示:在15000 QPS下,成功扣减率达99.98%,平均响应时间稳定在80ms以内,Redis CPU使用率控制在40%以下。

技术取舍与风险边界

尽管方案B表现优异,但团队仍明确其边界条件:

  • 不适用于超高频扣减:若单商品QPS超过5万,建议引入分桶扣减或预扣库存机制;
  • 依赖Redis稳定性:需部署Redis Cluster,配置持久化与哨兵机制;
  • 最终一致性容忍:异步日志补偿存在毫秒级延迟,需业务接受短暂不一致;
  • 锁续期开销:看门狗机制会定期续约,增加网络开销,需合理设置超时时间。

最终,团队决定分阶段上线:先灰度10%流量验证稳定性,再逐步全量。同时建立监控大盘,实时跟踪锁等待时间、扣减成功率、Redis延迟等关键指标。

技术补丁包

  1. Redisson分布式锁实现原理 原理:基于Redis的SET resource_name unique_value NX PX timeout命令实现互斥锁,通过看门狗线程自动续期(默认30秒,每10秒续一次)。 设计动机:解决原生Redis锁在业务执行时间长于锁超时时间时的误释放问题。 边界条件:必须确保unique_value唯一(通常用UUID),避免误删其他线程的锁;业务逻辑需在finally块中释放锁。 落地建议:使用tryLock(long waitTime, long leaseTime, TimeUnit unit)方法,明确指定等待时间和持有时间,避免无限阻塞。

  2. 本地锁在分布式环境中的局限性 原理:synchronizedReentrantLock仅作用于单个JVM,无法跨进程同步。 设计动机:简化单机并发控制,性能极高(纳秒级)。 边界条件:仅适用于单实例部署或无需跨节点协调的场景。 落地建议:在分布式系统中,本地锁仅可用于保护非关键路径的本地状态,如线程池配置、本地缓存更新等。

  3. 数据库乐观锁与悲观锁的适用场景 原理:乐观锁通过版本号或CAS机制实现无锁更新;悲观锁通过SELECT FOR UPDATE提前加锁。 设计动机:乐观锁适用于读多写少、冲突概率低的场景;悲观锁适用于写密集、强一致性要求的场景。 边界条件:乐观锁在高并发下重试成本高;悲观锁易造成死锁和性能瓶颈。 落地建议:秒杀场景优先选择分布式锁+数据库校验,而非纯乐观/悲观锁。

  4. 异步日志补偿机制设计要点 原理:核心操作成功后,异步记录操作日志,后台任务定期扫描异常状态进行修复。 设计动机:解耦核心流程与对账逻辑,提升系统吞吐量。 边界条件:需保证日志写入的可靠性(如写入本地文件+MQ双写);补偿任务需具备幂等性。 落地建议:日志表设计包含操作类型、商品ID、数量、时间戳、状态等字段,便于追踪与修复。

  5. 锁粒度细化的最佳实践 原理:将全局锁拆分为多个细粒度锁(如按商品ID、用户ID、订单类型等维度)。 设计动机:减少锁竞争,提升并发能力。 边界条件:避免过度细化导致锁数量爆炸(如百万级商品ID);需防止死锁(按固定顺序加锁)。 落地建议:使用ConcurrentHashMap缓存锁对象,避免频繁创建;设置最大锁数量限制,防止内存溢出。

相关推荐
她说..2 小时前
Java 注解核心面试题
java·spring boot·spring·spring cloud·自定义注解
用户8307196840822 小时前
Spring Boot @Qualifier深度解密:从“按名查找”到“分组批量注入”,一文掌握它的全部“隐藏技能”。
java·spring boot
亦暖筑序2 小时前
Message 四分天下:Spring AI 如何统一消息格式
java·人工智能
镜花水月linyi2 小时前
JDK 8 → 17 → 21 → 25:一次性讲清四代版本的关键跃迁
java·后端
0xDevNull3 小时前
JDK 25 新特性概览与实战教程
java·开发语言·后端
Yiyi_Coding3 小时前
BUG列表:如何定位线上 OOM ?
java·linux·bug
gelald3 小时前
Spring - 循环依赖
java·后端·spring
凤山老林3 小时前
Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库
java·人工智能·知识库·rag·spring ai
爱码驱动3 小时前
文件操作和IO
java·开发语言·io·文件操作