Python sqlite3和并发


87

我有一个使用“线程”模块的Python程序。每秒一次,我的程序启动一个新线程,该线程从Web上获取一些数据,并将这些数据存储到我的硬盘中。我想使用sqlite3来存储这些结果,但是我无法使其正常工作。问题似乎与以下几行有关:

conn = sqlite3.connect("mydatabase.db")
  • 如果将这行代码放入每个线程中,则会收到OperationalError通知我数据库文件已锁定。我想这意味着另一个线程通过sqlite3连接打开了mydatabase.db并已将其锁定。
  • 如果将这行代码放入主程序中,并将连接对象(conn)传递给每个线程,则会收到一个ProgrammingError,表示在线程中创建的SQLite对象只能在同一线程中使用。

以前,我将所有结果存储在CSV文件中,并且没有任何这些文件锁定问题。希望使用sqlite可以实现。有任何想法吗?


5
我想指出,Python的最新版本包括应该解决此问题的新版本的sqlite3。
瑞安·福格

@RyanFugger您知道支持此功能的最早版本是什么吗?我正在使用2.7
notbad.jpeg 2013年

@RyanFugger AFAIK没有预先构建的版本,该版本包含已修复的更新版本的SQLite3。不过,您可以自己建立一个。
shezi 2013年

Answers:


44

您可以使用消费者-生产者模式。例如,您可以创建线程之间共享的队列。从Web上获取数据的第一个线程将该数据排队在共享队列中。拥有数据库连接的另一个线程使队列中的数据出队,并将其传递给数据库。


8
FWIW:较新版本的sqlite声称您可以跨线程(游标除外)共享连接和对象,但是在实际实践中我发现不是。
理查德·勒瓦瑟

是上述Evgeny Lazin的示例。
dugres

4
将数据库隐藏在共享队列后面是一个非常糟糕的解决方案,因为一般来说SQL和SQLite都已经具有内置的锁定机制,这种锁定机制可能比您可以自行构建的任何东西都要精细得多。
shezi 2013年

1
您需要阅读问题,那时还没有内置的锁定机制。由于性能原因,许多现代嵌入式数据库都缺少此机制(例如:LevelDB)。
Evgeny Lazin

179

与流行的看法相反,较新版本的sqlite3确实支持从多个线程进行访问。

这可以通过可选的关键字参数启用check_same_thread

sqlite.connect(":memory:", check_same_thread=False)

4
我遇到了不可预测的异常,甚至使用此选项的Python崩溃(Windows 32上的Python 2.7)。
reclosedev

4
根据文档,在多线程模式下,不能在多个线程中使用单个数据库连接。还有一个序列化模式
Casebash 2013年

1
没关系,只是找到了它:http
//sqlite.org/compile.html#threadsafe

1
@FrEaKmAn,对不起,很久以前,也没有:memory:数据库。之后,我没有在多个线程中共享sqlite连接。
reclosedev

2
@FrEaKmAn,我在使用多线程访问的python进程核心转储时遇到了这个问题。该行为是不可预测的,并且没有记录异常。如果我没有记错的话,那么读写都是正确的。到目前为止,这是我实际上已经使python崩溃的一件事:D。我没有尝试使用在线程安全模式下编译的sqlite进行此操作,但是当时,我没有重新编译系统默认sqlite的自由。我最终做了类似于Eric建议的操作,并禁用了线程兼容性
verboze

17

mail.python.org.pipermail.1239789

上找到以下内容,我找到了解决方案。我不知道为什么python文档对此选项一言不发。因此,我们必须在连接函数中添加一个新的关键字参数,然后才能在不同的线程中创建游标。因此使用:

sqlite.connect(":memory:", check_same_thread = False)

非常适合我。当然,从现在开始,我需要注意对数据库的安全多线程访问。无论如何,都想提供帮助。


(使用GIL,无论如何,我所看到的,对数据库的真正多线程访问实际上并没有太多方法)
Erik Aronesty

警告:Python文档对此check_same_thread选项有这样的说法:“当使用具有相同连接的多个线程时,用户应对写入操作进行序列化,以避免数据损坏。” 因此,可以,只要您的代码确保在任何给定时间只有一个线程可以写入数据库,就可以将SQLite与多个线程一起使用。如果没有,您可能会损坏数据库。
Ajedi32

14

切换到多处理。它好得多,可扩展性很好,可以通过使用多个CPU超越使用多个内核,并且接口与使用python线程模块相同。

或者,如Ali所建议的那样,只需使用SQLAlchemy的线程池机制。它会自动为您处理所有事情,并具有许多额外功能,仅引用其中一些功能:

  1. SQLAlchemy包括针对SQLite,Postgres,MySQL,Oracle,MS-SQL,Firebird,MaxDB,MS Access,Sybase和Informix的方言;IBM还发布了DB2驱动程序。因此,如果您决定离开SQLite,则不必重写应用程序。
  2. 工作单元系统是SQLAlchemy的对象关系映射器(ORM)的核心部分,将待处理的创建/插入/更新/删除操作组织到队列中,并批量刷新它们。为此,它对队列中所有已修改项执行拓扑“依赖关系排序”,以便遵守外键约束,并将多余的语句分组在一起,有时甚至可以对它们进行批处理。这样可以最大程度地提高效率和交易安全性,并最大程度地减少死锁的机会。

