MySQL连接池DBUtils与线程池ThreadPoolExecutor的结合使用实例

特别注意DBUtils包的版本

  在实际业务中,如果读者们使用笔者的代码上报了下面的错误:

ModuleNotFoundError: No module named 'DBUtils'

  但是实际上检查pip3已经安装了这个模块!

  出现问题的原因是DBUtils包版本的问题。

  我的代码使用下面这种方式导入模块:

from DBUtils.PooledDB import PooledDB

  这种导入方式要求DBUtils包的版本是1.3的,但是如果没有指定版本安装的话,现在默认会安装2.0版本。因此会导致上面的错误产生。

  解决的方法很简单,只要指定版本安装该模块就可以了:

pip3 install DBUtils==1.3

  当然,如果读者们想要使用2.0最新的版本,需要以下面这种方式导入模块:

from dbutils.pooled_db import PooledDB

  参考文章:ModuleNotFoundError: No module named 'DBUtils'

前言

  之前业务中实现并发是在线程池/进程池中的每个线程/进程中单独与MySQL建立链接,处理完后再释放链接。

  这样虽然也能实现功能,但是频繁的与MySQL建立与释放链接会损耗资源。所以打算使用MySQL的连接池去优化之前的代码。

  专门做了一个demo验证了一下,重点是本人封装的MySQL连接池类的实现与在线程池中的使用方法,希望能帮助到读者朋友们。

关联的文章

  关于MySQL连接池的使用可以参考笔者这篇文章:pymysql链接池、事物的使用浅谈

  另外本文的demo与后面这篇文章实现的需求与用到的数据库表都是一样的:具体请看这里:并发处理IO任务与MySQL中ON DUPLICATE KEY UPDATE的使用

项目目录与代码

代码

MySQL连接池的配置:

# -*- coding:utf-8 -*-

# MySQL连接池的配置
MySQLS = {
    'student': {
        "maxconnections": 0,  # 连接池允许的最大连接数,0和None表示不限制连接数
        "mincached": 2,  # 初始化时,链接池中至少创建的空闲的链接,0表示不创建
        "maxcached": 0,  # 链接池中最多闲置的链接,0和None不限制
        "maxusage": 1,  # 一个链接最多被重复使用的次数,None表示无限制
        "blocking": True,  # 连接池中如果没有可用连接后,是否阻塞等待。True,等待;False,不等待然后报错
        'host': '127.0.0.1',
        'user': 'root',
        'password': '123',
        'db': 'students',
        'port': 3306,
        'charset': 'utf8',
    }
}
config.py

日志的封装:

# -*- coding:utf-8 -*-
import os
import threading
import logging


class Log(object):
    # 单例模式
    _instance_lock = threading.Lock()
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            with cls._instance_lock:
                cls._instance = super().__new__(cls)
        return cls._instance

    # 级别默认是DEBUG
    def __init__(self, log_path='logs/my_log.log', level=logging.DEBUG):
        # 如果之前没创建过 就新建一个logger对象,后面相同的实例共用一个logger对象
        if 'logger' not in self.__dict__:
            logger = logging.getLogger()
            # 设置日志级别
            logger.setLevel(level)
            # 格式化
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
            # 创建文本输入对象与屏幕打印对象
            fh = logging.FileHandler(log_path, encoding='utf-8')
            # ch = logging.StreamHandler()
            fh.setFormatter(formatter)
            # ch.setFormatter(formatter)
            logger.addHandler(fh)
            # logger.addHandler(ch)
            # 初始化logger属性
            self.logger = logger

    # 为不同的日志级别设置输出方法
    def debug(self,msg):
        return self.logger.debug(msg)

    def info(self,msg):
        return self.logger.info(msg)

    def warring(self,msg):
        return self.logger.warning(msg)

    def error(self,msg):
        return self.logger.error(msg)

    def critical(self,msg):
        return self.logger.critical(msg)


# 单例
logger = Log()
log.py

MySQL连接池类的实现(特别注意类里面的pool必须是一个类变量,不能写在初始化方法中!否则实例化后的每个对象使用的是不同的连接池!):

# -*- coding:utf-8 -*-
import pymysql
import logging
import traceback
import threading

from config import MySQLS
from DBUtils.PooledDB import PooledDB


# 获取MySQL连接池的配置
mysql_pool_config = MySQLS["student"]


# MySQL连接池
class MySQLPool(object):

    # 类变量
    pool = PooledDB(creator=pymysql,**mysql_pool_config)

    print("cls_pool_id>>>",id(pool))

    # with上下文
    def __enter__(self):
        self.conn = self.pool.connection()
        self.cursor = self.conn.cursor(cursor=pymysql.cursors.DictCursor)
        # 记得return self
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # 关闭连接池
        self.cursor.close()
        self.conn.close()

    # 插入或修改操作
    def insert_or_update(self, sql):
        try:
            self.cursor.execute(sql)
            self.conn.commit()
            return 1
        except Exception as error:
            print(traceback.format_exc())
            # 回滚
            self.conn.rollback()
            # 简单的日志处理
            logging.error("=======ERROR=======\n%s\nsql:%s" % (error, sql))
            raise

    # 查询操作
    def query(self, sql):
        try:
            self.cursor.execute(sql)
            results = self.cursor.fetchall()
            return results
        except Exception as error:
            # 简单的日志处理
            logging.error("=======ERROR=======:\n%s\nsql:%s" % (error, sql))
            raise

    ### 注意不可以将MySQL连接池的类写成单例模式!!!
    ### 因为本质上 线程池+MySQL连接池本质上使用的是同一个连接池 PooledDB 对象,而不是单例的MySQL连接池的对象!!!
    # _instance_lock = threading.Lock()
    # # 重写 __new__ 实现单例模式
    # def __new__(cls, *args, **kwargs):
    #     if not hasattr(cls, "_instance"):
    #         with cls._instance_lock:
    #             cls._instance = super().__new__(cls)
    #     return cls._instance
