1.4 技术栈对齐:PHP 库与云产品地图
场景:库装了一堆,链路却串不起来
选型定了 PHP 之后,组里迅速拉了一长串依赖清单:Redis 客户端、队列驱动、日志 SDK、支付 SDK、监控系统,能找的轮子都找来了。装完一跑发现根本串不起来:登录态放在 PHP 的 session 里被负载均衡打散到不同机器,队列消费者抢库存时出现重复扣减,CLS 日志只打了报错没有请求上下文,排查时根本串不起来一条完整调用。问题出在我们对整个技术栈没有一张统一的地图——每个组件各管一段,链路就断了。
这一节先把地图立起来,后面二十篇正文都按这张地图走。
顺带说一下SDK选型的一个原则:能用官方 SDK 就不要用第三方封装。我们早期图省事用一个社区封装的支付库,结果微信支付某次升级接口后该库半年没更新,回调验签悄悄失效,掉单问题就是那时候埋下的。换成官方 SDK 之后,至少升级时机掌握在自己手里。云产品的 SDK 同理,腾讯云的 PHP SDK 更新及时,文档入口在 cloud.tencent.com/document/product/213 各产品页都能找到对应指引。依赖治理的另一个动作是每周跑一次依赖安全扫描,有 CVE 的包当周必须升级,这条流程是在掉单事故之后加上的。
方案:秒杀链路的五个角色与对应选型
整条秒杀链路按请求流向拆成五段,每一段我们都对比过两三个库或云产品,最终选型与弃选原因如下:
| 链路段 | 职责 | 最终选型 | 备选与弃选原因 |
|---|---|---|---|
| 静态加速 | 活动页、商品图片 | 腾讯云 CDN | 自建节点太贵,Nginx 直出首屏慢 |
| 限流防刷 | 洪峰削峰 | Nginx 限流加 PHP 令牌桶 | 只靠应用层挡不住,太靠接入层会误伤 |
| 库存扣减 | 原子减库存 | 云数据库 Redis 加 Lua 脚本 | MySQL 行锁吞吐不够 |
| 异步下单 | 排队与最终一致 | Laravel Queue 走 Redis 驱动 | 自研消息队列没人维护 |
| 日志归集 | 问题定位 | 日志服务 CLS | 文件查日志靠 grep 太慢 |
几个关键决策展开说一下。库存扣减为什么必须上 Redis 的 Lua 脚本:因为判断库存是否足够、扣减、记录已购用户这三步必须是原子的。如果拆成三条命令执行,两条命令之间会插进别的请求,超卖就发生在这种缝隙里。完整方案在 2.2 节给出。
日志为什么选 CLS 而不是自建 ELK:秒杀日志在洪峰期每秒数万条,自建一套能抗住这个量级的 ELK 机器和运维成本都不低;而日志服务 CLS 按量付费、自带检索与告警能力,接入只要一个 SDK 几行代码,产品文档入口是 cloud.tencent.com/document/product/614。这个选型的实操细节在 5.3 节展开。
队列驱动选 Redis 还有另一层考虑:秒杀下单排队与库存扣减共用一个 Redis 实例,订单消费失败时可以立即回补库存,一次网络往返都不用多走。我们对比过几种队列方案的延迟与可靠性,结论是用 Redis 做驱动在秒杀这种短洪峰场景最合适:
| 队列方案 | 消费延迟 | 可靠性 | 我们的判断 |
|---|---|---|---|
| Redis 驱动 | 极低 | 中,需处理丢消息 | 秒杀首选,补偿成本低 |
| 云消息队列 | 低 | 高,自带 ACK | 长周期订单可考虑 |
| MySQL 轮询 | 高 | 一般 | 只有历史包袱才用 |
实际跑下来,洪峰期下单请求从入队到消费完成压在 200 毫秒以内,用户几乎无感,而库存回补走的还是同一个 Redis 实例,好的一致性设计是选型对齐出来的。
配置:核心依赖与版本约束
技术栈定稿后,核心包就下面几个,版本约束必须锁定主版本,避免大促前依赖意外升级:
{
"require": {
"php": "^8.2",
"laravel/framework": "^10.0",
"predis/predis": "^2.2",
"tencentcloud/tencentcloud-sdk-php": "^3.0",
"tencentcloud/cls-sdk-php": "^1.0"
}
}
各组件的对接配置统一收在一个服务注册文件里,保证所有模块拿到的是同一个客户端实例,避免每处 new 一个连接把连接池打爆:
// app/Providers/SeckillServiceProvider.php
public function register(): void
{
$this->app->singleton('cls.client', function ($app) {
return new ClsClient(
endpoint: config('cls.endpoint'),
secretId: config('cls.secret_id'),
secretKey: config('cls.secret_key')
);
});
$this->app->singleton('seckill.stock', function ($app) {
return new StockService(
redis: $app['redis']->connection('stock'),
ttl: config('seckill.stock_ttl')
);
});
}
云产品也做了对齐,四个核心产品各司其职,本文档入口列表建议收藏,后续各篇都会提到:
| 云产品 | 用途 | 文档入口 | 控制台位置 |
|---|---|---|---|
| 云服务器 CVM | 应用与接入层部署 | cloud.tencent.com/document/product/213 | CVM 实例 |
| 云数据库 MySQL | 商品、订单、库存持久化 | cloud.tencent.com/document/product/236 | MySQL 实例 |
| 云数据库 Redis | 库存扣减、热点缓存 | cloud.tencent.com/document/product/239 | Redis 实例 |
| CDN | 活动页静态化加速 | cloud.tencent.com/document/product/228 | CDN 域名管理 |
| 日志服务 CLS | 秒杀日志归集与检索 | cloud.tencent.com/document/product/614 | CLS 主题 |
踩坑:依赖管理的三个坑
坑一:predis 与 phpredis 混用。 Laravel 的 Redis 门面默认走 predis,又有同事在镜像里装了 phpredis 扩展,两个客户端连接池各算各的,Redis 连接数直接翻倍。最后统一只用 phpredis 扩展并在配置里显式指定 client => phpredis,predis 从依赖里移除。
坑二:SDK 版本与实例特性不匹配。 CLS 新实例默认走新的投递协议,旧版 SDK 不支持,日志投频频丢失。升级 SDK 之后又加了批量缓冲与重试策略才算稳住,5.3 节有完整接入代码。
坑三:本地能跑、生产缺扩展。 本地开发用包管理器装环境,CVM 镜像里缺 sockets 和 bcmath 两个扩展,上线当天秒杀接口直接 500。后来所有扩展清单写进镜像构建脚本,并加了一个启动自检接口 /health/deps,任何扩展缺失立刻报警,自检脚本的实现放在 5.2 节。
坑四:环境配置散落在各处。 一开始 Nginx 的限流参数、PHP 的超时参数、镜像的扩展清单各管各的,分散在三份文档里,每次发版都要人肉对一遍。最后一次因为限流参数文档更新了但镜像没重建,大促限流直接不生效。痛定思痛,我们把整套环境配置收敛成一个 Git 仓库,镜像构建脚本、Nginx 配置、php.ini、composer.json 全部版本化管理,发版时 CI 自动按同一版本号构建镜像并发布到所有 CVM。环境即代码之后,再没出现过本地与生产不一致的事故。
小结
技术栈对齐的本质是让每个环节的选型互相配合,而不是各自为政。记住一条主线:CDN 挡静态、限流挡异常、排队削洪峰、Redis 保库存、CLS 记全程。这条主线就是全书的目录结构,后面各篇按这个顺序依次展开。技术栈对齐还有一条隐性收益:新人入职时照着这张地图走一遍,一周就能摸清系统全貌,比任何口头培训都高效。有位新同学的评价很到位:看完技术栈地图,第一次觉得这个系统不庞大,只是被清晰地分了层。
