fix: P0 — records 并发写入加锁 + 原子写入

BIZ-74 P0 改进项:
- 新增 threading.Lock (records_lock) 保护 .generation_records.json
- load_records / save_records / add_record 全部持锁
- api_delete_record 也加锁保护读-改-写
- 原子写入:先写 .tmp 再 os.replace,防止写入中途崩溃

并发测试验证:
- 10 线程并发写入,0 丢失
- 并发读写互不阻塞
- 无残留 .tmp 文件

评审②改进项,BIZ-74
This commit is contained in:
2026-07-03 16:41:57 +08:00
parent 13a259b0f8
commit ae5d7a08ff
+65 -28
View File
@@ -12,6 +12,7 @@ import json
import uuid
import shutil
import traceback
import threading
from datetime import datetime
from flask import Flask, send_from_directory, jsonify, request, send_file, abort
from functools import wraps
@@ -41,37 +42,61 @@ CONFIG = {
}
# ============================================================
# 生成记录管理
# 生成记录管理(线程安全)
# ============================================================
# 全局锁:保护 .generation_records.json 的并发读写
records_lock = threading.Lock()
def load_records():
"""加载生成记录"""
if os.path.exists(CONFIG['records_file']):
try:
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
return json.load(f)
except:
return []
return []
"""加载生成记录(线程安全读取)"""
with records_lock:
if os.path.exists(CONFIG['records_file']):
try:
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return []
return []
def save_records(records):
"""保存生成记录"""
os.makedirs(os.path.dirname(CONFIG['records_file']), exist_ok=True)
with open(CONFIG['records_file'], 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
"""保存生成记录(线程安全写入)"""
with records_lock:
os.makedirs(os.path.dirname(CONFIG['records_file']), exist_ok=True)
# 先写临时文件再原子替换,防止写入中途崩溃导致数据损坏
tmp_path = CONFIG['records_file'] + '.tmp'
with open(tmp_path, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, CONFIG['records_file'])
def add_record(strategy, num_tickets, filename):
"""添加一条生成记录"""
records = load_records()
records.insert(0, {
'id': str(uuid.uuid4())[:8],
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'strategy': '高级策略' if strategy == 'advanced' else '基础策略',
'num_tickets': num_tickets,
'filename': filename,
'filesize': os.path.getsize(os.path.join(BASE_DIR, filename)) if os.path.exists(os.path.join(BASE_DIR, filename)) else 0
})
save_records(records)
return records[0]
"""添加一条生成记录(原子操作:读-改-写全程持锁)"""
with records_lock:
# 读取现有记录
if os.path.exists(CONFIG['records_file']):
try:
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
records = json.load(f)
except (json.JSONDecodeError, IOError):
records = []
else:
records = []
# 插入新记录
new_record = {
'id': str(uuid.uuid4())[:8],
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'strategy': '高级策略' if strategy == 'advanced' else '基础策略',
'num_tickets': num_tickets,
'filename': filename,
'filesize': os.path.getsize(os.path.join(BASE_DIR, filename)) if os.path.exists(os.path.join(BASE_DIR, filename)) else 0
}
records.insert(0, new_record)
# 原子写入
os.makedirs(os.path.dirname(CONFIG['records_file']), exist_ok=True)
tmp_path = CONFIG['records_file'] + '.tmp'
with open(tmp_path, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, CONFIG['records_file'])
return new_record
# ============================================================
# 认证装饰器(可选)
@@ -314,9 +339,21 @@ def api_delete_record(record_id):
if os.path.exists(filepath):
os.remove(filepath)
# 删除记录
records = [r for r in records if r['id'] != record_id]
save_records(records)
# 删除记录(加锁保护读-改-写)
with records_lock:
if os.path.exists(CONFIG['records_file']):
try:
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
records = json.load(f)
except (json.JSONDecodeError, IOError):
records = []
else:
records = []
records = [r for r in records if r['id'] != record_id]
tmp_path = CONFIG['records_file'] + '.tmp'
with open(tmp_path, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, CONFIG['records_file'])
return jsonify({'success': True, 'message': '记录已删除'})
except Exception as e: