你知道 Python 怎么异步操作数据库吗?(aiomysql、asyncpg、aioredis)

楔子

Python 目前已经进化到了 3.8 版本,对操作数据库也提供了相应的异步支持。当我们做一个 Web 服务时,性能的瓶颈绝大部分都在数据库上,如果一个请求从数据库中读数据的时候能够自动切换、去处理其它请求的话,是不是就能提高并发量了呢。

下面我们来看看如何使用 Python 异步操作 MySQL、PostgreSQL 以及 Redis,以上几个可以说是最常用的数据库了。至于 SQLServer、Oracle,本人没有找到相应的异步驱动,有兴趣可以自己去探索一下。

而操作数据库无非就是增删改查,下面我们来看看如何异步实现它们。

异步操作 MySQL

异步操作 MySQL 的话,需要使用一个 aiomysql,直接 pip install aiomysql 即可。aiomysql 底层依赖于 pymysql,所以 aiomysql 并没有单独实现相应的连接驱动,而是在 pymysql 之上进行了封装。

查询记录

下面先来看看如何查询记录。

import asyncio
import aiomysql.sa as aio_sa


async def main():
    # 创建一个异步引擎
    engine = await aio_sa.create_engine(host="xx.xxx.xx.xxx",
                                        port=3306,
                                        user="root",
                                        password="root",
                                        db="_hanser",
                                        connect_timeout=10)

    # 通过 engine.acquire() 获取一个连接
    async with engine.acquire() as conn:
        # 异步执行, 返回一个 <class 'aiomysql.sa.result.ResultProxy'> 对象
        result = await conn.execute("SELECT * FROM girl")
        # 通过 await result.fetchone() 可以获取满足条件的第一条记录, 一个 <class 'aiomysql.sa.result.RowProxy'> 对象
        data = await result.fetchone()

        # 可以将 <class 'aiomysql.sa.result.RowProxy'> 对象想象成一个字典
        print(data.keys())  # KeysView((1, '古明地觉', 16, '地灵殿'))
        print(list(data.keys()))  # ['id', 'name', 'age', 'place']

        print(data.values())  # ValuesView((1, '古明地觉', 16, '地灵殿'))
        print(list(data.values()))  # [1, '古明地觉', 16, '地灵殿']

        print(data.items())  # ItemsView((1, '古明地觉', 16, '地灵殿'))
        print(list(data.items()))  # [('id', 1), ('name', '古明地觉'), ('age', 16), ('place', '地灵殿')]

        # 直接转成字典也是可以的
        print(dict(data))  # {'id': 1, 'name': '古明地觉', 'age': 16, 'place': '地灵殿'}
    
    # 最后别忘记关闭引擎, 当然你在创建引擎的时候也可以通过 async with aio_sa.create_engine 的方式创建
    # async with 语句结束后会自动执行下面两行代码
    engine.close()
    await engine.wait_closed()
    

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

怎么样,是不是很简单呢,和同步库的操作方式其实是类似的。但是很明显,我们在获取记录的时候不会只获取一条,而是会获取多条,获取多条的话使用 await result.fetchall() 即可。

import asyncio
from pprint import pprint
import aiomysql.sa as aio_sa


async def main():
    # 通过异步上下文管理器的方式创建, 会自动帮我们关闭引擎
    async with aio_sa.create_engine(host="xx.xxx.xx.xxx",
                                    port=3306,
                                    user="root",
                                    password="root",
                                    db="_hanser",
                                    connect_timeout=10) as engine:
        async with engine.acquire() as conn:
            result = await conn.execute("SELECT * FROM girl")
            # 此时的 data 是一个列表, 列表里面是 <class 'aiomysql.sa.result.RowProxy'> 对象
            data = await result.fetchall()
            # 将里面的元素转成字典
            pprint(list(map(dict, data)))
            """
            [{'age': 16, 'id': 1, 'name': '古明地觉', 'place': '地灵殿'},
             {'age': 16, 'id': 2, 'name': '雾雨魔理沙', 'place': '魔法森林'},
             {'age': 400, 'id': 3, 'name': '芙兰朵露', 'place': '红魔馆'}]
            """


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

