news 2026/10/19 2:06:30

Redis分布式锁防超卖实战详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis分布式锁防超卖实战详解

一、为什么高并发下会出现超卖

做电商的同学应该都遇到过这样的场景:明明库存只有 10 件,结果用户下单却成功了 12 单。问题出在哪?说白了就是多个请求同时读到库存,然后各自扣减,最后把库存扣成了负数。

举个最简单的例子,两个用户同时发起购买请求,都读到库存是 1,然后都执行了减库存操作,结果库存变成了 -1。这就是典型的并发竞争问题。在单机环境下,我们可以用数据库的行锁或者乐观锁来解决,但一旦流量上来、服务做了多实例部署,单靠数据库锁就不太够用了。

这时候就需要引入分布式锁,让多个进程之间互相协调,保证同一时刻只有一个请求能执行扣减库存的操作。

二、分布式锁的基本思路

分布式锁的核心思想其实很简单:让所有请求去争抢同一个资源,谁抢到了谁才能继续往下走,没抢到的要么等待、要么直接返回失败。

在 PHP 生态里,最常用的方案就是借助 Redis 的原子操作来实现。Redis 单线程执行命令的特性,天然保证了操作的原子性,非常适合做分布式锁。

实现分布式锁通常需要满足几个条件:

  • 互斥性:同一时刻只能有一个客户端持有锁。
  • 安全性:锁必须设置过期时间,防止持有锁的进程崩溃后锁永远不释放。
  • 可重入性:同一个客户端在持有锁期间可以再次获取锁(视业务需要)。

三、基于 Redis 的分布式锁实现

下面直接看代码。这里我用 PHP 配合 Predis 客户端来写一个简单的分布式锁工具类。