db.conn

多线程中执行的具体方法的实现(utils.py中的change_db函数):

# -*- coding:utf-8 -*-
import time
import random
from datetime import datetime

from log import logger
from db_conn import MySQLPool


### 根据class_id获取数据是一个耗时的任务! ———— 模拟实际中的耗时任务!!!
def get_class_info(class_id):
    r = random.randint(2,4)
    time.sleep(r)
    # print("耗时...... {} 秒".format(r))
    now = datetime.now()
    ret = dict(
        class_name=f"class_{class_id}",
        stu_id=random.randint(1,1000),
        inschool_datetime=now
    )
    return ret


### 获取class信息并且批量更新或插入数据的操作
def change_db(class_id):

    # 在这里面使用MySQL的连接池!!!
    with MySQLPool() as pool_db:

        # 这里每个对象使用的连接池都与类中的连接池一样!说明所有的线程池使用的是同一个MySQL的连接池!
        print("pool_id>>>",id(pool_db.pool))

        ### 这是个耗时的操作!!!
        ret = get_class_info(class_id)

        # 赋值
        class_name, stu_id, inschool_datetime = ret["class_name"], ret["stu_id"], ret["inschool_datetime"]
        # 模拟要往数据库中插入或修改的数据 —— insert与update的格式必须一样
        change_lst = list()
        # 记得将datetime类型转换为str才能往数据库中写入!
        inup_lst = [class_id, class_name, stu_id, str(inschool_datetime)]
        change_lst.append(inup_lst) # 列表套列表的形式
        ### 批量操作 insert或者update ——— ON DUPLICATE KEY UPDATE 前面的values是字符串形式的多个tuple
        if change_lst:
            str_values = ""
            ### 注意这里的处理!!!values 后面可以跟多条数据,并且是str的元组形式的数据!中间用逗号隔开
            for index,lst in enumerate(change_lst,1):
                if index < len(change_lst):
                    str_values += str(tuple(lst))+","
                else:
                    str_values += str(tuple(lst))
            ### 批量插入或更新
            change_sql = """ insert into student(class_id,class_name,stu_id,inschool_datetime) values
                            {} ON DUPLICATE KEY UPDATE
                            class_id=VALUES(class_id),class_name=VALUES(class_name),stu_id=VALUES(stu_id),
                            inschool_datetime=VALUES(inschool_datetime)
                            """.format(str_values)

            # 测试 query 操作
            # change_sql = """ select count(1) from student """
            # exe_ret = pool_db.query(change_sql)

            exe_ret = pool_db.insert_or_update(change_sql)
            print(change_sql)
            print("exe_ret>>>",exe_ret)

            if exe_ret == 1:
                logger.info("insert/update successfully!class_id:{}".format(class_id))
            else:
                logger.error("sql执行失败!")
utils

项目启动文件(线程池的执行操作):

# -*- coding:utf-8 -*-
import time
from multiprocessing import Pool
from concurrent.futures import ThreadPoolExecutor,wait,as_completed

from utils import change_db


### 主函数
def main_func():

    class_id_lst = ["01","02","03","04","05"]

    start = time.time()

    ### 1、普通方法
    """
    for class_id in class_id_lst:
        change_db(class_id)
    print("普通方法耗时:{}".format(time.time() - start))
    # 普通方法耗时:17.036139011383057
    """

    ## 2、线程池 + MySQL连接池
    executor = ThreadPoolExecutor(max_workers=3)
    # class_id 是参数
    all_tasks = [executor.submit(change_db,class_id) for class_id in class_id_lst]
    ## 获取返回值 ———— 本例不用获取返回值
    for future in as_completed(all_tasks):
        data = future.result()
    print("线程池耗时:{}".format(time.time() - start))
    # 线程池耗时:7.016117095947266


if __name__ == '__main__':
    main_func()
run.py

关键点说明

  相信经验丰富的你很快会掌握里面的内容。

  笔者这里只强调一个问题:MySQL连接池的那个类一定不能做成单例模式!

  自己在设计之初有点想当然了:之前以为做成单例模式的话多个线程共用一个连接池的类的实例化对象即可。但是实际中会在执行commit与rollback操作的时候发生资源的争夺导致异常!

  因为这个类实例化的每个对象中的链接属性conn本质上就是从连接池PooleDB中获取的单独的一个链接,如果做成单例模式的话,那么每个线程中的链接变成了同一个conn!多个线程共享一个conn肯定会在执行insert或update这样的操作的时候发生异常的!

  我在代码中打印了类中连接池的id与每个对象的连接池的id,结果都一样,说明不同线程中的链接conn都是从同一个连接池中获取的,只要保证这一点即可!

参考文章

python数据库连接工具DBUtils

python数据库连接池DBUtils.PooledDB

posted on 2020-10-09 19:15  江湖乄夜雨  阅读(737)  评论(0编辑  收藏  举报