除了 fetchone、fetchall 之外,还有一个 fetchmany,可以获取指定记录的条数。

import asyncio
from pprint import pprint
import aiomysql.sa as aio_sa


async def main():
    # 通过异步上下文管理器的方式创建, 会自动帮我们关闭引擎
    async with aio_sa.create_engine(host="xx.xxx.xx.xxx",
                                    port=3306,
                                    user="root",
                                    password="root",
                                    db="_hanser",
                                    connect_timeout=10) as engine:
        async with engine.acquire() as conn:
            result = await conn.execute("SELECT * FROM girl")
            # 默认是获取一条, 得到的仍然是一个列表
            data = await result.fetchmany(2)
            pprint(list(map(dict, data)))
            """
            [{'age': 16, 'id': 1, 'name': '古明地觉', 'place': '地灵殿'},
             {'age': 16, 'id': 2, 'name': '雾雨魔理沙', 'place': '魔法森林'}]
            """


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

以上就是通过 aiomysql 查询数据库中的记录,没什么难度。但是值得一提的是,await conn.execute 里面除了可以传递一个原生的 SQL 语句之外,我们还可以借助 SQLAlchemy。

import asyncio
from pprint import pprint
import aiomysql.sa as aio_sa
from sqlalchemy.sql.selectable import Select
from sqlalchemy import text


async def main():
    async with aio_sa.create_engine(host="xx.xxx.xx.xxx",
                                    port=3306,
                                    user="root",
                                    password="root",
                                    db="_hanser",
                                    connect_timeout=10) as engine:
        async with engine.acquire() as conn:
            sql = Select([text("id, name, place")], whereclause=text("id != 1"), from_obj=text("girl"))
            result = await conn.execute(sql)
            data = await result.fetchall()
            pprint(list(map(dict, data)))
            """
            [{'id': 2, 'name': '雾雨魔理沙', 'place': '魔法森林'},
             {'id': 3, 'name': '芙兰朵露', 'place': '红魔馆'}]
            """


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

添加记录

然后是添加记录,我们同样可以借助 SQLAlchemy 帮助我们拼接 SQL 语句。

import asyncio
from pprint import pprint
import aiomysql.sa as aio_sa
from sqlalchemy import Table, MetaData, create_engine


async def main():
    async with aio_sa.create_engine(host="xx.xx.xx.xxx",
                                    port=3306,
                                    user="root",
                                    password="root",
                                    db="_hanser",
                                    connect_timeout=10) as engine:
        async with engine.acquire() as conn:
            # 我们还需要创建一个 SQLAlchemy 中的引擎, 然后将表反射出来
            s_engine = create_engine("mysql+pymysql://root:root@xx.xx.xx.xxx:3306/_hanser")
            tbl = Table("girl", MetaData(bind=s_engine), autoload=True)

            insert_sql = tbl.insert().values(
                [{"name": "十六夜咲夜", "age": 17, "place": "红魔馆"},
                 {"name": "琪露诺", "age": 60, "place": "雾之湖"}])

            # 注意: 执行的执行必须开启一个事务, 否则数据是不会进入到数据库中的
            async with conn.begin():
                # 同样会返回一个 <class 'aiomysql.sa.result.ResultProxy'> 对象
                # 尽管我们插入了多条, 但只会返回最后一条的插入信息
                result = await conn.execute(insert_sql)
                # 返回最后一条记录的自增 id
                print(result.lastrowid)
                # 影响的行数
                print(result.rowcount)
        
        # 重新查询, 看看记录是否进入到数据库中
        async with engine.acquire() as conn:
            data = await (await conn.execute("select * from girl")).fetchall()
            data = list(map(dict, data))
            pprint(data)
            """
            [{'age': 16, 'id': 1, 'name': '古明地觉', 'place': '地灵殿'},
             {'age': 16, 'id': 2, 'name': '雾雨魔理沙', 'place': '魔法森林'},
             {'age': 400, 'id': 3, 'name': '芙兰朵露', 'place': '红魔馆'},
             {'age': 17, 'id': 16, 'name': '十六夜咲夜', 'place': '红魔馆'},
             {'age': 60, 'id': 17, 'name': '琪露诺', 'place': '雾之湖'}]
            """


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

