【基于 Swoole+Hyperf 的微服务实战】第八周·周六:高并发场景——“秒杀抢购 + 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 脚本:
localkey=KEYS[1]localstock=tonumber(redis.call('get',key)or'0')ifstock>0thenredis.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{$script=<<<LUAlocal key=KEYS[1]local stock=tonumber(redis.call('get',key)or'0')ifstock>0then 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['code'=>0,'message'=>'已抢光'];}// 2. 生成订单数据$orderData=['order_id'=>(string)Str::uuid(),'user_id'=>$userId,'product_id'=>$productId,'amount'=>99.00,];// 3. 启动 Saga(发送 order.create 命令)$sagaId=(string)Str::uuid();Db::table('saga_transactions')->insert(['saga_id'=>$sagaId,'status'=>'running','current_step'=>SagaConstants::STEP_ORDER_CREATE,'payload'=>json_encode($orderData),]);$this->producer->produce(newGenericProducer(['saga_id'=>$sagaId,'step'=>SagaConstants::STEP_ORDER_CREATE,'payload'=>$orderData,],SagaConstants::EXCHANGE_COMMANDS,SagaConstants::STEP_ORDER_CREATE));return['code'=>200,'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'];// 数据库冻结操作(前面已实现)$affected=Db::update('UPDATE products SET frozen_stock = frozen_stock + 1 WHERE id = ? AND total_stock - frozen_stock >= 1',[$productId]);if($affected===0){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(['status'=>'cancelled']);// 恢复 Redis 库存$this->redis->incr('seckill:stock:'.$productId);echo"[订单服务] 订单取消,Redis库存+1\n";}库存解冻依旧在InventoryCommandConsumer中。
步骤 5:压测与观察
重置 Redis 库存为 100。使用 wrk 进行高并发测试:
# 获取登录 Token(JWT)TOKEN=$(curl-s-XPOST http://localhost:9500/auth/login-d'username=admin&password=123456'|jq-r'.data.token')# 压测秒杀接口(注意需要携带 Token,若网关要求)wrk-t4-c100-d30s--latency-H"Authorization: Bearer$TOKEN"-spost.lua http://localhost:9500/seckill/buy# post.lua 构造 POST 请求,body 中包含 product_id=1压测期间观察:
- 日志中协调器驱动订单创建、库存冻结、支付。支付消费者随机失败(我们保留随机失败以触发补偿)。
- 最终 Redis 库存可能为 0(抢光)。
orders表中成功的订单数 + 取消的订单数 = 总发放订单数(但有些订单可能还在处理中,最终应为 100 左右)。products表中frozen_stock和total_stock应逻辑一致。- 补偿流程恢复的 Redis 库存会被后来的请求抢走,体现最终一致性。
步骤 6:模拟支付全面失败场景
把支付消费者设置为全部失败,测试大规模补偿。此时:
- Redis 库存先减后增(补偿恢复),但可能因为顺序问题,恢复的库存又立即被抢,导致最终成功订单少于库存?其实这是正确的:补偿恢复库存后,后续请求可以再次抢购,所以最终能卖出的商品数量由实际支付成功的订单决定,而非预扣。这就是 Saga 补偿的正常效果。
- 确认数据库中没有冻结库存残留。
四、成果测试与数据一致性验证(约 1.5 小时)
1. 数据核对
压测结束后,执行以下 SQL:
-- 已支付(或处理中?)订单数SELECTcount(*)FROMordersWHEREstatus='paid';-- 我们流程最终成功会标记为 paid,但需确认流程-- 取消订单数SELECTcount(*)FROMordersWHEREstatus='cancelled';-- 产品库存SELECT*FROMproductsWHEREid=1;-- 总订单数SELECTcount(*)FROMorders;通过计算:successful_orders + (frozen_stock) = total_stock或类似公式验证没有超卖。
2. 压测报告
- QPS:记录加锁秒杀的吞吐量。
- 延迟:P50、P99 延迟。
- 正确率:库存未出现负数,订单状态与库存吻合。
- 补偿成功率:所有失败 Saga 都触发了补偿,无遗留中间态。
3. 测试清单
| 检验项 | 方法 | 通过标准 |
|---|---|---|
| 分布式锁防超卖 | 高并发压测,Redis 库存减到 0 后不再产生新订单 | 最终订单数 ≈ 初始库存,无超卖 |
| Saga 正向流程 | 支付成功订单,订单状态变为 paid,库存冻结转实际扣减? | 数据一致 |
| 支付失败补偿 | 支付失败订单,状态变为 cancelled,Redis 库存恢复 | 库存数字正确,冻结库存清零 |
| 补偿幂等 | 模拟重复补偿命令 | 操作日志唯一,Redis 库存只恢复一次 |
| 锁性能 | 压测期间 Redis CPU 和网络 | 锁获取成功率 > 95% |
| 系统恢复 | 停止并重启协调器,未完成 Saga 继续执行 | 最终一致 |
五、今日作业与学习产出
- 提交代码:包含秒杀接口、Lua 脚本、改造后的补偿逻辑、压测脚本等。
- 完善监控:
- 在 Grafana 中展示秒杀接口 QPS、库存变化曲线、Saga 成功率。
- 添加告警:库存低于 10 时通知。
- 学习笔记:
- 绘制秒杀 + Saga 全链路时序图,标出锁、缓存、消息、数据库的交互。
- 总结“缓存库存 + 异步事务”模式的优缺点,以及何时需要将库存完全放在数据库中。
- 挑战任务:
- 实现数据库库存的最终一致性检查任务:一个定时任务扫描 orders 和 products,修复不一致(如冻结未解)。
- 引入Kafka替代 RabbitMQ 传输 Saga 命令,对比性能。
通过今天的综合实战,你成功将分布式锁和分布式事务协同应用在超高并发场景下,既能保证性能,又能确保数据最终一致。这标志着你已经掌握了微服务中并发与一致性的平衡之道。下周我们将进入第一个大型综合项目——电商核心系统,将这些能力全部整合,并引入更多治理与监控实践。