日誌/計費系統需將記錄追加寫入文本文件,且第一行始終保持統計摘要:總記錄數|總費用。每次寫入新記錄後,需原子更新首行統計值。
文件格式示例:
3|300 user1|productA|100 user2|productB|100 user3|productC|100
fseek(0) 覆寫會殘留舊字符或截斷後續數據。
#include
#include
#include
#define RECORD_FMT "%s|%s|%d\n" // 用戶|產品|費用
#define STAT_FMT "%d|%d\n" // 記錄數|總費用
int appendRecordWithStat(const char* filename, const char* user, const char* product, int fee)
{
FILE* fp = fopen(filename, "r+"); // 讀寫模式,文件必須存在
if (!fp) {
fp = fopen(filename, "w+"); // 不存在則創建
if (!fp) return -1;
}
/* 1. 讀取並解析首行統計 */
char statLine[64];
int count = 0, totalFee = 0;
if (fgets(statLine, sizeof(statLine), fp)) {
sscanf(statLine, STAT_FMT, &count, &totalFee);
}
/* 2. 定位文件末尾追加數據行 */
fseek(fp, 0, SEEK_END);
long dataPos = ftell(fp); // 記錄數據起始位置 (用於回滾)
fprintf(fp, RECORD_FMT, user, product, fee);
fflush(fp); // 強制落盤
/* 3. 更新統計 */
count++;
totalFee += fee;
/* 4. 回到文件頭覆寫統計行 */
fseek(fp, 0, SEEK_SET);
fprintf(fp, STAT_FMT, count, totalFee);
fflush(fp);
/* 5. 處理統計行變長導致的覆蓋問題:
* 若新統計行比舊行長,會覆蓋數據行開頭;
* 若較短,會殘留舊字符。
* 簡單修復:統計行固定寬度 (如 %010d|%010d\n) 或重寫整個文件。
*/
fclose(fp);
return 0;
}
預留足夠寬度,如 %010d|%010d\n (固定 23 字節),確保覆寫不越界、無殘留。
#define STAT_FMT_FIXED "%010d|%010d\n" // 固定 23 字節
// 寫入時:
fseek(fp, 0, SEEK_SET);
fprintf(fp, STAT_FMT_FIXED, count, totalFee);
// 讀取時:
fgets(buf, 24, fp); // 讀固定長度
sscanf(buf, "%d|%d", &count, &totalFee); // sscanf 自動跳過前導零
優點:O(1) 更新、無需移動數據、適合高頻寫入。
缺點:統計值有上限 (10 位數),人類可讀性稍差。
int appendRecordAtomic(const char* filename, const char* user, const char* product, int fee)
{
char tmpName[FILENAME_MAX];
snprintf(tmpName, sizeof(tmpName), "%s.tmp", filename);
FILE* fpSrc = fopen(filename, "r");
FILE* fpDst = fopen(tmpName, "w");
if (!fpDst) { if (fpSrc) fclose(fpSrc); return -1; }
int count = 0, totalFee = 0;
char line[256];
// 1. 讀取舊統計行
if (fpSrc && fgets(line, sizeof(line), fpSrc))
sscanf(line, "%d|%d", &count, &totalFee);
// 2. 寫入新統計行
count++; totalFee += fee;
fprintf(fpDst, "%d|%d\n", count, totalFee);
// 3. 複製舊數據行
if (fpSrc) {
while (fgets(line, sizeof(line), fpSrc))
fputs(line, fpDst);
fclose(fpSrc);
}
// 4. 追加新記錄
fprintf(fpDst, "%s|%s|%d\n", user, product, fee);
fclose(fpDst);
// 5. 原子替換 (POSIX rename / Windows MoveFileEx)
#ifdef _WIN32
MoveFileEx(tmpName, filename, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
#else
rename(tmpName, filename); // 原子操作
#endif
return 0;
}
優點:崩潰安全 (原文件不變)、並發友好 (配合文件鎖)、無統計行長度限制。
缺點:每次寫入需全量複製文件,大文件性能差。
// 概念框架 (Linux mmap / Windows MapViewOfFile)
// 1. 文件結構: [Header: count, totalFee, dataOffset] [Data...]
// 2. 映射整個文件到內存
// 3. 使用原子操作 (__sync_fetch_and_add / InterlockedIncrement) 更新 count/totalFee
// 4. 在 dataOffset 追加數據 (需文件擴展 ftruncate/SetEndOfFile)
// 5. msync/FlushViewOfFile 落盤
// 適用於極高併發、GB 級日誌、數據庫 WAL 風格。
| 場景 | 推薦方案 | 關鍵點 |
|---|---|---|
| 單進程、低頻 (<10/s)、小文件 (<1MB) | 方案一 + 固定寬度 | 代碼最簡單,fflush 確保落盤 |
| 單進程、高頻、中等文件 | 方案二 (固定寬度覆寫) | O(1) 更新,無拷貝開銷 |
| 多進程/崩潰安全要求高 | 方案三 (臨時文件+原子重命名) | POSIX rename / MoveFileEx 原子性保證 |
| 極高性能、大文件、共享內存 | 方案四 (mmap + 原子指令) | 零拷貝、頁面級落盤、需處理擴展 |
fopen/fseek/fprintf/fflush/rename。fflush + fsync/_commit:確保數據真正寫入磁盤物理介質,而非頁面緩存。flock/LockFileEx。「文件頭統計行動態更新」看似簡單,實則涉及文件系統語義、崩潰一致性、併發控制、性能權衡。根據業務規模選擇:
掌握這三種模式,應對 99% 的文件統計更新需求綽綽有餘。