直播高并发下采用redis stream分片存弹幕、lua脚本原子扣减打赏、双队列异步聚合推送,并通过nginx限流与熔断开关实现抗压兜底。

直播平台中弹幕刷屏和打赏瞬时爆发,用户每秒发送上千条弹幕、数百次打赏请求,传统同步写库方式会导致MySQL连接池耗尽、Redis缓存击穿、PHP-FPM进程阻塞,页面卡顿甚至服务不可用。
弹幕写入层:Redis Stream + 按频道分片
第一步:为每个直播间创建独立Stream,key格式为live:stream:{room_id},避免不同房间消息混杂与竞争。
第二步:用户发送弹幕时,调用$redis->xadd("live:stream:{$roomId}", '*', 'uid', $uid, 'content', $content, 'ts', time())写入。注意必须用*让Redis自动生成唯一ID,不能手动拼接——否则高并发下ID重复会丢消息。
第三步:前端轮询或WebSocket订阅时,用XREAD GROUP group_name consumer_name COUNT 50 STREAMS live:stream:{$roomId} >拉取未消费消息。【GROUP名必须全局唯一且带房间前缀,如group_room_1001】,否则多房间共用同一GROUP会导致消息错乱或漏读。
打赏原子扣减:Lua脚本防超发
方法一:库存型打赏(如限量道具)
将道具ID对应剩余数量存入Redis Hash:HSET gift:stock:101 total 1000 used 0。调用以下Lua脚本执行扣减:
local total = redis.call("hget", KEYS[1], "total")local used = redis.call("hget", KEYS[1], "used")if tonumber(used) <br><code> redis.call("hincrby", KEYS[1], "used", 1) return 1else return -1end
ThinkPHP中调用:$redis->eval($script, ['gift:stock:101'], []);。【KEYS必须传数组,不能传字符串,否则Lua里KEYS[1]取不到值】
方法二:余额型打赏(如用户金币扣除)
直接用DECRBY user:balance:{$uid} {$amount},但需前置校验:EXISTS user:balance:{$uid}且GET user:balance:{$uid} >= {$amount}。这两步必须合并进Lua,否则存在“查余额→扣减”间隙被并发插队。
弹幕与打赏聚合推送:双队列异步组装
步骤一:弹幕消息投递到queue:push:danmu,打赏消息投递到queue:push:reward,使用不同队列名隔离消费逻辑。
步骤二:启动两个独立Worker进程,分别监听两个队列。每个Worker启用批量消费模式:php think queue:listen --delay=0 --memory=128 --sleep=3 --tries=3。
步骤三:消费时,将同秒内收到的弹幕和打赏按room_id分组,组装成结构化JSON包:{"room_id":1001,"danmu":[{"uid":123,"text":"666"}],"rewards":[{"uid":456,"gift":"火箭","count":1}]}。
步骤四:调用Swoole HTTP Server或Nginx FastCGI Push Module,向对应房间的WebSocket连接广播该JSON包。【广播前必须校验room_id是否存在有效连接池,避免空发触发大量无效回调】
抗压兜底:Nginx层限流+熔断开关
在Nginx配置中启用漏桶限流:limit_req zone=chatburst burst=200 nodelay;,对/api/danmu和/api/reward路径单独设限。
添加熔断开关配置项:config('chat.circuit_breaker', false)。当Redis Stream写入失败率连续30秒超过5%,自动置为true,所有新弹幕/打赏请求立即返回{"code":503,"msg":"服务繁忙"},不进队列、不查库、不调Lua。
运维可通过命令行一键恢复:php think chat:enable,该命令会清除熔断标记并重置统计计数器。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