还是很方便的,但是插入多条记录的话只会返回插入的最后一条记录的信息,所以如果你希望获取每一条的信息,那么就一条一条插入。

修改记录

修改记录和添加记录是类似的,我们来看一下。

import asyncio
from pprint import pprint
import aiomysql.sa as aio_sa
from sqlalchemy import Table, MetaData, create_engine, text


async def main():
    async with aio_sa.create_engine(host="xx.xx.xx.xxx",
                                    port=3306,
                                    user="root",
                                    password="root",
                                    db="_hanser",
                                    connect_timeout=10) as engine:
        async with engine.acquire() as conn:
            s_engine = create_engine("mysql+pymysql://root:root@xx.xx.xx.xxx:3306/_hanser")
            tbl = Table("girl", MetaData(bind=s_engine), autoload=True)
            update_sql = tbl.update().where(text("name = '古明地觉'")).values({"place": "东方地灵殿"})

            # 同样需要开启一个事务
            async with conn.begin():
                result = await conn.execute(update_sql)
                print(result.lastrowid)  # 0
                print(result.rowcount)   # 1
		
        # 查询结果
        async with engine.acquire() as conn:
            data = await (await conn.execute("select * from girl where name = '古明地觉'")).fetchall()
            data = list(map(dict, data))
            pprint(data)
            """
            [{'age': 16, 'id': 1, 'name': '古明地觉', 'place': '东方地灵殿'}]
            """


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

可以看到,记录被成功的修改了。

删除记录

删除记录就更简单了,直接看代码。

import asyncio
import aiomysql.sa as aio_sa
from sqlalchemy import Table, MetaData, create_engine, text


async def main():
    async with aio_sa.create_engine(host="xx.xx.xx.xxx",
                                    port=3306,
                                    user="root",
                                    password="root",
                                    db="_hanser",
                                    connect_timeout=10) as engine:
        async with engine.acquire() as conn:
            s_engine = create_engine("mysql+pymysql://root:root@xx.xx.xx.xxx:3306/_hanser")
            tbl = Table("girl", MetaData(bind=s_engine), autoload=True)
            update_sql = tbl.delete()  # 全部删除

            # 同样需要开启一个事务
            async with conn.begin():
                result = await conn.execute(update_sql)
                # 返回最后一条记录的自增 id, 我们之前修改了 id = 0 记录, 所以它跑到最后了
                print(result.lastrowid)  # 0
                # 受影响的行数
                print(result.rowcount)   # 6


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

此时数据库中的记录已经全部被删除了。

整体来看还是比较简单的,并且支持的功能也比较全面。

异步操作 PostgreSQL

异步操作 PostgreSQL 的话,我们有两个选择,一个是 asyncpg 库,另一个是 aiopg 库。

asyncpg 是自己实现了一套连接驱动,而 aiopg 则是对 psycopg2 进行了封装,个人更推荐 asyncpg,性能和活跃度都比 aiopg 要好。

下面来看看如何使用 asyncpg,首先是安装,直接 pip install asyncpg 即可。

查询记录

首先是查询记录。

import asyncio
from pprint import pprint
import asyncpg

