Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 339

Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 392

Deprecated: parse_url(): Passing null to parameter #1 ($url) of type string is deprecated in /www/wwwroot/blog_qqvbc_com/usr/plugins/Access/Access_Core.php on line 394
Joyber 的博客

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>摄像头拍照保存</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:system-ui;}
body{padding:20px;max-width:800px;margin:0 auto;}
#video{width:100%;border:1px solid #ccc;background:#000;transform:scaleX(-1);/*前置预览镜像*/}
#canvas{display:none;}
#preview{margin:15px 0;max-width:100%;border:1px solid #ccc;}
.btns{margin:15px 0;display:flex;gap:10px;flex-wrap:wrap;}
button{padding:10px 18px;font-size:16px;cursor:pointer;}
</style>
</head>
<body>
<h3>摄像头拍照</h3>
<video id="video" autoplay playsinline></video>
<canvas id="canvas"></canvas>
<div class="btns">
<button id="startBtn">开启摄像头</button>
<button id="stopBtn">关闭摄像头</button>
<button id="takeBtn">拍照</button>
<button id="saveBtn">保存图片</button>
</div>
<img id="preview" alt="预览图">

<script>
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const preview = document.getElementById('preview');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const takeBtn = document.getElementById('takeBtn');
const saveBtn = document.getElementById('saveBtn');

let mediaStream = null;
let imgUrl = '';

// 开启摄像头
startBtn.onclick = async () => {
  try{
    mediaStream = await navigator.mediaDevices.getUserMedia({
      video:{facingMode:'user'}
    });
    video.srcObject = mediaStream;
  }catch(err){
    alert('无法打开摄像头,请授予摄像头权限:' + err.message);
    console.error(err);
  }
};

// 关闭摄像头
stopBtn.onclick = ()=>{
  if(mediaStream){
    mediaStream.getTracks().forEach(track=>track.stop());
    mediaStream = null;
    video.srcObject = null;
  }
}

// 拍照,前置画面镜像翻转
takeBtn.onclick = () => {
  if(!mediaStream){
    alert("请先开启摄像头");
    return;
  }
  if(!video.videoWidth){
    alert("视频还未就绪,请稍等");
    return;
  }
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  // 水平翻转画布,拍出来和预览一样镜像
  ctx.save();
  ctx.scale(-1, 1);
  ctx.drawImage(video, -canvas.width, 0, canvas.width, canvas.height);
  ctx.restore();
  imgUrl = canvas.toDataURL('image/png');
  preview.src = imgUrl;
};

// 保存下载图片
saveBtn.onclick = () => {
  if(!imgUrl){
    alert('请先拍照');
    return;
  }
  const a = document.createElement('a');
  a.href = imgUrl;
  a.download = `photo_${Date.now()}.png`;
  a.click();
};
</script>
</body>
</html>

摄像头视频流怎么推送到后端,做直播视频流

网页摄像头推流到后端做直播:完整技术方案

浏览器端采集摄像头,向后端推送视频流,主流分两大类:

  1. WebRTC(推荐低延迟直播,实时互动,延迟<500ms)
  2. MediaRecorder + HTTP上传(简单HLS分片直播,延迟高,5~20秒,上手简单)
前置基础:还是用 navigator.mediaDevices.getUserMedia 获取摄像头MediaStream,和前面拍照网页是同一个媒体源。

方案1:MediaRecorder + HTTP分片上传(简易直播,适合入门)

原理:浏览器拿到摄像头流,用MediaRecorder编码成小块视频片段(blob),通过fetch持续POST发给后端;后端接收片段,转封装为HLS,前端用video.js播放。

  • 优点:技术栈简单,不需要复杂的信令服务器;后端用Node/Python/PHP都能写接收接口。
  • 缺点:延迟高,不适合实时互动,适合监控、低要求直播。

前端核心代码片段

let mediaStream;
let mediaRecorder;
const chunkList = [];

// 获取摄像头
mediaStream = await navigator.mediaDevices.getUserMedia({
    video: { facingMode: 'user', width: {ideal:1280}, height:{ideal:720} },
    audio: true // 同时采集音频
});

// 实例化录制器,编码格式
mediaRecorder = new MediaRecorder(mediaStream, {
    mimeType: 'video/webm;codecs=vp9,opus'
});

// 每500ms生成一块视频分片
mediaRecorder.ondataavailable = async (e) => {
    if(e.data.size > 0){
        const formData = new FormData();
        formData.append('videoChunk', e.data);
        // POST上传分片到后端接口
        await fetch('/api/upload-chunk', {
            method: 'POST',
            body: formData
        })
    }
}
mediaRecorder.start(500); // 每500ms触发一次ondataavailable

后端职责:

  1. 接收webm分片
  2. 把分片追加到视频文件,或者交给ffmpeg转码,生成HLS m3u8 + ts切片
  3. 对外提供HLS地址给播放器观看
限制:浏览器编码格式有限,大多输出webm;如果要mp4,浏览器原生MediaRecorder不支持mp4实时流式输出。

方案2:WebRTC(低延迟实时直播,首选)

WebRTC可以直接把摄像头MediaStream实时推流到后端,延迟极低。
两种部署模式:

模式A:SFU 媒体转发服务器(最常用直播方案)

浏览器(推流端) ↔ SFU服务器 ↔ 多个观看浏览器

  • 浏览器采集摄像头流,通过SDP信令,把RTP音视频包发给SFU
  • SFU负责接收、转发视频流给所有观众,不做转码(性能好)
  • 常用开源SFU:Mediasoup、Janus、LiveKit、Pion

