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

Posted

技术标签:

【中文标题】如何知道哪个进程负责“OperationalError:数据库已锁定”?【英文标题】:How to know which process is responsible for a "OperationalError: database is locked"? 【发布时间】:2019-04-15 16:01:00 【问题描述】:

我有时会随机遇到:

OperationalError: 数据库被锁定

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

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

我已经看过OperationalError: database is locked

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

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

【问题讨论】:

几十年没用过sqlite,会不会是I/O到磁盘锁定数据库?例如,一个大的INSERT,如果操作系统正在磁盘上执行其他一些繁重/维护负载,它会触发磁盘锁定数据库一段时间?我怀疑是否有用于此的 sqlite 工具。出于某种原因,它本质上很轻。如果可能的话,如果这成为一个问题,我会考虑转移到一个实际的数据库引擎(PostgreSQL或其他东西) 当 SQLite API 发出 SQLITE_BUSY 信号时会引发此特定异常,并且该信号没有上下文。您无法确定导致 SQLite 返回该响应的原因可能是什么 PID。 换一种说法,阅读导致SQLITE_BUSY问题的进程的PID是什么。 更新 SQLite 数据库的过程可能在这里出错。围绕交易安排时间;记录事务开始的时间,并记录提交时的持续时间。这里的某些事情需要 5 秒或更长时间(sqlite3.connect() 调用的默认 timeout 值)。 如果lsof 支持您的操作系统,您可以使用它来查找打开文件的每个进程,并查看哪个进程有锁——可能是在选择中间的那个。如果您希望数据库读取器不阻塞数据库写入器,反之亦然,请查看 WAL 日志模式。 【参考方案1】:

解决方案:始终关闭cursor 进行(甚至是只读)查询!

首先,这里有一个重现问题的方法:

    首先运行这段代码,一次:

    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: 数据库被锁定

为什么?因为无法删除当前被读取查询访问的数据:如果游标仍然打开,则意味着仍然可以使用fetchonefetchall 获取数据。

这是解决错误的方法:在步骤#2中,只需添加:

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

然后当它还在运行时,启动脚本#3,你会看到没有更多的错误。

【讨论】:

如果数据库处于wal 模式,这种情况会发生吗?如果第 3 步是对表的插入而不是删除呢?【参考方案2】:

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

不,发生异常时不会记录该信息。 OperationalError: database is locked 异常通常在超时(默认为 5 分钟)后在 SQLite 内部尝试获取互斥锁和文件锁时引发,此时 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)

上面将创建开始和结束条目,包括异常信息(如果有),可选地跟踪正在连接上执行的所有语句,并且可以包括一个堆栈跟踪,它将告诉您使用上下文管理器的位置。请务必发送至include the date and time in when formatting log messages,以便跟踪交易何时开始。

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

with logged_transaction(connection):
    cursor = connection.cursor()
    # ...

可能只使用这个上下文管理器会让你的问题消失,此时你必须分析为什么没有这个上下文管理器的代码会在没有提交的情况下保持打开事务。

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

关于线程的注意事项:启用跟踪时,假定您为单独的线程使用单独的连接。如果不是这种情况,那么您需要永久注册一个跟踪回调,然后整理出用于当前线程的事务 id。

【讨论】:

以上是关于如何知道哪个进程负责“OperationalError:数据库已锁定”?的主要内容,如果未能解决你的问题,请参考以下文章

在互斥锁中,处理器(CPU)如何知道要解锁哪个进程?

Linux 有问必答:如何知道进程运行在哪个 CPU 内核上?

linux 共享内存 可不可以不加锁呢? 系统有两个进程,一个负责写入,一个负责读取

如何确定哪个 w3wp.exe 进程属于哪个网站?

如何了解哪个进程删除了硬盘上的文件

查看文件被哪个进程占用