async def main():
    # 创建连接数据库的驱动
    conn = await asyncpg.connect(host="localhost",
                                 port=5432,
                                 user="postgres",
                                 password="zgghyys123",
                                 database="postgres",
                                 timeout=10)
    # 除了上面的方式,还可以使用类似于 SQLAlchemy 的方式创建
    # await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")

    # 调用 await conn.fetchrow 执行 select 语句,获取满足条件的单条记录
    # 调用 await conn.fetch 执行 select 语句,获取满足条件的全部记录
    row1 = await conn.fetchrow("select * from girl")
    row2 = await conn.fetch("select * from girl")

    # 返回的是一个 Record 对象,这个 Record 对象等于将返回的记录进行了一个封装
    # 至于怎么用后面会说
    print(row1)  # <Record id=1 name='古明地觉' age=16 place='地灵殿'>
    pprint(row2)
    """
    [<Record id=1 name='古明地觉' age=16 place='地灵殿'>,
     <Record id=2 name='椎名真白' age=16 place='樱花庄'>,
     <Record id=3 name='古明地恋' age=15 place='地灵殿'>]
    """

    # 关闭连接
    await conn.close()


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

以上我们演示了如何使用 asyncpg 来获取数据库中的记录,我们看到执行 select 语句的话,我们可以使用 conn.fetchrow(query) 来获取满足条件的单条记录,conn.fetch(query) 来获取满足条件的所有记录。

Record 对象

我们说使用 conn.fetchone 查询得到的是一个 Record 对象,使用 conn.fetch 查询得到的是多个 Record 对象组成的列表,那么这个 Rcord 对象怎么用呢?

import asyncio
import asyncpg


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    row = await conn.fetchrow("select * from girl")

    print(type(row))  # <class 'asyncpg.Record'>
    print(row)  # <Record id=1 name='古明地觉' age=16 place='地灵殿'>

    # 这个 Record 对象可以想象成一个字典
    # 我们可以将返回的字段名作为 key, 通过字典的方式进行获取
    print(row["id"], row["name"])  # 1 古明地觉

    # 除此之外,还可以通过 get 获取,获取不到的时候会返回默认值
    print(row.get("id"), row.get("name"))  # 1 古明地觉
    print(row.get("xxx"), row.get("xxx", "不存在的字段"))  # None 不存在的字段

    # 除此之外还可以调用 keys、values、items,这个不用我说,都应该知道意味着什么
    # 只不过返回的是一个迭代器
    print(row.keys())  # <tuple_iterator object at 0x000001D6FFDAE610>
    print(row.values())  # <tuple_iterator object at 0x000001D6FFDAE610>
    print(row.items())  # <RecordItemsIterator object at 0x000001D6FFDF20C0>

    # 我们需要转成列表或者元组
    print(list(row.keys()))  # ['id', 'name', 'age', 'place']
    print(list(row.values()))  # [1, '古明地觉', 16, '地灵殿']
    print(dict(row.items()))  # {'id': 1, 'name': '古明地觉', 'age': 16, 'place': '地灵殿'}
    print(dict(row))  # {'id': 1, 'name': '古明地觉', 'age': 16, 'place': '地灵殿'}

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

当然我们也可以借助 SQLAlchemy 帮我们拼接 SQL 语句。

import asyncio
from pprint import pprint
import asyncpg
from sqlalchemy.sql.selectable import Select
from sqlalchemy import text


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    sql = Select([text("id, name, place")], whereclause=text("id != 1"), from_obj=text("girl"))
    # 我们不能直接传递一个 Select 对象, 而是需要将其转成原生的字符串才可以
    rows = await conn.fetch(str(sql))
    pprint(list(map(dict, rows)))  
    """
    [{'id': 2, 'name': '椎名真白', 'place': '樱花庄'},
     {'id': 3, 'name': '古明地恋', 'place': '地灵殿'}]
    """

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

此外,conn.fetch 里面还支持占位符,使用百分号加数字的方式,举个例子:

import asyncio
from pprint import pprint
import asyncpg

async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    rows = await conn.fetch("select * from girl where id != $1", 1)
    pprint(list(map(dict, rows)))
    """
    [{'age': 16, 'id': 2, 'name': '椎名真白', 'place': '樱花庄'},
     {'age': 15, 'id': 3, 'name': '古明地恋', 'place': '地灵殿'}]
    """

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