流程:

  1. 前端getUserMedia拿到mediaStream
  2. 创建RTCPeerConnection
  3. 将摄像头track添加到peer连接
  4. 通过websocket作为信令通道,交换SDP、ICE候选
  5. 建立P2P/服务器中转,RTP数据包持续推送到后端SFU
// WebRTC极简前端示例
const pc = new RTCPeerConnection({
  iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
});
// 将摄像头流加入peer连接
mediaStream.getTracks().forEach(track=>pc.addTrack(track, mediaStream));
// 创建offer,通过websocket发给后端信令服务器
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 发送offer到后端websocket信令
ws.send(JSON.stringify({type:'offer', sdp:offer.sdp}));

模式B:P2P直连

浏览器直接和观看端P2P,后端只做信令转发,不转发媒体。
缺点:不适合多人直播,网络穿透失败率高,生产直播很少用

方案3:WebRTC 推流到RTMP服务器(传统直播流)

很多后端直播服务是RTMP(Nginx+rtmp-module,SRS),浏览器本身不原生支持RTMP
两个解决办法:

  1. 前端WebRTC推流 → 后端SRS/Mediasoup接收WebRTC,内部转RTMP,推到CDN。(推荐,SRS原生支持WebRTC转RTMP)
  2. 用wasm在浏览器内把MediaStream编码成H.264,封装RTMP,这个方案前端CPU占用很高,不推荐生产。
SRS 是国内最常用开源直播服务器:支持 WebRTC 推流、转RTMP、转HLS,部署简单。

关键概念区分

  1. MediaStream:摄像头原始未编码音视频帧(就是你拍照网页里的流),只能在浏览器内部使用,不能直接发给后端,必须编码打包。
  2. MediaRecorder:浏览器内置编码器,把MediaStream编码成webm片段。
  3. WebRTC RTP:实时编码,小包传输,低延迟。
  4. RTMP:传统直播协议,浏览器不原生支持,多用于CDN。

选型建议

  • 低延迟互动直播、远程监控、实时画面:WebRTC + SRS / LiveKit
  • 简单监控,允许10秒以上延迟,快速开发:MediaRecorder分片上传 + ffmpeg转HLS
  • 需要对接阿里云/腾讯云直播CDN:前端WebRTC推SRS,SRS转RTMP推CDN

常见坑

  1. 必须HTTPS/localhost,否则getUserMedia无法打开摄像头。
  2. WebRTC需要信令服务器(websocket),媒体流和信令是两条独立通道。
  3. 带宽:1080p30视频推流,至少需要4Mbps上行带宽。
  4. 编码:浏览器支持H.264 / VP8 / VP9,不同浏览器支持不一样。
  5. 高DPI屏幕不影响视频流本身,视频流分辨率由getUserMedia的constraints指定,不是屏幕分辨率。

拓展:和你之前拍照代码的关联

getUserMedia拿到的MediaStream,既可以给video预览、canvas截图,也可以直接喂给MediaRecorder或者RTCPeerConnection做推流,同一个媒体源复用。

Serena MCP 完整使用教程(适配你的 Cocos+Oops TS游戏项目,解决一次性读大量源码耗token问题)

Serena核心作用:符号级代码检索,AI不再一次性读取整个文件/整个仓库;需要哪个函数、哪个类,才单独拉取那一小段代码,大幅削减token消耗,刚好解决你之前Cursor索引Oops框架直接把额度耗尽的痛点。
原理:基于LSP语言服务器,识别TS的类、函数、接口,而不是把文件当纯文本全文读取。
项目地址:https://github.com/oraios/serena

一、前置依赖安装(Windows/Mac通用)

