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