Python 中的基本并发 SQLite 编写器

Jey*_*mon 6 python sqlite concurrency

我创建了一个非常基本的脚本,定期将一些数据写入数据库:

测试.py

import sqlite3
import sys
import time

DB_CREATE_TABLE = 'CREATE TABLE IF NOT EXISTS items (item TEXT)'
DB_INSERT = 'INSERT INTO items VALUES (?)'
FILENAME = 'test.db'


def main():
    index = int()
    c = sqlite3.connect(FILENAME)
    c.execute(DB_CREATE_TABLE)
    c.commit()

    while True:
        item = '{name}_{index}'.format(name=sys.argv[1], index=index)
        c.execute(DB_INSERT, (item,))
        c.commit()
        time.sleep(1)
        index += 1

    c.close()


if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

现在我可以通过多次运行脚本来实现简单的并发:

python3 test.py foo &
python3 test.py bar &
Run Code Online (Sandbox Code Playgroud)

我尝试阅读一些有关脚本同时写入同一数据库文件的文章,但我仍然不确定我的脚本将如何处理此类事件,并且我不知道如何测试它。

我的期望是,在不太可能发生的情况下,当我的脚本的两个实例尝试在同一毫秒内写入数据库时​​,后一个实例将只是默默地等待,直到前一个实例完成其工作。

我当前的实施是否满足我的期望?如果没有,发生此类事件时它会如何表现以及如何修复?

Jey*_*mon 6

长话短说

这个脚本满足预期。

解释

当两个脚本实例尝试同时写入的罕见事件发生时,第一个脚本实例会锁定数据库,而第二个脚本实例会静默等待一段时间,直到第一个脚本实例完成其事务,以便数据库解锁以再次写入。

更准确地说,第二个脚本实例等待 5 秒(默认情况下),然后引发OperationalError并显示消息database is locked。正如 @roganjosh 评论的那样,这种行为实际上是 Python SQLite 包装器特有的。该文档指出

当多个连接访问数据库并且其中一个进程修改数据库时,SQLite 数据库将被锁定,直到提交该事务。timeout 参数指定连接应等待锁定消失的时间,直到引发异常。超时参数的默认值为 5.0(五秒)。

测试

为了演示两个实例的碰撞事件,我修改了函数main

def main():
    c = sqlite3.connect(FILENAME)
    c.execute(DB_CREATE_TABLE)
    c.commit()
    print('{} {}: {}'.format(time.time(), sys.argv[1], 'trying to insert ...'))

    try:
        c.execute(DB_INSERT, (sys.argv[1],))
    except sqlite3.OperationalError as e:
        print('{} {}: {}'.format(time.time(), sys.argv[1], e))
        return

    time.sleep(int(sys.argv[2]))
    c.commit()
    print('{} {}: {}'.format(time.time(), sys.argv[1], 'done')) 
    c.close()
Run Code Online (Sandbox Code Playgroud)

文档指出数据库将被锁定,直到事务提交为止。因此,只需在交易期间睡觉就足以测试它。

测试1

我们运行以下命令:

python3 test.py first 10 & sleep 1 && python3 test.py second 0
Run Code Online (Sandbox Code Playgroud)

第一个实例正在运行,1 秒后第二个实例正在运行。第一个实例创建一个 10 秒长的事务,在此期间第二个实例尝试写入数据库、等待然后引发异常。该日志表明:

1540307088.6203635 first: trying to insert ...
1540307089.6155508 second: trying to insert ...
1540307094.6333485 second: database is locked
1540307098.6353421 first: done
Run Code Online (Sandbox Code Playgroud)

测试2

我们运行以下命令:

python3 test.py first 3 & sleep 1 && python3 test.py second 0
Run Code Online (Sandbox Code Playgroud)

第一个实例正在运行,1 秒后第二个实例正在运行。第一个实例创建一个 3 秒长的事务,在此期间第二个实例尝试写入数据库并等待。由于它是在 1 秒后创建的,因此必须等待 3 秒 - 1 秒 = 2 秒,这小于默认的 5 秒,因此两个事务都会成功完成。该日志表明:

1540307132.2834115 first: trying to insert ...
1540307133.2811155 second: trying to insert ...
1540307135.2912169 first: done
1540307135.3217440 second: done
Run Code Online (Sandbox Code Playgroud)

结论

事务完成所需的时间(毫秒)明显小于锁定时间限制(5 秒),因此在这种情况下,脚本确实符合预期。但正如@HarlyH。评论说,事务在队列中等待提交,因此对于频繁使用或非常大的数据库来说,这不是一个好的解决方案,因为与数据库的通信会变得很慢。

  • 很好,这是我第一次在 SO 上看到围绕这个问题的一组测试代码。这里已经有足够的信息来满足我自己的应用程序。看看我们是否能找到默认的 SQLite 重试频率(每 0.1 秒?或更长时间?)可能会很有趣,因为理论上,如果它每秒尝试一次并且您的使用量更大,则可能会发生多次冲突并保持未锁定时丢失点。 (2认同)