ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【基于 Swoole+Hyperf 的微服务实战】第八周·周四:分布式锁与并发控制

【基于 Swoole+Hyperf 的微服务实战】第八周·周四:分布式锁与并发控制 【基于 SwooleHyperf 的微服务实战】第八周·周四分布式锁与并发控制今天我们进入第八周周四主题是分布式锁与并发控制。昨天我们的 Saga 协调器已经能够保证分布式事务的最终一致性但在高并发场景下如秒杀单纯的数据库行锁可能成为瓶颈并且微服务架构中服务间竞争资源需要更通用的锁机制。今天我们将引入Redis 分布式锁通过hyperf/redis-lock组件实现对共享资源的安全访问防止超卖等并发问题并实现一个简单的秒杀接口亲身体验锁的威力。今日目标理解分布式锁的核心要求互斥、防死锁、高可用、可重入。掌握 Redis 实现分布式锁的原理SETNX、锁续期、RedLock 算法。在 Hyperf 中安装并配置hyperf/redis-lock使用注解或 API 方式加锁。改造库存扣减逻辑在冻结库存时添加分布式锁防止并发超卖。使用 JMeter 或ab进行高并发秒杀压测对比加锁前后的库存一致性。简单介绍 Redis 红锁RedLock配置提升生产环境的锁可靠性。一、环境准备约 20 分钟继续在hyperf-app容器中操作确保 Redis 容器正常运行。docker-composeexecswoolebashcd/var/www/hyperf-app安装锁组件composerrequire hyperf/redis-lock该组件依赖hyperf/redis已安装。无需额外发布配置锁的配置通过 Redis 连接池即可。二、知识核心分布式锁与 Redis 实现约 1 小时1. 为什么需要分布式锁在单体应用中我们可以使用语言级别的锁如synchronized或数据库行锁来保证并发安全。但在微服务中多个实例可能同时操作同一个资源如扣减库存需要跨进程的锁。分布式锁应满足互斥性同一时刻只有一个客户端持有锁。防死锁即使持有锁的进程崩溃锁也能被自动释放通过 TTL。高可用锁服务本身不能单点故障如 Redis 集群或红锁。可重入性同一线程可多次获取同一把锁Hyperf 支持。2. Redis 分布式锁演进阶段一SETNX EXPIRE不推荐// 问题SETNX 和 EXPIRE 不是原子操作若 SETNX 后进程崩溃则锁永不过期阶段二SET key value NX EX timeout原子操作Redis 2.6.12 起支持SET key value NX EX seconds获取锁和设置过期时间原子执行。$ok$redis-set(lock:product:1,$uniqueValue,[NX,EX30]);阶段三锁续期看门狗若任务执行时间超过锁的 TTL锁会自动释放导致其他进程获取锁产生并发问题。需要后台协程定期续期。阶段四RedLock 算法在多个独立的 Redis 实例上依次获取锁大多数成功才算获取成功防止单点 Redis 故障导致锁失效。3. hyperf/redis-lock 组件基于以上原理hyperf/redis-lock提供了Hyperf\Redis\Lock\RedisLock基本的锁实现支持续期WatchDog。#[RedisLock]注解可应用于方法自动加锁释放。配置红锁通过RedisLockFactory或配置文件指定多个 Redis 连接。我们将在秒杀场景中使用注解方式简单高效。三、实战秒杀接口加锁防超卖约 2.5 小时步骤 1秒杀业务准备我们需要一个高并发场景来体现锁的作用。设计一个秒杀接口POST /seckill逻辑为扣减指定商品的库存直接扣减total_stock不使用冻结模式简化演示。不加锁时必然超卖加锁后库存正确。创建app/Controller/SeckillController.php?phpnamespaceApp\Controller;useHyperf\DbConnection\Db;useHyperf\HttpServer\Annotation\Controller;useHyperf\HttpServer\Annotation\RequestMapping;useHyperf\Redis\Lock\RedisLock;useHyperf\Di\Annotation\Inject;useHyperf\Redis\Redis;#[Controller(prefix:/seckill)]classSeckillControllerextendsAbstractController{#[Inject]privateRedis$redis;#[RequestMapping(path:buy,methods:post)]publicfunctionbuy(){$productId$this-request-input(product_id,1);// 无锁版本用于对比return$this-doBuyWithoutLock($productId);}privatefunctiondoBuyWithoutLock(int$productId){// 查询当前库存$stockDb::table(products)-where(id,$productId)-value(total_stock);if($stock0){return[code0,message库存不足];}// 模拟并发间隙\Swoole\Coroutine::sleep(0.001);// 扣减Db::table(products)-where(id,$productId)-decrement(total_stock);return[code200,message购买成功];}}测试将商品库存设为 10并发 100 请求不加锁时最终库存可能为负数。步骤 2使用 RedisLock 加锁修改buy方法使用注解方式自动加锁useHyperf\Redis\Lock\Annotation\RedisLock;#[RequestMapping(path:buy,methods:post)]#[RedisLock(key:lock:seckill:#{product_id},ttl:5)]publicfunctionbuy(){$productId(int)$this-request-input(product_id,1);return$this-doBuyWithLock($productId);}privatefunctiondoBuyWithLock(int$productId){$stockDb::table(products)-where(id,$productId)-value(total_stock);if($stock0){return[code0,message库存不足];}\Swoole\Coroutine::sleep(0.001);Db::table(products)-where(id,$productId)-decrement(total_stock);return[code200,message购买成功];}说明#[RedisLock]注解支持 SpEL 表达式#{product_id}会从请求参数中获取生成锁键lock:seckill:1。ttl设置锁的有效期 5 秒防止死锁。当方法执行完毕或异常时锁自动释放通过 AOP 切面。如果未引入hyperf/redis-lock的自动配置可能需要注册切面但该组件已自动完成。步骤 3测试锁效果初始化库存UPDATEproductsSETtotal_stock100WHEREid1;使用ab或wrk并发测试# 无锁版本注释掉 RedisLock 注解wrk-t4-c100-d5s--latency-spost.lua http://localhost:9501/seckill/buy# post.lua 可构造 POST 请求或用 ab -n 500 -c 50 -p data.txt ...观察最终库存SELECT total_stock FROM products WHERE id1;无锁版本会小于 0 或远小于预期。加锁版本库存精确归零。Redis 监控在压测期间使用redis-cli monitor或keys *lock*观察锁的创建和删除。步骤 4锁续期与可重入RedisLock支持watchDog参数当任务执行时间可能超过ttl时可以开启续期。但注解模式默认不开启需手动创建RedisLock实例。示例手动加锁演示$lock$this-redis-lock(lock:product:.$productId,5);if($lock-get()){try{// 业务}finally{$lock-release();}}lock()方法返回一个锁对象内部实现包含 WatchDog。可重入性同一协程再次获取相同锁时自动支持不会死锁。步骤 5Redis 红锁RedLock配置概念与实现在多个 Redis 实例上获取锁可以提高可靠性。hyperf/redis-lock支持红锁需要在config/autoload/redis.php中配置多个连接或使用 Redis Cluster然后通过RedisLockFactory创建。简单配置多个 Redis 实例开发环境可模拟不同端口在redis.php中添加额外连接然后使用$factory$this-container-get(\Hyperf\Redis\Lock\RedisLockFactory::class);$lock$factory-make(lock:redlock:product:1,5,[connection[default,redis2]]);由于我们开发环境只有一个 Redis今天重点是理解红锁思路向 N 个独立 Redis 实例依次获取锁若成功数 N/21且总耗时小于锁有效期则获取成功。四、成果测试与并发验证约 1 小时1. 无锁超卖验证设置库存 10。发送 100 并发请求。查询库存total_stock值可能为负。2. 加锁一致性验证重置库存 10。同样 100 并发请求。库存归零订单表中成功订单数等于 10。性能对比加锁会引入少量延迟Redis 网络往返但保证了数据正确性。记录加锁前后的 QPS 和 P99 延迟分析取舍。3. 锁超时模拟将锁 TTL 设为 2 秒在业务代码中sleep(5)观察锁是否在 2 秒后释放其他请求是否能获取锁造成并发。再开启 WatchDog验证锁是否被续期直到任务结束。4. 测试清单检验项方法通过标准无锁超卖并发请求无锁接口库存超卖最终 total_stock 0注解加锁防超卖并发请求加锁接口库存扣减至 0无超卖锁自动释放方法正常结束或异常Redis 中锁消失keys 不再包含锁键锁重入同一方法内再次获取同一锁不阻塞红锁配置使用多个 Redis 连接当部分 Redis 故障时锁仍可用需多实例死锁预防杀死持锁进程等待 TTL 后锁释放锁过期自动删除五、今日作业与学习产出提交代码将秒杀接口、锁注解用法、Redis 锁配置提交到 Git。完善秒杀场景将秒杀与 Saga 冻结库存逻辑结合下单时用锁保护冻结库存操作避免超卖。编写一个锁监控命令用于查看当前所有活跃锁。学习笔记画出 Redis 分布式锁的状态转换图获取、续期、释放、过期。对比 Redis 锁与 ZooKeeper 锁的实现复杂度及适用场景。挑战任务实现基于 Lua 脚本的原子库存扣减不依赖锁比较性能。研究Swoole\Lock或etcd实现的分布式锁与 Redis 锁进行压力测试对比。通过今天的学习你已经掌握了高并发下保证数据一致性的关键武器——分布式锁。现在你的 Saga 流程即使在秒杀场景下也能安全运行不会出现超卖等数据异常。明天我们将结合分布式会话和跨服务认证优化让整个系统的安全认证体系更加成熟。
返回列表