Python多函数并发写JSON文件防数据丢失(Discord Bot场景)
Discord Bot并发写JSON导致数据丢失的解决方案
出现这个问题的核心是**「读-改-写」的全流程不是原子操作**,属于典型的竞态条件。Discord Bot基于asyncio事件循环运行,事件触发的更新逻辑在IO阻塞间隙会被上下文切换,两个逻辑很可能同时读到同一份旧数据,各自修改后全量覆盖写回,最终后写入的内容会直接冲掉前一个逻辑的更新,造成数据丢失。
适配Bot场景的落地方案
1. 单实例Bot首选:全局异步锁包裹全流程
单实例运行的Bot不需要引入重型组件,用Python原生asyncio.Lock即可从根源规避竞态:
- 初始化一个全局共享的异步锁,所有涉及JSON读写的逻辑必须先拿到锁才能进入临界区
- 临界区必须覆盖读文件、修改数据、写回文件的完整流程,不能只锁写操作
- 写回时采用「临时文件+原子替换」的方式,避免Bot意外崩溃时把原JSON文件写坏
参考实现代码:
import asyncio import json import os # 全局共享锁,所有JSON文件操作必须持有该锁 json_lock = asyncio.Lock() CONFIG_FILE = "你的JSON文件实际路径" async def update_channels(stuff): async with json_lock: # 以下逻辑全程不会被其他协程插队 with open(CONFIG_FILE, 'r', encoding='utf-8') as f: data = json.load(f) data["Channels"].append(stuff) # 先写临时文件,再原子替换原文件,防止文件损坏 temp_path = f"{CONFIG_FILE}.tmp" with open(temp_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) os.replace(temp_path, CONFIG_FILE) async def update_key(key_no, stuff): async with json_lock: with open(CONFIG_FILE, 'r', encoding='utf-8') as f: data = json.load(f) data[key_no].append(stuff) temp_path = f"{CONFIG_FILE}.tmp" with open(temp_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) os.replace(temp_path, CONFIG_FILE)
2. 多实例Bot部署:跨进程文件锁
如果Bot是多副本/多实例部署,单进程内的协程锁无法跨进程同步,这时候需要用跨进程文件锁实现互斥:
- 用轻量的文件锁能力绑定JSON文件本身,哪个进程要操作文件先拿到锁权限,再执行读-改-写的完整流程
- 同样保留临时文件原子替换的写逻辑,避免文件损坏
3. 长期维护首选:替换存储方案
如果JSON文件存储的内容持续增长,全量读全量写的JSON方案性能会逐渐下降,直接换Python自带的sqlite3存储即可:
- SQLite支持事务级原子操作,天然规避并发写的竞态问题
- 不需要全量读全量写文件,修改单条数据时直接执行SQL更新即可,性能远好于JSON文件
- 不需要额外搭建服务,对中小型Discord Bot来说是零成本的升级
常见踩坑提醒
- 不要只在写文件步骤加锁:如果两个逻辑同时读到旧数据,哪怕写的时候加锁,读出来的旧数据写完还是会覆盖之前的更新,必须把读、改、写全流程都放进锁的临界区
- 不要直接往原JSON文件写内容:写文件过程中如果Bot进程异常退出,会导致原文件截断损坏,用临时文件+
os.replace的方式可以保证文件要么是旧的完整版本,要么是新的完整版本 - 不要在异步逻辑里用阻塞的线程锁:会卡住整个事件循环,导致Bot响应延迟,异步场景优先用原生
asyncio提供的锁能力
内容的提问来源于stack exchange,提问作者Shubham
相关产品推荐
相关产品推荐

