Python 中的 MySQL 数据库连接池
来源:SegmentFault
时间:2023-01-28 08:45:50 117浏览 收藏
IT行业相对于一般传统行业,发展更新速度更快,一旦停止了学习,很快就会被行业所淘汰。所以我们需要踏踏实实的不断学习,精进自己的技术,尤其是初学者。今天golang学习网给大家整理了《Python 中的 MySQL 数据库连接池》,聊聊MySQL、python,我们一起来看看吧!
从 Java 到 Python
本文为我和同事的共同研究成果
当跨语言的时候,有些东西在一门语言中很常见,但到了另一门语言中可能会很少见。
例如 C# 中,经常会关注拆箱装箱,但到了 Java 中却发现,根本没人关注这个。
后来才知道,原来是因为 Java 中没有真泛型,就算放到泛型集合中,一样会装箱。既然不可避免,那也就没人去关注这块的性能影响了。
而 C# 中要是写出这样的代码,那你明天不用来上班了。
同样的场景发生在了学习 Python 的过程中。
什么?数据库连接竟然没有连接池!?
完全不可理解啊,Java 中不用连接池对性能影响挺大的。
Python 程序员是因为 Python 本来就慢,然后就自暴自弃了吗?
突然想到一个笑话
问:为什么 Python 程序员很少谈论内存泄漏?
答:因为 Python 重启很快。
? 说多了都是泪,我之前排查 Java 内存泄漏的问题,超高并发的程序跑了1-2个月后就崩溃。我排查了好久,Java GC 参数也研究了很多,最后还是通过控制变量法找到了原因。
如果在 Python 中,多简单的事啊,写一个定时重启脚本,解决…
问题来源
那本文的问题怎么来的呢?正是我给公司的代码加上连接池后产生的。一加连接池,就会有一定几率出现;一去掉连接池,就不会有。
db = get_connection() try: cursor = db.cursor() if not cursor.execute("SELECT id FROM user WHERE name = %s", ['david']) > 0: return None return cursor.fetchone()[0] finally: db.close()
一段很简单的代码,基本上整个项目中所有的数据库查询都是这么写的,本来没任何问题。
但当我给底层加上连接池后,问题来了。
这边后报出这样一个异常:
'NoneType' object has no attribute '__getitem__'
意思就是说
cursor.fetchone()取出来的结果是
None。
但是,代码在调用之前命名已经检查过
affected rows了,根据文档
cursor.execute()返回的就是
affected rows。
文档也是这么写的:
Returns long integer rows affected, if any。
解决问题第一步:网上找答案
什么测试驱动开发,敏捷开发,我觉得都不对,一句话形容我们那应该是:基于 Google 的 Bug 驱动开发。?
可惜网上无任何结果,去 stackoverflow 上问也没人知道。
感觉又来到了一片无人区……
目前唯一能确认的就是和连接池相关了。
大致分析下应该是和连接复用有关,代码没写好?底层连接池并发处理的代码有 Bug?
先抓个详细的异常看看吧。
解决问题第二步:分析异常日志
我们项目用了 Sentry,一个异常跟踪系统。可以把报错时的调用堆栈和临时变量都记录下来。
第一个有用的信息是,我们竟然发现
cursor.execute()的返回结果在 Sentry 上记录的是
18446744073709552000。
这是一个非常诡异的数字,因为它接近
2^64-1 (18446744073709551615),而且还比它大了一点。
网上也找不到太多相关资料,和这个数字相关的都是 Javascript 相关的问题。
因为 Javascript 中是无法表示
2^64-1的,相关讨论:传送门
简单的一句话解释就是:这个数字超过了 Javascript Integer 的最大范围,所以底层用 Float 来表示了,所以导致丢失了精度。
但我们的程序没用 Javascript。到了这边,我们的第一反应一定是,要么 MySQL 出了 Bug。要么 MySQL-Python 出了 Bug。
解决问题第三步:一层层看源码分析
先看 MySQL-Python 源码,
cursor.execute()内部调用了
affected_rows()方法得到了这个数字,而
affected_rows()这个方法内部使用 C 实现了。
MySQL-Python 的 C 部分源码很简单,没什么逻辑:
return PyLong_FromUnsignedLongLong(mysql_affected_rows(&(self->connection)));
看样子也没什么特别的,这里就两个地方可能有问题,
PyLong_FromUnsignedLongLong()和
mysql_affected_rows()。
先自己尝试写了一段代码,调用
PyLong_FromUnsignedLongLong()函数,发现无论如何都不会出现
18446744073709552000这个数字。
然后看 MySQL 源码,
mysql_affected_rows()返回类型是
my_ulonglong,源码中其实是这么定义的:
typedef unsigned long long my_ulonglong;
也就是说,在 C 代码中,这个数字最大就是
2^64-1 (18446744073709551615),不可能返回
18446744073709552000的。
然后在
mysql_affected_rows()的官方文档中又发现了一些有用的信息:
An integer greater than zero indicates the number of rows affected or retrieved. Zero indicates that no records were updated for an UPDATE statement, no rows matched the WHERE clause in the query or that no query has yet been executed. -1 indicates that the query returned an error or that, for a SELECT query, mysql_affected_rows() was called prior to calling mysql_store_result().
Because mysql_affected_rows() returns an unsigned value, you can check for -1 by comparing the return value to (my_ulonglong)-1 (or to (my_ulonglong)~0, which is equivalent).
好了,遇到第一个坑了,为什么 MySQL 官方文档说这里可能有
-1,而 MySQL-Python 的文档中却没说?而且返回类型是无符号的,
-1就变成
18446744073709551615了。
那么如果我用
if cursor.execute() > 0这种方式来判断命中行数时,明明出错了,我却会得到
True的结果了。
很明显 MySQL-Python 写的是有问题的,同事联系了 MySQL-Python 的作者,作者承认了这里的问题,把代码修复了,下一个版本会修复。
神奇的数字
但是,看源码发现的东西还是没解决我们的问题,为什么我们的到的数字是
18446744073709552000,而不是
18446744073709551615?
整个调用链我们都检查过了,不可能出现这个数字。
然后一个周末,我在快睡醒的时候突然想到了一个问题,这个数字是不是在 Python 报错的时候,还是
18446744073709551615,而到了 Sentry 中,就变成了
18446744073709552000?
因为 Sentry Web 界面用的是 ajax,而 Javascript 中转换这个数字的时候就会出错。
最后一验证,果然是 Sentry 的问题,Javascript 真的处处是坑。
好了,到了这一步,等 MySQL-Python 作者修复完后,我们的代码也就不会报错了。问题解决?
但是,MySQL 官方却没有说为什么这里会出现
-1,而且为什么去掉了连接池就不会报错?
就算我们的代码不报错了,但如果这里的返回数字不符合我们预期或者说不可控的话,会导致更多隐形的数据上的问题。
Root Cause
目前为止,依然没找到 Root Cause。
别动,看好了,我要用压测大法了!既然这个问题是在高并发使用连接池时出现的,那就压测看看能不能重现吧。
用了同样的代码,10个进程,没有 sleep。没想到不需要一分钟,这个问题就会立刻重现。
而且每次重现时,都会有一些 MySQL 底层的警告,说出现了错误的调用顺序。
这时,我试了一下加了一行代码:
db = get_connection() cursor = None try: cursor = db.cursor() if not cursor.execute("SELECT id FROM user WHERE name = %s", ['david']) > 0: return None return cursor.fetchone()[0] finally: if cursor: # new code cursor.close() # new code db.close()
加完后就再也没看到任何错误了。
嗯,这里我们的代码写的是不到位,我后来仔细看了官方教程,是有主动关闭
cursor的代码的。(偷偷告诉你们,这里都是 CTO 以前写的 ?)
粗略看了下
cursor.close()的代码,里面其实就是在把未读完的数据读完:
while self.nextset(): pass。
那这里出问题的原因也就好理解了,高并发情况下复用连接池,如果上一次请求由于某些原因没有读完所有数据,后面直接复用这个连接的时候,就会出现问题了。
然后,我又奇怪了,连接池框架在关闭连接的时候不应该做清理工作吗?
Java JDBC 源码也看过不少了,
Connection关闭的时候会清理
Statement,
Statement关闭的时候会清理
ResultSet。因为单个连接只会在单线程中操作,是线程安全的,所以实现这样的自动清理是非常简单的。
以前写 Java 中间件的时候,就总是把用户当?,要尽量考虑各种情况避免内存泄漏。我们默认都是认为用户是从来不会去调用
close方法的。所以常常会想方设法帮用户去自动处理。
解决问题
最后要来解决问题了,代码量很大,所有调用都改一遍其实也不难,因为这里都是有规律的,正则啊脚本啊什么的齐上阵,总是能解决的。
但是,其实也可以像 JDBC 那样搞自动关闭。
class AutoCloseCursorConnection(object): cursor = None conn = None def __init__(self, conn): self.conn = conn def __getattr__(self, key): return getattr(self.conn, key) def cursor(self, *args, **kwargs): self.cursor = self.conn.cursor(*args, **kwargs) return self.cursor def close(self): if self.cursor: self.cursor.close() self.conn.close()
每次创建的连接包一下,就解决问题了。
源地址:http://www.dozer.cc/2016/07/mysql-connection-pool-in-python.html
到这里,我们也就讲完了《Python 中的 MySQL 数据库连接池》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于mysql的知识点!
-
499 收藏
-
244 收藏
-
235 收藏
-
157 收藏
-
101 收藏
-
278 收藏
-
231 收藏
-
120 收藏
-
393 收藏
-
482 收藏
-
481 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 务实的向日葵
- 很详细,码起来,感谢老哥的这篇技术文章,我会继续支持!
- 2023-04-11 12:14:55
-
- 热心的裙子
- 这篇博文真及时,太详细了,真优秀,mark,关注老哥了!希望老哥能多写数据库相关的文章。
- 2023-03-25 11:00:06
-
- 重要的鲜花
- 这篇技术文章真及时,好细啊,赞 👍👍,码起来,关注作者大大了!希望作者大大能多写数据库相关的文章。
- 2023-03-24 09:12:03
-
- 无辜的背包
- 好细啊,码住,感谢楼主的这篇技术贴,我会继续支持!
- 2023-03-10 20:39:13
-
- 兴奋的小蘑菇
- 写的不错,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢老哥分享文章内容!
- 2023-02-28 12:05:07
-
- 温柔的铃铛
- 很有用,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢老哥分享技术文章!
- 2023-02-24 08:28:22
-
- 寒冷的大碗
- 这篇博文太及时了,老哥加油!
- 2023-02-07 11:06:00