如何避免WordPress抽奖插件函数在多实例下重复执行产生多名中奖者
根因分析
这是典型的读写竞态条件问题:多个请求进程在高负载下几乎同时执行getLastDiceDay查询,此时还没有任何进程完成当日中奖记录写入,所有进程都通过了校验,后续各自执行抽奖、写入逻辑,最终出现多中奖者的异常。共享主机的IO延迟会放大查询和写入的时间差,导致问题更容易触发,低负载的测试环境自然难以复现。
文件标识方案评估
该方案理论上可以解决问题,但存在较多隐患:
- 共享主机的文件系统权限不稳定,容易出现锁文件写入失败、异常崩溃后锁文件无法删除的问题,会导致后续抽奖流程完全卡住
- 需要额外开发锁过期、异常兜底逻辑,增加代码复杂度
- 如果站点后续扩展为多节点部署,文件锁跨节点不生效
更优解决方案
推荐优先采用数据库层面或WordPress原生机制实现,对现有代码改动小,适配性更强:
方案1:添加唯一索引(改动最小)
给lottery表的day_of_lottery字段添加唯一索引,同一天的中奖记录只能写入一次,后续进程执行写入时会触发数据库唯一键冲突,无法插入重复数据。
如果业务规则允许每日多名中奖者,可以改为
day_of_lottery + 抽奖批次的联合唯一索引。
方案2:乐观锁原子操作(性能最优)
将原有先查后写的非原子逻辑,改为带条件判断的原子写入,使用INSERT ... SELECT语法:
INSERT INTO {$wpdb->prefix}lottery (winner_mail, time, day_of_lottery, email_log_id) SELECT %s, %s, %d, %d FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM {$wpdb->prefix}lottery WHERE day_of_lottery = %d)
只有当天无中奖记录时才会写入成功,可通过$wpdb->rows_affected判断写入结果,返回0则直接终止流程。
方案3:WordPress原生Transient锁(适配性最好)
不想修改表结构可以用WP自带的Transient机制实现分布式锁,对现有代码侵入极小:
public function init() { $day = +date("j"); $day--; // 加锁,有效期5分钟,避免异常死锁 $lock_key = "lottery_run_lock_{$day}"; if (false !== get_transient($lock_key)) { return; } set_transient($lock_key, 1, 300); $lastDiceDay = Model::getLastDiceDay(); if ($lastDiceDay >= $day) { delete_transient($lock_key); return; } $dateDay = new \DateTime($this->_from->format('Y-m') . "-$day"); $mails = Model::getMailsByDay($dateDay); $winners = $this->dice($mails, $day); Model::saveWinner($winners, $day); $this->mail($winners, $day); // 执行完成释放锁 delete_transient($lock_key); }
内容的提问来源于stack exchange,提问作者Grummel
相关产品推荐
相关产品推荐

