如何知道哪个进程负责“OperationalError: database is locked”错误?

11

有时我会偶然遇到:

OperationalError: database is locked

在更新SQLite数据库的过程中,但我发现很难重现这个错误:

  • 没有其他进程同时插入/删除行
  • 只有一个进程可能会做一些只读查询(SELECT等),但没有提交

我已经阅读了OperationalError: database is locked

问题:是否有一种方法,在发生此错误时记录负责锁定的其他进程ID?

更一般地说,如何调试OperationalError: database is locked


几十年没用过sqlite了,但是它是否会将I/O锁定到磁盘上以锁定数据库呢?例如,如果操作系统正在对磁盘进行一些其他的重负载/维护负载,则会触发大型“INSERT”,导致磁盘锁定数据库一段时间。我怀疑是否有sqlite工具可以解决这个问题。它本质上是轻量级的,有其原因。如果可能的话,如果出现问题,我会考虑转移到实际的数据库引擎(PostgreSQL或其他)。 - Torxed
当SQLite API发出“SQLITE_BUSY”信号并且没有上下文与该信号时,会引发此特定异常。您无法确定可能是什么PID导致SQLite返回该响应。 - Martijn Pieters
换句话说,先阅读这里已经提出的“导致'SQLITE_BUSY'的进程的PID是什么”问题。 - Martijn Pieters
更新SQLite数据库的过程可能有问题。在事务周围放置时间,记录事务开始时的时间,并在提交时记录持续时间。这里某些操作需要5秒或更长时间(sqlite3.connect()调用的默认timeout值)。 - Martijn Pieters
如果您的操作系统支持 lsof,您可以使用它来查找打开该文件的每个进程,并查看哪个进程有锁定 - 可能是正在进行 select 操作的那个进程。如果您希望数据库读取器不会阻塞数据库写入器,反之亦然,请查看 WAL 日志模式。 - Shawn
2个回答

17
解决方案:始终关闭(即使是只读)查询的游标!
首先,这里是一个复现问题的方法:
1.首先运行此代码一次:
import sqlite3
conn = sqlite3.connect('anothertest.db')
conn.execute("CREATE TABLE IF NOT EXISTS mytable (id int, description text)")
for i in range(100):
    conn.execute("INSERT INTO mytable VALUES(%i, 'hello')" % i)
conn.commit()
  • 接着进行一个只读查询:

    用于初始化测试。

  • import sqlite3, time
    conn = sqlite3.connect('anothertest.db')
    c = conn.cursor()
    c.execute('SELECT * FROM mytable')
    item = c.fetchone()
    print(item)
    print('Sleeping 60 seconds but the cursor is not closed...')
    time.sleep(60)
    

    在执行下一步操作时,请保持此脚本运行

  • 然后尝试删除一些内容并提交:

    import sqlite3
    conn = sqlite3.connect('anothertest.db')
    conn.execute("DELETE FROM mytable WHERE id > 90")
    conn.commit()
    

    的确会触发这个错误:

    sqlite3.OperationalError: database is locked

  • 为什么呢?因为无法删除当前正在被读取查询访问的数据:如果游标仍然处于打开状态,那么数据仍然可以使用 fetchonefetchall 获取。

    以下是解决此错误的方法:在第2步中,只需添加:

    item = c.fetchone()
    print(item)
    c.close()
    time.sleep(60)
    

    在此同时,启动脚本#3,您会发现没有更多的错误。


    如果数据库处于wal模式,这种情况可能发生吗?如果步骤3是向表中插入而不是删除数据会怎样? - Neil
    嘿,如何在Django中手动关闭游标? - Meet Gondaliya

    5
    当这个错误发生时,有没有一种方法可以记录哪个其他进程ID负责锁定?
    不,当异常发生时不会记录该信息。在SQLite内部尝试获取互斥锁和文件锁时(默认为5分钟),通常会引发“OperationalError: database is locked”异常,此时SQLite返回“SQLITE_BUSY”,但“SQLITE_BUSY”也可能在其他点上报告。 SQLite错误代码不带任何进一步的上下文,例如持有锁的另一个进程的PID,并且可以想象,在当前进程放弃获取它之前,锁已在两个其他进程之间传递!
    最好的方法是使用“lsof <数据库文件名>”枚举当前访问文件的进程,但这并不能让您更接近找出哪一个实际上需要很长时间才能提交。
    相反,我建议您使用显式事务和详细日志记录来仪表化您的代码,以便在遇到“OperationalError”异常时,您可以检查日志以了解在该时间窗口内发生了什么。
    一个可用于此的Python上下文管理器是:
    import logging
    import sys
    import time
    import threading
    from contextlib import contextmanager
    from uuid import uuid4
    
    logger = logging.getLogger(__name__)
    
    
    @contextmanager
    def logged_transaction(con, stack_info=False, level=logging.DEBUG):
        """Manage a transaction and log start and end times.
    
        Logged messages include a UUID transaction ID for ease of analysis.
    
        If trace is set to True, also log all statements executed.
        If stack_info is set to True, a stack trace is included to record
        where the transaction was started (the last two lines will point to this
        context manager).
    
        """
        transaction_id = uuid4()
        thread_id = threading.get_ident()
    
        def _trace_callback(statement):
            logger.log(level, '(txid %s) executing %s', transaction_id, statement)
        if trace:
            con.set_trace_callback(_trace_callback)
    
        logger.log(level, '(txid %s) starting transaction', transaction_id, stack_info=stack_info)
    
        start = time.time()
        try:
            with con:
                yield con
        finally:
            # record exception information, if an exception is active
            exc_info = sys.exc_info()
            if exc_info[0] is None:
                exc_info = None
            if trace:
                con.set_trace_callback(None)
            logger.log(level, '(txid %s) transaction closed after %.6f seconds', transaction_id, time.time() - start, exc_info=exc_info)
    

    上述内容将创建起始和结束条目,如果有异常信息则包括在内,可选择跟踪连接上执行的所有语句,并可以包括一个堆栈跟踪,告诉您上下文管理器使用的位置。确保在格式化日志消息时包含日期和时间,以便跟踪事务启动时间。
    我会在使用连接的任何代码周围使用它,这样您也可以计时选择操作:
    with logged_transaction(connection):
        cursor = connection.cursor()
        # ...
    

    也许仅使用这个上下文管理器就可以解决您的问题,此时您需要分析为什么没有这个上下文管理器的代码会留下未提交的事务。

    您可能还想在sqlite3.connect()调用中使用较低的timeout值以加快进程速度;您可能不必等待完整的5分钟来检测情况。

    关于线程的注意事项:启用跟踪时,假定您为不同的线程使用单独的连接。如果不是这种情况,则需要永久注册跟踪回调,然后解决当前线程要使用哪个事务 ID 的问题。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接