我试图在我的python
程序中找出一个错误,其中有两个进程正在访问sqlite
数据库。
如果由于某种原因插入失败,我设计了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
插入数据?
con.rollback()
似乎是一种解决方法,但是我不确定这是否意味着其他警告。并且context manager是否应该自动应用?
答案的补充:
如果正在读取进程1 ,则进程2 中的异常是由于共享锁而在上下文管理器__exit__
中的提交时发生的。如果另一个锁未决,则尝试回滚,因为较早发生了异常,并且使用设置了exc_type/exc_value/exc_tb
的参数调用了__exit__
方法。
答案 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
所有行均已插入。显式回滚将防止此问题。