尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【基于 Swoole+Hyperf 的微服务实战】第八周·周六:高并发场景——“秒杀抢购 + Saga 事务”

【基于 Swoole+Hyperf 的微服务实战】第八周·周六:高并发场景——“秒杀抢购 + Saga 事务” 【基于 SwooleHyperf 的微服务实战】第八周·周六高并发场景——“秒杀抢购 Saga 事务”今天我们进入第八周周六综合实战日。本周我们深入分布式事务Saga和分布式锁从理论到代码完整落地了一个协调器驱动的 Saga 流程并用 Redis 锁保护了并发安全。今天我们将通过一个高并发场景——“秒杀抢购 Saga 事务”——将本周所学全部串联用户争抢秒杀商品时用分布式锁防止超卖下单后由 Saga 协调器驱动订单创建、库存冻结和支付失败则自动补偿。你将亲眼见证锁与事务如何协同保护数据一致性。今日目标构建一个秒杀接口集成 Redis 分布式锁和 Saga 事务启动。在高并发下wrk 压测验证分布式锁防止超卖库存扣减准确。对秒杀成功的订单Saga 协调器自动执行正向流程创建订单、冻结库存、模拟支付。人为制造支付失败触发 Saga 补偿确保库存完整恢复无数据残留。输出压测报告对比有锁无锁、成功补偿后的数据准确性。一、环境准备与需求分析约 30 分钟确保所有容器启动RabbitMQ、MySQL、Redis、Consul、Nacos 等。docker-composeup-d进入hyperf-app容器docker-composeexecswoolebashcd/var/www/hyperf-app功能设计秒杀商品使用products表的total_stock和frozen_stock可用库存 total_stock - frozen_stock。秒杀接口POST /seckill/buy加分布式锁 (lock:seckill:{productId})检查库存若充足则冻结库存然后启动 Saga发送第一条命令order.create。Saga 流程同第八周周二、周三的协调器但调整第一步为“订单创建”库存已在秒杀时冻结所以 Saga 中不再冻结库存改为“确认冻结”或直接跳过库存步骤或者将冻结库存纳入 Saga 第一步。为简化我们今天将整个“冻结库存”操作移入 Saga 的第一步秒杀锁内只做库存预留检查不行检查库存和冻结必须原子。所以秒杀锁内直接调用库存服务的冻结接口通过消息同步不能同步等待。较好的做法秒杀锁内预扣 Redis 库存快速然后发送 Saga 启动消息Saga 再异步操作数据库库存。这样锁的作用是保护 Redis 库存计数数据库库存由 Saga 最终一致。我们采用此方案。调整方案在 Redis 中维护商品库存seckill:stock:{productId}初始化为数据库库存。秒杀接口加锁后检查 Redis 库存并原子递减DECR若 0 则抢购成功发送 Saga 启动消息包含订单信息。Saga 流程order.create→inventory.freeze数据库冻结 →payment.debit。如果 Saga 失败补偿则补偿时需恢复 Redis 库存在order.cancel中增加 Redis 库存回滚和数据库冻结。这样实现了 Redis 缓存的高并发扣减数据库库存通过 Saga 最终一致。二、知识核心秒杀架构与缓存库存约 1 小时1. 秒杀的核心挑战极高并发瞬间流量冲击数据库需要缓存拦截。防止超卖库存扣减需原子操作。数据一致缓存与数据库库存最终一致。快速失败库存不足时直接返回不进入 Saga。2. 缓存库存方案使用 Redis Stringseckill:stock:1存储可用库存DECR原子操作判断是否抢到。若返回 0 表示抢到0 表示库存不足需要INCR回滚但由于 DECR 已经使库存减一如果并发判断可能多个请求同时 DECR 得到负数需要回滚我们只需在 0 时 INCR 回去并返回失败。更严谨使用 Lua 脚本保证检查和扣减原子。我们采用 Lua 脚本localkeyKEYS[1]localstocktonumber(redis.call(get,key)or0)ifstock0thenredis.call(decr,key)return1elsereturn0end在 PHP 中执行。3. Saga 与库存的一致性如果抢购成功Redis 库存已减后续 Saga 失败补偿时需要恢复 Redis 库存。我们在OrderCommandConsumer的cancelOrder中除了取消订单还增加INCR秒杀库存。三、实战秒杀 Saga 协同约 3 小时步骤 1初始化 Redis 库存在秒杀开始前将数据库库存同步到 Redis// 可在控制器中临时调用$redis-set(seckill:stock:1,100);// 设置100件步骤 2编写 Lua 脚本扣减库存在SeckillController中注入 Redis编写方法privatefunctiondeductStock(int$productId):bool{$scriptLUAlocal keyKEYS[1]local stocktonumber(redis.call(get,key)or0)ifstock0then redis.call(decr,key)return1elsereturn0endLUA;return(bool)$this-redis-eval($script,[seckill:stock:.$productId],1);}步骤 3秒杀接口改造#[RequestMapping(path:buy,methods:post)]#[RedisLock(key:lock:seckill:#{product_id},ttl:5)]publicfunctionbuy(){$productId(int)$this-request-input(product_id,1);$userId$this-request-getAttribute(user_id,1);// 1. Lua 脚本扣减 Redis 库存if(!$this-deductStock($productId)){return[code0,message已抢光];}// 2. 生成订单数据$orderData[order_id(string)Str::uuid(),user_id$userId,product_id$productId,amount99.00,];// 3. 启动 Saga发送 order.create 命令$sagaId(string)Str::uuid();Db::table(saga_transactions)-insert([saga_id$sagaId,statusrunning,current_stepSagaConstants::STEP_ORDER_CREATE,payloadjson_encode($orderData),]);$this-producer-produce(newGenericProducer([saga_id$sagaId,stepSagaConstants::STEP_ORDER_CREATE,payload$orderData,],SagaConstants::EXCHANGE_COMMANDS,SagaConstants::STEP_ORDER_CREATE));return[code200,message抢购成功订单处理中,order_id$orderData[order_id]];}注意这里使用#[RedisLock]注解加锁但锁的范围包含了发送消息可能会影响吞吐。我们可以将锁仅限制在 Redis 扣减部分使用手动锁$lock$this-redis-lock(lock:seckill:.$productId,5);if($lock-get()){try{// 扣减 Redis 库存...}finally{$lock-release();}}这样锁粒度更细。今天先用注解简化。步骤 4修改 Saga 中的库存冻结和补偿在InventoryCommandConsumer中冻结数据库库存privatefunctionfreezeStock(array$payload):void{$productId$payload[product_id];// 数据库冻结操作前面已实现$affectedDb::update(UPDATE products SET frozen_stock frozen_stock 1 WHERE id ? AND total_stock - frozen_stock 1,[$productId]);if($affected0){thrownew\Exception(库存不足);}}取消订单时恢复 Redis 库存在OrderCommandConsumer::cancelOrder中privatefunctioncancelOrder(array$payload):void{$orderId$payload[order_id];$productId$payload[product_id];// 取消数据库订单Db::table(orders)-where(order_id,$orderId)-update([statuscancelled]);// 恢复 Redis 库存$this-redis-incr(seckill:stock:.$productId);echo[订单服务] 订单取消Redis库存1\n;}库存解冻依旧在InventoryCommandConsumer中。步骤 5压测与观察重置 Redis 库存为 100。使用 wrk 进行高并发测试# 获取登录 TokenJWTTOKEN$(curl-s-XPOST http://localhost:9500/auth/login-dusernameadminpassword123456|jq-r.data.token)# 压测秒杀接口注意需要携带 Token若网关要求wrk-t4-c100-d30s--latency-HAuthorization: Bearer$TOKEN-spost.lua http://localhost:9500/seckill/buy# post.lua 构造 POST 请求body 中包含 product_id1压测期间观察日志中协调器驱动订单创建、库存冻结、支付。支付消费者随机失败我们保留随机失败以触发补偿。最终 Redis 库存可能为 0抢光。orders表中成功的订单数 取消的订单数 总发放订单数但有些订单可能还在处理中最终应为 100 左右。products表中frozen_stock和total_stock应逻辑一致。补偿流程恢复的 Redis 库存会被后来的请求抢走体现最终一致性。步骤 6模拟支付全面失败场景把支付消费者设置为全部失败测试大规模补偿。此时Redis 库存先减后增补偿恢复但可能因为顺序问题恢复的库存又立即被抢导致最终成功订单少于库存其实这是正确的补偿恢复库存后后续请求可以再次抢购所以最终能卖出的商品数量由实际支付成功的订单决定而非预扣。这就是 Saga 补偿的正常效果。确认数据库中没有冻结库存残留。四、成果测试与数据一致性验证约 1.5 小时1. 数据核对压测结束后执行以下 SQL-- 已支付或处理中订单数SELECTcount(*)FROMordersWHEREstatuspaid;-- 我们流程最终成功会标记为 paid但需确认流程-- 取消订单数SELECTcount(*)FROMordersWHEREstatuscancelled;-- 产品库存SELECT*FROMproductsWHEREid1;-- 总订单数SELECTcount(*)FROMorders;通过计算successful_orders (frozen_stock) total_stock或类似公式验证没有超卖。2. 压测报告QPS记录加锁秒杀的吞吐量。延迟P50、P99 延迟。正确率库存未出现负数订单状态与库存吻合。补偿成功率所有失败 Saga 都触发了补偿无遗留中间态。3. 测试清单检验项方法通过标准分布式锁防超卖高并发压测Redis 库存减到 0 后不再产生新订单最终订单数 ≈ 初始库存无超卖Saga 正向流程支付成功订单订单状态变为 paid库存冻结转实际扣减数据一致支付失败补偿支付失败订单状态变为 cancelledRedis 库存恢复库存数字正确冻结库存清零补偿幂等模拟重复补偿命令操作日志唯一Redis 库存只恢复一次锁性能压测期间 Redis CPU 和网络锁获取成功率 95%系统恢复停止并重启协调器未完成 Saga 继续执行最终一致五、今日作业与学习产出提交代码包含秒杀接口、Lua 脚本、改造后的补偿逻辑、压测脚本等。完善监控在 Grafana 中展示秒杀接口 QPS、库存变化曲线、Saga 成功率。添加告警库存低于 10 时通知。学习笔记绘制秒杀 Saga 全链路时序图标出锁、缓存、消息、数据库的交互。总结“缓存库存 异步事务”模式的优缺点以及何时需要将库存完全放在数据库中。挑战任务实现数据库库存的最终一致性检查任务一个定时任务扫描 orders 和 products修复不一致如冻结未解。引入Kafka替代 RabbitMQ 传输 Saga 命令对比性能。通过今天的综合实战你成功将分布式锁和分布式事务协同应用在超高并发场景下既能保证性能又能确保数据最终一致。这标志着你已经掌握了微服务中并发与一致性的平衡之道。下周我们将进入第一个大型综合项目——电商核心系统将这些能力全部整合并引入更多治理与监控实践。
返回列表