还是推荐使用 SQLAlchemy 的方式,这样更加方便一些,就像 aiomysql 一样。但是对于 asyncpg 而言,实际上接收的是一个原生的 SQL 语句,是一个字符串,因此它不能像 aiomysql 一样自动识别 Select 对象,我们还需要手动将其转成字符串。而且这样还存在一个问题,至于是什么我们下面介绍添加记录的时候说。

添加记录

然后是添加记录,我们看看如何往库里面添加数据。

import asyncio
from pprint import pprint
import asyncpg
from sqlalchemy.sql.selectable import Select
from sqlalchemy import text


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    # 执行 insert 语句我们可以使用 execute
    row = await conn.execute("insert into girl(name, age, place) values ($1, $2, $3)",
                             '十六夜咲夜', 17, '红魔馆')
    pprint(row)  # INSERT 0 1
    pprint(type(row))  # <class 'str'>

    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

通过 execute 可以插入单条记录,同时返回相关信息,但是说实话这个信息没什么太大用。除了 execute 之外,还有 executemany,用来执行多条插入语句。

import asyncio
import asyncpg

async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    # executemany:第一条参数是一个模板,第二条命令是包含多个元组的列表
    # 执行多条记录的话,返回的结果为 None
    rows = await conn.executemany("insert into girl(name, age, place) values ($1, $2, $3)",
                                  [('十六夜咲夜', 17, '红魔馆'), ('琪露诺', 60, '雾之湖')])
    print(rows)  # None

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

注意:如果是执行大量 insert 语句的话,那么 executemany 要比 execute 快很多,但是 executemany 不具备事务功能。

await conn.executemany("insert into girl(id, name) values($1, $2)",
                       [(7, "八意永琳"), (7, "八意永琳"), (8, "zun")])

我们表中的 id 是主键,不可以重复,这里插入三条记录。显然第二条记录中的 id 和第一条重复了,执行的时候会报错。但是第一条 (7, "八意永琳") 是进入到数据库了的,尽管第二条记录在插入的时候执行失败。当然第二条执行失败,那么第二条之后的也就无法执行了,这一点务必注意。那如果我们想要实现事务的话该怎么做呢?

import asyncio
import asyncpg


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")

    # 调用 conn.transaction() 会开启一个事务,当然这里建议使用异步上下文管理
    async with conn.transaction():
        await conn.executemany("insert into girl(id, name) values($1, $2)",
                               [(999, "太田顺也"), (999, "zun")]
                               )

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

两条记录主键重复,最终这两条记录都没进入到数据库中。因此插入记录的话,个人建议直接开启一个事务,然后通过 executemany 即可。

因此 fetchrow、fetch 是专门针对 select 语句,而 execute 和 executemant 是针对 select 语句之外的其它语句。

但是问题来了,无论是 execute 还是 executemany,它们都没有返回插入记录之后的具体信息。比如:插入一条记录之后,我们希望返回插入记录的自增 id,这个时候该怎么做呢?答案是依旧使用 fetch。

import asyncio
import asyncpg

async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    async with conn.transaction():
        # 对于 fetch 而言, 我们不能像 executemany 那样, 传递一个包含元组的列表
        # 因此想插入多条记录的话, 只能先拼接好 SQL 语句, 并且想返回对应的自增 id 的话, 需要在语句结尾加上 returning id
        # 当然这个是数据库的语法, 否则得到的就是一个 None
        rows = await conn.fetch("insert into girl(name, age, place) values ('太田顺也', 43, 'japan'), ('zun', 43, 'japan') returning id")

    print(rows)  # [<Record id=15>, <Record id=16>]
    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

因此如果只是关注记录是否插入成功,那么推荐使用 executemany 加事务的方式,这样我们只需要传递一个包含元组的列表即可。如果还需要获取插入记录的自增 id,那么需要使用 fetch 加事务的方式(如果是多条 SQL 的话),但是需要事先将 SQL 语句拼接好才行,并且还要使用 PostgreSQL 提供的 returning 字句,否则是不会返回信息的(只能得到一个 None)。

