一次使用fastapi开发的异步接口、异步redis、异步mysql

688 阅读6分钟

最近做了一个小项目,尝试用fastapi框架异步方式开发了接口。

开发过程中【通义千问帮了很大的忙,很感谢这个免费的GPT,我用的国产产品中助力最大的GPT!】

从掘金看到很多帮助很大的文章,不需要关注作者,不需要点击广告,不需要花钱就看到学到,很感激。 所以想略尽薄力,填充下掘金的笔记库。

在fastapi开发异步hello world

from fastapi import FastAPI, status

async def lifespan(app: FastAPI):
    # startup
    print('lifespan startup....')
    yield
    # stopup
    print('lifespan stopup....')
    
app = FastAPI(title="my api 2024-01", docs_url=None, redoc_url=None, lifespan=lifespan)

@app.get("/")
async def root():
    return {"message": "Hello World."}

if __name__ == "__main__":
    # Use this for debugging purposes only
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=23400)

异步操作redis

pip install redis

直接贴我用的代码,功能如下

  1. 可以异步读写redis
  2. 主库挂掉自动切换备库,不包含自动切回主库
  3. 通义千问提供代码结构,我进行debug。
from redis.asyncio import StrictRedis
from app.core.config import settings
from app.logger import logger

async def create_redis_connection(host: str):
    redis_conn = await StrictRedis(
        host=host,
        password=settings.REDIS_PASSWORD,
        port=6379,
        encoding="utf8",
        socket_connect_timeout=1,
        decode_responses=True,
        db=0,
    )
    return redis_conn

is_try_slave = False
class RedisStore:
    def __init__(self):
        self.redis_conn = None  # 不应在初始化时直接调用异步方法

    async def init(self, is_master=True):
        if is_master:
            tmp_conn = await create_redis_connection(settings.REDIS_HOST)
        else:
            tmp_conn = await create_redis_connection(settings.REDIS_HOST_SLAVE)
        try:
            await tmp_conn.ping()
            self.redis_conn = tmp_conn
            if is_master:
                logger.info('master redis connect success.')
            else:
                logger.info('slave redis connect success.')
        except Exception as e:
            if is_master:
                logger.info('master redis connect failed, ready to connect slave redis')
                await self.init(is_master=False)
            else:
                logger.info('slave redis connect failed')

    async def set(self, key, value):
        await self.redis_conn.set(key, value)
        await self.redis_conn.expire(key, 60 * 60 * 12)  # 12 hours

    async def get(self, key):
        res = await self.redis_conn.get(key)
        return res

redis_store = RedisStore()

初始化redis

# main.py
async def lifespan(app: FastAPI):
    logger.info('lifespan startup....')
    await check_mysql_connection()
    # startup
    await redis_store.init()
    if redis_store.redis_conn is not None:
        logger.info('redis init done....')
    else:
        logger.info('redis init failed')
    yield
    # stopup
    logger.info('lifespan stopup....')
        

异步操作mysql

这部分查了很久的资料,有好的知识不知道怎么整理,直接贴代码吧~

功能如下:

  1. 异步对MySQL读写
  2. 主库挂掉自动切换备库,不包含自动切回主库
  3. 通义千问提供代码结构,我进行debug。

pip install SQLAlchemy cryptography asyncmy

class Settings():
    MYSQL_ENGIN: str = 'asyncmy' # pymysql, asyncmy
    SQLALCHEMY_DATABASE_URI: str = f"mysql+{MYSQL_ENGIN}://root:123456@localhost_master/demo"
    SQLALCHEMY_DATABASE_URI_SLAVE: str = f"mysql+{MYSQL_ENGIN}://root:123456@localhost_slave/demo"
# app/db/session.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
from sqlalchemy import select
from app.models.user import User
from app.logger import logger
# 使用元组存储引擎和会话创建器
db_dict = {
    'engine': create_async_engine(settings.SQLALCHEMY_DATABASE_URI),
}
db_dict['session_local'] = sessionmaker(bind=db_dict['engine'], autocommit=False, autoflush=False, class_=AsyncSession)

engines = {
    'master': create_async_engine(settings.SQLALCHEMY_DATABASE_URI),
    'slave': None,
}
session_locals = {
    'master': sessionmaker(bind=engines['master'], autocommit=False, autoflush=False, class_=AsyncSession),
    'slave': None,
}

session_key = 'master'
engine = None
SessionLocal = None

async def check_mysql_connection(is_master=True):
    async with db_dict['session_local']() as db:
        try:
            await db.execute(select(User))
            if is_master:
                logger.info(f"master MySQL connect success.")
            else:
                logger.info(f"slave MySQL connect success.")
        except Exception as e:
            # 如果当前是主库且连接失败,则尝试连接从库
            if is_master:
                logger.info('Master MySQL connect failed, ready to connect slave MySQL.')
                # logger.info(f'{settings.SQLALCHEMY_DATABASE_URI_SLAVE}')
                db_dict['engine'] = create_async_engine(settings.SQLALCHEMY_DATABASE_URI_SLAVE)
                db_dict['session_local'] = sessionmaker(bind=db_dict['engine'], autocommit=False, autoflush=False, class_=AsyncSession)
                await check_mysql_connection(is_master=False)
            else:
                logger.info(f"Slave MySQL connect failed.")

