You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP读取CSV发放随机码出现重复发放的原因及解决方法

重复发码的核心缺陷

当前代码出现重复发码完全是并发竞态+无原子操作保障导致的,具体问题点:

  • 整个「读全量CSV→取第一个码→移除首行→全量重写CSV」的流程没有任何互斥机制,不是原子操作。高并发场景下,多个请求会在同一时间窗口内读到完全相同的CSV内容,拿到同一个首行随机码,之后各自执行移除首行、重写文件的操作,最终多个请求会向不同用户发放同一个码。举个实际场景:请求A、B同时启动流程,都读到码abc123在CSV第一行,A取走abc123后把移除了首行的内容写回文件;B根本感知不到A的操作,还是用自己内存里存的、首行是abc123的数据集重写文件,最终两个请求都把abc123发给了用户,同时A发完之后的文件状态还被B的写操作覆盖,还会额外丢码。
  • 写文件时直接用w模式打开,会瞬间清空原文件内容,如果此时有其他请求刚好执行读文件操作,会读到空文件或者半写入的损坏内容,进一步引发空码、丢码问题。
关于文件重写逻辑的说明

是的,当前代码在取走第一个码之后,会遍历所有剩余的随机码逐行重新写入CSV文件。如果CSV里存了几万、几十万条随机码,每次发1个码就要重写几万几十万行内容,磁盘IO开销极高,码量越大、并发越高,流程执行时间越长,竞态问题出现的概率就越高。

防重复&性能优化方案

临时兼容CSV存储的修复方案

如果暂时无法更换存储介质,必须先给整个文件操作流程加排他锁,保证同一时间只有一个请求能执行完整的读改写流程,核心调整点:

  • 放弃「先读全量文件、再用w模式打开清空重写」的逻辑,改用c模式打开文件(打开时不自动清空内容),通过flock加非阻塞独占锁,只有拿到锁的请求才能操作文件,拿不到锁的请求做短延迟重试,避免请求堆积。
  • 拿到锁之后再读取全量内容、取码、截断清空文件、回写剩余内容,写完立刻释放锁,不要把后续的表单校验、用户通知这类业务逻辑放在锁区间内,尽可能缩短锁的持有时间。

参考实现代码:

$file = fopen($csv_relative_path, 'c');
// 尝试加非阻塞独占锁
if (!flock($file, LOCK_EX | LOCK_NB)) {
    fclose($file);
    // 这里可加10~50ms的短延迟后重试,不要直接返回错误
    exit("系统繁忙,请稍后重试");
}

$data = [];
// 读取全量CSV内容
while (($line = fgetcsv($file)) !== FALSE) {
    $data[] = $line;
}
$code = '';
if (!empty($data)) {
    $code = array_shift($data)[0];
    // 截断文件清空旧内容
    ftruncate($file, 0);
    // 文件指针回到开头
    rewind($file);
    // 回写剩余码
    foreach ($data as $fields) {
        fputcsv($file, $fields);
    }
}
// 释放锁、关闭文件
flock($file, LOCK_UN);
fclose($file);

// 锁释放后再执行后续发码业务逻辑

注意:CSV加锁的方案性能上限极低,只适合低并发场景临时救急,高并发下不推荐使用。

高并发场景长期方案

要彻底解决性能和重复发码问题,必须替换CSV存储,选择天然支持原子操作的存储介质:

  • 方案1:数据库存储
    建单独的随机码表,包含主键ID、码值、发放状态(0未发放/1已发放)、发放时间、用户ID字段,给码值加唯一索引,给发放状态加索引。发码时直接用数据库原子更新语句拿码,比如MySQL 8.0+可执行:
    UPDATE random_codes 
    SET status = 1, user_id = ?, grant_time = NOW() 
    WHERE status = 0 
    LIMIT 1 RETURNING code;
    
    数据库本身会保证这条更新语句的原子性,不会出现两个请求同时拿到同一条记录的情况,性能远高于文件操作,同时可以很方便的统计发码进度、查询码的发放记录。
  • 方案2:Redis队列存储
    提前把所有随机码批量推入Redis的List结构,发码时直接执行LPOP操作弹出队首的码即可。LPOP是Redis原生原子命令,哪怕十万级并发也不会返回重复值,性能比数据库更高,非常适合大流量高并发场景。注意开启Redis的RDB+AOF持久化避免服务重启丢码,同时配置队列长度告警,库存不足时及时补码。
  • 兜底方案:不管用哪种存储,都要在用户发码记录表上对「用户ID+活动ID」加唯一索引,避免同一个用户重复领码;如果是码只能用一次的场景,对「码值」加唯一索引,从数据库层面挡住重复写入,做最后一层容错。

内容的提问来源于stack exchange,提问作者user1190132

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 08:57:21