游乐游手机版
首页/编程语言/文章详情

Webman整合Redis实现高并发秒杀核心逻辑

时间:2026-06-24 07:35
秒杀场景中先rPop再查MySQL库存会导致超卖和漏单,正确做法需将库存校验与出队绑定为原子操作,如使用Lua脚本或结构化快照。Webman需启用use_lua支持,多Worker下应通过带值校验的分布式锁避免重复消费。缓存Key设计需包含业务前缀、参数签名和版本号,防止线上事故。

秒杀场景里,很多人都踩过一个坑——先 rPop 再从 MySQL 查库存。表面上看 rPop 是原子操作,但整个链路“取ID → 查库存 → 扣库存 → 写订单”并不原子。MySQL 那边事务一旦失败(比如唯一索引冲突、主从延迟导致读到旧库存),商品 ID 已经出队了,订单却没生成,这个 ID 就永远消失了——库存漏单,不可逆。而并发更高时,两个请求同时 rPop 拿到同一个 ID,都查到 num = 1,然后都执行成功,超卖就成了必然。

Webman集成Redis实现高并发秒杀系统的核心逻辑

为什么不能先rPop再查MySQL库存

常见错误写法如下:

if ($redis->rPop('goods:queue:1001')) {
    $stock = $pdo->query("SELECT num FROM stock WHERE id = 1001")->fetchColumn();
    if ($stock > 0) {
        $pdo->exec("UPDATE stock SET num = num - 1 WHERE id = 1001");
        // … 写订单
    }
}

这段代码的问题在于:两个请求同时 rPop,各自拿到同一个 ID,然后都读到 num = 1,接着都执行扣减——超卖就这么发生了。更糟的是 MySQL 事务回滚时,ID 已经出队,再也找不回来。

所以正确的做法必须把库存校验压到 Redis 端,并且和出队动作绑定在一起。要么写 Lua 脚本,把 decrexistslRem 封装成一个原子操作,脚本返回值直接决定是否继续;要么队列里存结构化快照数据,比如 {"goods_id":1001,"stock_snapshot":5},消费时拿快照值做比对,而不是实时查 MySQL。这两种方式都能从根本上切断“出队 → 查库”的不安全链条。

Webman里调用Redis Lua脚本的硬性要求

Webman 默认不启用 Lua 支持,这点很多人不知道。如果直接调 eval,Redis 会退化成多次网络往返,原子性直接失效。不配 use_lua => true,脚本写得再严谨也没用。

先检查 config/redis.php 里是否加了这一行:

'use_lua' => true,

调用时要特别注意几点:

  • KEYS 数组传 Redis key(比如 ['seckill:stock:1001', 'user:participated:1001:123']),顺序一定不能错,脚本里 KEYS[1] 就是第一个 key。
  • ARGV 数组传参数(比如 [1, 'token_abc']),顺序错一个,脚本里 ARGV[1] 就取不对。
  • 别在脚本里写 redis.log(),Webman 的 Redis 扩展不支持。
  • 返回值尽量用整数,比如 1 表示成功、-1 表示库存不足,省掉 JSON 解析的开销。

这些细节虽然小,但线上跑起来任何一个没注意,脚本逻辑就可能完全走偏。

多Worker下如何避免重复消费同一商品

Webman 默认启动多个 Worker 进程,如果每个 Worker 都定时 rPop,必然出现多个进程抢同一个队列元素。这其实不是 Redis 的问题,而是调度逻辑本身有缺陷。

唯一可靠的解法是加分布式锁,而且锁必须带 value 校验。具体来说:

  • SET key value EX 10 NX 尝试获取锁,value 设置为当前 Worker 的 PID 或随机 UUID。
  • 解锁必须走 Lua 脚本:EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 queue:lock:1001 abc123
  • 千万别用 SETNX + DEL 两步操作,否则 A 进程可能删掉 B 进程的锁,导致锁失效。
  • 也别尝试 flock(),Webman 跑在 Swoole 协程下,文件锁根本不起作用。

这套方案看起来繁琐,但线上环境里,它就是兜底的保险。

缓存Key设计不当会直接引发线上事故

写死 Cache::set('goods_list', $data) 这种操作,看着省事,实际等于埋了个定时冲击波。分页参数一变、筛选条件一换,缓存永远不会更新;测试环境和生产环境共用 Redis,key 一撞,数据全乱套。

正确的缓存 Key 必须包含三要素:

  • 业务域前缀,比如 seckill:goods:,一眼就能看出是哪块业务。
  • 参数签名,用 md5(json_encode([$goods_id, $user_id])) 来做,千万别用 serialize(),避免 PHP 版本升级导致 hash 变化。
  • 版本号,比如 :v3,上线时直接改版本号就能让新缓存生效,比批量 DEL seckill:goods:* 可控得多。

额外提醒一句:像 $user_id 这类敏感字段,别明文拼进 key 里,防止缓存探测和越权访问。办法是用哈希或加密后再拼接。

这些设计看起来是细节,但线上事故往往就出在这些“看起来没问题”的地方。

来源:https://www.php.cn/faq/2675298.html
上一篇前端渲染下的Blade模板引擎路由高亮判断编译期优化 下一篇Java线程池性能测试与效率科学评估方法
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
CentOS与Golang打包常见兼容性问题探讨
编程语言 · 2026-07-01

CentOS与Golang打包常见兼容性问题探讨

CentOS与Golang打包的兼容性问题集中在glibc版本不匹配、交叉编译环境变量错误、依赖库缺失及Go依赖管理不规范。可通过Docker容器编译、选择兼容Go版本、正确设置GOOS GOARCH环境变量、安装对应开发包及使用GoModules解决。

CentOS中Fortran与Python如何协同工作从入门到实战完整教程
编程语言 · 2026-07-01

CentOS中Fortran与Python如何协同工作从入门到实战完整教程

在CentOS中,Fortran与Python可通过f2py、SWIG、共享库调用或subprocess协同。f2py封装Fortran为Python模块,支持数组运算;共享库需手动对齐数据类型;系统调用适合独立计算。

CentOS中Golang打包优化方法
编程语言 · 2026-07-01

CentOS中Golang打包优化方法

在CentOS中优化Golang编译打包,可显著提升编译速度并减小二进制文件体积。关键技巧包括:设置环境变量、使用Go模块管理依赖、编译时添加-ldflags= "-s-w "去除调试信息、利用UPX工具压缩、运行strip清理符号表,以及优化cgo内C代码的编译选项。综合运用这些方法能有效优化最终程序。

在CentOS系统中cpustat与其他工具协同使用的完整方法
编程语言 · 2026-07-01

在CentOS系统中cpustat与其他工具协同使用的完整方法

cpustat作为sysstat包的CPU监控工具,可通过管道与grep等命令配合过滤数据,利用脚本自动记录带时间戳的日志,或结合图形工具查看,也可格式化输出后接入Zabbix、Grafana等Web监控系统,实现可视化与告警。

CentOS中readdir与其他Linux发行版的差异
编程语言 · 2026-07-01

CentOS中readdir与其他Linux发行版的差异

CentOS基于RHEL,与Ubuntu、Debian、Fedora在包管理器(yum dnfvsapt)、默认文件系统(XFSvsext4)等存在差异,但readdir等系统调用遵循POSIX标准,行为一致。