Serena推荐用uv包管理器(比pip简单很多)

  1. 安装 uv

    # Windows(PowerShell)
    powershell -ExecutionPolicy ByPass -c "irm [https://astral.sh/uv/install.ps1](https://astral.sh/uv/install.ps1) | iex"
    
    # Mac
    curl -LsSf [https://astral.sh/uv/install.sh](https://astral.sh/uv/install.sh) | sh

    安装完成重启终端,让环境变量生效。

  2. 安装 Serena-agent

    uv tool install -p 3.13 serena-agent@latest --prerelease=allow
  3. 初始化Serena(下载语言服务依赖,只需要跑一次)

    serena init

    执行完输入 serena --help,能正常输出帮助就代表安装成功。

二、接入到 Trae(推荐,你准备切换的免费AI编辑器)

Trae支持MCP,直接配置项目级MCP服务:

  1. 打开你的Cocos游戏项目(根目录)
  2. 在项目根新建文件夹 .trae,里面新建 mcp.json

    {
      "servers": [
     {
       "name": "serena",
       "transport": "stdio",
       "command": "serena",
       "args": [
         "start-mcp-server",
         "--context=ide",
         "--project",
         "${workspaceFolder}"
       ]
     }
      ]
    }
  3. 重启Trae,进入项目,打开AI聊天面板 → 设置 → MCP,看到serena状态绿色就是成功。

    ${workspaceFolder} 自动读取当前项目根目录,也就是你的Cocos项目根,Serena会自动索引。

接入Cursor的配置(如果你后续还要用Cursor)

在项目根新建 .cursor/mcp.json

{
  "mcpServers": {
    "serena": {
      "command": "serena",
      "args": [
        "start-mcp-server",
        "--context=ide",
        "--project-from-cwd"
      ]
    }
  }
}

保存,重启Cursor。

三、针对你的《弹力幻境》项目,Serena的配置优化(重点)

Serena会读取项目文件,你需要过滤不需要索引的目录,避免它扫描library、缓存、demo,和.traeignore配合:

  1. 在项目根新建 .serenaignore,内容:

    # Cocos编译缓存
    library/
    temp/
    build/
    dist/
    node_modules/
    
    # Oops示例代码,不需要让serena索引demo
    oops/demo/
    oops/example/
    
    # 只保留 code/业务代码 + oops核心源码
    *.log
    .DS_Store
    Serena会读取这个忽略清单,不会去索引缓存文件,只解析code/下面你的TS业务脚本 + oops框架核心代码,极大减少索引体积。

四、使用方式(对话指令,直接复制丢给AI)

激活成功之后,在AI聊天框直接发指令,Serena会自动调用符号检索工具

使用serena:查找code/下所有弹力相变相关函数,只读取函数签名和实现,不要读取整个文件。
使用serena:查找oops.message的on和off方法,查看定义。
使用serena:找到GameManager类,分析依赖,新增小球对象池逻辑。

✅ Serena能做到:

  • 查找函数、类、接口定义,按需读取片段,不加载完整文件,省大量token
  • 查找函数调用关系,找到哪里调用了某个方法
  • 重命名类/函数,跨文件批量修改符号(TS类型安全
  • 查看类型继承关系

❌ Serena不做:写完整大段业务代码、运行游戏、调试Cocos引擎物理。它是代码检索工具,用来减少AI读代码时的token开销。

五、使用工作流(适配你的游戏开发)

  1. 打开Trae,加载Cocos项目,serena自动后台索引(首次索引会慢几十秒,后续走缓存很快)
  2. 写代码时,直接让AI调用serena检索code/目录里的类/函数
  3. AI不再@整个项目,只通过serena符号检索按需拉取代码片段
  4. 配合 .traeignore + .serenaignore 双重过滤缓存目录,从根源解决一次性加载海量源码耗尽额度

六、坑点提醒(必看)

  1. Serena依赖TypeScript语言服务,本地需要安装Node+TS,否则TS解析会失败
  2. 第一次索引会扫描项目,一定要配置.serenaignore,否则还是会扫描library、node_modules,索引慢、耗资源
  3. Serena免费版使用LSP后端,足够TS项目使用;JetBrains后端是付费,你不需要
  4. 不能替代.traeignore,两者搭配:.traeignore控制Trae内置索引,.serenaignore控制Serena符号索引

备选简化方案:不想装Serena,用Repomix

如果你觉得Serena配置略复杂,Repomix是命令行一次性打包精简代码,适合一次性给AI看架构,不用常驻后台。

repomix --include "code/**/*,oops/**/*" --exclude "oops/demo/**/*,library/**/*,temp/**/*,build/**/*,node_modules/**/*" --compress

--compress AST精简,只保留函数签名,砍掉函数内部实现,token大幅压缩。

推荐你当前的组合方案

Trae + Serena MCP + .traeignore + .serenaignore

  • Trae免费SOLO Agent无硬限额
  • Serena按需读取代码片段,避免一次性读大量源码
  • 双重ignore过滤缓存、demo目录,杜绝爆token的问题
  • 适配 Cocos3.8.8 + Oops + TS微信小游戏

Yii2 joinWith left join 附加 on 额外 and 条件

joinWith 默认把 closure 里面的条件放到 WHERE,不是 ON。想要 LEFT JOIN xxx ON a=b AND c=d,必须用 onCondition,不能写 andFilterWhere / andWhere

关键点

  • joinWith('关联名', false):第二个参数 false不执行 eager loading 预加载,只做 JOIN SQL;true 会额外做一次 IN 查询。
  • onCondition 里面写的条件,会拼接在 ON 后面,生成 AND xxx=xxx
  • andWhere / andFilterWhere 会跑到 WHERE 子句,会把 left join 变成等价 inner join,这是高频踩坑。

模型关联定义(必须先定义 relation)

// Order 模型
public function getLog()
{
    /**
     * LEFT JOIN log ON log.order_id = order.id AND log.type = 1
     */
    return $this->hasMany(Log::class, ['order_id' => 'id'])
        ->onCondition(['log.type' => 1]); // 这里的条件直接拼 ON AND
}
如果条件需要动态变量,用数组格式防注入:
->onCondition([
    'log.type' => $typeVal,
    'log.status' => 2
]);

查询使用 joinWith

$query = Order::find()
    ->joinWith([
        'log' => function($q) {
            // ❌ 不要在这里写 andWhere,会进 WHERE
            // ✅ 如果需要再追加 ON 的 AND,继续 onCondition
            $q->onCondition(['log.is_del' => 0]);
        }
    ], false) // false:只JOIN,不贪婪加载
    ->all();

生成 SQL:

SELECT `order`.* FROM `order`
LEFT JOIN `log` ON `log`.`order_id` = `order`.`id` AND `log`.`type`=1 AND `log`.`is_del`=0

这里说一下 joinWith 第二个参数 $eagerLoading(bool)

方法签名:

public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')

$eagerLoading = false:只执行 SQL JOIN,不做贪婪(eager)预加载。

$eagerLoading = true(默认)

Order::find()->joinWith('log', true);

会执行2条SQL

  1. 主查询:SELECT order.* LEFT JOIN log ON ... 拿到主表+关联表连接后的数据集
  2. 额外再跑一条 IN 查询:SELECT * FROM log WHERE order_id IN (1,2,3...),把关联模型数据查出来,给模型的 $order->log 属性赋值。
目的:做贪婪加载,循环遍历 $orders 的时候,访问 $order->log 不会触发数据库查询。
代价:多一次数据库查询。即使你根本不需要读取 $order->log,这条SQL依旧会执行。

$eagerLoading = false

Order::find()->joinWith('log', false);

只执行1条SQL:带LEFT JOIN的主查询。

  • SQL里会做 LEFT JOIN,你可以在select里取log表字段、可以where过滤log表字段;
  • 不会额外执行IN查询,不会填充模型关联属性
  • 此时你访问 $order->log,会触发延迟加载(lazy load),单独再查一次数据库。

使用场景什么时候写 false

  1. 仅仅为了做表连接,用于过滤、排序、取关联表字段,业务代码不读取 $model->relation

    绝大多数报表、列表查询场景,都用 false,节省一次SQL。
    // 用于 where、orderBy、select取log.*,不需要读取$order->log
    Order::find()
     ->joinWith('log',false)
     ->select('order.*,log.content')
     ->andWhere(['log.type'=>1])
     ->orderBy('log.create_time desc');
  2. 你只是需要 LEFT JOIN xxx ON ... AND ...,不需要拿到关联模型对象。

什么时候用 true(默认)

需要循环遍历结果,并且要读取关联模型属性:

$list = Order::find()->joinWith('log',true)->all();
foreach($list as $item){
    echo $item->log[0]->content; // 不会N+1,全部已经预加载完毕
}

场景2:不想修改模型关联,查询时临时追加 ON AND

模型本身不带 onCondition,查询动态增加 ON 条件

$query = Order::find()
    ->joinWith([
        'log' => function ($q) {
            /** @var $q ActiveQuery */
            // 追加 ON AND 条件,不会污染模型定义
            $q->onCondition([
                'log.type' => 1,
                'log.is_del' => 0
            ]);
        }
    ], false)
    ->all();

场景3:onCondition 需要大于、不等于这类非等于条件

onCondition 支持 yii 的条件数组格式:

$q->onCondition([
    'and',
    ['log.type' => 1],
    ['>', 'log.create_time', '2026-01-01 00:00:00']
]);

输出:ON ... AND log.type=1 AND log.create_time > '2026‑01‑01 00:00:00'

场景4:原生 SQL 习惯,直接手写 join(备选)

如果逻辑复杂,也可以直接 leftJoin,完全手写on:

Order::find()
    ->leftJoin('log', 'log.order_id = `order`.id AND log.type = :t', [':t' => 1])
    ->all();

常见错误对比

❌ 错误写法(条件跑到 WHERE,left join 失效)

->joinWith(['log' => function($q){
    $q->andWhere(['log.type' =>1]); // 条件进 WHERE,不是 ON
}], false);

✅ 正确:onCondition,条件拼接在 ON ... AND xxx

小提示

  1. joinWith(relName, false):只做表连接,不查询关联数据;如果需要同时取出关联模型数据,第二个参数改为 true
  2. onCondition 是 ActiveQuery 的方法,只作用于 JOIN 的 ON,不会影响 where。
  3. 如果使用 andFilterWhere,同样会跑到 WHERE,绝对不要拿来写 ON 的 and 条件

Redis Lua原子操作 + 电商秒杀超卖超卖完整解决方案

说明:全文极简、重实战、无废话,包含 Redis Lua基础用法 + 原子特性原理 + 高并发秒杀防超卖完整落地方案,可直接用于生产环境。

一、Redis Lua 核心知识点(必懂)

1. 为什么Redis需要Lua脚本

作用:将多条Redis命令打包为单个原子指令执行

  • 普通多条命令:存在并发时差、线程安全问题
  • Lua脚本:执行不插针、不中断、原子串行
  • 解决:并发竞争、超卖、库存错乱、分布式锁竞争问题

2. Redis Lua 执行特性(硬核规则)

  • 单个Lua脚本:要么全部执行成功,要么全部失败
  • 脚本执行期间,其他客户端命令全部阻塞等待
  • 不支持事务回滚,但天然具备事务原子性
  • 性能极高:一次网络IO完成多条命令,减少网络开销

3. Redis Lua 标准语法模板

作用:固定传参、取值、返回格式,生产统一规范

-- KEYS[1]、KEYS[2] 传入key
-- ARGV[1]、ARGV[2] 传入参数值
return redis.call("命令名", KEYS[1], ARGV[1])

4. 常用Redis Lua命令

-- 取值
redis.call("GET", KEYS[1])
-- 设值
redis.call("SET", KEYS[1], ARGV[1])
-- 自增/自减
redis.call("INCR", KEYS[1])
redis.call("DECR", KEYS[1])
-- 判断存在
redis.call("EXISTS", KEYS[1])

5. Java/Python 调用Redis Lua通用格式

Python示例

import redis

r = redis.Redis(decode_responses=True)

# 加载Lua脚本
lua_script = """
return redis.call('GET', KEYS[1])
"""

# 执行脚本:keys列表、args列表
res = r.eval(lua_script, 1, "goods_stock")
print(res)

二、秒杀超卖问题根源

场景:库存10件,1000人同时抢购

普通代码bug流程

  1. 线程A查询库存 = 10
  2. 线程B查询库存 = 10(未扣减,并发同时读到充足库存)
  3. 线程A、B同时扣减 → 库存变为8,卖出12件,超卖

本质原因查库存、判库存、扣库存 非原子操作

三、Lua原子防超卖核心思路

一条Lua脚本完成三件原子操作(不可分割)

  1. 查询当前商品库存
  2. 判断库存是否大于0
  3. 满足则扣减库存,不满足直接返回失败

全程单线程执行,杜绝并发同时判定通过

四、完整可上线:秒杀防超卖Lua脚本

1. 脚本功能说明

  • 支持单商品限购1件(防止单人多抢)
  • 原子判定库存 + 原子扣减
  • 记录已抢购用户,防止重复下单
  • 返回状态码,前端/后端可直接判断结果

2. 最终生产级Lua脚本

-- KEYS[1]:商品库存key
-- KEYS[2]:用户抢购记录key
-- ARGV[1]:用户ID
-- ARGV[2]:本次抢购数量(固定1)

-- 1. 判断用户是否已抢购过(防重复下单)
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
    return 0  -- 0:重复抢购,失败
end

-- 2. 获取当前库存
local stock = tonumber(redis.call("get", KEYS[1]) or 0)

-- 3. 库存不足直接返回
if stock < 1 then
    return -1 -- -1:库存不足,秒杀结束
end

-- 4. 原子扣减库存
redis.call("decr", KEYS[1])

-- 5. 记录用户抢购记录
redis.call("sadd", KEYS[2], ARGV[1])

-- 6. 秒杀成功
return 1

3. 返回码统一规范

  • 1:秒杀成功
  • 0:用户重复抢购
  • -1:库存不足、超卖拦截

五、高并发秒杀完整业务流程(无漏洞)

1. 秒杀预热(提前初始化库存)

# 初始化商品库存100件
SET seckill:goods:1001:stock 100
# 用户抢购记录集合
SADD seckill:goods:1001:user

2. 完整执行链路(层层拦截,抗百万并发)

前端拦截:按钮置灰、防抖、倒计时

网关拦截:限流、防刷、IP限制

Redis Lua原子拦截:防超卖、防重复抢(核心)

异步落库:秒杀成功后,MQ异步写入数据库(扛高并发)

六、Python 完整可运行秒杀Demo

功能:模拟100并发抢10库存,绝对无超卖

import redis
import threading

# 连接Redis
r = redis.Redis(decode_responses=True)

# 秒杀Lua脚本
SECKILL_LUA = """
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
    return 0
end

local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
    return -1
end

redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
"""

# 初始化库存10
r.set("seckill:stock", 10)

def seckill(user_id):
    """单个用户抢购"""
    res = r.eval(SECKILL_LUA, 2, "seckill:stock", "seckill:user", user_id)
    if res == 1:
        print(f"用户{user_id} 秒杀成功")
    elif res == 0:
        print(f"用户{user_id} 重复抢购")
    else:
        print(f"用户{user_id} 秒杀失败,库存不足")

# 模拟100个并发线程
if __name__ == "__main__":
    thread_list = []
    for i in range(1,101):
        t = threading.Thread(target=seckill, args=(f"user{i}",))
        thread_list.append(t)
        t.start()

    for t in thread_list:
        t.join()

    # 最终库存校验
    final_stock = r.get("seckill:stock")
    print("最终剩余库存:", final_stock)
    print("成功抢购人数:", r.scard("seckill:user"))

七、PHP 完整可运行秒杀Demo(生产可用+高并发模拟)

功能:基于PHP Redis扩展 + Lua原子脚本,模拟高并发秒杀,彻底防超卖、防重复抢购,适配PHP框架(ThinkPHP/Laravel)原生使用

环境要求:安装PHP Redis扩展 pecl install redis

<?php
// 环境要求:安装PHP Redis扩展、开启pcntl多进程扩展(模拟高并发)
// 生产适配:兼容Laravel/ThinkPHP/Swoole所有PHP项目

// 1. 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 解决Redis超时、并发连接问题
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);

// 2. 生产级秒杀Lua脚本(原子防超卖、防重复抢购)
$luaScript = <<<LUA
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
    return 0
end

local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
    return -1
end

redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
LUA;

// 3. 初始化秒杀库存(仅首次执行,可后台预热执行)
$redis->del('seckill:stock');
$redis->del('seckill:user');
$redis->set('seckill:stock', 10);

/**
 * 秒杀核心原子方法
 * @param string $userId 唯一用户ID
 * @return int 1=成功 0=重复抢购 -1=库存不足
 */
function seckill(string $userId): int
{
    global $redis, $luaScript;
    // PHP eval固定参数:脚本内容、[KEYS+ARGV数组]、KEYS参数数量
    return $redis->eval($luaScript, [
        'seckill:stock',
        'seckill:user',
        $userId
    ], 2);
}

/**
 * 多进程模拟高并发秒杀(100用户并发抢10库存)
 */
function batchSeckill()
{
    $processNum = 100;
    for ($i = 1; $i <= $processNum; $i++) {
        $pid = pcntl_fork();
        if ($pid == 0) {
            // 子进程执行秒杀
            $userId = 'user_' . $i;
            $res = seckill($userId);
            switch ($res) {
                case 1:
                    echo "【成功】{$userId} 秒杀下单完成\n";
                    break;
                case 0:
                    echo "【拦截】{$userId} 禁止重复抢购\n";
                    break;
                case -1:
                    echo "【售罄】{$userId} 库存不足,秒杀失败\n";
                    break;
            }
            // 必须退出子进程,避免循环派生
            exit(0);
        }
    }

    // 等待所有子进程执行完毕
    while (pcntl_waitpid(0, $status) != -1);
}

// 执行高并发秒杀模拟
batchSeckill();

// 4. 最终数据校验(杜绝超卖核心校验)
global $redis;
$finalStock = $redis->get('seckill:stock');
$successNum = $redis->sCard('seckill:user');

echo "\n========== 秒杀最终统计结果 ==========\n";
echo "剩余库存数量:{$finalStock}\n";
echo "成功下单人数:{$successNum}\n";
echo "校验结果:" . ($finalStock + $successNum == 10 ? "✅ 无超卖、无漏卖,数据正常" : "❌ 数据异常") . "\n";

$redis->close();
?>

一、PHP版本核心特性说明

  • 原子性互通:Lua脚本与Python版本完全一致,底层原子锁库存逻辑通用,跨语言无差异
  • PHP独有规则:Redis eval方法参数顺序固定,必须严格区分KEYS数量,否则脚本执行报错
  • 框架无缝适配:可直接移植 Laravel/ThinkPHP/Swoole 项目,仅需替换框架Redis实例
  • 高并发适配性:基于多进程模拟真实秒杀流量,兼容PHP-FPM、Swoole协程环境,分布式集群完全生效
  • 数据强一致性:最终库存+成功人数恒等于初始库存,彻底杜绝超卖、少卖、漏卖问题

二、PHP生产部署注意事项

  • 线上关闭pcntl多进程代码,仅用业务接口接收请求,多进程仅用于本地压测验证
  • Lua脚本建议使用script load预加载缓存,通过sha1值调用,减少网络传输开销
  • 秒杀Redis Key设置过期时间,自动清理过期秒杀数据,避免内存堆积
  • 接口层增加限流、防刷、IP黑白名单,拦截恶意黄牛请求
  • 脚本预加载优化:生产环境禁止每次请求传递完整Lua脚本,采用script\_load预加载+sha1哈希调用,大幅减少网络IO、提升并发吞吐量

三、PHP Lua脚本缓存(SHA1)生产优化代码

优化作用:首次加载脚本生成唯一SHA1哈希值,后续所有秒杀请求直接通过哈希值调用脚本,无需重复传输大段脚本内容,高并发场景性能提升30%+

<?php
// 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);

// 秒杀Lua脚本
$luaScript = <<<LUA
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
    return 0
end
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
    return -1
end
redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
LUA;

// 核心:预加载脚本,获取SHA1哈希值(项目初始化执行一次即可)
$sha1Hash = $redis->script('load', $luaScript);
echo "脚本缓存SHA1值:" . $sha1Hash . "\n";

/**
 * 缓存版秒杀方法(生产正式使用)
 * 无需传递完整脚本,仅通过SHA1调用,性能更高
 */
function fastSeckill(string $userId, string $sha1Hash): int
{
    global $redis;
    // evalsha 哈希调用脚本
    return $redis->evalsha($sha1Hash, [
        'seckill:stock',
        'seckill:user',
        $userId
    ], 2);
}

// 初始化库存
$redis->del('seckill:stock', 'seckill:user');
$redis->set('seckill:stock', 10);

// 测试缓存版秒杀
$res = fastSeckill('user_001', $sha1Hash);
var_dump($res);

// 脚本状态校验(判断脚本是否已缓存)
$isExists = $redis->script('exists', $sha1Hash);
echo "脚本是否缓存成功:" . ($isExists[0] ? "是" : "否") . "\n";

$redis->close();
?>

四、脚本缓存生产高可用方案(自动降级容错)

核心解决问题:Redis重启、缓存脚本丢失、evalsha执行失败等线上突发异常,无感知自动降级,保障秒杀服务不宕机、不报错

<?php
// 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);

// 原始秒杀Lua脚本
$luaScript = <<<LUA
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
    return 0
end
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
    return -1
end
redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
LUA;

// 项目初始化预加载脚本,全局缓存SHA1值
$sha1Hash = $redis->script('load', $luaScript);

/**
 * 高可用秒杀方法(缓存优先 + 自动降级)
 * 优先evalsha高性能调用,失败自动降级eval原生调用
 * @param string $userId 用户唯一ID
 * @return int 1成功 0重复 -1库存不足
 */
function safeSeckill(string $userId): int
{
    global $redis, $sha1Hash, $luaScript;

    $keys = ['seckill:stock', 'seckill:user', $userId];
    $keyNum = 2;

    try {
        // 优先使用缓存脚本(高性能)
        return $redis->evalsha($sha1Hash, $keys, $keyNum);
    } catch (RedisException $e) {
        // 捕获脚本不存在异常,自动降级原生执行
        if (str_contains($e->getMessage(), 'NOSCRIPT')) {
            // 重新加载脚本缓存,修复异常
            $GLOBALS['sha1Hash'] = $redis->script('load', $luaScript);
            // 降级执行完整脚本
            return $redis->eval($luaScript, $keys, $keyNum);
        }
        // 其他异常直接抛出
        throw $e;
    }
}

// 初始化秒杀库存
$redis->del('seckill:stock', 'seckill:user');
$redis->set('seckill:stock', 10);

// 模拟多次秒杀调用,验证容错性
for ($i = 1; $i <= 5; $i++) {
    $res = safeSeckill('user_' . $i);
    echo "用户user_{$i} 结果:" . ($res === 1 ? "秒杀成功" : "失败") . "\n";
}

// 最终数据校验
$finalStock = $redis->get('seckill:stock');
$successNum = $redis->sCard('seckill:user');
echo "\n========== 高可用秒杀结果 ==========\n";
echo "剩余库存:{$finalStock}\n";
echo "成功人数:{$successNum}\n";

$redis->close();
?>

五、高可用方案核心特性

  • 智能优先策略:正常场景使用 evalsha 缓存调用,性能拉满
  • 无感自动降级:捕捉 NOSCRIPT 脚本丢失异常,自动重新加载脚本并降级原生执行
  • 自愈修复能力:异常后自动更新全局SHA1缓存,后续请求恢复高性能模式
  • 业务零影响:整个容错过程无报错、无中断,用户完全感知不到

六、脚本缓存生产使用规范

  • 加载时机:项目启动、框架初始化时执行一次 script load,常驻Redis缓存,无需重复加载
  • 缓存有效期:Redis不主动清理缓存脚本,重启Redis后依赖自动降级逻辑自愈
  • 统一复用:全局统一SHA1常量,所有秒杀接口共用缓存脚本,避免重复加载
  • 异常监控:线上可捕获降级日志,用于监控Redis脚本缓存异常频次,及时排查问题
  • 加载时机:项目启动、框架初始化时执行一次 script load,常驻Redis缓存,无需重复加载
  • 缓存有效期:Redis不主动清理缓存脚本,重启Redis后需重新加载
  • 降级方案:若evalsha报错(脚本丢失),自动降级为eval完整脚本加载,保障服务不中断
  • 统一复用:全局统一SHA1常量,所有秒杀接口共用缓存脚本,避免重复加载

八、彻底解决超卖超买的核心原理总结

1. 为什么普通代码会超卖

查库存、判库存、扣库存 三步非原子,并发间隙导致多人同时抢到资格

2. 为什么Lua能彻底杜绝超卖

Redis单线程执行Lua脚本,整个秒杀逻辑串行原子执行,无间隙、无并发穿插,从底层锁死库存竞争

九、生产环境额外优化(高阶)

  • 脚本缓存:使用 script load + sha1 缓存脚本,不用每次传完整脚本
  • 过期时间:秒杀key设置过期时间,自动释放内存
  • 限流熔断:接口层增加QPS限流,避免打垮Redis
  • 异步落库:Redis只做预扣减,MQ异步更新MySQL,提升吞吐量
  • 防黄牛:绑定用户、IP、设备号,限制单用户限购次数

十、常见误区避坑

  • 误区:Java加锁(synchronized/lock)防超卖

问题:分布式集群部署,单机锁完全失效

  • 误区:Redis多条普通命令判库存

问题:命令之间存在并发空隙,必然超卖

  • 唯一稳妥方案Redis Lua原子脚本(行业通用秒杀最终方案)

Python 进阶极简教程(高阶核心+纯用法)

python基础版

说明:承接Python基础教程,全文无冗余描述,仅保留进阶核心知识点 + 作用 + 可运行代码,覆盖Python后端、脚本、工程化全部高阶刚需内容。

一、变量与深浅拷贝(进阶核心)

1. 引用机制原理

作用:弄懂赋值、浅拷贝、深拷贝区别,解决数据篡改问题

# 赋值:只是绑定同一内存地址(完全共享数据)
a = [1, 2, [3, 4]]
b = a
b[0] = 99
print(a) # a同步被修改

2. 浅拷贝 copy

作用:拷贝一层数据,内层嵌套对象依旧引用

import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
b[0] = 99       # 外层独立,不影响原数据
b[2][0] = 999   # 内层嵌套共享,原数据被修改
print(a)

3. 深拷贝 deepcopy

作用:递归拷贝所有层级,完全独立数据,互不影响

import copy
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)
b[2][0] = 999
print(a) # 原数据完全不变