获取session的函数

# app/api/deps.py
from app.db.session import db_dict
from sqlalchemy.ext.asyncio import AsyncSession
        
async def get_db() -> AsyncSession:
    async with db_dict['session_local']() as session:
        yield session

在接口获取数据库session

# app/api/endpoints/user.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
import app.crud as crud
from app.api import deps

router = APIRouter()

@router.get("/users")
async def get_all_users(db: AsyncSession = Depends(deps.get_db)):
    users = await crud.user.get_users(db=db)
    return { 'code': 200, 'user_list': users, 'total_count': len(users) }

crud

# app/crud/crud_user.py
from fastapi.encoders import jsonable_encoder
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.crud.base import CRUDBase
from app.models.user import User
import app.schemas as schemas

class CRUDUser(CRUDBase):
    async def create(self, db: AsyncSession, *, obj_in: schemas.CreateUserType) -> None:
        db_obj = User(**jsonable_encoder(obj_in))
        db.add(db_obj)
        await db.commit()

    async def get_users(self, db: AsyncSession):
        r = await db.execute(select(User.name, User.uuid, User.phone).filter(User.is_del == False))
        users = r.all()
        # 需要转成dict,否则无法json dumps
        return [dict(user._mapping) for user in users]

user = CRUDUser(User)

jMeter

截止目前发现jmeter很方便使用。我是在mac使用,直接brew install jmeter就完事~ 当然电脑要装好Java,具体怎么装网上搜索或者使用GPT工具,比如阿里的通义千问就挺好用。

安装插件

直接可以在菜单上打开插件管理器进行安装,不需要像以前一样手动安装了。

image.png

我装了这两个插件,用于生成图表。

image.png

开始测试

  1. 输入计划名称,保存为文件 image.png

  2. 点击计划右键->创建线程组 image.png

  3. 线程组点击右键->创建http header image.png 3.1. http heade点击左键 -> 点击最下方Add按钮 -> 在表格新增内容 Screen Shot 2024-01-18 at 15.03.39.png 根据实际情况设置接口通用头部信息

  4. 线程组点击右键 -> 创建http request image.png

  5. 点击http request进行编辑 image.png

  6. 线程组右键 -> 创建两个数据收集监视器 View Results Tree: 可以查看每个请求具体信息,还要将数据导出的指定文件,用于后续生成图表。 Screen Shot 2024-01-18 at 15.12.57.png

Summary Report: 可以查看综合信息,我主要是看Throughput,可以简单理解为业务QPS Screen Shot 2024-01-18 at 15.18.23.png

6.1. 设置请求信息保存路径 image.png

  1. 线程组右键 -> 新增Transactions per Second 要记得设置步骤6.1中的结果路径,会自动读取这个结果生成图表 image.png

  2. 线程组设置 我喜欢设置的是:线程数量设置100个,线程生成时间设置3秒,循环100次。总计会请求100 x 100 = 10000次。一发入魂。 image.png

  3. 然后在Summary Report和Transactions per Second查看请求速度。

  4. 报错信息可以在View Results Tree中查看,比如下面这个图红了一片有报错。 image.png

以上, 只是小白,对这个工具不熟。简单使用记录结束。

以下是优秀文章链接:

Docker相关常识

避免每次都运行pip安装依赖

栗子A:

FROM python:3.10.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
RUN pip install --no-cache-dir -r requirements.txt --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
COPY . .

栗子B:

FROM python:3.10.13-slim
WORKDIR /app
COPY . .
RUN pip install --upgrade pip --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
RUN pip install --no-cache-dir -r requirements.txt --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com

两个栗子的区别是一个是A一个是B。

还有就是栗子A在只是修改接口代码,不修改requirements.txt的时候不会触发重新pip安装。

栗子B是不修改requirements.txt,只是修改接口代码的时候都会触发pip安装。

原理网上很多,反正我就记住这样了~

通过本次吸取经验是:可以基于基础镜像制作一份包含pip依赖库的环境镜像,然后制作接口镜像时候引用环境镜像。 比如我的环境镜像配置文件是:

# 使用官方Python基础镜像
FROM python:3.10.13-slim

# 设置工作目录
WORKDIR /app

# 复制仅包含requirements.txt的文件夹
COPY requirements.txt .

# 安装依赖,这里分开两步是为了充分利用缓存
# 若requirements.txt未变,则只会使用缓存
RUN pip install --upgrade pip --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com
RUN pip install --no-cache-dir -r requirements.txt --index-url https://mirrors.cloud.tencent.com/pypi/simple --trusted-host mirrors.cloud.tencent.com

以上, 网上文章很多很杂,先GTP一下然后再搜索会学到新知识效率杠杠的~

ps: 掘金网页编辑器吃字的bug什么时候能修好?每次写完都要先换行改字。