这篇文章,我将和你一起拆解这个问题,并分享如何用几个轻量级方案,让这个基于FastAPI和TinyDB的应用稳定地支撑起了日均数千次的并发请求。它不是一套放之四海而皆准的架构,但绝对是你在原型开发、小型工具或特定轻量场景下,性价比最高的解决方案。
本文针对在FastAPI框架下使用TinyDB(JSON文件数据库)时遇到的并发写入数据冲突、错乱问题,深入浅出地解释了问题根源,并提供了从“文件锁”到“内存队列”再到“乐观锁”的三种由浅入深的实战解决方案,帮助你根据实际场景选择,确保数据一致性。
🔍 一、问题根源:为什么简单的JSON文件会“打架”?
🛠️ 二、解决方案:从“锁”到“队列”的三层防御
- 方案一:文件锁(fcntl / portalocker)—— 给文件上个“请勿打扰”牌
- 方案二:内存操作队列(asyncio.Queue)—— 让请求排好队,一个一个来
- 方案三:应用层乐观锁(版本号校验)—— “我改的时候,东西还是原来的样子吗?”
💻 三、实战代码:将方案融入FastAPI依赖项与路由
⚠️ 四、重要提醒与边界探讨:这不是银弹
想象一下,TinyDB的
db.json文件就是一个共享的笔记本。FastAPI的每个工作进程(Worker)就像一个快速记录员。
当用户A的请求到来时,记录员1打开笔记本,读到某个值(比如库存为5),准备将其改为4。
就在这“读到”和“改写”的毫秒之间,用户B的请求也来了。记录员2也打开了同一个笔记本,他读到的库存 仍然是5(因为记录员1还没写回去),然后他也计算,将库存改为3。
结果就是,无论谁最后保存,另一个人的修改都会被完全覆盖。这就是典型的“并发写冲突”。在高并发的Web API场景下,这个问题会被急剧放大。
原理:在读写文件前,先给这个文件加一把系统级的锁。其他进程尝试加锁时,会被阻塞或失败,直到锁被释放。这就像给笔记本的房间门上了锁,一次只进一个人。
适用场景: 低并发(如内部工具)、读写不那么频繁的场景。
# 安装:pip install portalockerimport portalockerdef gov.js.zuzb.info safe_update_db(): with open('db.json', 'r+') as f:
portalocker.lock(f, portalocker.LOCK_EX) # 获取独占锁
# 在这里安全地读取和修改数据
data = json.load(f)
data['counter'] += 1
f.seek(0)
json.dump(data, f)
f.truncate() # 退出with块时,锁会自动释放
原理:利用Python的
asyncio.Queue,将所有对TinyDB的写操作封装成任务,放入一个队列。由一个单独的“消费者”协程从队列中依次取出任务执行。这样,无论外部请求多么并发,对数据库的写操作都是
串行化的。
优点: 完全在内存中操作,速度极快,避免了文件锁可能带来的死锁或跨平台问题。非常适合FastAPI的异步模式。
关键警告: 此方案仅在
单个服务进程内有效。如果你使用多个工作进程(如
Uvicorn with --workers 4),每个进程有自己的内存和队列,冲突依然会发生。此时需搭配案一或方案三。
原理:不阻止“读”,只在“写”的时候检查冲突。为每条数据增加一个
version字段。每次读取数据时,连带版本号一起读出。修改后写回时,检查当前文件中的版本号是否和自己读到的版本号一致。如果一致,则写入,并将版本号+1;如果不一致,则说明在此期间数据已被他人修改,本次操作失败,需要提示用户重试。
这就像两个人编辑在线文档,系统会提示你“在你编辑期间,文档已被他人更新”。
下面是一个在FastAPI中整合 内存队列与 乐观锁的核心示例:
from fastapi import FastAPI, Depends, HTTPExceptionfrom contextlib import asynccontextmanagerimport asynciofrom tinydb import TinyDB, Queryimport jsonfrom pydantic import BaseModel
app = FastAPI()
write_queue = asyncio.Queue()
db_path = 'db.json'# 数据模型class ItemUpdate(BaseModel):
item_id: int
new_value: str
read_version: int # 客户端传来的读取时的版本号# 启动时启动写任务消费者@asynccontextmanagerasync def lifespan(app: FastAPI): # 启动时
asyncio.create_task(db_write_consumer()) yield
# 关闭时...app = FastAPI(lifespan=lifespan)async def db_write_consumer(): """写操作消费者,常驻后台,串行处理写队列"""
while True:
task_data =gov.soccer.cl.zuqzb.info await write_queue.get() await _perform_safe_write(task_data)
write_queue.task_done()async def _perform_safe_write(task_data: dict): """执行带乐观锁检查的写入"""
with TinyDB(db_path) as db:
Item = Query()
record = db.get(Item.id == task_data['item_id']) if not record: # 处理记录不存在的情况...
return
# 乐观锁检查!!!
if record['version'] != task_data['read_version']: raise ValueError(f"数据版本冲突。当前版本{record['version']},提交版本{task_data['read_version']}") # 通过检查,执行更新
db.update({ gov.www.dj.jrzb2.info 'value': task_data['new_value'], 'version': record['version'] + 1 # 版本号递增
}, Item.id == task_data['item_id'])@app.put("/update_item/")async def update_item(update: ItemUpdate): """更新接口"""
try: # 将写操作封装成任务,放入队列,等待消费者处理
await write_queue.put(update.dict()) # 这里可以返回一个任务ID,让客户端轮询结果,或者使用WebSocket推送
return {"message": "更新请求已加入队列"} except asyncio.QueueFull: raise HTTPException(status_code=429, detail="系统繁忙,请稍后重试")@app.get("/get_item/{item_id}")async def get_item(item_id: int): """读取接口,返回数据和当前版本号"""
with TinyDB(db_path) as db:
Item = Query()
record = db.get(Item.id == item_id) if record: return {"value": record['value'], "version": record['version']} raise HTTPException(status_code=404, detail="Item not found")
🚨 重要提醒:
1. 性能瓶颈: 所有方案的核心都是“串行化写”。这意味着你的数据库写吞吐量存在上限。对于超高并发写入场景,JSON文件本身就会成为瓶颈。
2. 多进程限制: 内存队列方案在单进程内完美,多进程需配合分布式锁(如Redis锁)或回归到数据库方案。
3. 故障恢复: 队列中的任务在服务重启时会丢失。对数据一致性要求极高的场景,需要引入持久化消息队列(如RabbitMQ)或直接使用真正的数据库。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70048957/viewspace-3107255/ ,如需转载,请注明出处,否则将追究法律责任。