二、函数高阶特性

1. 闭包

作用:内层函数捕获外层局部变量,保存私有状态,数据常驻内存

def counter():
    num = 0
    def inner():
        nonlocal num # 声明使用外层变量
        num += 1
        return num
    return inner

# 多个实例状态独立
c1 = counter()
c2 = counter()
print(c1()) # 1
print(c1()) # 2
print(c2()) # 1

2. 装饰器(无参/有参)

作用:不修改原函数代码,动态扩展功能(日志、校验、计时)

# 无参装饰器
def wrapper(func):
    def inner(*args, **kwargs):
        print("执行前")
        res = func(*args, **kwargs)
        print("执行后")
        return res
    return inner

@wrapper
def test():
    print("核心逻辑")

test()

3. 递归优化与尾递归

作用:简化循环逻辑,处理层级嵌套数据

# 阶乘递归
def fact(n):
    if n == 1:
        return 1
    return n * fact(n-1)

print(fact(10))

4. 函数缓存 lru\_cache

作用:缓存函数返回值,解决递归重复计算,大幅提速

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n <= 2:
        return 1
    return fib(n-1) + fib(n-2)

print(fib(30))

三、高阶语法与推导式

1. 四大推导式

作用:极简代码快速生成容器数据,开发高频用法

# 列表推导式
lst = [x for x in range(10) if x % 2 == 0]

