SQLite INSERT失败,但出现异常,但记录显示在数据库中

时间:2018-09-11 12:38:27

标签: python sqlite locking

我试图在我的python程序中找出一个错误,其中有两个进程正在访问sqlite数据库。

  • 进程1 正在定期读取
  • 进程2 正在定期插入

如果由于某种原因插入失败,我设计了python应用程序,将失败的插入内容附加到缓冲区中,并在下次尝试通过executemany进行插入时重试。

我最近发现该数据库具有重复的条目。 我可以使用以下代码来重现该行为。代码在非常慢的硬件上运行,这就是为什么我伪造长时间查询并减少锁定超时的原因。

process1.py

import sqlite3
import time

con = sqlite3.connect("data.db", timeout=0.1)
con.executescript("CREATE TABLE IF NOT EXISTS data (counter int NOT NULL);")
con.create_function("sleep", 1, time.sleep)

while True:
    try:
        with con:
            con.execute("SELECT counter, sleep(1) FROM data LIMIT 1;")
    except sqlite3.OperationalError as e:
        print("Reading failed. Ex: {}".format(e))
    time.sleep(0.25)

process2.py

import sqlite3
import time

con = sqlite3.connect("data.db", timeout=0.1)
counter = 0

while True:
    try:
        with con:
            con.executemany("INSERT INTO data (counter) VALUES (?);", [(counter,)])
    except sqlite3.OperationalError as e:
        # con.rollback()  # possible workaround?
        print("Writing failed at '{}'. Ex: {}".format(counter, e))            
    counter += 1
    time.sleep(0.25)

在两个单独的终端中运行代码以模仿上述问题时,您会看到进程2 抛出OperationalError,并且消息数据库已锁定。到目前为止,一切都很好。

奇怪的是,当您查询数据库时,看似失败的插入仍然存在。这是设计使然吗?


当您将“ 过程1 ”从阅读切换为书写时,行为会有所不同。现在,过程2 中失败的插入内容不会显示在表格中。

process1_extended.py

import sqlite3
import time

con = sqlite3.connect("data.db", timeout=0.1)
con.create_function("sleep", 1, lambda x: time.sleep(1) or x)
con.executescript("CREATE TABLE IF NOT EXISTS data (counter int NOT NULL);")
counter = 1000000  # distinguishes between the two writing processes

while True:
    try:
        with con:
            con.executemany("INSERT INTO data (counter) SELECT sleep(?)", [(counter,)])
    except sqlite3.OperationalError as e:
        print("Writing2 failed. Ex: {}".format(e))
    counter += 1
    time.sleep(0.25)

我知道INSERT/DML statement操作与SELECT不同,这与不同类型的锁(共享锁与排他锁)有关,但是我无法解释我自己是两个不同的结果。引发异常时,如何防止sqlite插入数据?

过程2 的except块中的

con.rollback()似乎是一种解决方法,但是我不确定这是否意味着其他警告。并且context manager是否应该自动应用?


答案的补充:

如果正在读取进程1 ,则进程2 中的异常是由于共享锁而在上下文管理器__exit__中的提交时发生的。如果另一个锁未决,则尝试回滚,因为较早发生了异常,并且使用设置了exc_type/exc_value/exc_tb的参数调用了__exit__方法。

1 个答案:

答案 0 :(得分:0)

这可能是锁之间的区别(SHARED诉RESERVED)。来自SQLite Doc):

  

如果另一个线程或进程在数据库上具有阻止数据库更新的共享锁,则尝试执行COMMIT可能还会导致SQLITE_BUSY返回代码。当COMMIT以这种方式失败时,事务将保持活动状态,并且在读者有机会清除之后,可以稍后重试COMMIT。

在读取的示例中,它是一个SHARED锁,该事务保持活动状态,并在下一次成功执行INSERT之后被提交。我知道API doc说上下文管理器将回滚。但是我也知道我所看到的。

连接对象具有in_transaction属性:

  

True(如果事务处于活动状态(有未提交的更改),False   除此以外。只读属性。

我在我的repro中添加了一些print,发现实际上,当插入失败时,事务保持有效。

before insert  0  in tx?  False
after insert  0  in tx?  False
before insert  1  in tx?  False
Writing failed at '1'. Ex: database is locked  in tx?  True
before insert  2  in tx?  True
Writing failed at '2'. Ex: database is locked  in tx?  True
before insert  3  in tx?  True
Writing failed at '3'. Ex: database is locked  in tx?  True
before insert  4  in tx?  True
after insert  4  in tx?  False
before insert  5  in tx?  False
Writing failed at '5'. Ex: database is locked  in tx?  True
before insert  6  in tx?  True
Writing failed at '6'. Ex: database is locked  in tx?  True
before insert  7  in tx?  True
Writing failed at '7'. Ex: database is locked  in tx?  True
before insert  8  in tx?  True
after insert  8  in tx?  False
before insert  9  in tx?  False
Writing failed at '9'. Ex: database is locked  in tx?  True
before insert  10  in tx?  True
Writing failed at '10'. Ex: database is locked  in tx?  True
before insert  11  in tx?  True

所有行均已插入。显式回滚将防止此问题。