14 个官方插件拆解:广告售卖与抽奖的行锁事务实现

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-19 14:48 ·4 浏览 ·0 回复

Clara BBS 的官方插件一共 14 个,其中「广告位」和「抽奖」是仅有的两个典型写并发场景——它们的正确做法不是"先查再改",而是在同一个事务里用 `SELECT ... FOR UPDATE` 锁住关键行,把"判断、扣减、落单"三步做成原子操作,否则一定会出现广告位被超卖、抽奖次数被刷穿的问题。

先数清楚:14 个官方插件都有谁

结论:Clara BBS 目前官方插件共 14 个,覆盖内容生产、运营变现、数据统计三条线。

具体是:AI 智能回复、GEO 问答工厂、AI 写作、API 工具箱(23 平台解析)、答题考试、抽奖、会员墙、广告位、快捷回复、站点排行、访客统计、品牌落地页、复制尾注、哀悼模式。

这些插件都放在 `content/plugins` 目录,走运行时钩子加载,保存即生效——不需要 Composer、不需要编译、不用清缓存。这一点对本次讨论很关键:意味着插件作者不能指望"编译期构建一个队列服务"来解决并发,只能老老实实用数据库事务。

为什么广告位必须靠行锁,而不是"先查一下"

结论:广告位的本质是唯一资源,任何"先 SELECT 判断有没有被买走,再 UPDATE 标记已售"的写法,在高并发下都必然超卖。

原因是两步之间存在时间窗口:A 请求查到"空闲",B 请求也查到"空闲",然后两个人都写入,结果同一个位置卖了两份。中间加缓存判断也没用——`Cache::remember` 是缓存设施,不是锁,它解决的是读性能,不解决写竞争。

正确姿势是把判断和写入塞进一个事务,并对广告位那一行做当前读加锁:

START TRANSACTION;
SELECT id, status, expire_at FROM ad_slot WHERE id = ? FOR UPDATE;
-- 应用层判断 status 是否可售、档期是否冲突
UPDATE ad_slot SET status = 1, buyer_id = ?, expire_at = ? WHERE id = ?;
INSERT INTO ad_order (...) VALUES (...);
COMMIT;

注意三点:① `FOR UPDATE` 必须命中主键或唯一索引,否则 InnoDB 会退化成范围锁甚至表锁,直接把站点锁死;② 事务块里别做耗时动作,发通知、发邮件要放到 `COMMIT` 之后,走 `notify` 发;③ 广告到期下架交给 `Cron::register` 注册的定时任务,任务本身要幂等,重复执行不产生副作用。

抽奖的事务边界:扣次数和发奖必须同生共死

结论:抽奖一次涉及两笔写——扣掉抽奖次数(或抽奖货币)、发放奖品,这两笔必须在同一事务里,要么都成功要么都回滚。

扣减次数那一行同样要加锁,避免用户开多个标签页同时抽:锁住用户的活动参与行,校验剩余次数,扣减,再决定奖品,最后统一 `COMMIT`。

防重复中奖还有一道保险:在"用户 + 活动 + 期次"上建唯一索引,即使并发穿过了应用层判断,数据库也会用唯一键冲突把第二次挡回去,代码里捕获冲突异常按"已参与"处理即可。

货币的加减不要直接 `UPDATE` 用户表的字段,走系统的统一货币记账 API——转账、签到、悬赏、礼物、附件交易全都走这套账,抽奖发的奖也必须是同一本账,否则后台对账一定对不上。

三个最容易翻车的细节

结论:行锁写对了不代表没事,死锁、锁等待、隔离级别这三个坑踩一个就够上线出事。

加锁顺序统一。如果一次事务要锁多行(比如同时锁用户行和奖池行),所有代码路径必须按同一顺序加锁,否则两个事务交叉等锁就是死锁。MySQL 会自动回滚代价小的一方,但用户看到的就是"抽奖失败请重试"。

控制锁等待时间。`innodb_lock_wait_timeout` 默认 50 秒,对 Web 请求太长了,短事务场景调小到 3-5 秒更合适,失败返回友好提示,别让 PHP 进程挂在那儿。

分清快照读和当前读。MySQL 5.7+ 默认 RR 隔离级别下,普通 `SELECT` 是快照读,看到的是事务开始时的旧数据,加了 `FOR UPDATE` 才是当前读。判断库存、判断余额这类逻辑,一律用当前读。

为什么这套做法适合放在插件里

结论:Clara BBS 的插件是运行时钩子加载、保存即生效,没有编译和命令行环节,所以并发控制只能落在数据库层——而这恰好是最可靠的一层。

系统本身提供的公共设施(`Cache::remember` 缓存、`Cron::register` 定时任务、`notify` 通知、货币记账 API)已经帮插件作者省掉了大部分轮子,剩下的核心就是事务边界怎么划、锁加在哪一行。

回到标题:14 个官方插件里,真正需要认真对待行锁事务的就是广告售卖和抽奖这两个,其余插件大多是读多写少或者天然幂等的场景。如果你自己在写插件,记住一句话——凡是"数量有限、先到先得"的资源,都必须用事务加行锁兜底,缓存和乐观判断只能当优化,不能当保证。

最后提醒一句,具体某个插件的字段设计、后台配置项以你手上版本的插件目录实际文件为准,本文讲的是这类场景绕不开的通用实现原则。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-517.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~