You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI+SQLAlchemy操作SQLite时并发写入避免“数据库已锁定”错误的方案咨询

FastAPI+SQLAlchemy操作SQLite时并发写入避免“数据库已锁定”错误的方案咨询

问题分析

首先得拆解清楚锁错误的根源:
SQLite是单写型数据库,同一时间仅允许一个写入事务持有锁。你的测试代码里,session_b通过db.flush()提前触发了SQL执行(数据写入数据库但未提交),直接抢占了SQLite的写入锁;而session_a等待0.5秒后执行commit时,发现锁已被session_b持有,再加上SQLAlchemy默认的SQLite连接锁等待逻辑很激进(几乎不等待就报错),所以直接抛出了锁定异常。

另外你配置了connect_args={"check_same_thread": False},这打破了SQLite原生的线程安全限制,再结合FastAPI异步接口的事件循环线程调度机制,多个请求复用线程的情况进一步提升了锁冲突的概率。

安全可靠的解决方案

下面给出几个从易到难、适配不同场景的方案:


方案1:给SQLite连接添加锁超时配置

SQLite原生支持设置锁等待超时,我们可以在创建engine时加入timeout参数,让连接在获取锁失败时等待一段时间,而非直接报错。等持有锁的事务提交后,等待的事务就能自动获取锁完成操作。

修改engine创建代码:

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False, "timeout": 30}  # 最多等待30秒再报错
)

优点:改动极小,无业务代码侵入,对现有逻辑零影响。
缺点:高并发场景下会拉长接口响应时间,更适合低并发的内部工具或原型项目。


方案2:给commit操作添加重试机制

针对commit时可能出现的锁错误,我们可以封装一个重试逻辑,当捕获到OperationalError且错误信息包含锁提示时,自动重试几次。

先写一个简易的重试工具函数:

from sqlalchemy.exc import OperationalError
import asyncio

async def retry_commit(db, max_retries=3, delay=0.2):
    for _ in range(max_retries):
        try:
            db.commit()
            return True
        except OperationalError as e:
            if "database is locked" in str(e):
                await asyncio.sleep(delay)
                continue
            raise
    return False

再在接口中调用这个函数:

@app.post("/session_a")
async def session_a(db: Session = Depends(get_db)):
    logger.info("A start")
    uuid_str = str(uuid.uuid4())
    item = Item(name=f"session_a{uuid_str}")
    db.add(item)
    await asyncio.sleep(0.5)
    logger.info(f"A commit {uuid_str}")
    
    await retry_commit(db)
    return {"status": "A committed"}

优点:针对性强,仅处理锁冲突场景,不干扰正常业务流程。
缺点:需要修改业务代码,高并发下仍存在等待,但比方案1更灵活。


方案3:使用单线程连接池控制并发

可以改用SQLAlchemy的SingletonThreadPool连接池,确保同一时间只有一个连接操作数据库,从根源上避免锁竞争。同时如果你担心autoflush=False影响其他接口,可以只在长事务接口中临时禁用自动刷新。

修改连接池配置:

from sqlalchemy.pool import SingletonThreadPool

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False},
    poolclass=SingletonThreadPool  # 单线程连接池,同一时间仅一个连接可用
)
SessionLocal = sessionmaker(autocommit=False, autoflush=True, bind=engine)

优点:从连接层面控制并发,彻底消除锁冲突,适合中小并发场景。
缺点:会牺牲部分并发性能,同一时间仅能处理一个写入请求。


方案4:将写入操作移至线程池执行

FastAPI的异步接口中,若存在阻塞IO操作(如数据库commit),可以将其放入asyncio.to_thread中执行,结合重试机制,既不阻塞事件循环,也能更好地处理锁等待。

修改接口代码:

@app.post("/session_a")
async def session_a(db: Session = Depends(get_db)):
    logger.info("A start")
    uuid_str = str(uuid.uuid4())
    item = Item(name=f"session_a{uuid_str}")
    db.add(item)
    await asyncio.sleep(0.5)
    logger.info(f"A commit {uuid_str}")
    
    # 把commit移到线程池执行,避免阻塞事件循环
    def commit_task():
        for _ in range(3):
            try:
                db.commit()
                return
            except OperationalError as e:
                if "database is locked" in str(e):
                    time.sleep(0.2)
                    continue
                raise
    await asyncio.to_thread(commit_task)
    return {"status": "A committed"}

优点:不阻塞FastAPI事件循环,同时兼容锁冲突处理,适合异步架构下的中高并发场景。
缺点:需要修改业务代码,对开发者的异步编程逻辑有一定要求。


方案5:升级为多写型数据库(终极方案)

如果你的项目后续有高并发写入需求,SQLite的单写特性会成为天然瓶颈,此时最好的方案是替换为PostgreSQL、MySQL这类支持多写的数据库,从根源上解决SQLite的锁限制。

优点:彻底解决并发写入问题,适配生产环境高并发场景。
缺点:需要额外部署和维护数据库服务,增加运维成本。

总结

  • 低并发内部工具/原型项目:优先选方案1或方案3,改动小成本低;
  • 异步架构中并发场景:方案2+方案4的组合是安全且灵活的选择;
  • 生产环境高并发需求:直接升级到多写型数据库(方案5)是长远之计。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 11:29:33