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

本文介绍了在Python中如何结合DBUtils的PooledDB实现MySQL连接池和线程池ThreadPoolExecutor,以提高并发处理效率。通过示例代码展示了在多线程环境下正确使用连接池的关键点,特别强调了不能将MySQL连接池的类设计为单例模式,以避免资源竞争导致的异常。同时,提供了完整的项目目录结构和关键代码片段。
摘要由CSDN通过智能技术生成

特别注意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

前言

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

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

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

关联的文章

关于MySQL连接池的使用可以参考笔者这篇文章:

另外本文的demo与后面这篇文章实现的需求与用到的数据库表都是一样的:具体请看这里:

项目目录与代码

3524c19aa72eb5a2b30e436bb72010b4.png

代码

MySQL连接池的配置:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

#-*- 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

日志的封装:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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

importosimportthreadingimportloggingclassLog(object):#单例模式

_instance_lock =threading.Lock()def __new__(cls, *args, **kwargs):if not hasattr(cls, "_instance"):

with cls._instance_lock:

cls._instance= super().__new__(cls)returncls._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#为不同的日志级别设置输出方法

defdebug(self,msg):returnself.logger.debug(msg)definfo(self,msg):returnself.logger.info(msg)defwarring(self,msg):returnself.logger.warning(msg)deferror(self,msg):returnself.logger.error(msg)defcritical(self,msg):returnself.logger.critical(msg)#单例

logger = Log()

log.py

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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

importpymysqlimportloggingimporttracebackimportthreadingfrom config importMySQLSfrom DBUtils.PooledDB importPooledDB#获取MySQL连接池的配置

mysql_pool_config = MySQLS["student"]#MySQL连接池

classMySQLPool(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

returnselfdef __exit__(self, exc_type, exc_val, exc_tb):#关闭连接池

self.cursor.close()

self.conn.close()#插入或修改操作

definsert_or_update(self, sql):try:

self.cursor.execute(sql)

self.conn.commit()return 1

exceptException as error:print(traceback.format_exc())#回滚

self.conn.rollback()#简单的日志处理

logging.error("=======ERROR=======\n%s\nsql:%s" %(error, sql))raise

#查询操作

defquery(self, sql):try:

self.cursor.execute(sql)

results=self.cursor.fetchall()returnresultsexceptException 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函数):

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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

importtimeimportrandomfrom datetime importdatetimefrom log importloggerfrom db_conn importMySQLPool### 根据class_id获取数据是一个耗时的任务! ———— 模拟实际中的耗时任务!!!

defget_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

)returnret### 获取class信息并且批量更新或插入数据的操作

defchange_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

ifchange_lst:

str_values= ""

### 注意这里的处理!!!values 后面可以跟多条数据,并且是str的元组形式的数据!中间用逗号隔开

for index,lst in enumerate(change_lst,1):if index

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

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

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

importtimefrom multiprocessing importPoolfrom concurrent.futures importThreadPoolExecutor,wait,as_completedfrom utils importchange_db### 主函数

defmain_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 inclass_id_lst]## 获取返回值 ———— 本例不用获取返回值

for future inas_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都是从同一个连接池中获取的,只要保证这一点即可!

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值