云计算百科
云计算领域专业知识百科平台

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

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


今天我们进入第八周周四,主题是 分布式锁与并发控制。昨天我们的 Saga 协调器已经能够保证分布式事务的最终一致性,但在高并发场景下(如秒杀),单纯的数据库行锁可能成为瓶颈,并且微服务架构中服务间竞争资源需要更通用的锁机制。今天我们将引入 Redis 分布式锁,通过 hyperf/redis-lock 组件实现对共享资源的安全访问,防止超卖等并发问题,并实现一个简单的秒杀接口,亲身体验锁的威力。


在这里插入图片描述

今日目标

  • 理解分布式锁的核心要求:互斥、防死锁、高可用、可重入。
  • 掌握 Redis 实现分布式锁的原理(SETNX、锁续期、RedLock 算法)。
  • 在 Hyperf 中安装并配置 hyperf/redis-lock,使用注解或 API 方式加锁。
  • 改造库存扣减逻辑,在冻结库存时添加分布式锁,防止并发超卖。
  • 使用 JMeter 或 ab 进行高并发秒杀压测,对比加锁前后的库存一致性。
  • 简单介绍 Redis 红锁(RedLock)配置,提升生产环境的锁可靠性。

  • 一、环境准备(约 20 分钟)

    继续在 hyperf-app 容器中操作,确保 Redis 容器正常运行。

    docker-compose exec swoole bash
    cd /var/www/hyperf-app

    安装锁组件

    composer require 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', 'EX' => 30]);

    阶段三:锁续期(看门狗)
    若任务执行时间超过锁的 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:

    <?php
    namespace App\\Controller;

    use Hyperf\\DbConnection\\Db;
    use Hyperf\\HttpServer\\Annotation\\Controller;
    use Hyperf\\HttpServer\\Annotation\\RequestMapping;
    use Hyperf\\Redis\\Lock\\RedisLock;
    use Hyperf\\Di\\Annotation\\Inject;
    use Hyperf\\Redis\\Redis;

    #[Controller(prefix: '/seckill')]
    class SeckillController extends AbstractController
    {
    #[Inject]
    private Redis $redis;

    #[RequestMapping(path: 'buy', methods: 'post')]
    public function buy()
    {
    $productId = $this->request->input('product_id', 1);
    // 无锁版本(用于对比)
    return $this->doBuyWithoutLock($productId);
    }

    private function doBuyWithoutLock(int $productId)
    {
    // 查询当前库存
    $stock = Db::table('products')->where('id', $productId)->value('total_stock');
    if ($stock <= 0) {
    return ['code' => 0, 'message' => '库存不足'];
    }
    // 模拟并发间隙
    \\Swoole\\Coroutine::sleep(0.001);
    // 扣减
    Db::table('products')->where('id', $productId)->decrement('total_stock');
    return ['code' => 200, 'message' => '购买成功'];
    }
    }

    测试:将商品库存设为 10,并发 100 请求,不加锁时最终库存可能为负数。

    步骤 2:使用 RedisLock 加锁

    修改 buy 方法,使用注解方式自动加锁:

    use Hyperf\\Redis\\Lock\\Annotation\\RedisLock;

    #[RequestMapping(path: 'buy', methods: 'post')]
    #[RedisLock(key: 'lock:seckill:#{product_id}', ttl: 5)]
    public function buy()
    {
    $productId = (int)$this->request->input('product_id', 1);
    return $this->doBuyWithLock($productId);
    }

    private function doBuyWithLock(int $productId)
    {
    $stock = Db::table('products')->where('id', $productId)->value('total_stock');
    if ($stock <= 0) {
    return ['code' => 0, 'message' => '库存不足'];
    }
    \\Swoole\\Coroutine::sleep(0.001);
    Db::table('products')->where('id', $productId)->decrement('total_stock');
    return ['code' => 200, 'message' => '购买成功'];
    }

    说明:

    • #[RedisLock] 注解支持 SpEL 表达式,#{product_id} 会从请求参数中获取,生成锁键 lock:seckill:1。
    • ttl 设置锁的有效期 5 秒,防止死锁。
    • 当方法执行完毕或异常时,锁自动释放(通过 AOP 切面)。

    如果未引入 hyperf/redis-lock 的自动配置,可能需要注册切面,但该组件已自动完成。

    步骤 3:测试锁效果

    初始化库存:

    UPDATE products SET total_stock = 100 WHERE id = 1;

    使用 ab 或 wrk 并发测试:

    # 无锁版本(注释掉 RedisLock 注解)
    wrk -t4 -c100 -d5s –latency -s post.lua http://localhost:9501/seckill/buy
    # post.lua 可构造 POST 请求,或用 ab -n 500 -c 50 -p data.txt …

    观察最终库存:SELECT total_stock FROM products WHERE id=1; 无锁版本会小于 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。

    可重入性:同一协程再次获取相同锁时自动支持,不会死锁。

    步骤 5:Redis 红锁(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/2+1,且总耗时小于锁有效期,则获取成功。


    四、成果测试与并发验证(约 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 流程即使在秒杀场景下也能安全运行,不会出现超卖等数据异常。明天我们将结合分布式会话和跨服务认证优化,让整个系统的安全认证体系更加成熟。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【基于 Swoole+Hyperf 的微服务实战】第八周·周四:分布式锁与并发控制
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!