Кэширование / повторное использование соединения с БД для последующего использования - PullRequest
0 голосов
/ 05 ноября 2018

Я сохраняю подключение к базе данных пользователя. В первый раз, когда они вводят свои учетные данные, я делаю что-то вроде следующего:

self.conn = MySQLdb.connect (
    host = 'aaa',
    user = 'bbb',
    passwd = 'ccc',
    db = 'ddd',
    charset='utf8'
)
cursor = self.conn.cursor()
cursor.execute("SET NAMES utf8")
cursor.execute('SET CHARACTER SET utf8;')
cursor.execute('SET character_set_connection=utf8;')

Теперь у меня есть conn, готовый пойти на все запросы пользователя. Однако я не хочу повторно подключаться каждый раз, когда загружается view. Как бы я сохранил это «открытое соединение», чтобы я мог сделать что-то подобное в представлении:

def do_queries(request, sql):
    user = request.user
    conn = request.session['conn']
    cursor = request.session['cursor']
    cursor.execute(sql)

Обновление : кажется, что вышеупомянутое невозможно и не является хорошей практикой, поэтому позвольте мне перефразировать то, что я пытаюсь сделать:

У меня есть редактор SQL, который пользователь может использовать после ввода своих учетных данных (например, Navicat или SequelPro). Обратите внимание, что это НЕ соединение django db по умолчанию - я не знаю учетные данные заранее. Теперь, когда пользователь «подключился», я бы хотел, чтобы он мог выполнять столько запросов, сколько ему нужно, без необходимости повторного подключения каждый раз, когда они это делают. Например - повторить еще раз - что-то вроде Navicat или SequelPro. Как бы это сделать, используя python, django или mysql? Возможно, я не совсем понимаю, что здесь необходимо (кэширование соединения, пул соединений и т. Д.), Поэтому любые предложения или помощь будут с благодарностью.

Ответы [ 6 ]

0 голосов
/ 16 ноября 2018

Я действительно поделился своим решением этой проблемы. Здесь я создал пул соединений, с которым вы можете указать максимальное количество, а затем поставил в очередь асинхронные запросы запросов через этот канал. Таким образом, вы можете оставить определенное количество открытых соединений, но это приведет к очереди и асинхронности пула и сохранит скорость, к которой вы привыкли.

Для этого требуется gevent и postgres.

Python Postgres psycopg2 Резьбовое соединениеПул исчерпан

0 голосов
/ 15 ноября 2018

Это не очень хорошая идея делать это синхронно в контексте веб-приложения. Помните, что вашему приложению может потребоваться работа в многопроцессорном режиме, и вы не могли нормально разделить соединение между процессами. Таким образом, если вы создаете соединение для своего пользователя в процессе, нет никакой гарантии получить запрос на тот же запрос. Возможно, лучшей идеей будет иметь одного фонового рабочего процесса, который обрабатывает соединения в нескольких потоках (поток за сеанс) для выполнения запросов к базе данных и получения результатов в веб-приложении. Ваше приложение должно назначить уникальный идентификатор каждому сеансу, а фоновый работник отслеживает каждый поток, используя идентификатор сеанса. Вы можете использовать celery или любые другие очереди задач, поддерживающие асинхронный результат. Так что дизайн будет выглядеть примерно так:

             |<--|        |<--------------|                   |<--|
user (id: x) |   | webapp |   | queue |   | worker (thread x) |   | DB
             |-->|        |-->|       |-->|                   |-->|

Также вы можете создать очередь для каждого пользователя, пока у него не будет активного сеанса, в результате вы можете запустить отдельный фоновый процесс для каждого сеанса.

0 голосов
/ 15 ноября 2018

Я не понимаю, зачем вам здесь кэшированное соединение, и почему бы просто не переподключаться при каждом запросе кеширования учетных данных пользователя где-нибудь, но в любом случае я постараюсь наметить решение, которое может соответствовать вашим требованиям.