# 字典推导式
dic = {k:v for k,v in zip(["a","b"], [1,2])}

# 集合推导式
s = {x**2 for x in [1,2,2,3]}

# 生成器推导式(省内存)
gen = (x for x in range(10))
print(next(gen))

2. 高阶函数 map/filter/reduce

作用:批量处理、过滤、聚合数据

from functools import reduce
lst = [1,2,3,4]

# map 批量处理
res1 = list(map(lambda x:x*2, lst))

# filter 过滤数据
res2 = list(filter(lambda x:x>2, lst))

# reduce 累计聚合
res3 = reduce(lambda a,b:a+b, lst)

print(res1, res2, res3)

四、面向对象进阶

1. 类属性与实例属性

作用:区分全局类属性、私有实例属性,优化内存

class Demo:
    cls_val = 100 # 类属性(全局共享)
    def __init__(self, val):
        self.val = val # 实例属性(独立私有)

d1 = Demo(200)
d2 = Demo(300)
print(Demo.cls_val)
print(d1.val, d2.val)

2. 三大方法(实例/类/静态)

作用:适配不同场景的类调用逻辑

class Test:
    # 实例方法:必须实例调用,自带self
    def func1(self):
        pass

    # 类方法:类/实例均可调用,自带cls
    @classmethod
    def func2(cls):
        pass

    # 静态方法:无默认参数,纯工具方法
    @staticmethod
    def func3():
        pass

