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:
@@ -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():
|
||||||
"""加载生成记录"""
|
"""加载生成记录(线程安全读取)"""
|
||||||
if os.path.exists(CONFIG['records_file']):
|
with records_lock:
|
||||||
try:
|
if os.path.exists(CONFIG['records_file']):
|
||||||
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
|
try:
|
||||||
return json.load(f)
|
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
|
||||||
except:
|
return json.load(f)
|
||||||
return []
|
except (json.JSONDecodeError, IOError):
|
||||||
return []
|
return []
|
||||||
|
return []
|
||||||
|
|
||||||
def save_records(records):
|
def save_records(records):
|
||||||
"""保存生成记录"""
|
"""保存生成记录(线程安全写入)"""
|
||||||
os.makedirs(os.path.dirname(CONFIG['records_file']), exist_ok=True)
|
with records_lock:
|
||||||
with open(CONFIG['records_file'], 'w', encoding='utf-8') as f:
|
os.makedirs(os.path.dirname(CONFIG['records_file']), exist_ok=True)
|
||||||
json.dump(records, f, ensure_ascii=False, indent=2)
|
# 先写临时文件再原子替换,防止写入中途崩溃导致数据损坏
|
||||||
|
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):
|
def add_record(strategy, num_tickets, filename):
|
||||||
"""添加一条生成记录"""
|
"""添加一条生成记录(原子操作:读-改-写全程持锁)"""
|
||||||
records = load_records()
|
with records_lock:
|
||||||
records.insert(0, {
|
# 读取现有记录
|
||||||
'id': str(uuid.uuid4())[:8],
|
if os.path.exists(CONFIG['records_file']):
|
||||||
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
try:
|
||||||
'strategy': '高级策略' if strategy == 'advanced' else '基础策略',
|
with open(CONFIG['records_file'], 'r', encoding='utf-8') as f:
|
||||||
'num_tickets': num_tickets,
|
records = json.load(f)
|
||||||
'filename': filename,
|
except (json.JSONDecodeError, IOError):
|
||||||
'filesize': os.path.getsize(os.path.join(BASE_DIR, filename)) if os.path.exists(os.path.join(BASE_DIR, filename)) else 0
|
records = []
|
||||||
})
|
else:
|
||||||
save_records(records)
|
records = []
|
||||||
return records[0]
|
# 插入新记录
|
||||||
|
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):
|
if os.path.exists(filepath):
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
|
|
||||||
# 删除记录
|
# 删除记录(加锁保护读-改-写)
|
||||||
records = [r for r in records if r['id'] != record_id]
|
with records_lock:
|
||||||
save_records(records)
|
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': '记录已删除'})
|
return jsonify({'success': True, 'message': '记录已删除'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user