Я бы предложил сначала рассмотреть более общую задачу - кэшировать что-то между последующими запросами, которые ваше приложение должно обработать, и не может сериализоваться в сеансы django. В вашем конкретном случае это общее значение будет соединение с базой данных (или несколько соединений). Давайте начнем с простой задачи разделения простой счетной переменной между запросами, просто чтобы понять, что на самом деле происходит под капотом.

Удивительно, но ни в одном ответе не упоминалось ничего о веб-сервере, который вы могли бы использовать! На самом деле существует несколько способов обработки одновременных подключений в веб-приложениях:

  1. Имея несколько процессов , каждый запрос поступает в один из них случайным образом
  2. Имея несколько потоков , каждый запрос обрабатывается случайным потоком
  3. стр.1 и стр.2 в сочетании
  4. Различные асинхронные методы, когда есть один процесс + цикл обработки событий обработка запросов с предупреждением, что обработчики запросов не должны блок на долгое время

Исходя из собственного опыта, стр. 1-2 подходят для большинства типичных веб-приложений. Apache1.x может работать только с п.1, Apache2.x может обрабатывать все 1-3.

Давайте запустим следующее приложение django и запустим однопроцессный веб-сервер gunicorn . Я собираюсь использовать gunicorn, потому что его довольно легко настроить в отличие от apache (личное мнение: -)

views.py

import time

from django.http import HttpResponse

c = 0

def main(self):
    global c
    c += 1
    return HttpResponse('val: {}\n'.format(c))


def heavy(self):
    time.sleep(10)
    return HttpResponse('heavy done')

urls.py

from django.contrib import admin
from django.urls import path

from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.main, name='main'),
    path('heavy/', views.heavy, name='heavy')
]

Запуск в режиме одного процесса:

gunicorn testpool.wsgi -w 1

Вот наше дерево процессов - есть только один рабочий, который будет обрабатывать ВСЕ запросы

pstree 77292
-+= 77292 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1
 \--- 77295 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1

Попытка использовать наше приложение:

curl 'http://127.0.0.1:8000'
val: 1

curl 'http://127.0.0.1:8000'
val: 2

curl 'http://127.0.0.1:8000'
val: 3

Как видите, вы можете легко разделить счетчик между последующими запросами. Проблема в том, что вы можете обслуживать только один запрос параллельно. Если вы запрашиваете / heavy / в одной вкладке, / не будет работать, пока / heavy не будет выполнено

Теперь давайте используем 2 рабочих процесса:

gunicorn testpool.wsgi -w 2

Вот так будет выглядеть дерево процессов:

 pstree 77285
-+= 77285 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
 |--- 77288 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2
 \--- 77289 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 2

Тестирование нашего приложения:

curl 'http://127.0.0.1:8000'
val: 1

curl 'http://127.0.0.1:8000'
val: 2

curl 'http://127.0.0.1:8000'
val: 1

Первые два запроса были обработаны первым worker process, а 3-й - вторым рабочим процессом, который имеет собственное пространство памяти, поэтому вместо этого вы видите 1 3 . Обратите внимание, что ваш вывод может отличаться, потому что процессы 1 и 2 выбираются случайным образом. Но рано или поздно вы попадете на другой процесс.

Это не очень полезно для нас, потому что нам нужно обрабатывать несколько одновременных запросов, и нам нужно каким-то образом обрабатывать наш запрос конкретным процессом, который не может быть выполнен в общем случае.

Большинство пулирующих технологий, выходящих из коробки, будут кэшировать соединения только в рамках одного процесса, если ваш запрос обслуживается другим процессом - NEW соединение должно быть сделано.

Позволяет перейти к темам

gunicorn testpool.wsgi -w 1 --threads 2

Опять же - только 1 процесс

