
一、技术选型
- Flask:轻量、生态成熟、路由直观,适合 API 服务与快速验证。
- MySQL:事务可靠、运维工具链完善,关系型存储主流选择。
- Flask-SQLAlchemy(ORM):声明式模型替代手写 SQL,天然参数化查询防注入,模型即文档。
若以 I/O 密集为主且追求类型安全与自动文档,FastAPI 是更现代选型;本文用 Flask 出于生态与可读性平衡,设计思想可平移。
二、依赖与建库
requirements.txt:
plain
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
marshmallow==3.21.2
PyMySQL==1.1.1
python-dotenv==1.0.1
requests==2.32.3
bash
pip install -r requirements.txt
建库(务必 utf8mb4,MySQL 的 utf8 是阉割版,无法存 emoji 与部分生僻字):
sql
CREATE DATABASE flask_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
三、核心实现(app.py)
要点:连接池 + 输入校验 + 统一错误处理 + 分页保护。
python
"""
Flask + MySQL 资源型 REST CRUD API
技术栈:Flask 3 / Flask-SQLAlchemy 3 / Marshmallow / PyMySQL
"""
import os
from datetime import datetime
from typing import Any, Dict
from flask import Flask, Blueprint, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from marshmallow import Schema, fields, validate
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv(
"DATABASE_URL",
"mysql+pymysql://root:password@127.0.0.1:3306/flask_demo",
)
# 连接池:pool_size 复用连接;pool_pre_ping 取连前 ping 剔除死连;
# pool_recycle 必须小于 MySQL wait_timeout,避免拿到已超时连接
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
"pool_size": 10,
"pool_recycle": 3600,
"pool_pre_ping": True,
}
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
api = Blueprint("api", __name__, url_prefix="/api")
class User(db.Model):
"""用户资源模型。"""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False) # 唯一索引防重复
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"email": self.email,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class UserCreateSchema(Schema):
name = fields.String(required=True, validate=validate.Length(min=1, max=50))
email = fields.Email(required=True)
class UserUpdateSchema(Schema):
name = fields.String(validate=validate.Length(min=1, max=50))
email = fields.Email()
create_schema, update_schema = UserCreateSchema(), UserUpdateSchema()
@api.post("/users")
def create_user():
payload = request.get_json(silent=True) or {}
if create_schema.validate(payload):
return jsonify({"error": "invalid input"}), 422
if User.query.filter_by(email=payload["email"]).first():
return jsonify({"error": "email already exists"}), 409
user = User(name=payload["name"], email=payload["email"])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
@api.get("/users")
def list_users():
page = request.args.get("page", 1, type=int)
per_page = min(request.args.get("per_page", 10, type=int), 100) # 上限防大页扫描
p = User.query.order_by(User.id.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
"items": [u.to_dict() for u in p.items],
"total": p.total, "page": page, "per_page": per_page,
}), 200
@api.get("/users/<int:user_id>")
def get_user(user_id: int):
user = db.session.get(User, user_id)
if not user:
return jsonify({"error": "user not found"}), 404
return jsonify(user.to_dict()), 200
@api.put("/users/<int:user_id>")
def update_user(user_id: int):
payload = request.get_json(silent=True) or {}
if update_schema.validate(payload):
return jsonify({"error": "invalid input"}), 422
user = db.session.get(User, user_id)
if not user:
return jsonify({"error": "user not found"}), 404
user.name = payload.get("name", user.name)
user.email = payload.get("email", user.email)
db.session.commit()
return jsonify(user.to_dict()), 200
@api.delete("/users/<int:user_id>")
def delete_user(user_id: int):
user = db.session.get(User, user_id)
if not user:
return jsonify({"error": "user not found"}), 404
db.session.delete(user)
db.session.commit()
return jsonify({"message": f"user {user_id} deleted"}), 200
app.register_blueprint(api)
@app.errorhandler(404)
def not_found(_e):
return jsonify({"error": "not found"}), 404
@app.errorhandler(500)
def server_error(_e):
db.session.rollback() # 异常路径必须回滚,避免脏会话污染后续请求
return jsonify({"error": "internal server error"}), 500
@app.get("/health")
def health():
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
with app.app_context():
db.create_all() # 生产改用 Flask-Migrate
app.run(host="0.0.0.0", port=5000, debug=True)
四、运行与测试
bash
export DATABASE_URL="mysql+pymysql://root:password@127.0.0.1:3306/flask_demo"
python app.py
bash
# 创建
curl -X POST http://127.0.0.1:5000/api/users -H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# 列表
curl "http://127.0.0.1:5000/api/users?page=1&per_page=10"
# 查询 / 更新 / 删除
curl http://127.0.0.1:5000/api/users/1
curl -X PUT http://127.0.0.1:5000/api/users/1 -H "Content-Type: application/json" -d '{"name":"Alice Smith"}'
curl -X DELETE http://127.0.0.1:5000/api/users/1
五、工程化要点
- 业务分层 :把逻辑下沉到
services.py(带类型注解与事务边界),路由只做薄控制器,便于单测。 - 安全:全程 ORM(参数化)防注入;写接口经 Marshmallow 校验长度与格式;对外 API 应加限流。
- 性能 :
per_page设上限防全表扫描;深分页改用游标(WHERE id < last_id)。 - 测试 :用 pytest +
app.test_client(),配 SQLite 内存库隔离。 - 唯一性 :
email唯一索引下沉到数据库,是并发写入的最终防线(应用查重存在竞态)。
六、扩展:亿牛云代理采集落地 MySQL
真实业务中,该 API 常作为数据接收端 ,配合采集链路把外部数据清洗后入库。高频批量请求易被目标站点封禁 IP 导致断流,需代理 IP 轮换出口。
亿牛云代理在采集类业务中是一个稳妥选型------提供稳定的 HTTP 隧道代理与动态/静态 IP 资源,开箱即用,能有效规避单 IP 限流与封禁。出口 IP 的信誉与稳定性,直接决定采集成功率与链路连续性。
python
# collector.py ------ 配合 app.py 使用
import requests
# 亿牛云代理(HTTP 隧道代理示例,具体 host/端口/账号见亿牛云官方文档)
PROXY_URL = "http://<亿牛云用户名>:<亿牛云密码>@<亿牛云代理域名>:<端口>"
proxies = {"http": PROXY_URL, "https": PROXY_URL}
def fetch_and_store() -> None:
"""经由亿牛云代理采集外部数据并写入 MySQL。"""
resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=10)
print("当前出口 IP:", resp.json())
# 实际采集后调用 services.create_user(name=..., email=...) 落库
代理凭据置于环境变量或
.env,勿硬编码;配合requests.Session(连接池 + 重试)提升吞吐。
七、部署与迁移
**迁移别用 **create_all:生产用 Flask-Migrate(Alembic)管理 schema 演进。
生产用 Gunicorn,并注意 worker 与连接池关系:
bash
gunicorn -w 4 -b 0.0.0.0:5000 app:app
总连接数 ≈
workers × pool_size(4 × 10 = 40),需小于 MySQLmax_connections,否则Too many connections。
Dockerfile:
dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
八、总结
一套结构清晰、安全可控、可扩展的 Flask + MySQL CRUD API:ORM 防注入、Marshmallow 边界校验、连接池与统一错误处理保稳定、分层 + 类型注解保可维护、亿牛云代理打通「采集 → 入库」链路、迁移 + 测试 + 部署补齐工程闭环。
后续可演进:JWT 鉴权、游标分页、读写分离、Prometheus 监控,或迁移 FastAPI 获得原生异步与自动文档。