3. 继承、重写、多继承

作用:代码复用,实现多态特性

# 父类
class Father:
    def show(self):
        print("父类方法")

# 子类继承 + 方法重写
class Son(Father):
    def show(self):
        print("子类重写方法")

s = Son()
s.show()

4. 私有属性与属性装饰器

作用:数据封装,控制属性读写权限

class User:
    def __init__(self, age):
        self.__age = age # 私有属性(外部无法直接访问)

    # 属性装饰器:将方法转为属性调用
    @property
    def age(self):
        return self.__age

u = User(18)
print(u.age)

五、迭代器与生成器(性能优化)

1. 迭代器原理

作用:统一遍历规则,节省内存

lst = [1,2,3]
it = iter(lst) # 获取迭代器
print(next(it))
print(next(it))

2. 生成器 yield

作用:边执行边返回,不一次性加载所有数据,处理超大数据集

def gen():
    yield 1
    yield 2
    yield 3

g = gen()
print(next(g))
print(next(g))

# 遍历生成器
for i in gen():
    print(i)

六、异常处理进阶

1. 完整异常捕获结构

作用:精准捕获异常,程序不崩溃

try:
    num = 1 / 0
except ZeroDivisionError as e:
    print("除零错误:", e)
except Exception as e:
    print("通用错误:", e)