pstree 77310
-+= 77310 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1 --threads 2
 \--- 77313 oleg /Users/oleg/.virtualenvs/test3.4/bin/python /Users/oleg/.virtualenvs/test3.4/bin/gunicorn testpool.wsgi -w 1 --threads 2

Теперь, если вы запустите / heavy на одной вкладке, вы все равно сможете запросить / , и ваш счетчик будет сохраняться между запросами! Даже если количество потоков увеличивается или уменьшается в зависимости от вашей рабочей нагрузки, оно все равно должно работать нормально.

Проблемы : вам нужно синхронизировать доступ к общей переменной, как это, используя технику синхронизации потоков Python ( read more ). Другая проблема заключается в том, что одному и тому же пользователю может потребоваться выполнить несколько запросов параллельно - то есть открыть несколько вкладок.

Чтобы справиться с этим, вы можете открывать несколько соединений по первому запросу, когда у вас есть доступные учетные данные БД.

Если пользователю нужно больше подключений, чем ваше приложение может подождать, пока соединение не станет доступным.

Вернуться к вашему вопросу

Вы можете создать класс, который будет иметь следующие методы:

from contextlib import contextmanager

class ConnectionPool(object):

   def __init__(self, max_connections=4):
      self._pool = dict()
      self._max_connections = max_connections

   def preconnect(self, session_id, user, password):
       # create multiple connections and put them into self._pool
       # ...

    @contextmanager
    def get_connection(sef, session_id):
       # if have an available connection:
            # mark it as allocated
            # and return it
            try:
                yield connection
           finally:
              # put it back to the pool
              # ....
       # else
        # wait until there's a connection returned to the pool by another thread

pool = ConnectionPool(4)

def some_view(self):
     session_id = ...
     with pool.get_connection(session_id) as conn:
        conn.query(...)

Это не полное решение - вам нужно каким-то образом удалить устаревшие соединения, которые долгое время не использовались.

Если пользователь возвращается через длительное время и его соединение закрыто, ему нужно будет снова предоставить свои учетные данные - надеюсь, это нормально с точки зрения вашего приложения.

Также имейте в виду, что у python threads есть свои потери производительности, не уверен, что это проблема для вас.

Я не проверял его на apache2 (слишком большая нагрузка на конфигурацию, я не использовал его целую вечность и обычно использую uwsgi ), но он также должен работать там - был бы рад услышать от вас ответ если вам удастся запустить его)

И также не забывайте о p.4 (асинхронный подход) - вряд ли вы сможете использовать его на apache, но это стоит изучить - ключевые слова: django + gevent , Джанго + Асинсио . У него есть свои плюсы и минусы, и он может сильно повлиять на реализацию вашего приложения, поэтому трудно предложить какое-либо решение, не зная деталей вашего приложения подробно

0 голосов
/ 14 ноября 2018

Я просто делюсь своими знаниями здесь.

Установите PyMySQL для использования MySql

Для Python 2.x

pip install PyMySQL

Для Python 3.x

pip3 install PyMySQL

1. Если вы открыты для использования Django Framework, тогда очень легко выполнить SQL-запрос без повторного подключения.

В файле setting.py добавьте следующие строки

DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'test',
            'USER': 'test',
            'PASSWORD': 'test',
            'HOST': 'localhost',
            'OPTIONS': {'charset': 'utf8mb4'},
        }
    }

В файле views.py добавьте эти строки, чтобы получить данные. Вы можете настроить свой запрос в соответствии с вашими потребностями

from django.db import connection
def connect(request):
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM Tablename");
    results = cursor.fetchall()
    return results 

Вы получите желаемый результат.

Нажмите здесь для получения дополнительной информации об этом

2. Для питона Tkinter

from Tkinter import *
import MySQLdb

db = MySQLdb.connect("localhost","root","root","test")
# prepare a cursor object using cursor() method
cursor = db.cursor()
cursor.execute("SELECT * FROM Tablename")
if cursor.fetchone() is not None:
    print("In If")