至于 fetchrow 和 execute,它们只针对于单条,因此建议直接使用 fetch 和 executemany 即可。

修改记录

修改记录的话,仍然可以使用 executemany 或者 fetch,区别还是我们上面说的那样。

import asyncio
import asyncpg

async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    async with conn.transaction():
        # 修改一条记录, 返回一个字符串
        row = await conn.execute("update girl set name = $1 where name = $2", 'BAKA⑨', '琪露诺')
        print(row)  # UPDATE 1
        
        # 修改多条记录, 返回一个 None
        rows = await conn.executemany("update girl set name = $1 where name = $2",
                                      [("古明地盆", "古明地觉"), ("芙兰朵露斯卡雷特", "芙兰朵露")])
        print(rows)  # None

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

同样的,我们也可以使用 fetch,搭配 returning 语句。

import asyncio
import asyncpg


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    async with conn.transaction():
        row = await conn.fetch("update girl set name = 'BAKA9' where name = 'BAKA⑨' returning id")
        print(row)  # [<Record id=6>]

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

删除记录

没什么可说的了,直接看代码吧。

import asyncio
import asyncpg


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    async with conn.transaction():
        row = await conn.execute("delete from girl where name in ($1, $2)", "BAKA9", "古明地恋")
        print(row)  # DELETE 2

        rows = await conn.executemany("delete from girl where id = $1",
                                      # 嵌套元组的列表, 即使是一个值也要写成元组
                                      # 会将每一个元组里面的值和占位符进行逐一匹配
                                      [(2,), (3,)])
        print(rows)  # None

    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

同理如果想获取返回的自增 id的话,也是可以的,方面和上面一样。

import asyncio
import asyncpg


async def main():
    conn = await asyncpg.connect("postgres://postgres:zgghyys123@localhost:5432/postgres")
    async with conn.transaction():
        # 虽然不支持像 executemany 那样传递一个列表, 但是下面这种方式还是可以的
        # 会将元素和占位符逐一替换
        rows = await conn.fetch("delete from girl where name in ($1, $2) returning id", "太田顺也", "zun")
        print(rows)  # [<Record id=7>, <Record id=8>, <Record id=9>, <Record id=10>, <Record id=11>, <Record id=12>]
    # 关闭连接
    await conn.close()


if __name__ == '__main__':
    asyncio.run(main())

以上就是常见的增删改查操作,虽然没有 SQLAlchemy 那么强大,但是也足够用,当然 SQLAlchemy 内部提供的一些属性也是通过执行 SQL 语句获取的,然后封装成一个属性给你。如果需要的话,我们也可以手动实现执行 SQL 来获取。

连接池

asyncpg 还提供了连接池,需要的话往池子里面去取即可。

import asyncio
import asyncpg


async def main():
    pool = await asyncpg.create_pool(
        "postgres://postgres:zgghyys123@localhost:5432/postgres",
        min_size=10,  # 连接池初始化时默认的最小连接数, 默认为1 0
        max_size=10,  # 连接池的最大连接数, 默认为 10
        max_queries=5000,  # 每个链接最大查询数量, 超过了就换新的连接, 默认 5000
        # 最大不活跃时间, 默认 300.0, 超过这个时间的连接就会被关闭, 传入 0 的话则永不关闭
        max_inactive_connection_lifetime=300.0
    )
    # 如果还有其它什么特殊参数,也可以直接往里面传递,因为设置了 **connect_kwargs
    # 专门用来设置一些数据库独有的某些属性

    # 从池子中取出一个连接
    async with pool.acquire() as conn:
        async with conn.transaction():
            row = await conn.fetchrow("select '100'::int + 200")
            # 我们看到没有指定名字,随意返回字段名叫做 ?column?
            # 不要慌,PostgreSQL 中返回的也是这个结果
            print(row)  # <Record ?column?=300>

            # 解决办法就是起一个别名
            row = await conn.fetchrow("select '100'::int + 200 as result")
            print(row)  # <Record result=300>

    # 我们的连接是从池子里面取出的,上下文结束之后会自动放回到到池子里面


