如何知道哪个进程负责“ OperationalError:数据库已锁定”?

Bas*_*asj 6 python database sqlite

我有时会偶然遇到:

OperationalError:数据库已锁定

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

  • 没有其他进程同时插入/删除行
  • 只有一个进程可以SELECT在此处和那里执行一些只读查询(等),但是没有提交

我已经读过OperationalError:数据库已锁定

问题:是否有办法在发生此错误时记录哪个其他进程ID负责锁定?

更一般而言,如何调试OperationalError: database is locked

Mar*_*ers 5

当发生此错误时,有没有办法记录哪个其他进程 ID 负责锁定?

不,异常发生时不会记录该信息。当尝试在 SQLite 内部获取互斥锁和文件锁时,通常会在超时(默认为 5 分钟)后引发异常OperationalError: database is locked,此时 SQLite 返回SQLITE_BUSY,但SQLITE_BUSY也可以在其他点报告。SQLite 错误代码不携带任何进一步的上下文,例如持有锁的另一个进程的 PID,并且可以想象,在当前进程放弃尝试获取锁之前,该锁已在其他两个进程之间传递!

您最多可以使用 枚举当前正在访问该文件的进程lsof <filename of database>,但这不会让您更接近于弄清楚其中哪个进程实际上需要太长时间才能提交。

相反,我会使用显式事务和有关何时启动和提交事务的详细日志记录来检测您的代码。然后,当您遇到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)
Run Code Online (Sandbox Code Playgroud)

上面将创建开始和结束条目,包括异常信息(如果有),可选地跟踪在连接上执行的所有语句,并且可以包括堆栈跟踪,该堆栈跟踪将告诉您使用上下文管理器的位置。请确保在格式化日志消息时包含日期和时间,以便您可以跟踪事务何时开始。

我会在使用连接的任何代码中使用它,因此您也可以进行时间选择:

with logged_transaction(connection):
    cursor = connection.cursor()
    # ...
Run Code Online (Sandbox Code Playgroud)

也许仅仅使用这个上下文管理器就会让你的问题消失,此时你必须分析为什么没有这个上下文管理器的代码会打开一个事务而不提交。

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

关于线程的注意事项:启用跟踪时,假定您对单独的线程使用单独的连接。如果情况并非如此,那么您需要永久注册一个跟踪回调,然后该回调会整理出当前线程要使用的事务 ID。


Bas*_*asj 5

解决方案:始终关闭cursorfor(甚至只读)查询!

首先,这是重现问题的一种方法:

  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()
    
    Run Code Online (Sandbox Code Playgroud)

    初始化测试。

  2. 然后开始一个只读查询:

    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)
    
    Run Code Online (Sandbox Code Playgroud)

    在执行下一步时保持此脚本运行:

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

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

    确实会触发此错误:

    sqlite3.OperationalError:数据库已锁定

为什么?由于无法删除读取查询当前正在访问的数据:如果游标仍处于打开状态,则意味着仍可以使用fetchone或来获取数据fetchall

解决错误的方法如下:在步骤2中,只需添加:

item = c.fetchone()
print(item)
c.close()
time.sleep(60)
Run Code Online (Sandbox Code Playgroud)

然后,在此脚本仍在运行的同时,启动脚本#3,您将看到没有更多错误。