咨询解决JSON格式访客数据文件(guest.txt)意外损坏问题的具体方案
咨询解决JSON格式访客数据文件(guest.txt)意外损坏问题的具体方案
问题背景还原
先整理下你遇到的场景和提供的信息:
我的PHP处理代码
$serverGuestFile = fopen($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt', "r"); $serverGuestFileData = fread($serverGuestFile, filesize($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt')); fclose($serverGuestFile); $serverGuestFileData = json_decode($serverGuestFileData, true); $_SESSION['guestID'] = array_search($ipAddress, array_column($serverGuestFileData, 0)); if ($_SESSION['guestID'] === false) { array_push($serverGuestFileData, array($ipAddress, $time)); $_SESSION['guestID'] = sizeof($serverGuestFileData) - 1; $serverGuestFileData = json_encode($serverGuestFileData); $serverGuestFile = fopen($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt', "w"); fwrite($serverGuestFile, $serverGuestFileData); fclose($serverGuestFile); } else { $serverGuestFileData[$_SESSION['guestID']][1] = $time; $serverGuestFileData = json_encode($serverGuestFileData); $serverGuestFile = fopen($_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt', "w"); fwrite($serverGuestFile, $serverGuestFileData); fclose($serverGuestFile); }
正常的guest.txt内容
[["::1",1755951136],["158.62.69.152",1752225691],["54.36.148.143",1756166177],["124.222.209.139",1753035977]]
损坏的guest.txt内容
[["::1",1755951136],["158.62.69.152",1752225691],["54.36.148.143",1756166177]][["124.222.209.139",1753035977]]
错误点:本该以单个
]结尾,结果出现了多余的方括号拼接,导致JSON格式完全失效。
我一直确信自己的代码逻辑没问题,距离上次文件损坏已经过去好几个月了,这次怀疑是有人攻击网站导致的。目前手动修正多余的]后网站已恢复,想知道未来怎么从根源解决这个问题?
问题根源分析
先给你吃个定心丸:这大概率不是攻击导致的,而是并发写入冲突的经典问题。
当有多个访客同时访问网站时,会出现以下致命场景:
- 访客A的请求先打开
guest.txt读取数据,还没来得及写入; - 访客B的请求也同时打开了同一个文件,读取到了和A一样的旧数据;
- 两个请求的写入操作“撞车”,要么后写入的覆盖先写入的内容,要么更糟——两个请求的写入内容被强制拼接,就像你看到的两个JSON数组连在一起、多了方括号的情况。
你的代码里没有任何防止并发读写冲突的机制,所以在访问量突然上升时,就会触发文件损坏。
具体解决方案
下面给你几个从易到难的方案,你可以根据自己的需求选择:
方案1:给文件加排他锁(最快实现)
在打开文件时使用排他锁,确保同一时间只有一个请求能读写这个文件。修改后的代码如下:
$filePath = $_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt'; // 打开文件并加排他锁,直到操作完成才释放 $serverGuestFile = fopen($filePath, "r+"); flock($serverGuestFile, LOCK_EX); // 读取文件内容 rewind($serverGuestFile); $serverGuestFileData = fread($serverGuestFile, filesize($filePath)); $serverGuestFileData = json_decode($serverGuestFileData, true); // 处理访客数据逻辑 $_SESSION['guestID'] = array_search($ipAddress, array_column($serverGuestFileData, 0)); if ($_SESSION['guestID'] === false) { array_push($serverGuestFileData, array($ipAddress, $time)); $_SESSION['guestID'] = sizeof($serverGuestFileData) - 1; } else { $serverGuestFileData[$_SESSION['guestID']][1] = $time; } // 写入更新后的数据 $serverGuestFileData = json_encode($serverGuestFileData); ftruncate($serverGuestFile, 0); // 清空原文件 rewind($serverGuestFile); // 把指针移回文件开头 fwrite($serverGuestFile, $serverGuestFileData); // 释放锁并关闭文件 flock($serverGuestFile, LOCK_UN); fclose($serverGuestFile);
核心改进点:
- 用
LOCK_EX排他锁强制同一时间仅一个请求操作文件; - 把打开、读取、写入、关闭流程合并,锁的持有时间更短,减少等待;
- 用
ftruncate清空文件而非重新打开w模式,避免并发下的文件打开冲突。
方案2:使用临时文件写入(更安全)
如果担心锁机制在某些服务器环境下有兼容问题,可以用临时文件原子替换的方式:
$filePath = $_SERVER['DOCUMENT_ROOT'] . '/server/guest.txt'; $tempPath = $_SERVER['DOCUMENT_ROOT'] . '/server/guest.tmp'; // 读取原文件(加共享读锁,允许多个请求同时读取) $serverGuestFile = fopen($filePath, "r"); flock($serverGuestFile, LOCK_SH); $serverGuestFileData = fread($serverGuestFile, filesize($filePath)); flock($serverGuestFile, LOCK_UN); fclose($serverGuestFile); // 处理访客数据逻辑 $serverGuestFileData = json_decode($serverGuestFileData, true); $_SESSION['guestID'] = array_search($ipAddress, array_column($serverGuestFileData, 0)); if ($_SESSION['guestID'] === false) { array_push($serverGuestFileData, array($ipAddress, $time)); $_SESSION['guestID'] = sizeof($serverGuestFileData) - 1; } else { $serverGuestFileData[$_SESSION['guestID']][1] = $time; } // 先写入临时文件 $serverGuestFileData = json_encode($serverGuestFileData); $tempFile = fopen($tempPath, "w"); fwrite($tempFile, $serverGuestFileData); fclose($tempFile); // 原子替换原文件:这个操作在系统层面是原子性的,完全避免拼接问题 rename($tempPath, $filePath);
核心优势:rename操作在绝大多数操作系统里是原子性的——要么替换成功,要么失败,不会出现内容拼接的中间状态,从根源上杜绝了文件损坏。
方案3:改用数据库存储(长期最优解)
如果你的网站访问量持续增长,用文本文件存储始终是隐患,建议把访客数据迁移到数据库:
- SQLite不需要额外安装服务,配置简单,适合小流量网站;
- 数据库本身有完善的事务和锁机制,完美解决并发问题;
- 后续扩展功能(比如统计访客时长、过滤恶意IP)也会更方便。
举个SQLite的简单实现:
// 连接SQLite数据库(不存在则自动创建) $db = new SQLite3($_SERVER['DOCUMENT_ROOT'] . '/server/guest.db'); // 初始化表(第一次运行时自动创建) $db->exec("CREATE TABLE IF NOT EXISTS guests (ip TEXT PRIMARY KEY, last_active INTEGER)"); // 自动处理新增或更新:用REPLACE语句,IP存在则更新时间,不存在则新增 $stmt = $db->prepare("REPLACE INTO guests (ip, last_active) VALUES (:ip, :time)"); $stmt->bindValue(':ip', $ipAddress, SQLITE3_TEXT); $stmt->bindValue(':time', $time, SQLITE3_INTEGER); $stmt->execute(); // 获取guestID(用数据库的rowid作为唯一标识) $stmt = $db->prepare("SELECT rowid FROM guests WHERE ip = :ip"); $stmt->bindValue(':ip', $ipAddress, SQLITE3_TEXT); $result = $stmt->execute(); $row = $result->fetchArray(); $_SESSION['guestID'] = $row['rowid']; $db->close();
额外防护措施
- 添加JSON格式校验:每次读取文件后,先检查
json_decode的结果,如果返回null,说明文件损坏,此时可以初始化空数组或用备份恢复:
$serverGuestFileData = json_decode($serverGuestFileData, true); if ($serverGuestFileData === null) { // 文件损坏,初始化空数组 $serverGuestFileData = []; // 可选:记录错误日志,方便排查 error_log("guest.txt 已损坏,已重置为空数组"); }
- 定期备份文件:比如每次写入前,把原文件复制一份作为备份,万一损坏可以快速恢复;
- 限制写入频率:同一个IP在短时间内不需要频繁更新
last_active时间,比如判断如果距离上次更新不到1分钟,就跳过写入,减少并发写入的概率。
按照上面的方案调整后,就能完全解决你遇到的文件损坏问题了,有其他细节问题可以随时问我~




