尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

秒杀系统踩坑复盘:从库存 -15937 到 100 并发 0 超卖

秒杀系统踩坑复盘:从库存 -15937 到 100 并发 0 超卖 秒杀系统踩坑复盘从库存 -15937 到 100 并发 0 超卖原创声明本文基于本人课程实训期间独立开发的 CoolShark 微服务电商平台Spring Cloud Alibaba实战经验整理为第一手踩坑记录内容已脱敏。项目代码已开源https://github.com/yunxuan4309/csmall实战复盘 · 高并发场景项目背景基于 Spring Cloud Alibaba 的微服务电商平台11 个微服务秒杀模块采用 Redis 预扣库存 RabbitMQ 异步落库架构。本文记录了从库存被扣成负数到压测 0 超卖的全过程。一、问题的起点库存出现负数 -15937某次检查数据库发现seckill_sku表的seckill_stock字段变成了-15937。库存怎么会负一万多这暴露了秒杀系统多个层面的问题。二、五个问题的完整拆解问题 1布隆过滤器不可用RedisBloom 模块缺失现象SeckillBloomInitialJob使用bf.madd、bf.exists等 RedisBloom 命令但本地开发环境的 Redis 3.2.100Windows不支持该模块导致布隆过滤器 key 无法创建秒杀列表接口直接报错布隆过滤器未创建。解决开发阶段用Redis Set 替代布隆过滤器SADD/SISMEMBER所有 Redis 版本均支持生产部署安装 RedisBloom 模块后切回布隆过滤器实现代码中留 TODO 注释标记切换点问题 2秒杀订单创建缓慢 日志刷屏现象创建秒杀订单卡顿数秒控制台疯狂输出 DEBUG 日志导致 IDE 卡死。根因三层问题叠加——success表结构字段不匹配user_phone字段不存在导致 INSERT 失败RabbitMQ 消费者basicNack(requeuetrue)导致失败消息无限重试日志级别trace过低每次重试输出大量 DEBUG 信息解决修复success表结构对齐实体类消费者异常时抛出 RuntimeException让Transactional回滚事务日志级别从trace改为info问题 3库存变负数超卖问题—— 核心问题现象seckill_stock出现 -15937。根因消费者无限重试时扣减库存 SQL 每次都执行成功事务未回滚扣减 SQL 没有防超卖条件——只要 sku_id 匹配就扣不管库存够不够解决SQL 加上防超卖条件并检查影响行数-- 修复前问题代码UPDATEseckill_skuSETseckill_stockseckill_stock-#{quantity}WHEREsku_id#{skuId}-- 修复后UPDATEseckill_skuSETseckill_stockseckill_stock-#{quantity}WHEREsku_id#{skuId} AND seckill_stock #{quantity}消费者检查返回影响行数为 0 说明库存不足直接失败不扣减。问题 4success 表 id 字段不能为空现象saveSuccess是自定义 XML INSERT 语句不经过 MyBatis-Plus 的ASSIGN_ID生成逻辑导致id为 null数据库报Column id cannot be null。解决INSERT 语句显式加上id列和#{id}参数创建 Success 对象时手动调用IdWorker.getId()生成雪花 ID消费者中增加 null 检查兼容旧消息经验自定义 XML INSERT 不会经过 MyBatis-Plus 的主键生成逻辑必须手动IdWorker.getId()——这是用 MP 自定义 SQL 最容易踩的坑。问题 5其他一揽子修复问题修复布隆过滤器空数组导致 Lua 脚本语法错误添加空数组检查Redis 缓存 TTL 过短5分钟改为 2 小时 5 分钟秒杀阈值使用测试值100、-100改为生产值1、0RabbitMQ 消息使用 Java 序列化改为 JSON 序列化未配置 Seata 分布式事务添加依赖和 GlobalTransactional未配置 Sentinel 流控规则新增 SentinelFlowRuleConfig三、最终成果经过以上修复秒杀系统在100 并发QPS压测下 0 超卖、0 重复库存准确。四、经验总结扣库存必须带条件WHERE stock #{quantity}是防超卖的最后一道防线靠先查后扣永远有竞态窗口MQ 无限重试是隐形炸弹basicNack(requeuetrue) 业务异常 死循环。业务异常应该抛出 RuntimeException 回滚事务而不是重试自定义 XML INSERT 要手动处理主键MyBatis-Plus 的 ASSIGN_ID 只对 BaseMapper 方法生效日志级别影响的不只是性能trace 级别 无限重试 日志风暴排查问题时先看日志级别生产环境组件差异要在开发期就留开关RedisBloom 不可用时用 Set 兜底代码留 TODO 切换点避免上线才暴露五、生产环境待办安装 RedisBloom 模块redis/redis-stack-serverDocker 镜像将 Redis Set 实现切换回布隆过滤器代码有 TODO 标记生产 Redis 升级 7.x RedisBloom
返回列表