else:
    print("In Else")
cursor.close()

См. это для получения дополнительной информации

PS: Вы можете проверить эту ссылку, чтобы задать вопрос о повторном использовании соединения с БД для дальнейшего использования.

Как включить автоматическое повторное подключение клиента MySQL к MySQLdb?

0 голосов
/ 13 ноября 2018

Я не эксперт в этой области, но я верю, что PgBouncer сделает эту работу за вас, при условии, что вы можете использовать серверную часть PostgreSQL (это одна деталь, которую вы не делали) прояснить). PgBouncer - это пул соединений , который позволяет вам повторно использовать соединения, избегая затрат на соединение при каждом запросе.

Согласно их документации :

пользователь, пароль

Если задано значение user =, все подключения к целевой базе данных будут выполняться с указанным пользователем, что означает, что для этой базы данных будет только один пул.

В противном случае PgBouncer попытается войти в базу данных назначения с именем клиента, что означает, что на пользователя будет один пул.

Таким образом, вы можете иметь один пул соединений на пользователя, который звучит так же, как вы хотите.

В MySQL, модуль mysql.connector.pooling позволяет вам выполнять некоторые пулы соединений, хотя я не уверен, что вы можете делать пулы для отдельных пользователей. Учитывая, что вы можете установить имя пула, я думаю, вы могли бы использовать имя пользователя для идентификации пула.

Независимо от того, что вы используете, у вас, скорее всего, будут случаи, когда переподключение неизбежно (пользователь подключается, делает несколько вещей, уходит на встречу и обед, возвращается и хочет предпринять дополнительные действия).

0 голосов
/ 09 ноября 2018

Вы можете использовать контейнер IoC для хранения одноэлементного провайдера. По сути, вместо того, чтобы создавать новое соединение каждый раз, оно будет создавать его только один раз (вызывается первый раз ConnectionContainer.connection_provider()), а после этого оно всегда будет возвращать ранее построенное соединение.

Для моего примера вам понадобится пакет dependency-injector:

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class ConnectionProvider():
    def __init__(self, host, user, passwd, db, charset):
        self.conn = MySQLdb.connect(
            host=host,
            user=user,
            passwd=passwd,
            db=db,
            charset=charset
        )


class ConnectionContainer(containers.DeclarativeContainer):
    connection_provider = providers.Singleton(ConnectionProvider,
                                              host='aaa',
                                              user='bbb',
                                              passwd='ccc',
                                              db='ddd',
                                              charset='utf8')


def do_queries(request, sql):
    user = request.user
    conn = ConnectionContainer.connection_provider().conn
    cursor = conn.cursor()
    cursor.execute(sql)

Я жестко запрограммировал здесь строку подключения, но также возможно сделать ее переменной в зависимости от изменяемой конфигурации. В этом случае вы также можете создать контейнер для файла конфигурации и сделать так, чтобы контейнер соединений считал его конфигурацию оттуда. Затем вы устанавливаете конфигурацию во время выполнения. Следующим образом:

import dependency_injector.containers as containers
import dependency_injector.providers as providers

class ConnectionProvider():
    def __init__(self, connection_config):
        self.conn = MySQLdb.connect(**connection_config)

class ConfigContainer(containers.DeclarativeContainer):
    connection_config = providers.Configuration("connection_config")

class ConnectionContainer(containers.DeclarativeContainer):
    connection_provider = providers.Singleton(ConnectionProvider, ConfigContainer.connection_config)

def do_queries(request, sql):
    user = request.user
    conn = ConnectionContainer.connection_provider().conn
    cursor = conn.cursor()
    cursor.execute(sql)


# run code
my_config = {
    'host':'aaa',
    'user':'bbb',
    'passwd':'ccc',
    'db':'ddd',
    'charset':'utf8'
}

ConfigContainer.connection_config.override(my_config)
request = ...
sql = ...

do_queries(request, sql)
...