if __name__ == '__main__':
    # 这里就不要使用asyncio.run(main())了
    # 而是创建一个事件循环,然后运行
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

通过以上的例子,我们看到 asyncpg 还是非常好用的。另外值得一提的是,asyncpg 不依赖 psycopg2,asyncpg 是自己独立实现了连接 PostgreSQL 的一套驱动,底层不需要依赖 psycopg2 这个模块。

效率对比

我们之所以使用 asyncpg,无非是为了效率,那么 asyncpg 和传统的 psycopg2 相比,在效率上究竟有多少差距呢?我们来测试一下。

SELECT count(*) FROM interface; -- 8459729

我数据库中有一张表叫做 interface,是之前工作的时候从对方接口获取的,我们就用它来进行测试吧。

先使用同步版本的来访问,看看用多长时间。

import time
from sqlalchemy import create_engine, text

engine = create_engine("postgres://postgres:zgghyys123@localhost:5432/postgres")

with engine.begin() as conn:
    start = time.perf_counter()
    for _ in range(20):
        res = conn.execute(
            text('select * from interface where "ProwlerPersonID" = :arg'),
            {"arg": "c9fcbed8-fa47-481a-9d73-5fd1dd344f19"})
        print(f"满足条件的记录有:{len(res.fetchall())}条")
    end = time.perf_counter()
    print("总耗时:", end - start)  # 50.0419027
"""
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
满足条件的记录有:228186条
总耗时: 50.0419027
"""

我们看到记录有 20 多万条,所以就不打印记录了,因为执行的是同一条 SQL,所以结果是一样的,然后我们看到花了 50 秒钟。

再来看看使用异步版本用多长时间。

import time
import asyncio
import asyncpg


async def run_sql(conn, query_list):
    result = []
    for query in query_list:
        result.append(await conn.fetch(*query))
    await conn.close()
    return [f"满足条件的记录有:{len(_)}条" for _ in result]


async def main():
    async with asyncpg.create_pool("postgres://postgres:zgghyys123@localhost:5432/postgres") as pool:
        query_list = [('select * from interface where "ProwlerPersonID" = $1',
                       "c9fcbed8-fa47-481a-9d73-5fd1dd344f19")
                      for _ in range(20)]

        # 我们要创建5个连接异步访问
        count = len(query_list) // 5
        # 将 20 个任务分成 5 份
        query_list = [query_list[c * 4: (c + 1) * 4] for c in range(count + 1)]

        tasks = []
        for q in query_list:
            conn = await pool.acquire()
            tasks.append(run_sql(conn, q))
        results = await asyncio.gather(*tasks)
        return results

if __name__ == '__main__':
    start = time.perf_counter()
    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(main())
    end = time.perf_counter()
    for result in results:
        for _ in result:
            print(_)
    print("总耗时:", end - start)
    """
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    满足条件的记录有:228186条
    总耗时: 9.8730488
    """

我们看到花了将近十秒钟,正好是同步版本的五分之一,因为我们使用了连接池中的五个连接。

注意:如果是 SQLAlchemy,即便你给每一个 SQL 开一个连接,如果不使用多线程,只是同步访问的话,那么耗时还是 50 秒左右,因为它是同步访问的。

而使用异步的模式访问的话,每个连接都可以进行异步访问,那么我们创建的 5 个连接中,如果一个连接阻塞了,会切换到其它的连接去执行。所以耗时为五分之一,不过这里可能有人会觉得困惑,不知道我上面的代码做了些什么,这里来解释一下。

async def f1():
    for _ in [耗时协程1, 耗时协程2, 耗时协程3]:
        await _
        
def f2():
    for _ in [耗时函数1, 耗时函数2, 耗时函数3]:
        _()

我们上面的两段代码,如果函数和协程里面的代码做了相同的事情的话,那么这两个 for 循环耗时基本是一致的。首先函数无需解释,关键是协程为什么会这样。

