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

时间:2018-11-12 21:42:28

标签: python database sqlite

我有时会随机遇到:

  

OperationalError:数据库已锁定

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

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

我已经读过OperationalError: database is locked

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

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

2 个答案:

答案 0 :(得分:2)

  

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

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

充其量,您可以使用SQLITE_BUSY来枚举当前正在访问哪个进程的文件,但这并不能使您更确切地确定哪个进程实际上花费了太长时间提交。

我将使用显式事务和有关启动和提交事务的详细日志记录来检测代码。然后,当您遇到lsof <filename of database>异常时,可以检查日志以了解在该时间段内发生的情况。

可用于此目的的Python上下文管理器是:

OperationalError

上面将创建开始和结束条目,包括异常信息(如果有),可以选择跟踪连接上正在执行的所有语句,还可以包括堆栈跟踪,该跟踪将告诉您使用上下文管理器的位置。请确保include the date and time in when formatting log messages,以便您可以跟踪交易开始的时间。

我会在使用该连接的所有代码中使用它,因此您也可以选择时间:

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() # ... 调用中使用较低的timeout值来加快处理过程;您可能不必等待整整5分钟才能发现情况。

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

答案 1 :(得分:1)

解决方案:对于所有查询(即使是只读的),始终关闭cursor

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

  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()
    

    初始化测试。

  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)
    

    并在执行下一步时使该脚本运行

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

    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,您将看到没有更多错误。