12

您根本不应该为此使用线程。对于扭曲而言,这是一项微不足道的任务,无论如何,它可能会带给您极大的进步。

仅使用一个线程,并在请求完成时触发一个事件来执行写操作。

扭曲将为您处理调度,回调等。它将把整个结果作为一个字符串传递给您,或者您可以通过流处理器来运行它(我有一个Twitter API和一个FriendFeed API,它们都向下载者发送事件,因为结果仍在下载中)。

根据您对数据的处理方式,您可以将完整结果转储到sqlite中,然后将其烹饪并转储,或者在读取数据时将其烹饪并最终转储。

我有一个非常简单的应用程序,它可以完成您在github上想要的操作。我称它为pfetch(并行提取)。它按时间表获取各个页面,将结果流式传输到文件,并在成功完成每个页面后选择运行脚本。它也可以做一些有条件的事情,例如有条件的GET,但是仍然可以成为您所做任何事情的良好基础。


7

或者,如果像我一样懒惰,则可以使用SQLAlchemy。它将为您处理线程(使用线程本地和一些连接池),并且它的配置方式甚至是可配置的

为了获得额外的好处,如果/当您意识到/确定将Sqlite用于任何并发应用程序将是一场灾难时,您将不必更改代码即可使用MySQL,Postgres或其他任何东西。您可以切换。


1
为什么它没有在官方网站上的任何地方指定Python版本?
显示名称

3

您需要session.close()每个数据库事务之后使用,以便在同一线程中使用同一游标,而不在多线程中使用同一游标,这会导致此错误。



0

我喜欢Evgeny的回答-队列通常是实现线程间通信的最佳方法。为了完整性,这里有一些其他选择:

  • 产生的线程使用完数据库后,请关闭它。这样可以解决您的问题OperationalError,但是由于性能开销,像这样打开和关闭连接通常是不可以的。
  • 不要使用子线程。如果每秒执行一次的任务相当轻巧,则可以执行获取和存储操作,然后休眠直到合适的时机。这是不可取的,因为获取和存储操作可能花费> 1秒的时间,并且您失去了使用多线程方法获得的多路复用资源的好处。

0

您需要为程序设计并发。SQLite有明确的限制,您需要遵守这些限制,请参阅FAQ(以及以下问题)。


0

Scrapy似乎是我问题的潜在答案。它的主页描述了我的确切任务。(尽管我不确定代码的稳定性如何。)


0

我想借此看看y_serial Python模块的数据持久性:http://yserial.sourceforge.net

处理围绕单个SQLite数据库的死锁问题。如果对并发的需求变得沉重,则可以轻松地建立许多数据库的Farm类,以在随机时间内分散负载。

希望这对您的项目有帮助...应该足够简单,可以在10分钟内实施。


0

我在上述任何答案中都找不到基准,因此我编写了一个测试以对所有基准进行基准测试。

我尝试了3种方法

  1. 从SQLite数据库顺序读取和写入
  2. 使用ThreadPoolExecutor进行读写
  3. 使用ProcessPoolExecutor进行读取/写入

基准测试的结果和要点如下

  1. 顺序读取/顺序写入效果最佳
  2. 如果必须并行处理,请使用ProcessPoolExecutor并行读取
  3. 不要使用ThreadPoolExecutor或使用ProcessPoolExecutor进行任何写操作,因为这样会遇到数据库锁定错误,并且您将不得不再次尝试重新插入块

您可以在我的SO答案中找到基准测试的代码和完整解决方案,在这里希望对您有所帮助!


-1

锁定数据库出错的最可能原因是必须发出

conn.commit()

完成数据库操作后。否则,数据库将被写锁定并保持这种状态。其他等待写入的线程将在一段时间后超时(默认设置为5秒,有关详细信息,请参见http://docs.python.org/2/library/sqlite3.html#sqlite3.connect)。 。

正确并发插入的示例如下:

import threading, sqlite3
class InsertionThread(threading.Thread):

    def __init__(self, number):
        super(InsertionThread, self).__init__()
        self.number = number

    def run(self):
        conn = sqlite3.connect('yourdb.db', timeout=5)
        conn.execute('CREATE TABLE IF NOT EXISTS threadcount (threadnum, count);')
        conn.commit()

        for i in range(1000):
            conn.execute("INSERT INTO threadcount VALUES (?, ?);", (self.number, i))
            conn.commit()

# create as many of these as you wish
# but be careful to set the timeout value appropriately: thread switching in
# python takes some time
for i in range(2):
    t = InsertionThread(i)
    t.start()

如果您喜欢SQLite,或者拥有其他可用于SQLite数据库的工具,或者想要用SQLite db文件替换CSV文件,或者必须执行平台间IPC之类的罕见工作,那么SQLite是一个很好的工具,非常适合此目的。如果感觉不合适,不要让自己承受使用其他解决方案的压力!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.