pymysql 与 DBUtils的那些事

3,039 阅读5分钟

前言

本文主要讲述pymysql操作数据库的基本流程,以及DBUtils模块如何创建连接池的那些事。

pymsql操作数据库的流程

import pymysql
  
# 创建连接
conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', passwd='', db='py_test', charset='utf8')

# 创建游标
cursor = conn.cursor()
  
# 执行SQL,并返回收影响行数
effect_row = cursor.execute("select * from table1")

# 提交,不然无法保存新建或者修改的数据
conn.commit()
  
# 关闭游标
cursor.close()

# 关闭连接
conn.close()

如上,每一次数据库操作都需要执行commit和关闭游标、关闭连接的操作,所以一般会做一层封装,如下:

# base_model.py

mysql_config = mysql.mysql_config()

mysql_pool = PooledDB(
    creator=pymysql,
    maxconnections=10,
    host=mysql_config['host'],
    port=mysql_config['port'],
    user=mysql_config['user'],
    password=mysql_config['password'],
    database=mysql_config['db'],
    charset='utf8mb4'
)

# test_model.py
class TestModel:

    # 使用依赖注入
    def __init__(self, db_pool):
        self.db_conn = db_pool.connection()

        # 游标设置为字典类型,即返回的数据类型为dict,而不是tuple
        self.cursor = self.db_conn.cursor(cursor=pymysql.cursors.DictCursor)

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cursor.close()
        self.db_conn.close()

对增删改做一层封装

# sql通过拼接有注入风险
def _base_selector(self, sql):
    self.cursor.execute(sql)
    rows = self.cursor.fetchall()
    data = list(rows)
    return data

# sql与参数分开,内部有做检查,无注入风险
def _base_selector(self, sql, params: tuple):
    self.cursor.execute(sql, params)
    rows = self.cursor.fetchall()
    data = list(rows)
    return data

注:如果不习惯类的封装方式,也可尝试使用上下文管理器进行封装

增删查改

使用pymysql操作数据库

数据库连接池

从某种层面看,数据库可看做服务端,而我们程序员(调用者)可看做客户端,而数据库连接就相当于HTTP连接,且实际上数据库连接的确包含 HTTP连接Socket连接 两种方式。

另外,pymysql并不是线程安全的,所以当有多个线程同时使用pymysql操作数据库时,就会出现问题。

所以从线程安全和支持更高的并发的角度,我们一般会使用数据库连接池来解决,一般自己实现一个比较麻烦,通常是使用第三方库,如 DBUtils

DBUtils有两种变体,通用的DB-API 2变体以及经典的PyGreSQL变体,下面只分析 通用的DB-API 2变体 的 PooledDBPersistentDB(内容主要来自官方文档)。

PersistentDB

mysql_persist.gif

对于PersistentDB,每当线程首次打开数据库连接时,都会打开一个新的数据库连接,此连接将从现在开始用于该特定线程。当线程关闭数据库连接时,它仍将保持打开状态,以便下次同一线程请求连接时,可以使用该已打开的连接。线程死亡后,连接将自动关闭

简而言之:PersistentDB会尝试回收数据库连接以提高数据库访问性能,且它能确保连接永远不会在线程之间共享(即使你的基础数据库驱动,比如pymysql不是线程安全的,也能确保不共享)。

注:每次有新线程,就会有新数据库连接,所以注意避免短时间内创建很多连接

class PersistBaseModel:
    def __init__(self):
        mysql_pool = PersistentDB(
            # 使用的mysql驱动
            creator=pymysql,
            host='127.0.0.1',
            port='root',
            user='root',
            password='root',
            database='py_test',
            charset='utf8mb4',
            # 一个连接最多被重复使用的次数,None表示无限制
            maxusage=None,
        )
        self.db_conn = mysql_pool.connection()

        # 设置返回的数据类型为dict,而不是tuple
        self.cursor = self.db_conn.cursor(cursor=pymysql.cursors.DictCursor)

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cursor.close()
        self.db_conn.close()

PoolDB

mysql_pool.gif

如图,对于PoolDB来说,它与PersistentDB最大的区别在于: PersistentDB是每个线程就创建一个连接,所以存在连接暴涨的风险;而 PoolDB 可以控制连接的总数量,所以不会存在该问题。

另外,PoolDB还有一个比较有趣的特性,就是连接共享,即不同线程之间可以共享打开的数据库连接,比如上图的Thread1和Thread3就共享了 pcon1 和 pcon2 连接。

但是,连接共享必须建立在线程安全的基础上,不然就会导致数据错乱。比如有两个线程共享一个连接,A线程修改了数据,但是发现有问题,需要回滚,而B线程不巧插了一脚,在回滚前执行了commit操作,就会导致数据错乱,这也是为什么连接共享必须建立在线程安全的基础上。

对于PoolDB,连接共享对应两种情况:
1.你的数据库驱动(基础DB-API)是在连接级别上是线程安全的,且设置了 maxshared 参数,则会产生连接共享的情况。
2.你的数据库驱动(基础DB-API)不是线程安全的,如pymysql,则 PoolDB 会通过线程锁来保证线程安全,且不会产生连接共享的情况(因为 maxshared 参数被置为0,所以设置这个参数是无效的)

class PoolDB:
    # pymysql 的 threadsafety=1
    if threadsafety > 1 and maxshared:
        self._maxshared = maxshared
        self._shared_cache = []  # the cache for shared connections
    else:
        self._maxshared = 0

注:网上部分文章提到,当 maxshared 参数为0或者None时,表示连接全部共享,这其实是一个错误的说法,当参数为0或者None时,表示的是连接不共享,即每个连接都是专用的,从源码从可以看到:

maxshared: maximum number of shared connections
    (0 or None means all connections are dedicated)
    When this maximum number is reached, connections are
    shared if they have been requested as shareable.

示例代码:

class BaseModel:
    def __init__(self):
        mysql_pool = PooledDB(
            # 使用的mysql驱动
            creator=pymysql,
            host='127.0.0.1',
            port='root',
            user='root',
            password='root',
            database='py_test',
            charset='utf8mb4',
            # 连接池允许的最大连接数
            maxconnections=10,
            # 初始化时,连接池中至少创建的空闲的链接
            mincached=2,
            # 连接池中最多共享的连接数量
            maxshared=3,
            # 一个连接最多被重复使用的次数,None表示无限制
            maxusage=None,

        )
        self.db_conn = mysql_pool.connection()

        # 设置返回的数据类型为dict,而不是tuple
        self.cursor = self.db_conn.cursor(cursor=pymysql.cursors.DictCursor)

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cursor.close()
        self.db_conn.close()

参考

官方文档
pymysql访问数据库线程安全问题