为了账号安全,请及时绑定邮箱和手机立即绑定

Redis 分布式锁 从 0 到 1 实战教程:一步一步搭建秒杀库存扣减场景

标签:
Redis


秒杀库存扣减场景很多人踩坑,我来手把手带你过一遍。别管多少种实现方案,能跑通、防超卖、可运维才是硬道理。


## 学完能做什么

学完这篇,你将掌握 Redis 分布式锁 的完整实现逻辑,能独立搭建一套支持并发安全、带唯一标识释放的库存扣减原型。前置条件:本地已安装 PHP 8.0+、Redis 6.0+ 并启动服务,已开启 `phpredis` 扩展。假设你熟悉基础 PHP 语法与 Redis 常用命令,环境确认完毕后直接进入第 1 步。


## Step by Step


### Step 1:基础依赖与连通性验证(预计耗时 3 分钟)

**前置条件**:Redis 服务端 6379 端口已开启,PHP CLI 可调用。

打开终端执行扩展检查:

```bash

php -m | grep redis

```

**预期输出**:终端直接返回 `redis`。若为空,需通过 `pecl install redis` 编译安装或在 `php.ini` 中取消 `extension=redis.so` 的注释。接着验证连通性,执行:

```bash

php -r "echo (new Redis())->connect('127.0.0.1', 6379) ? 'OK' : 'FAIL';"

```

返回 `OK` 即可继续。


### Step 2:封装原子加锁与安全释放(预计耗时 8 分钟)

**前置条件**:新建 `DistributedLock.php`,准备面向对象结构。

分布式锁的核心矛盾在于“网络抖动导致锁过期”与“误删他人锁”。直接 `DEL` 是危险操作,必须引入客户端唯一 Token 与 Lua 脚本。写入以下完整类定义:

```php

<?php

class DistributedLock

{

    private $redis;

    private $lockName;

    private $token;

    private $expireMs;


    public function __construct(Redis $redis, string $lockKey, int $expireMs = 3000)

    {

        $this->redis = $redis;

        $this->lockName = 'dist_lock:' . $lockKey;

        $this->token = bin2hex(random_bytes(16)); // 当前请求唯一标识

        $this->expireMs = $expireMs;

    }


    // 尝试抢占锁,成功返回 true,已被占用或超时返回 false

    public function acquire(): bool

    {

        $options = ['nx', 'px' => $this->expireMs];

        return (bool)$this->redis->set($this->lockName, $this->token, $options);

    }


    // 仅当值与当前 token 一致时才删除,Lua 保证原子性

    public function release(): bool

    {

        $lua = <<<'LUA'

if redis.call('GET', KEYS[1]) == ARGV[1] then

    return redis.call('DEL', KEYS[1])

end

return 0

LUA;

        $result = $this->redis->eval($lua, [$this->lockName, $this->token], 1);

        return (int)$result === 1;

    }


    // 暴露 Token 便于调试或监控打点

    public function getToken(): string

    {

        return $this->token;

    }

}

?>

```


### Step 3:串联库存扣减业务流(预计耗时 5 分钟)

**前置条件**:Redis 中预设库存数据,终端执行 `redis-cli SET item:stock:sku_886 10`

新建 `run_demo.php` 调用锁类,模拟高并发下的单次请求处理:

```php

<?php

require_once 'DistributedLock.php';


$redis = new Redis();

$redis->connect('127.0.0.1', 6379);


$sku = 'sku_886';

$lock = new DistributedLock($redis, $sku, 5000);


// 抢占锁

if ($lock->acquire()) {

    try {

        $currentStock = (int)$redis->get("item:stock:{$sku}");

        if ($currentStock > 0) {

            $redis->decr("item:stock:{$sku}");

            echo "扣减成功 | 剩余库存: " . ($currentStock - 1) . PHP_EOL;

        } else {

            echo "已售罄,拦截请求" . PHP_EOL;

        }

    } finally {

        // 无论业务成功或异常,必须尝试释放

        $lock->release();

    }

} else {

    echo "并发过高,请稍后重试" . PHP_EOL;

}

?>

```

执行命令:`php run_demo.php`

**预期输出**:首次运行显示 `扣减成功 | 剩余库存: 9`。使用 `ab -n 100 -c 10 http://localhost/run_demo.php` 压测或开多个终端并行执行 `php run_demo.php`,库存严格递减至 0 后停止,终端不会出现负数或重复扣减提示。


## 代码详解

很多实现方案把加锁和释放拆开,但在真实网络里,业务逻辑执行时间若超过锁的 TTL,锁会自动失效,后续请求就能拿到新锁。此时前一个业务还没跑完,后一个已经开始操作共享变量,数据一致性直接崩溃。


上述代码用 `nx` 选项确保只有键不存在时才能写入,配合 `px` 毫秒级过期策略,防止服务端异常挂起导致死锁。`bin2hex(random_bytes(16))` 生成的 Token 绑定当前请求周期,释放阶段不走原生的 `del`,而是把 `GET` 比对与 `DEL` 操作打包进 Lua 脚本。Redis 执行 Lua 期间是单线程阻塞模型,其他客户端的请求会被排队,彻底切断并发释放时的竞态条件。


在我们自研的 taocarts 订单流转项目中,早期库存同步全靠数据库行锁硬扛,峰值期慢查询堆积,后来把热点商品库存切换至这套 Redis 分布式锁 模式后,接口 P99 延迟压到 20ms 内,线上再未触发超卖告警。


`finally` 块是兜底设计。PHP 在遇到未捕获异常或显式 `return` 时,`finally` 依然会执行。这保证了即使扣减逻辑中途抛出 PDOException 或网络超时,锁资源也能被安全回收,不会残留脏数据阻塞后续请求。


## 小作业 + 延伸

修改 `DistributedLock.php``$expireMs``100`,再次连续运行三次 `run_demo.php`,观察输出是否出现库存跳变或报错。这个实验能直观暴露“业务耗时 > 锁存活时间”的经典缺陷。


下一步可研究 Redisson 框架的 WatchDog 看门狗续期机制。建议对比单节点锁与 Redlock 多节点算法在分区容错下的差异,结合实际业务对一致性与可用性的容忍度做技术选型。实际生产环境务必配合 Prometheus 指标采集锁等待队列长度与持有时间,数据达标后再决定是否引入异步队列削峰。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
全栈工程师
手记
粉丝
0
获赞与收藏
0

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消