<?php class RedisLock { private $redis; private $lockKey; private $lockValue; private $expireTime; public function __construct($redis, $lockKey, $expireTime = 10) { $this->redis = $redis; $this->lockKey = $lockKey; $this->expireTime = $expireTime; $this->lockValue = uniqid('', true); } // 尝试获取锁 public function acquire() { // 使用 setnx 加过期时间,保证原子性 $result = $this->redis->set( $this->lockKey, $this->lockValue, 'NX', 'EX', $this->expireTime ); return $result === true; } // 释放锁 public function release() { // 用 Lua 脚本保证判断和删除的原子性 $script = <<<LUA if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end LUA; return $this->redis->eval($script, 1, $this->lockKey, $this->lockValue); } } ?>

这里有几个细节值得注意。获取锁的时候,我用的是 set 命令的 NX 和 EX 参数,一条命令同时完成「加锁」和「设置过期时间」,避免分成两步操作带来的原子性问题。

释放锁的时候,我用了 Lua 脚本。为什么要这么麻烦?因为要防止一个常见的坑:线程 A 持有锁,结果执行时间太长锁过期了,这时候线程 B 拿到了锁。如果 A 直接 del 锁,就会把 B 的锁给删掉。所以释放锁之前必须先判断 value 是不是自己的,这个判断和删除必须放在同一个 Lua 脚本里保证原子性。

四、在秒杀场景中防止超卖

有了锁工具类,接下来就是怎么在业务代码里用了。下面是一个简单的秒杀下单流程示例。

<?php function seckill($userId, $goodsId) { $redis = new Predis\Client(); $lock = new RedisLock($redis, 'lock:goods:' . $goodsId, 10); // 尝试获取锁,失败直接返回 if (!$lock->acquire()) { return ['code' => 400, 'msg' => '系统繁忙,请稍后再试']; } try { // 检查库存 $stock = $redis->get('stock:' . $goodsId); if ($stock <= 0) { return ['code' => 400, 'msg' => '已售罄']; } // 扣减库存 $redis->decr('stock:' . $goodsId); // 创建订单(省略数据库操作) // ... return ['code' => 200, 'msg' => '抢购成功']; } finally { // 释放锁 $lock->release(); } } ?>

这段代码的逻辑很清晰:先抢锁,抢到了就检查库存、扣库存、下单,最后在 finally 里释放锁。用 finally 是为了保证即使中间抛了异常,锁也能被正常释放,不会造成死锁。

五、实际使用中的几个坑

代码写完了,但真正上线前还有几个问题需要想清楚。

第一个坑:锁的过期时间怎么定。如果业务执行时间超过了锁的过期时间,锁会自动释放,这时候其他请求就能进来,还是会出现并发问题。所以过期时间一定要大于业务的最大执行时间,同时要留出一定的缓冲。

第二个坑:Redis 主从切换。如果 Redis 做了主从架构,客户端往主节点写了锁,但主节点还没来得及同步到从节点就挂了,这时候从节点升级为主节点,锁就丢了。对于要求极高的场景,可以考虑用 Redlock 算法,但实现复杂度会高不少。

第三个坑:锁粒度。锁的粒度越小,并发能力越强。比如上面例子中锁的 key 是 goodsId,也就是每个商品一把锁,不同商品的购买请求互不影响,这样并发能力就比全局一把锁高很多。

六、小结

用 Redis 实现分布式锁来防止超卖,是目前 PHP 项目里比较成熟的做法。核心就两点:一是用 set NX EX 保证加锁的原子性,二是用 Lua 脚本保证释放锁的安全性。代码本身不复杂,但真正用好还需要结合业务场景去考虑过期时间、锁粒度这些细节。

如果你的项目并发量不是特别大,这套方案完全够用。如果量级真的很大,那就要考虑消息队列削峰、库存预扣减等更复杂的方案了。希望这篇文章能帮到你,有问题欢迎在评论区交流。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:06:00

STM32F407+FreeRTOS开发环境搭建:Keil MDK与CubeMX实战详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 2:04:07

芯参谋(58):BLDC 电机驱动电路设计指南

BLDC 电机驱动电路设计指南 编制日期 2026-10-05 &#xff5c; 版本号 Rev 1.4 从三相全桥拓扑、上桥供电、死区与互锁、MOSFET 选型&#xff0c;到电流采样、保护体系、PCB 布局与换相软件实现的完整落地方案 先读这一条&#xff1a;本文给的是设计方法与判断准则&#xff…

作者头像 李华
网站建设 2026/10/19 2:03:52

ollama外部无法访问.ollama.service 文件无法写入.ollama外部无法访问.

一、修改配置文件&#xff0c;看看行不行 【修改配置】编辑ollama文件时&#xff0c;要在两行注释之间的设置才不会被抹除 在这里【总体过程】修改的过程 1. 编辑 ollama service 环境变量 sudo systemctl edit ollama进入 nano 编辑器&#xff0c;粘贴下面内容&#xff1a; [S…

作者头像 李华
网站建设 2026/10/19 2:03:45

租约还剩两年的底铺,到期会发生什么

「带租约」在商铺推介里几乎是褒义词&#xff1a;接手即可收租&#xff0c;不必自己招租。但租约是一份有期限的合同&#xff0c;而不是一项永久权利。买家真正要回答的问题不是「有没有租约」&#xff0c;而是「这份租约还能覆盖多久&#xff0c;到期那天我准备做什么」。一、…

作者头像 李华
网站建设 2026/10/19 2:03:16

BERT预训练模型(Encoder-Only架构)

目录 1.模型架构 2.BERT Tokenizer 3.预训练任务 3.1.MLM Task 3.2.NSP Task 3.3.动态学习率 4.各种类Bert模型 5.下游任务 5.1.文本分类 5.2.文本关系分类 5.3.序列标注 5.4.多任务学习 扩展阅读&#xff1a;https://kexue.fm/archives/8747 论文&#xff1a;http…

作者头像 李华
网站建设 2026/10/19 2:03:08

我与C语言不能说的秘密——初绷

初C,敬我们初相遇。 目录 1.你我的初相遇 2. main函数 3. 7. printf和库函数 5. 关键字介绍 6. 字符和ASCII编码 7. 字符串和\0 8. 转义字符 9. 语句和语句分类 10…注释是什么&#xff1f;为什么写注释&#xff1f; ()里的真是想到啥写啥&#xff0c;写的嘿嘿我都不好意思说&a…

作者头像 李华