缓存/重用数据库连接以供以后使用

时间:2018-11-05 02:58:27

标签: python sql database database-connection connection-pooling

我正在保存用户的数据库连接。他们第一次输入凭据时,我会执行以下操作:

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数据库连接 NOT -我事先不知道凭据。现在,一旦用户“连接”完毕,我希望他们能够执行任意数量的查询,而不必每次执行此操作时都重新连接。例如,要再次重申,例如Navicat或SequelPro。如何使用python,django或mysql完成此操作?也许我真的不太了解这里有什么必要(缓存连接?连接池?等等),所以任何建议或帮助都将不胜感激。

6 个答案:

答案 0 :(得分:2)

您可以使用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)

答案 1 :(得分:1)

我看不到为什么您在这里需要一个缓存的连接,为什么不只是在每个请求中重新连接某个将用户的凭据缓存在某处的原因,但是无论如何,我将尽力概述一个适合您要求的解决方案。

我建议先研究一个更通用的任务-在应用程序需要处理的后续请求之间缓存一些内容,这些请求不能序列化到django的会话中。 在您的特定情况下,此共享值将是数据库连接(或多个连接)。 让我们从一个简单的任务开始,即在请求之间共享一个简单的计数器变量,只是为了了解幕后的实际情况。

令人惊奇的是,但是没有一个答案提到您可能使用的Web服务器! 实际上,有多种方法可以处理Web应用程序中的并发连接:

  1. 具有多个进程,每个请求随机进入一个
  2. 具有多个线程,每个请求均由随机的线程
  3. 处理
  4. p.1和p.2组合
  5. 各种 async 技术,当存在单个 进程 + 事件循环时,需要处理带有警告的请求处理程序不应长时间阻塞

根据我的经验,第1-2页适合大多数典型的Web应用程序。 Apache1.x仅适用于p.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

这是我们的进程树-只有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 < / strong>,而不是 3 。 请注意,您的输出可能会有所不同,因为过程1和2是随机选择的。但是迟早您会遇到一个不同过程。

这对我们不是很有帮助,因为我们需要处理多个并发请求,并且需要以某种方式使我们的请求由通常情况下无法完成的特定过程处理。

如果您的请求由其他进程处理-开箱即用的

大多数 pooling 技术将仅在单个进程范围内缓存连接。 > 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 < / em>, django + asyncio 。它有其优点/缺点,并且可能会极大地影响您的应用实施,因此,在不了解您的应用详细要求的情况下,很难提出任何解决方案

答案 2 :(得分:1)

在Web应用程序上下文中同步执行此操作不是一个好主意。请记住,您的应用程序可能需要以多进程/线程的方式工作,并且您无法正常共享进程之间的连接。因此,如果您在一个进程上为您的用户创建连接,则无法保证在同一进程上接收查询请求。最好有一个进程后台工作程序,该进程处理多个线程(每个会话一个线程)中的连接,以对数据库进行查询并在Web应用程序上检索结果。您的应用程序应为每个会话分配一个唯一的ID,后台工作人员使用会话ID跟踪每个线程。您可以使用celery或任何其他支持异步结果的任务队列。因此设计如下所示:

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

您还可以为每个用户创建一个队列,直到他们有活动的会话为止,这样您就可以为每个会话运行一个单独的后台进程。

答案 3 :(得分:1)

我实际上分享了我对这个确切问题的解决方案。我在这里所做的是创建一个连接池,您可以使用该连接池指定最大连接数,然后通过该通道将查询请求排队异步。这样,您可以保持一定数量的连接处于打开状态,但是它将使队列和池异步并保持您惯用的速度。

这需要gevent和postgres。

Python Postgres psycopg2 ThreadedConnectionPool exhausted

答案 4 :(得分:0)

我不是该领域的专家,但是我相信PgBouncer会为您完成这项工作,前提是您能够使用PostgreSQL后端(这是您未清楚的一个细节) )。 PgBouncer是一个连接池,它使您可以重新使用连接,从而避免了在每个请求上进行连接的开销。

根据他们的documentation

  

用户,密码

     

如果设置了user =,则到目标数据库的所有连接都将由指定的用户完成,这意味着该数据库将只有一个池。

     

否则,PgBouncer尝试使用客户端用户名登录到目标数据库,这意味着每个用户将只有一个池。

因此,每个用户可以有一个连接池,这听起来像您想要的。

在MySQL领域中,mysql.connector.pooling模块允许您进行一些连接池化,尽管我不确定是否可以按用户进行池化。既然您可以设置池名称,那么我猜您可以使用用户名来标识池。

无论使用什么功能,都可能会不可避免地出现重新连接的情况(用户连接,做一些事情,去开会和吃午餐,回来并想采取更多措施)。

答案 5 :(得分:0)

我只是在这里分享我的知识。

安装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 

您将获得渴望的结果。

点击here以获得更多信息

2。对于python 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()

有关更多信息,请参考this

PS::您可以检查此链接以查询问题,以便将来重新使用数据库连接。

How to enable MySQL client auto re-connect with MySQLdb?