12

我目前正在使用 tornado 中的 Web 服务器,但是在尝试同时访问数据库的不同代码位时遇到问题。

我通过简单地使用一个基本上可以做到这一点的查询功能来简化这一点(但稍微高级一些):

def query(command, arguments = []):
    db = sqlite3.open("models/data.db")
    cursor = db.cursor()
    cursor.execute(command, arguments)
    result = cursor.findall()
    db.close()
    return result

我只是想知道在每次查询后重新打开数据库的效率如何(我猜这是一个非常大的恒定时间操作,或者它会缓存一些东西吗?),以及是否有更好的方法来做到这一点。

4

4 回答 4

18

我正在添加自己的答案,因为我不同意当前接受的答案。它声明该操作不是线程安全的,但这是完全错误的——SQLite 使用适合其当前平台的文件锁定来确保所有访问都符合ACID

在 Unix 系统上,这将是fcntl()flock()锁定,这是每个文件句柄的锁定。结果,每次建立新连接的代码总是会分配一个新的文件句柄,因此 SQLite 自己的锁定将防止数据库损坏。这样做的结果是,在 NFS 共享或类似设备上使用 SQLite 通常是个坏主意,因为这些通常不能提供特别可靠的锁定(不过,它确实取决于您的 NFS 实现)。

正如@abernert 已经在评论中指出的那样,SQLite 存在线程问题,但这与线程之间共享单个连接有关。正如他还提到的那样,这意味着如果您使用应用程序范围的池,如果第二个线程从池中拉出回收的连接,则会出现运行时错误。这些也是您在测试中可能没有注意到的那种恼人的错误(轻负载,可能只有一个线程在使用),但以后很容易引起头痛。Martijn Pieters 后来关于线程本地池的建议应该可以正常工作。

正如版本3.3.1的SQLite FAQ中所述,只要线程不持有任何锁,在线程之间传递连接实际上是安全的 - 这是 SQLite 的作者添加的一个让步,尽管它对线程的使用持批评态度一般的。任何明智的连接池实现都将始终确保在替换池中的连接之前所有内容都已提交或回滚,因此实际上如果不是 Python 检查共享,应用程序全局池可能是安全的我相信即使使用了更新版本的 SQLite,它仍然存在。当然,我的 Python 2.7.3 系统有一个报告3.7.9的模块,但它仍然抛出一个sqlite3sqlite_version_infoRuntimeError如果您从多个线程访问它。

在任何情况下,当检查存在时,即使底层 SQLite 库支持,连接也无法有效地共享。

至于你原来的问题,每次创建一个新连接肯定比保持一个连接池效率低,但是已经提到这需要一个线程本地池,实现起来有点麻烦。创建与数据库的新连接的开销本质上是打开文件并读取标题以确保它是有效的 SQLite 文件。实际执行语句的开销更高,因为它需要查看并执行相当多的文件 I/O,因此大部分工作实际上被推迟到语句执行和/或提交。

然而,有趣的是,至少在 Linux 系统上,我查看过执行语句的代码会重复读取文件头的步骤——因此,打开一个新的连接并不会因为最初的读取而变得那么糟糕打开连接时,会将标头拉入系统的文件系统缓存。所以它归结为打开单个文件句柄的开销。

我还应该补充一点,如果您希望您的代码能够扩展到高并发,那么 SQLite 可能是一个糟糕的选择。正如他们自己的网站所指出的那样,它并不真正适合高并发,因为随着并发线程数量的增加,必须通过单个全局锁来压缩所有访问的性能开始受到影响。如果您为了方便而使用线程,那很好,但如果您真的期望高度并发,那么我会避免使用 SQLite。

简而言之,我认为你每次打开的方法实际上并没有那么糟糕。线程本地池可以提高性能吗?可能是。这种性能提升会很明显吗?在我看来,除非你看到相当高的连接率,而且那时你会有很多线程,所以你可能还是想离开 SQLite,因为它不能很好地处理并发。如果您决定使用一个,请确保它在将连接返回到池之前清理连接 - SQLAlchemy具有一些连接池功能,即使您不希望所有 ORM 层都在顶部,您也可能会发现这些功能很有用。

编辑

正如相当合理地指出的那样,我应该附上真实的时间。这些来自功率相当低的 VPS:

>>> timeit.timeit("cur = conn.cursor(); cur.execute('UPDATE foo SET name=\"x\"
    WHERE id=3'); conn.commit()", setup="import sqlite3;
    conn = sqlite3.connect('./testdb')", number=100000)
5.733098030090332
>>> timeit.timeit("conn = sqlite3.connect('./testdb'); cur = conn.cursor();
    cur.execute('UPDATE foo SET name=\"x\" WHERE id=3'); conn.commit()",
    setup="import sqlite3", number=100000)
16.518677949905396

您可以看到大约 3 倍的差异,这并不是微不足道的。但是,绝对时间仍然是亚毫秒,所以除非您对每个请求进行大量查询,否则可能还有其他地方需要首先优化。如果您进行大量查询,一个合理的折衷方案可能是每个请求建立一个新连接(但没有池的复杂性,每次都重新连接)。

对于读取(即SELECT),则每次连接的相对开销会更高,但挂钟时间的绝对开销应该是一致的。

正如在这个问题的其他地方已经讨论过的那样,您应该使用真实的查询进行测试,我只是想记录我为得出结论所做的工作。

于 2013-01-25T11:15:25.157 回答
2

如果您想知道某件事的效率有多低,请编写一个测试并自己看看。

一旦我修复了错误以使您的示例首先工作,并编写代码来创建一个测试用例来运行它,弄清楚如何计时它timeit就像通常一样微不足道。

http://pastebin.com/rd39vkVa

那么,当你运行它时会发生什么?

$ python2.7 sqlite_test.py 10000
reopen: 2.02089715004
reuse:  0.278793811798
$ python3.3 sqlite_test.py 10000
reopen: 1.8329595914110541
reuse:  0.2124928394332528
$ pypy sqlite_test.py 10000
reopen: 3.87628388405
reuse:  0.760829925537

因此,打开数据库所花费的时间大约是对几乎没有返回任何内容的空表运行简单查询的时间的 4 到 8 倍。这是你最坏的情况。

于 2013-01-24T23:49:55.227 回答
0

这是非常低效的,并且启动时不是线程安全的。

改用一个不错的连接池库。sqlalchemy提供池和更多功能,或者为 sqlite 找到一个更轻量级的池。

于 2013-01-24T21:50:39.820 回答
0

为什么不每隔 N 秒重新连接一次。在我的 30-40 行瓶子的 ajax 前瞻/数据库服务中,我每小时重新连接一次以获取更新,如果您需要处理实时数据,有更好的数据库适合:

t0 = time.time()
con = None
connect_interval_in_sec = 3600

def myconnect(dbfile=<path to dbfile>):
    try:
        mycon = sqlite3.connect(dbfile)
        cur = mycon.cursor()
        cur.execute('SELECT SQLITE_VERSION()')
        data = cur.fetchone()
    except sqlite3.Error as e:
        print("Error:{}".format(e.args[0]))
        sys.exit(1)
    return mycon

在主循环中:

if con is None or time.time()-t0 > connect_interval_in_sec:
    con = myconnect()
    t0 = time.time()
<do your query stuff on con>
于 2018-03-01T01:20:30.447 回答