如何在django中避免“数据库被锁定”sqlite3错误?

时间:2017-12-11 21:17:49

标签: django multithreading python-3.x sqlite

Django版本1.11,sqlite3版本3.11。

我正在使用WAL模式和长时间超时:

from django.apps import AppConfig
from django.db.backends.signals import connection_created


class SQLite3Config(AppConfig):
    name = 'sqlite3_config'

    def ready(self):
        connection_created.connect(configure_sqlite)


# noinspection PyUnusedLocal
def configure_sqlite(sender, connection, **_):
    if connection.vendor == 'sqlite':
        cursor = connection.cursor()
        cursor.execute('PRAGMA journal_mode=WAL;')
        cursor.execute('PRAGMA busy_timeout=5000;')

我想保留sqlite3而不是移动到mysql或postgres,因为应用程序很小并且由多个服务器上的用户安装。

我相信WAL应该通过序列化来允许“并发”写入。当一起收到小爆发(半打左右)时,观察到“数据库被锁定”的问题。

我可以用shell重现shell中的问题。 django模型方法只是设置一个标志并保存模型:

def activate(self):
    self.activate = True
    self.save()

当我使用线程时,如果我启动了几个同时尝试它的线程,我发现它失败了。没有等待,因此不涉及超时。错误发生在5秒忙碌超时(不到两秒钟)之前:

In [2]: [NGThread(notifier_group.id).start() for notifier_group in NotifierGroup.objects.all()[:2]]
Out[2]: [None, None]

In [3]: [NGThread(notifier_group.id).start() for notifier_group in NotifierGroup.objects.all()[:3]]
Out[3]: [None, None, None]

In [4]: [NGThread(notifier_group.id).start() for notifier_group in NotifierGroup.objects.all()[:4]]
Out[4]: [None, None, None, None]

In [5]: Exception in thread Thread-97:
Traceback (most recent call last):
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 65, in execute
    return self.cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: database is locked

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
    self.run()
  File "/home/paul/wk/cliosoft/sosadmin/scratch.py", line 41, in run
    toggle_active(notifier_group)
  File "/home/paul/wk/cliosoft/sosadmin/scratch.py", line 30, in toggle_active
    model.activate()
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 67, in activate
    self.save()
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 33, in save
    self.verify()
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 46, in verify
    self.create_notifier(base_spec, model_set, group_event_condition)
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 57, in create_notifier
    notifier.users = self.users.all()
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 534, in __set__
    manager.set(value)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 1004, in set
    self.add(*new_objs)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 931, in add
    self._add_items(self.source_field_name, self.target_field_name, *objs)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 1100, in _add_is
    for obj_id in new_ids
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/query.py", line 442, in bulk_create
    ids = self._batched_insert(objs_without_pk, fields, batch_size)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/query.py", line 1083, in _batched_insert
    self._insert(item, fields=fields, using=self.db)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/query.py", line 1060, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/sql/compiler.py", line 1099, in execute_sql
    cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 80, in execute
    return super(CursorDebugWrapper, self).execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 65, in execute
    return self.cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/utils.py", line 94, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/utils/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 65, in execute
    return self.cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: database is locked

Exception in thread Thread-98:
Traceback (most recent call last):
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 65, in execute
    return self.cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: database is locked

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
    self.run()
  File "/home/paul/wk/cliosoft/sosadmin/scratch.py", line 41, in run
    toggle_active(notifier_group)
  File "/home/paul/wk/cliosoft/sosadmin/scratch.py", line 28, in toggle_active
    model.deactivate()
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 72, in deactivate
    self.save()
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 33, in save
    self.verify()
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 46, in verify
    self.create_notifier(base_spec, model_set, group_event_condition)
  File "/home/paul/wk/cliosoft/sosadmin/notifications/models/notifier_group.py", line 57, in create_notifier
    notifier.users = self.users.all()
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 534, in __set__
    manager.set(value)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 1004, in set
    self.add(*new_objs)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 931, in add
    self._add_items(self.source_field_name, self.target_field_name, *objs)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 1100, in _add_is
    for obj_id in new_ids
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/query.py", line 442, in bulk_create
    ids = self._batched_insert(objs_without_pk, fields, batch_size)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/query.py", line 1083, in _batched_insert
    self._insert(item, fields=fields, using=self.db)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/query.py", line 1060, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/models/sql/compiler.py", line 1099, in execute_sql
    cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 80, in execute
    return super(CursorDebugWrapper, self).execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 65, in execute
    return self.cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/utils.py", line 94, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/utils/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/utils.py", line 65, in execute
    return self.cursor.execute(sql, params)
  File "/home/paul/.virtualenvs/sosadmin/lib/python3.5/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: database is locked

2 个答案:

答案 0 :(得分:0)

我在发行说明中看不到任何内容,但我已升级到Django 2.0,此问题现已消失。随着在sqlite3中设置WAL,它现在运行得非常好。

答案 1 :(得分:0)

随着系统的扩展,此错误再次发生。由于某些原因,我被要求使用sqlite3。

Django中的timeout选项似乎被忽略了。我尝试使用sqlite周围的包装程序在db游标级别上自己实现它,并且遇到了相同的问题(在该级别上,您不能退出到足以放弃事务并尝试建立新连接的程度)。

我会喜欢上下文管理器,例如with db_locked_retries():...,但是无法在with上下文中进行迭代,因此我这样创建了db_retry函数:

import time
import logging
import random

from django.conf import settings
import django.db

logger = logging.getLogger('django.db.backends')


def db_retry(fn, timeout=None):
    """Call fn with no arguments. If OperationalError exception, make retries until timeout has passed"""
    timeout = timeout or settings.DATABASES['default'].get('OPTIONS', dict()).get('timeout', 5)
    now = time.time()
    give_up_time = now + timeout
    retries = 0
    while now < give_up_time:
        now = time.time()
        try:
            result = fn()
            if retries:
                logger.warning(f'db_retry: Succeeded after {retries} retries')
            return result
        except django.db.OperationalError as exception:
            msg = str(exception)
            if 'locked' in msg:  # pragma: no cover
                retries += 1
                wait_time = random.uniform(1, timeout / 10)
                logger.warning(f'db_retry: {msg}: Retrying after {wait_time} seconds')
                django.db.close_old_connections()
                time.sleep(wait_time)
            else:  # pragma: no cover
                logger.exception(f'db_retry: {msg}: Giving up')
                raise

由于在模型方法等中定义传递给该函数的函数非常容易。这可以解决问题并成功使用超时重试访问。在我的特定用例中,当前每三十分钟处理一次数百次并发使用和重试。

我只需要“包装”到目前为止可能进行多次写操作的模型代码。