Python中实现异步并发查询数据库
这周又填了一个以前挖下的坑。
这个博客系统使用Psycopy库实现与PostgreSQL数据库的通信。前期,只是泛泛地了解了一下SQL语言,然后就胡乱拼凑出这么一个简易博客系统。
10月份找到工作以后,认真读了《数据库系统概念》这本书,对数据库有了更深的认识。然后就开始对博客系统的数据库查询模块开始重构。
改进之前
之前,我的查询步骤很简单,就是:
前端提交查询请求 --> 建立数据库连接 --> 新建游标 --> 执行命令 --> 接受结果 --> 关闭游标、连接
这几大步骤的顺序执行。
这里面当然问题很大:
- 建立数据库连接实际上就是新建一个套接字。这是进程间通信的几种方法里,开销最大的了。
- 在“执行命令”和“接受结果”两个步骤中,线程在阻塞在数据库内部的运行过程中,数据库连接和游标都处于闲置状态。
这样一来,每一次查询都要顺序的新建数据库连接,都要阻塞在数据库返回结果的过程中。当前端提交大量查询请求时,查询效率肯定是很低的。
第一次改进
之前的模块里,问题最大的就是第一步——建立数据库连接套接字了。如果能够一次性建立连接,之后查询能够反复服用这个连接就好了。
所以,首先应该把数据库查询模块作为一个单独的守护进程去执行,而前端app作为主进程响应用户的点击操作。那么两条进程怎么传递消息呢?翻了几天Python文档,终于构思出来:用队列queue作为生产者(web前端)向消费者(数据库后端)传递任务的渠道。生产者,会与SQL命令一起,同时传递一个管道pipe的连接对象,作为任务完成后,回传结果的渠道。确保,任务的接收方与发送方保持一致。
作为第二个问题的解决方法,可以使用线程池来并发获取任务队列中的task,然后执行命令并回传结果。
第二次改进
第一次改进的效果还是很明显的,不用任何测试手段。直接点击页面链接,可以很直观地感觉到反应速度有很明显的加快。
但是对于第二个问题,使用线程池还是有些欠妥当。因为,CPython解释器存在GIL问题,所有线程实际上都在一个解释器进程里调度。线程稍微开多一点,解释器进程就会频繁的切换线程,而线程切换的开销也不小。线程多一点,甚至会出现“抖动”问题(也就是刚刚唤醒一个线程,就进入挂起状态,刚刚换到栈帧或内存的上下文,又被换回内存或者磁盘),效率大大降低。也就是说,线程池的并发量很有限。
试过了多进程、多线程,只能在单个线程里做文章了。
Python中的asyncio库
Python里有大量的协程库可以实现单线程内的并发操作,比如Twisted、Gevent等等。Python官方在3.5版本里提供了asyncio库同样可以实现协程并发。asyncio库大大降低了Python中协程的实现难度,就像定义普通函数那样就可以了,只是要在def前面多加一个async关键词。async def函数中,需要阻塞在其他async def函数的位置前面可以加上await关键词。
import asyncio
async def wait():
await asyncio.sleep(2)
async def execute(task):
process_task(task)
await wait()
continue_job()
async def函数的执行稍微麻烦点。需要首先获取一个loop对象,然后由这个对象代为执行async def函数。
loop = asyncio.get_event_loop()
loop.run_until_complete(execute(task))
loop.close()
loop在执行execute(task)函数时,如果遇到await关键字,就会暂时挂起当前协程,转而去执行其他阻塞在await关键词的协程,从而实现协程并发。
不过需要注意的是,run_until_complete()函数本身是一个阻塞函数。也就是说,当前线程会等候一个run_until_complete()函数执行完毕之后,才会继续执行下一部函数。所以下面这段代码并不能并发执行。
for task in task_list:
loop.run_until_complete(task)
对与这个问题,asyncio库也有相应的解决方案:gather函数。
loop = asyncio.get_event_loop()
tasks = [asyncio.ensure_future(execute(task))
for task in task_list]
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
当然了,async def函数的执行并不只有这两种解决方案,还有call_soon与run_forever的配合执行等等,更多内容还请参考官方文档。
Python下的I/O多路复用
协程,实际上,也存在上下文切换,只不过开销很轻微。而I/O多路复用则完全不存在这个问题。
目前,Linux上比较火的I/O多路复用API要算epoll了。Tornado,就是通过调用C语言封装的epoll库,成功解决了C10K问题(当然还有Pypy的功劳)。
在Linux里查文档,可以看到epoll只有三类函数,调用起来比较方便易懂。
创建epoll对象,并返回其对应的文件描述符(file descriptor)。
int epoll_create(int size);
int epoll_create1(int flags);
控制监听事件。第一个参数
epfd就对应于前面命令创建的epoll对象的文件描述符;第二个参数表示该命令要执行的动作:监听事件的新增、修改或者删除;第三个参数,是要监听的文件对应的描述符;第四个,代表要监听的事件。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
等候。这是一个阻塞函数,调用者会等候内核通知所注册的事件被触发。
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
在Python的select库里:
select.epoll()对应于第一类创建函数;epoll.register(),epoll.unregister(),epoll.modify()均是对控制函数epoll_ctl的封装;epoll.poll()则是对等候函数epoll_wait的封装。
Python里epoll相关API的最大问题应该是在epoll.poll()。相比于其所封装的epoll_wait,用户无法手动指定要等候的事件,也就是后者的第二个参数struct epoll_event *events。所以没法实现精确控制。
如果一定要精确控制
那只能使用替代方案:select.select()函数。
根据Python官方文档,select.select(rlist, wlist, xlist[, timeout])是对Unix系统中select函数的直接调用,与C语言API的传参很接近。前三个参数都是列表,其中的元素都是要注册到内核的文件描述符。如果想用自定义类,就要确保实现了fileno()方法。
这三个参数分别对应于:
rlist: 等候直到可读wlist: 等候直到可写xlist: 等候直到异常。这个异常的定义,要查看系统文档。
select.select(),类似于epoll.poll(),先注册文件和事件,然后保持等候内核通知,是阻塞函数。
select的缺点是:
- 复杂度为O(n)。每次都要轮询所有注册的描述符,然后返回结果;
- 每个进程只能支持注册1024个文件描述符,因为每一个select都在进程的内存中请求固定大小的空间,使用位图索引建立描述符集合数据结构,线程请求注册描述符的时候,select将描述符放入其中。
而epoll的ET边缘触发模式则是在文件描述符可用时方才所出响应,复杂度为O(1)。
实际应用
Psycopg2库支持对异步和协程,但和一般情况下的用法略有区别。普通数据库连接支持不同线程中的不同游标并发查询;而异步连接则不支持不同游标的同时查询。所以异步连接的不同游标之间必须使用I/O复用方法来协调调度。
所以,我的大致实现思路是这样的:首先并发执行大量协程,从任务队列中提取任务,再向连接池请求连接,创建游标,然后执行命令,并返回结果。在获取游标和接受查询结果之前,均要阻塞等候内核通知连接可用。
其中,连接池返回连接时,会根据引用连接的协程数量,返回负载最轻的连接。这也是自己定义AsyncConnectionPool类的目的。
我的代码位于:bottle-blog/dbservice.py
存在问题
当然了,这个流程目前还一些问题。
首先就是每次轮询拿到任务之后,都会走这么一个流程。
获取连接 --> 新建游标 --> 执行任务 --> 关闭游标 --> 取消连接引用
本来,最好的情况应该是:在轮询之前,就建好游标;在轮询时,直接等候内核通知,执行相应任务。这样可以减少轮询时的任务量。但是如果协程提前对应好连接,那就不能保证在获取任务时,保持各连接负载均衡了。
所以这一块,还有工作要做。
然后就是,应该建立一个缓存机制。将一定时限的查询结果存储起来,可以直接返回,避免短时间内重复查询相同内容。
最后,请允许我吐槽一下Python的epoll相关文档:简直太弱了!!!必须看源码才能弄清楚功能。
参考文档
- 18.5. asyncio — Asynchronous I/O, event loop, coroutines and tasks
- epoll机制:epoll_create、epoll_ctl、epoll_wait、close
- epoll(7) - Linux man page
- cpython/Modules/selectmodule.c
- 18.3. select — Waiting for I/O completion
- Psycopg 2.6.2 documentation >> Asynchronous support
- psycopg2/lib/pool.py
Python中实现异步并发查询数据库的更多相关文章
- [译]Python中的异步IO:一个完整的演练
原文:Async IO in Python: A Complete Walkthrough 原文作者: Brad Solomon 原文发布时间:2019年1月16日 翻译:Tacey Wong 翻译时 ...
- 48.Python中ORM模型实现mysql数据库基本的增删改查操作
首先需要配置settings.py文件中的DATABASES与数据库的连接信息, DATABASES = { 'default': { 'ENGINE': 'django.db.backends.my ...
- python中web应用与mysql数据库交互
7使用数据库 具体使用python的DB-API,这一章里介绍如何编写代码与MYSQL数据库技术交互,这里使用一个通用的数据库API,名为DB-API. 7.1基于数据库的web应用 之前我们把日志数 ...
- Python中的异步任务队列 arq
引言 最近在用 sanic 写东西,所有涉及到IO阻塞的代码都需要用 aio 的模块,好在近年来 asyncio 生态圈发展的还算不错,该有的都有 ~ 近期业务中 登录/注册 业务涉及的很复杂(涉及到 ...
- Python中的子进程并发
date: 2019-06-16 22:35:33 author: headsen chen notice:个人原创 实例代码: import os,time time.sleep(1) from ...
- 关于怎么C#控制台窗口中怎么创建连接查询数据库操作
首先需要新建一张表,为了测试随建了一张学生表 新建号一张表之后就可以对数据库进行操作了 列举了常用的增删改查 操作 static void Main(string[] args) { s ...
- 关于HTML中时间格式以及查询数据库的问题
1.默认时间格式,加入属性dateFormate="yyyy-MM-dd" 2.设置默认值,value="2017-6-22" 3.在JavaScript中将获 ...
- Python中通过cx_oracle操作ORACLE数据库的封闭函数
哈哈,看来我的SQL自动化发布,马上就全面支持ORACLE,MYSQL,POSTGRESQL,MSSQL啦... http://blog.csdn.net/swiftshow/article/deta ...
- 在laravel中,使用DB查询数据库后,返回的对象,可以用下面的办法变为数组
$nodes = Db::table('account')->orderBy('sort', 'asc')->orderBy('id' ,'asc')->get()->map( ...
随机推荐
- 使用generator生成排序动画
相信作为一个程序猿,大家应该都已经看过一些排序算法的过程教学动画,现在我来总结一下自己使用ES6的generator实现动画的过程,以作为自己对generator的使用实践的一个记录. 为什么用gen ...
- JS動態綁定下拉單內容
function req00_Line1_onChange(obj) { //if ($(obj).val() != "" && $(obj).val() ...
- HBase的伪分布式安装(原创)
准备工作: 1)安装了伪分布式hadoop:参照http://blog.csdn.net/zolalad/article/details/11472207 2)修改已安装好的hadoop配置文件: a ...
- 9.openssl ca
用于签名证书请求.生成CRL.维护一个记录已颁发证书和这些证书状态的数据库. 证书请求私用CA的私钥签名之后就是证书. [root@xuexi tmp]# man ca SYNOPSIS openss ...
- Res_Orders_01
一.燃尽图展示 二.项目进展 1.讨论选题内容 2.确定项目的版本(Web版) 3.讨论能达到的效果和内容 4.确定编程方面的难点 5.开始制作大概的框架 三.遇到问题 1.不知道怎么部署能达到最好的 ...
- linux学习4 网络命令和关机重启
网络命令 write write 用户名 回车后输入信息 以 ctrl+d 保存结束 wall wall [message] //that is all,发送给所有人 ping ping 选项 IP地 ...
- DataSnap 2009 系列之三 (生命周期篇)
DataSnap 2009 系列之三 (生命周期篇) DataSnap 2009的服务器对象的生命周期依赖于DSServerClass组件的设置 当DSServer启动时从DSServerClass组 ...
- 【原】如何在jQuery中实现闭包
原生JS中,闭包虽好用,但是很难用好,在jQuery中一样,都有一些点需要我们注意.jQuery中使用闭包的常见情况有以下几种: 1.$(document).ready()的参数 我们在写jQuery ...
- kendoWindow 与 bootbox 冲突,显示层次问题
今天在使用bootbox做弹出提示遇到些麻烦,由于使用kendoWindow先弹出数据输入窗口, 然后在检核输入时,又用bootbox做为提示窗口,这下悲剧了,后弹出的bootbox窗口总是在kend ...
- NSDate管理日期和时间
//时间初始化 NSDate *date = [[NSDate alloc]initWithString:@"2010-01-01 23:59:59 +0900"]; ...