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

张开发
2026/4/18 17:31:36 15 分钟阅读

分享文章

一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
2026年4月5日某电商平台在备战618大促前夕技术团队召开了一场关于秒杀系统架构升级的评审会。当前系统在高并发场景下频繁出现超卖问题QPS峰值突破8000时库存扣减错误率高达3.7%。业务方明确要求在30天内完成架构改造保证库存强一致性同时将系统吞吐量提升至15000 QPS以上且不允许引入新的中间件依赖如ZooKeeper。团队最初提出两套方案方案A采用本地锁 数据库乐观锁方案B采用Redis分布式锁 Lua脚本原子扣减。评审会上双方围绕性能、一致性、运维成本和故障恢复能力展开激烈讨论。最终团队在充分评估后选择了一条折中路径基于Redisson实现分布式锁结合本地缓存预热与异步日志补偿机制构建最终一致性的高可用秒杀架构。问题背景超卖频发系统濒临崩溃当前秒杀系统采用Spring Boot MySQL架构核心扣减逻辑如下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主张“既然分布式锁复杂不如回归本地锁 数据库乐观锁”。其核心逻辑是使用synchronized保证单节点内线程安全在更新时增加版本号校验UPDATE product SET stock stock - ?, version version 1 WHERE id ? AND version ?若更新失败重试3次。表面看似乎合理乐观锁避免了行锁竞争重试机制可应对冲突。但评审会上资深架构师指出三大致命缺陷跨节点失效4台机器各自持锁无法阻止并发写入重试风暴高并发下大量请求重试数据库连接池被打满版本号竞争即使库存充足因版本号冲突导致大量请求失败用户体验差。压测结果验证了担忧在5000并发下成功扣减率仅68%平均响应时间飙升至1.2秒MySQL CPU使用率持续超过90%。正确方案Redisson分布式锁 异步补偿机制方案B提出使用Redis分布式锁但直接使用SETNX存在锁过期、误删等问题。团队最终选择Redisson框架其内置看门狗机制可自动续期避免业务未执行完锁已释放。核心实现如下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延迟等关键指标。技术补丁包Redisson分布式锁实现原理原理基于Redis的SET resource_name unique_value NX PX timeout命令实现互斥锁通过看门狗线程自动续期默认30秒每10秒续一次。 设计动机解决原生Redis锁在业务执行时间长于锁超时时间时的误释放问题。 边界条件必须确保unique_value唯一通常用UUID避免误删其他线程的锁业务逻辑需在finally块中释放锁。 落地建议使用tryLock(long waitTime, long leaseTime, TimeUnit unit)方法明确指定等待时间和持有时间避免无限阻塞。本地锁在分布式环境中的局限性原理synchronized或ReentrantLock仅作用于单个JVM无法跨进程同步。 设计动机简化单机并发控制性能极高纳秒级。 边界条件仅适用于单实例部署或无需跨节点协调的场景。 落地建议在分布式系统中本地锁仅可用于保护非关键路径的本地状态如线程池配置、本地缓存更新等。数据库乐观锁与悲观锁的适用场景原理乐观锁通过版本号或CAS机制实现无锁更新悲观锁通过SELECT FOR UPDATE提前加锁。 设计动机乐观锁适用于读多写少、冲突概率低的场景悲观锁适用于写密集、强一致性要求的场景。 边界条件乐观锁在高并发下重试成本高悲观锁易造成死锁和性能瓶颈。 落地建议秒杀场景优先选择分布式锁数据库校验而非纯乐观/悲观锁。异步日志补偿机制设计要点原理核心操作成功后异步记录操作日志后台任务定期扫描异常状态进行修复。 设计动机解耦核心流程与对账逻辑提升系统吞吐量。 边界条件需保证日志写入的可靠性如写入本地文件MQ双写补偿任务需具备幂等性。 落地建议日志表设计包含操作类型、商品ID、数量、时间戳、状态等字段便于追踪与修复。锁粒度细化的最佳实践原理将全局锁拆分为多个细粒度锁如按商品ID、用户ID、订单类型等维度。 设计动机减少锁竞争提升并发能力。 边界条件避免过度细化导致锁数量爆炸如百万级商品ID需防止死锁按固定顺序加锁。 落地建议使用ConcurrentHashMap缓存锁对象避免频繁创建设置最大锁数量限制防止内存溢出。

更多文章