1. 项目定位与整体拆解思路
聊聊这个"Python基于Flask的微服务的手机商城管理系统"。第一次听到这个名字,很多人第一反应是:Flask那么轻、那么小,真能拿来做微服务?我的答案是,不仅能做,而且Flask这种轻量级框架放在微服务架构里反而很合适。微服务本来就不要求每个服务是一个重型框架,它要求的是每个服务足够轻、足够独立、能独立部署、能独立扩展。Flask刚好踩在这个点上。
这个项目的本质是什么?它不是一个大而全的单体商城,而是把手机商城的用户、商品、购物车、订单、支付这些业务模块拆成独立的服务,每个服务跑在独立的进程里,通过HTTP或者消息队列相互协作。手机商城在这个项目里的定位不只是"卖手机的商城",它更是一个能验证微服务落地能力的业务载体。用户管理要拆、商品管理要拆、订单流程要拆,拆完以后还要能合起来跑通一个完整的下单链路。
这个项目适合谁?想系统学习微服务架构但不想一上来就碰Java那一套重型微服务体系的Python开发者;需要做毕业设计或者课程项目、想体现"架构能力"而非"CRUD能力"的学生;还有想把自己的单体商城重构为微服务、但担心步子迈太大的外包或小团队开发者。看完你可以获得一条可以直接照抄的落地路径,包括服务划分、目录结构、核心接口设计、关键代码实现和真实环境里的排查经验。
1.1 为什么用Flask而不是其他框架?
我在选择微服务的Web框架时,其实是拿Flask、Django、FastAPI三个做过一次对比。Django功能太全,自带ORM、Admin、表单、认证,这些东西在一个微服务里多数是冗余的;而且Django的App机制容易让人把微服务又写成"微内核大泥球"。FastAPI性能好、自带OpenAPI文档,如果是纯Python 3.6+的新项目确实有优势,但它在团队里的普及度和生态成熟度不如Flask。
最终选Flask,核心理由是三个。第一,启动一个Flask服务只需要几行代码,非常适合一个服务一个进程的微服务模型;第二,Flask的Application Factory模式可以轻松创建多个服务实例,配合Blueprint可以实现模块化路由,这一点在做服务拆分时特别顺手;第三,Flask的路由、请求上下文、错误处理机制非常简单直观,出问题的时候你可以很快定位到是框架层面的锅还是业务逻辑的锅。很多团队在微服务网关或者BFF层也会用Flask,就是因为它的"薄"恰好是中间层最需要的特性。
Flask的Request上下文还有一个隐藏价值:它天然是线程隔离的。每个请求进来时,request、g、session这些对象都是独立的,不会互相污染。这一点在微服务场景下特别重要,因为服务之间可能会用一个线程池去并发调上游接口,如果全局变量共用,就会遇到数据错乱的诡异问题。
1.2 服务拆分的边界:不是越细越好
微服务最核心的问题是"拆成几个服务、按什么标准拆"。有人会把一个商城拆成十几个服务,然后发现开发效率极低,改一个字段要发布四五个服务。我在这个项目里采用的是按业务能力拆分,这是微服务领域最经典也最稳妥的方法。
我把它拆成了六个服务:用户服务(负责注册、登录、Token鉴权、收货地址)、商品服务(负责手机品牌、型号、SKU、库存)、购物车服务(负责加购、改数量、勾选结算)、订单服务(负责生成订单、订单状态流转、取消订单)、支付服务(负责订单支付、回调处理,这里用一个内部模拟支付网关)、以及后台管理服务(负责商品上下架、库存调整、订单审核、数据统计)。这套拆分逻辑遵循了三个原则:
- 独立的业务生命周期:商品和订单都有自己独立的生命周期,商品从编辑到上架到下架,订单从创建到待付款到已发货到已完成,生命周期不重叠的模块适合拆开。
- 独立的数据库边界:每个服务的数据库物理隔离,不允许直接跨库查表,只能通过API拿数据。这是微服务的第一条红线。
- 独立的扩展维度:比如手机新品首发时,商品服务和订单服务的压力曲线完全不一样,拆开后可以单独给商品服务扩容,不必把整个系统都扛上。
我的切身体会是:六是这个项目的最优规模。再少体现不出微服务的优势,再多对个人开发者和中小型团队就是负担。60%的架构收益在把单体拆到六个服务时就能拿到,再往下拆就是纯增成本了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与基础架构搭建
2.1 技术栈与中间件选型明细
这个项目不是一个单技术栈的东西,真正写项目的时候你会发现它是"Python为主、多中间件配合"的组合。我把关键组件和选择理由列出来,方便你对照自己的实际情况做取舍。
| 组件 | 选型 | 选型理由 |
|---|---|---|
| Web框架 | Flask 2.x | 轻量、灵活、生态成熟,适合微服务场景 |
| ORM | SQLAlchemy 2.x | 摆脱手写SQL的繁琐,配合Flask-SQLAlchemy使用体验最好 |
| 数据库 | MySQL(主存储)+ Redis(缓存/分布式锁) | 商品、订单等核心数据必须走关系型数据库保证事务;热数据走Redis |
| 服务间通信 | HTTP REST + Redis消息队列 | REST用于同步查询,消息队列用于最终一致性业务 |
| 服务注册发现 | Consul | 支持健康检查、KV存储、DNS接口,Python客户端也算友好 |
| 部署 | Docker + Nginx | 每个服务独立镜像,Nginx做反向代理和负载均衡 |
这套组合在性能和开发效率之间比较均衡。如果你不想维护MySQL,用PostgreSQL也完全没问题;如果Consul对你来说太重,也可以用etcd;甚至你只用Docker Compose做服务间名称解析,不搞注册中心也能先把链路跑通。这些替换方案我在后面会提到。
2.2 项目目录与Flask服务骨架
微服务项目在代码组织上最重要的是"一个服务一个目录、每个目录独立成一套Flask应用"。我习惯的目录结构是这样的:
text复制phone-mall/
├── services/
│ ├── user_service/
│ │ ├── app.py
│ │ ├── config.py
│ │ ├── models.py
│ │ ├── views/
│ │ │ ├── auth.py
│ │ │ └── address.py
│ │ ├── utils.py
│ │ └── requirements.txt
│ ├── product_service/
│ ├── cart_service/
│ ├── order_service/
│ ├── payment_service/
│ └── admin_service/
├── common/
│ ├── response.py
│ ├── db.py
│ └── jwt_util.py
├── gateway/
│ └── app.py
├── docker-compose.yml
└── README.md
每个Flask服务的入口文件写得非常统一,核心就是Application Factory加Blueprint。比如用户服务的app.py长这样:
python复制from flask import Flask
from utils import register_blueprints, consul_register
def create_app(config_name="default"):
app = Flask(__name__)
app.config.from_object(config_name)
register_blueprints(app)
consul_register(app)
return app
application = create_app()
if __name__ == "__main__":
application.run(host="0.0.0.0", port=5001)
统一入口文件带来的直接好处是,后来新增服务时,新服务的开发者不需要去理解任何额外的启动逻辑,抄目录结构就行。这在业务服务多了以后价值非常大,因为微服务项目最大的痛点不是技术难度,而是服务一多、人的认知负担就上来了。只有把每个服务做得一模一样,团队才能"从一个服务推及所有服务"。
3. 核心业务服务逐一落地
3.1 用户服务:手机号验证码登录与JWT鉴权
手机商城的第一道关是用户登录。考虑到商城的移动端属性,我没有用传统的用户名密码模式,而是做了手机号验证码登录。验证码存在Redis里,5分钟有效,60秒内不能重复发送。登录成功之后签发JWT,后续所有服务都通过JWT识别用户身份。
用户表的设计重点在"标识唯一"和"扩展性好":
sql复制CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`phone` VARCHAR(20) NOT NULL COMMENT '手机号',
`nickname` VARCHAR(50) DEFAULT NULL,
`avatar_url` VARCHAR(255) DEFAULT NULL,
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
JWT签发这段代码是整个用户服务里被复用最多的部分:
python复制import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-change-me"
TOKEN_EXPIRE_MINUTES = 60 * 24 * 7
def generate_token(user_id: int, phone: str) -> str:
payload = {
"user_id": user_id,
"phone": phone,
"exp": datetime.utcnow() + timedelta(minutes=TOKEN_EXPIRE_MINUTES),
"iat": datetime.utcnow()
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
def parse_token(token: str) -> dict:
try:
return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
raise AuthError("登录已过期,请重新登录")
except jwt.InvalidTokenError:
raise AuthError("无效的令牌")
我实际用下来,JWT + Redis做手机商城鉴权有个细节容易踩坑:JWT是无状态的,服务端无法主动让token失效。如果你上线了"用户被封禁"功能,只删token没用,旧token在过期前依然能通过验证。我的做法是在请求里加一层check:解析token后,再从Redis查一个user:block:${user_id}的key,如果存在就直接拒绝。这既保留了JWT的无状态优势,又解决了主动吊销的难题。
3.2 商品服务:手机SKU设计与库存扣减
手机商品最大的特点是SKU维度多且深。一台手机会有品牌、系列、存储容量、运行内存、颜色、是否有货等多个属性。在设计商品表时,我把商品基础信息(Product)和SKU信息(Sku)拆成了两张表,这样新增一个SKU时不需要动商品主表。
商品主表的字段不用太多,但一定得包含搜索高频字段:
python复制class Product(db.Model):
__tablename__ = 'product'
id = db.Column(db.BigInteger, primary_key=True)
title = db.Column(db.String(128), nullable=False) # 商品标题
brand = db.Column(db.String(32), index=True) # 品牌
series = db.Column(db.String(32), index=True) # 系列
main_image = db.Column(db.String(255))
status = db.Column(db.SmallInteger, default=1) # 1上架 0下架
sales_count = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=db.func.now())
SKU表里面存的是真正参与库存扣减和价格计算的粒度。比如"某品牌手机 12GB+256GB 曜石黑"是一个SKU,"12GB+512GB 云白"是另一个SKU。这里要特别提醒:库存必须挂在SKU上,不能挂在商品上,否则就会出现"整个商品显示有货,但用户选中的那个颜色配置已经缺货"的尴尬场景。
商品服务对外暴露的核心接口有这几个:/api/v1/product/list(分页列表,支持品牌、价格区间搜索)、/api/v1/product/{id}(商品详情)、/api/v1/sku/{id}(SKU详情)、/api/v1/product/admin/up/{id}和/api/v1/product/admin/down/{id}(后台上下架接口)。后台管理服务在下架商品时,会调用商品服务完成状态变更,再把操作记录写入管理日志,这个链路我在第四章还会提到。
3.3 订单服务:创建订单的完整链路
订单服务是整个商城的技术核心,它的复杂度不在于SQL,而在于要协调多个服务的数据。标准的下单流程是这样的:
用户从购物车服务拿到选中的SKU列表,然后订单服务开始执行"预下单"流程。预下单并不直接生成订单,而是先把用户请求的SKU、数量、收货地址这些信息存到Redis临时缓存,同时调商品服务锁库存。锁库存成功之后才真正生成订单。库存锁存的是超时时间,比如15分钟,如果用户一直不付款,超时后订单自动取消、库存自动释放。
创建订单这一段我用的方案是"库存预占 + 唯一订单号 + 本地事务"。核心代码如下:
python复制def create_order(user_id: int, sku_list: list, address_id: int):
order_no = generate_order_no(user_id)
total_amount = 0
detail_list = []
for item in sku_list:
sku_id = item["sku_id"]
quantity = item["quantity"]
# 获取SKU信息(这里通过商品服务HTTP接口)
sku = product_service.get_sku(sku_id)
# 预占库存,带有乐观锁条件
success = product_service.lock_stock(sku_id, quantity)
if not success:
raise BizError(f"SKU {sku_id} 库存不足")
total_amount += sku["price"] * quantity
detail_list.append({
"sku_id": sku_id,
"product_title": sku["product_title"],
"spec": sku["spec"],
"price": sku["price"],
"quantity": quantity,
})
order = Order(
order_no=order_no,
user_id=user_id,
total_amount=total_amount,
status="UNPAID",
address_id=address_id
)
db.session.add(order)
db.session.flush()
for detail in detail_list:
order_detail = OrderDetail(order_id=order.id, **detail)
db.session.add(order_detail)
db.session.commit()
return order_no
我特意在代码里保留了注释,因为很多初学者会把"商品服务获取SKU价格"和"订单服务本地查询"混在一起。微服务落地后,你的订单库里根本没有商品表,获取SKU信息必须要走商品服务的HTTP接口。一开始会觉得多一次网络请求很别扭,但这正是微服务架构的必然代价:用网络换边界。
4. 微服务间的协作与数据一致性
4.1 服务间通信:HTTP同步与Redis消息异步
服务拆开之后,通信问题马上就会冒出来。在手机商城这个系统里,我把服务间调用按场景分成两类:同步调用和异步调用。
同步调用用于实时性要求高、需要立即返回结果的场景。比如订单服务向商品服务查SKU价格,用户服务向后台管理服务提供用户信息,都属于同步调用。这里我选择了RESTful JSON接口,没有引入gRPC或者Thrift,因为这套系统内部都是Python服务,JSON在网络传输和调试便利性上足够。同步调用的核心要义是"设置超时时间"。我每个服务里都封装了一个带超时和重试的HTTP客户端,最简单的实现是:
python复制import requests
def call_service(url, method="GET", json=None, timeout=3, retries=2):
for attempt in range(retries):
try:
resp = requests.request(method, url, json=json, timeout=timeout)
resp.raise_for_status()
return resp.json()
except requests.RequestException as e:
if attempt == retries - 1:
raise ServiceUnavailableError(f"调用 {url} 失败: {e}")
异步调用用于保证最终一致性的场景。创建订单之后,订单服务需要通知支付服务开始等待支付、通知库存服务可以扣减实际库存、通知购物车服务清空已下单的商品。这些动作一旦用同步调用来做,任何一个服务抖动都会拖垮下单主链路。我的做法是订单创建完成后,往Redis的特定队列里push一条消息,支付服务、库存服务、购物车服务各自订阅自己关心的队列,消费完消息后进行本地处理。
这样的好处是"生产者和消费者之间的解耦"。订单服务发完消息就返回客户端"下单成功",用户立刻得到反馈;后续的库存扣减、购物车清理都异步慢慢来,即使某个消费者挂了,消息也还留在队列里,等服务恢复后继续消费,不会丢。
4.2 分布式事务:不能只有"try"没有"confirm"
微服务面试里必问的一道题就是分布式事务。在实际项目中,分布式事务从来不是一个可以一刀切的问题。在手机商城的项目中,"创建订单同时扣减库存"这个动作,如果用一个强一致方案(比如两阶段提交),成本太高,而且这在电商业务里本来也不要求秒级一致。我用的方案是"本地消息表 + 状态机 + 补偿"。
具体来说,在订单服务里设计了一张order_event表,这个表和生产业务数据的order表在同一个数据库里,因此它们的事务可以合并成一个本地事务。流程是:
- 开启本地事务,创建订单,同时插入一条
order_event记录,状态为PENDING。 - 提交本地事务。
- 投递这条event到Redis消息队列。
- 库存服务消费event,执行扣减,成功后回调订单服务的
/api/v1/order/confirm接口,把event状态更新为CONFIRMED。 - 如果库存服务执行失败,或者回调超时,订单服务会有定时任务扫描超时的
PENDING事件,选择重发或者标记订单为CLOSED并释放库存。
这套方案的核心理念是"可靠事件 + 最终一致"。它不能做到全局强一致,但能在几秒内让系统收敛到一致状态,而且不会因为某个服务的瞬时不可用导致订单无法创建。同时它的实现成本远低于引入Seata这类分布式事务框架,对个人项目来说是最优解。
这里还必须提一下幂等设计。因为服务间调用存在超时重试,同一笔订单可能被支付回调通知两次,同一件商品可能被扣库存两次。我常用的防护手段有两个:数据库唯一索引 + 业务方幂等键。订单表里order_no有唯一索引,重复插入会直接抛错。支付回调接口则将order_no作为幂等键,先查local缓存,如果这个订单已经更新为PAID,就直接返回成功,不执行重复操作。
4.3 API网关:一个入口解决鉴权和限流
微服务都拆开了,客户端调用哪个服务却成了新问题。让App直接面对十几个不同的服务地址,既不方便也不安全。我在所有服务前面加了一个用Flask写的极简API网关,它是整个流量的统一入口。
网关承担三件事。第一是路由转发,它根据请求路径前缀把请求分发到对应的服务。比如/api/v1/user/*转到用户服务,/api/v1/product/*转到商品服务,/api/v1/order/*转到订单服务。第二是Token校验,除了登录注册等白名单接口,其他请求网关都会先解析JWT,然后把user_id塞进请求头一并转发给下游服务,下游服务不再重复做身份解析。第三是简单的限流,用Redis的滑动窗口实现每个IP每分钟最多100次请求,防止恶意刷接口。
网关代码不长,但它的存在让整个系统的安全策略可以集中管理:
python复制from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
SERVICE_MAP = {
"/api/v1/user": "http://user_service:5001",
"/api/v1/product": "http://product_service:5002",
"/api/v1/cart": "http://cart_service:5003",
"/api/v1/order": "http://order_service:5004",
"/api/v1/payment": "http://payment_service:5005",
}
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE"])
def proxy(path):
full_path = "/" + path
for prefix, target in SERVICE_MAP.items():
if full_path.startswith(prefix):
user_id = parse_token_from_header(request.headers.get("Authorization"))
if not user_id:
return jsonify({"code": 401, "message": "未登录"}), 401
headers = dict(request.headers)
headers["X-User-Id"] = str(user_id)
url = target + full_path
resp = requests.request(
request.method, url, headers=headers,
json=request.get_json(silent=True), timeout=5
)
return resp.content, resp.status_code
return jsonify({"code": 404, "message": "非法路径"}), 404
生产环境里,网关前面的Nginx负责SSL终止、静态资源、基础限流,Flask网关专注于业务层路由和安全策略。这一层加进来之后,后端服务不管怎么扩容,客户端只知道一个固定入口地址,省了非常多的联调精力。
5. 常见故障与排查实录
5.1 高频问题速查表
微服务系统的排障,很多时候难在"问题不知道出在哪一环"。我把自己开发这个项目时踩过的坑整理成了一张速查表,遇到问题时可以先对照一下:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 下单偶尔超时 | 服务间同步调用没有设置超时,上游服务GC停顿 | 检查requests调用是否配置了timeout;确认Redis连接池是否被打满 |
| 用户登录后访问其他服务仍401 | 网关和服务之间的JWT密钥不一致 | 检查所有服务共用的common/jwt_util.py是否读取同一份配置 |
| 商品库存扣成负数 | 扣库存SQL没有加库存充足条件 | 改为UPDATE sku SET stock = stock - qty WHERE id=? AND stock >= qty |
| 订单状态一直停在UNPAID | 支付回调地址错误,或者回调被网关挡掉 | 检查回调URL路径是否在网关白名单里 |
| 某个服务启动后注册不到Consul | 健康检查接口路径配置错误 | 查看Consul UI的service列表,检查/health端点能否直接访问 |
| 购物车选中的商品,订单里价格不对 | 购物车价格字段过期 | 下单时强制从商品服务取实时价格,不信任购物车冗余价 |
5.2 我被坑过的三个真实细节
第一个坑是SQLAlchemy的session线程安全问题。在没有接微服务概念时,我直接在单个Flask应用里注册了一个全局db对象,所有请求共用同一个session。在单体环境下它偶尔没事,但拆了微服务、用并发压测下单接口时,订单数据开始互相窜。后来查了源码才知道,Flask-SQLAlchemy的db.session默认是绑定在应用上下文上的scoped_session,但如果你手动绕过了应用上下文去创建session,就会出问题。我的解决方式非常保守:所有数据库操作严格在请求上下文中进行,并且统一用with app.app_context():包裹后台任务里的数据库操作。
第二个坑是服务启动顺序导致的"连环雪崩"。docker-compose一键启动所有服务时,商品服务先启动、跑去Consul注册,但其他服务还没起来,注册中心健康检查发现商品服务异常,就把它的实例标记为不健康。之后其他服务再拿服务列表就发现"服务不可用",然后开始互相重试,把CPU打满。我的经验是,在docker-compose里给每个服务都加上depends_on条件,同时给服务入口加上一个等待函数:启动后先轮询依赖的健康检查接口,全部就绪后再开始接受流量。
第三个坑是跨服务查详情时的"N+1请求"。管理员后台要显示订单列表,每页50条订单,每条订单又要去用户服务拿买家昵称、去商品服务拿商品名。如果直接在循环里逐条调用,一页订单要发100多次HTTP请求。我的解法是"批量接口"——在用户服务和商品服务里各加一个支持ID列表批量查询的接口,订单服务一次拿齐数据,在内存里组装成map,再拼装返回给前端。这个优化把后台订单页的响应时间从4秒降到了300毫秒以内,效果非常明显。
实操总结与个人体会
整个项目做下来,我最大的感受是:微服务架构真正的门槛不在写代码,而在做取舍。
用Flask做微服务,你不用纠结要不要把服务拆到极致,也不用为了追求业界标准的Service Mesh去引入Istio。对手机商城这个体量的项目,能保证服务边界清晰、数据库隔离、超时控制、消息异步化、关键链路最终一致,就已经是一个合格的微服务系统了。我在这个项目里比较得意的一个点是,每个服务都保持在自己能完全掌控的复杂度之内,没有为了微服务而微服务。
还有一个小技巧最后分享给大家:调服务间超时参数,不要在代码里写死,把它做成环境变量,然后压测时从3秒开始往下试探。找到那个"刚好跑到P99但不拖垮主流程"的值,比任何理论计算都靠谱。另外,刚开始做微服务项目时建议把每次服务间调用都打印一条日志,带上耗时和状态码,后面排障你会感谢当初这个决定。这套系统的可扩展空间还很大,比如接一个真正的移动端App、引入容器编排平台做自动扩缩容,都是很自然的下一步。