我们 await 协程,会等待这个协程完成,对于一个 for 循环来说,不可能说当前循环还没执行完毕就去执行下一层循环。所以无论是协程还是普通的函数,都要经历三轮循环,所以它们的耗时是基本一致的。

如果想解决这个问题,那么就要使用 asyncio.gather(*协程列表),如果是 await asyncio.gather(*[耗时协程1, 耗时协程2, 耗时协程3]),那么时间相比普通函数来说,就会有明显的缩短。因为此时这三个协程是同时发出的。

我们上面使用 asyncio.gather 的目的就在于此,但是问题是我们为什么要创建多个连接、为什么要把 20 个任务分成 5 份呢。

首先对于数据库来讲,一个连接只能同时执行一个 SQL,如果你使用多线程、但是每个线程用的却是同一个连接的话,你会发现耗时和原来基本没区别。虽然线程阻塞会自动切换,但是你使用的连接已经被别人用了,所以请求同时只能发一个。如果是 asyncpg 的话,一个连接同时执行多了 SQL,那么会直接报错。

import asyncio
import asyncpg


async def main():
    async with asyncpg.create_pool("postgres://postgres:zgghyys123@localhost:5432/postgres") as pool:
        async with pool.acquire() as conn:
            # 将20个请求同时发出,理论上可以,但是对于数据库来说是不同意的
            # 因为我们这里的conn都是同一个连接
            query_list = [conn.fetch('select * from interface where "ProwlerPersonID" = $1',
                                     "c9fcbed8-fa47-481a-9d73-5fd1dd344f19")
                          for _ in range(20)]
            await asyncio.gather(*query_list)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
"""
  File "asyncpg\protocol\protocol.pyx", line 301, in query
  File "asyncpg\protocol\protocol.pyx", line 664, in asyncpg.protocol.protocol.BaseProtocol._check_state
asyncpg.exceptions._base.InterfaceError: cannot perform operation: another operation is in progress
"""

阅读报错信息,提示我们:无法执行操作,另一个操作已经在执行了。

说明我们的20个请求都是同一个连接发出的,第一个连接在执行的时候,第二个连接理论上应该处于阻塞状态的,但是这里直接报错了。但不管怎么样,都不是我们想要的。

所以我们只能使用上面说的那样,使用 for 循环里面写 await 协程 的方式,但是这样和同步又没什么区别。因此我们创建 5 个连接,对 5 个连接使用 asyncio.gather,也就是让这五个连接同时执行。尽管每个连接内部执行的逻辑是同步的,但是这 5 个连接整体是异步的,因为它们彼此没有关联,是不同的连接,因此异步耗时为同步的五分之一。

异步操作 Redis

最后来看看如何异步操作 Redis,异步操作 Redis 我们需要使用 aioredis 这个第三方库。安装同样简单,直接 pip install aioredis 即可。

import asyncio
import aioredis

async def main():
    conn = await aioredis.create_connection("redis://:passwd@localhost:6379")
    # 执行的话, 执行通过 await conn.execute 即可, 就像在命令行里执行 Redis 命令一样
    data = await conn.execute("set", "name", "kugura_nana", "ex", 10)
    
    # 关闭连接
    await conn.close()
    # 判断连接是否关闭
    print(conn.closed)  # True
    
asyncio.run(main())

更详细的用法可以参考官网,Redis 的内容还是比较多的,但都比较简单。

此外,如果你编写的服务是一个同步的服务,那么至少在 Redis 方面其实没太大必要换成异步库,因为 Redis 本身已经足够快了。

总结

高并发在现如今是一个主流,任何服务都需要考虑到并发量的问题,如果能够很轻松地支持并发的话,那么肯定是非常受欢迎的。而 Go 语言之所以这么火,很大一部分原因就是它在语言层面就支持并发,当然其它语言也不甘落后。于是 Python 在 3.5 的时候也引入了原生协程,而紧接着围绕着相关生态的异步库自然随之诞生。

posted @ 2020-02-10 14:02  古明地盆  阅读(8150)  评论(0编辑  收藏  举报