else:
    print("无异常执行")
finally:
    print("必定执行")

2. 主动抛异常

作用:参数校验、逻辑拦截主动终止程序

def check_age(age):
    if age < 0:
        raise ValueError("年龄不能为负数")

check_age(-1)

七、文件与上下文管理器

1. with 上下文管理器

作用:自动关闭文件、释放资源,无需手动close

# 读写文件自动释放
with open("test.txt", "w", encoding="utf-8") as f:
    f.write("进阶测试")

with open("test.txt", "r", encoding="utf-8") as f:
    print(f.read())

2. 自定义上下文管理器

作用:自定义资源开启、关闭逻辑

class MyCtx:
    def __enter__(self):
        print("开启资源")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("关闭资源")

with MyCtx():
    print("执行业务逻辑")

八、模块、包与导包进阶

1. \_\_init\_\_.py 作用

作用:标识文件夹为Python包,统一导出接口

# 在包目录 __init__.py 中定义
__all__ = ["func1", "Demo"] # 控制from xxx import * 导入内容

# 批量导出
from .test import func1, Demo

2. 动态导包

作用:程序运行中动态导入模块,灵活解耦

import importlib

# 动态导入指定模块
module = importlib.import_module("math")
print(module.pi)

九、多线程与多进程

1. 多线程(IO密集型)

作用:处理等待、文件、网络IO任务

import threading
import time

def task(name):
    print(f"线程{name}执行")
    time.sleep(1)

t = threading.Thread(target=task, args=("A",))
t.start()
t.join() # 等待线程结束

2. 多进程(CPU密集型)

作用:绕过GIL全局锁,多核并行计算

from multiprocessing import Process

def task():
    print("子进程执行")

if __name__ == "__main__":
    p = Process(target=task)
    p.start()
    p.join()

十、协程异步编程

作用:超高并发IO处理,性能远超多线程

import asyncio

# 异步函数
async def async_task(num):
    print(f"任务{num}开始")
    await asyncio.sleep(1)
    print(f"任务{num}结束")

# 并发执行
async def main():
    tasks = [async_task(1), async_task(2)]
    await asyncio.gather(*tasks)

asyncio.run(main())

十一、正则表达式进阶

作用:复杂文本匹配、提取、替换、校验

import re

s = "Python123Java456"

# 匹配所有数字
res1 = re.findall(r"\d+", s)
# 匹配所有字母
res2 = re.findall(r"\a+", s)
# 替换指定内容
res3 = re.sub(r"\d", "", s)

print(res1, res2, res3)

十二、GIL锁与性能优化

作用:理解Python单线程特性,针对性优化代码

  • GIL全局解释器锁:同一时刻一个进程只有一个线程执行Python代码
  • CPU密集:用多进程规避GIL
  • IO密集:用多线程/协程提升效率

十三、常用工程化技巧

1. 枚举类

作用:固定常量状态,避免魔法值

from enum import Enum

class Status(Enum):
    SUCCESS = 200
    FAIL = 500

print(Status.SUCCESS.value)

2. 时间处理

作用:时间戳、格式化、时间差计算

from datetime import datetime

# 当前时间
now = datetime.now()
# 格式化时间
time_str = now.strftime("%Y-%m-%d %H:%M:%S")
print(time_str)