Python电商项目实战:Django+Flask混合架构与订单并发处理方案

📅 发布时间:2026/10/12 17:58:26
Python电商项目实战:Django+Flask混合架构与订单并发处理方案
高峰期系统疯狂报警别急先把这套Python电商实战方案吃透从开发第一个用户注册接口到上线前被并发打蒙再到重构出稳定的秒杀下单流程这些年我在电商系统这条路上踩过的坑比喝过的咖啡都多。今天不聊那些飞在天上的架构概念就实打实拆一个基于Python的电商网站项目——用Django和Flask双框架混合搭出来的系统从设计思路、核心模块实现到线上排查经验一次讲清楚。这篇文章适合刚学完框架基础、想动手做完整项目的同学也适合正在被订单模块和库存对账折磨的开发者内容全是能直接抄作业的东西。1. 为什么选Django和Flask混合架构而不是一棵树上吊死很多人一听“Python电商网站”第一反应就是“选Django就行大而全”或者“Flask轻量自由度高”。两种说法都有道理但真做一个包含用户、商品、购物车、订单、支付回调、后台管理的系统时你会发现单一框架都有别扭的地方。我这套方案的核心思路是Django管后台、管模型层、管那些需要“规范”的业务模块Flask管前端API、管高并发的读写接口、管那些需要“灵活”的查询场景两者通过同一个数据库和统一的认证机制协作各干各擅长的活。1.1 Django负责的模块后台管理、订单状态机、用户体系Django自带Admin后台这对电商后台来说是巨大的生产力。商品类目、SKU管理、优惠券配置、用户权限分配直接用Django Admin改几行代码就能出一个可用的运营后台省掉大量重复的CRUD页面开发。同时Django的ORM在处理复杂的关联查询订单-订单项-商品-物流时非常成熟模型关系清晰不容易写出烂SQL。订单状态流转这种强业务逻辑放在Django的Model层里用状态字段加自定义方法管理再配合信号量做库存回滚稳定性很高。1.2 Flask负责的模块前台API、购物车、商品搜索前台是面向C端用户的接口并发量大而且频繁变化。Flask的优势就是轻路由定义灵活配合RESTful风格写API特别顺手。比如商品列表的筛选、排序、分页购物车的增删改查这些接口逻辑相对独立用Flask写出来代码量小维护也直观。更重要的是Flask可以很方便地和Redis配合做接口缓存连数据库都不碰流量高峰期压力能缓解不少。你要是用Django写这些被它的中间件和全局配置拖住反而不利索。1.3 两个框架怎么协作共享数据库和统一Token有人问两个框架是不是各连各的数据库当然不是那样数据一致性就崩了。我这里用的是同一个MySQL实例Django用它的ORM建表、迁移Flask这边直接用SQLAlchemy连接同一套库或者干脆用原生SQL查视图。用户登录认证统一由Django签发JWT TokenFlask端的API装饰器验证这个Token两边都能识别同一套用户身份。简单说就是重活、规范活交给Django轻活、灵活活交给Flask数据库是唯一的真相来源。2. 核心模块拆解从用户注册到订单支付全流程电商网站掰开揉碎了看核心就三条线人用户、货商品、钱订单。每一个模块都有不少细节坑我按实际开发顺序一个个说。2.1 用户模块注册、登录、JWT认证与权限控制用户模块第一件事不是写注册接口而是设计用户表。别只用内置User表直接加字段电商用户会有手机号、邮箱、头像、收货地址等多个维度建议这类做法继承Django的AbstractUser扩展出用户profile表手机号唯一索引密码用Django自带的make_password加密。注册接口接收用户名、手机号、密码先校验手机号格式和唯一性再入库。密码绝不能明文存这是底线。登录接口验证密码后生成JWT Token把用户id塞进Payload里过期时间按业务需求设一般登录态设7天购物车临时会话也可以放宽到15天。权限控制分两层接口层面用装饰器验证Token是否有效页面层面控制用户是否登录。实际开发中还需要区分普通用户和管理员Django的Group权限加上自定义Permission就能搞定别自己造角色判断的一堆if-else。2.2 商品模块类目、SKU、库存与价格设计电商商品最忌讳的就是一张表存所有东西。要拆成分层结构类目表一级类目、二级类目、商品表SPU比如“苹果手机”、商品规格表SKU比如“苹果手机 256G 黑色”。SKU表里才存真正的价格、库存、SKU编码。购物车和订单明细全部关联SKU而不是关联SPU不然规格一变订单价格就乱了。价格字段用DecimalField绝不用FloatField浮点数精度问题会让对账对到崩溃。库存字段要加锁处理下单减去库存时不能先查再更新而是要直接用原子更新的写法。比如SKU.objects.filter(idsku_id, stock__gtequantity).update(stockF(stock) - quantity)这样并发下不会超卖。商品上架状态、删除标记要用is_on_sale和is_deleted字段控制别物理删除运营误操作能直接毁掉历史订单数据。2.3 购物车模块Redis存储和数据库存储怎么取舍购物车有两种做法登录用户存数据库游客存Cookie或者Redis。我推荐的方案是登录用户购物车直接存Redis Hash以cart:{user_id}为keyfield是SKU idvalue是数量。原因是购物车读写频率高、数据不要求永久可靠Redis的Hash天然适合这种场景。接口层面加购时直接HINCRBY增加数量查询时HGETALL一次性取出所有商品和数量再回MySQL查最新的价格和库存。游客购物车可以存Cookie但Cookie有4KB限制而且每次请求都得带不建议放大数据。更好的做法是游客也可以临时生成一个匿名Token存在Redis里登录时把匿名购物车合并到用户购物车。合并逻辑要注意数量相加而不是覆盖不然用户加了几个商品一登录全变了会被投诉的。2.4 订单模块下单流程、状态机、事务与库存扣减这是整个系统最核心也最容易出Bug的部分。一个正常的用户下单流程是这样的用户提交订单包含收货地址、SKU和数量后端先校验商品是否在售、库存是否充足、价格是否匹配然后在一个数据库事务里完成“创建订单主表”、“创建订单明细”、“扣减SKU库存”。这三个操作必须全部成功否则全部回滚当然也可以用Flask这边的事务管理来包住。订单状态机我一般设计成这样的状态序列待支付、已支付待发货、已发货、已签收、已取消、售后中等。每次状态变更都写在status记录表里方便追溯和客服查单。支付成功的回调也要处理接收到支付平台的异步通知验证签名再更新订单状态为已支付加一个锁防止回调重复处理。这里有个经典坑库存不能在创建订单时就扣除因为用户可能不支付库存白白锁住。我一般做法是“锁定库存”即创建订单后扣减可售库存、增加锁定库存支付成功后锁定库存转为已售超时未支付自动释放锁定库存。这样既防止超卖又避免不支付占库存。2.5 支付模块回调签名验证与掉单处理国内电商基本绕不开微信、支付宝我这边把回调封装成统一接口。支付模块的核心是回调处理函数的幂等性——同一笔订单回调可能发多次绝对不能重复把状态改成已支付。处理办法是查一下订单当前状态只有待支付才更新。回调验签要用官方SDK提供的方法别自己拼接字符串去签名对比容易出错。掉单处理是另一大痛点。用户支付成功但回调没收到怎么办主动查单定时任务每隔一段时间去支付平台查未支付订单的状态一旦发现已支付立刻本地更新订单状态。这个机制必须有否则用户钱扣了订单还是待支付客诉能把你淹没。3. 实战中的数据库设计和接口实现细节这一部分我用代码和SQL来具体展示。所有模型和代码都是常见的写法不能直接用于生产但思路可以完全照搬。3.1 数据库表结构模型字段和索引设计先上一组Django模型的关键片段# goods/models.py from django.db import models class Category(models.Model): name models.CharField(max_length64, uniqueTrue) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE) level models.PositiveSmallIntegerField(default1) # 1一级, 2二级 class SKU(models.Model): spu models.ForeignKey(SPU, on_deletemodels.CASCADE) name models.CharField(max_length128) price models.DecimalField(max_digits10, decimal_places2) stock models.PositiveIntegerField(default0) locked_stock models.PositiveIntegerField(default0) is_on_sale models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [ models.Index(fields[spu]), models.Index(fields[is_on_sale, price]), ]索引设计上最核心的是查询高频字段商品列表页按类目上架状态价格排序类目外键和is_on_sale字段组合索引就很有必要订单查询按用户创建时间加user_id和created_at的联合索引订单状态流转需要查待支付订单给status字段加普通索引即可。再来看订单表的核心字段以order_info为例订单号我们不用自增id而是用业务订单号。生成规则推荐“日期随机数用户id尾巴”拼接保证唯一且具备一定随机性避免订单号被遍历抓取。状态字段用SmallIntegerField注释好每个数字代表什么状态。3.2 商品列表查询的Flask API示例这段代码用的是Flask实现极简商品列表# api/goods.py from flask import Blueprint, request, jsonify from db import db_session from models import SKU bp Blueprint(goods, __name__) bp.route(/api/goods, methods[GET]) def goods_list(): category_id request.args.get(category_id, typeint) page request.args.get(page, 1, typeint) page_size request.args.get(page_size, 20, typeint) sort request.args.get(sort, default) query db_session.query(SKU).filter(SKU.is_on_sale True) if category_id: query query.filter(SKU.category_id category_id) if sort price_asc: query query.order_by(SKU.price.asc()) elif sort price_desc: query query.order_by(SKU.price.desc()) else: query query.order_by(SKU.sales.desc()) total query.count() items query.offset((page - 1) * page_size).limit(page_size).all() data [{ id: item.id, name: item.name, price: str(item.price), stock: item.stock, sales: item.sales } for item in items] return jsonify({code: 0, data: {total: total, list: data}})这个接口用到了一个细节price字段是Decimal类型不能直接jsonify必须转成字符串否则前端拿到的可能是奇怪的数字精度问题。另外query.count()在offset之前做不会因为分页而增加数据库压力。3.3 下单扣库存的事务实现事务可以放在Django这边写也可以Flask配合SQLAlchemy的session实现。我这里给出Django视角的事务代码# order/services.py from django.db import transaction from django.db.models import F from goods.models import SKU from order.models import OrderInfo, OrderItem transaction.atomic def create_order(user_id, address_id, items): # items [{sku_id: 1, count: 2}, ...] total_amount 0 order OrderInfo.objects.create( user_iduser_id, address_idaddress_id, statusOrderInfo.Status.PENDING_PAYMENT, order_nogenerate_order_no(user_id) ) for item in items: sku SKU.objects.select_for_update().get(pkitem[sku_id]) if sku.stock item[count]: raise Exception(库存不足) sku.stock F(stock) - item[count] sku.locked_stock F(locked_stock) item[count] sku.save(update_fields[stock, locked_stock]) amount sku.price * item[count] total_amount amount OrderItem.objects.create( orderorder, skusku, countitem[count], pricesku.price, amountamount ) order.total_amount total_amount order.save(update_fields[total_amount]) return orderselect_for_update()就是行锁保证多个请求同时处理同一个SKU时后面的请求会等着前面的提交完。配合事务就不会出现两个订单都把库存扣成负数的情况。这里要注意SKU查询必须在事务里执行否则锁会被自动释放——这是很多新手踩过的坑。4. 高并发场景的优化缓存、队列和读写分离电商项目上线后会遇到集中抢购的情况你不用一开始就上微服务但必须提前想好怎么扛突发流量。我这里分享三个性价比很高的优化手段。4.1 Redis缓存商品详情和列表页商品详情页和列表页是访问最频繁的页面如果每次请求都打数据库再好的数据库也会被拖垮。做法是给接口加Redis缓存商品详情以goods_detail:{sku_id}为keyJSON数据缓存5分钟列表页以URL参数组合作为key缓存3分钟。当商品库存变化、价格调整时主动删除对应的缓存key让下一次请求重新回源查库。这样并发量能轻松扛住原来的十倍代码就多了几十行。缓存更新策略要避免一个坑不能在用户下单扣库存后立即删除详情页缓存因为如果删了压测时大量请求同时回源还是会把数据库打爆。正确做法是设置一个短TTL或者采用“延迟双删”的思路——先删缓存等待几百毫秒再删一次确保并发期间不会写入旧数据。4.2 用Celery处理订单超时和异步通知订单超时未支付需要自动取消并释放库存这种任务用Celery的定时任务或延迟任务很自然。用户下单后我们提交一个eta为30分钟的延迟任务任务里检查订单状态如果仍未支付就取消。问题是如果用户中途支付了这个定时任务仍然会执行所以任务里必须再查一次状态只用状态仍然是待支付才处理。异步通知方面比如发送短信、邮件、微信模板消息这些都不应该在请求/响应链路里同步做。用Celery异步任务去执行接口响应时间从800ms降到50ms用户体验完全不一样。Celery Broker我用Redis两个队列一个default处理常规任务一个high_priority处理支付回调后的关键任务权重设置成10比1保证支付消息第一时间被处理。4.3 MySQL读写分离与慢查询分析单机MySQL在连接数超过500时就会出现明显的性能下降读写分离能帮上大忙。主库负责写操作下单、支付回调更新、库存变更从库负责读操作商品查询、订单列表查询。Django的DATABASES配置里可以配置主从多个数据库用路由规则区分读和写。Flask这边也类似SQLAlchemy的binds参数可以配置读写绑定。上线之后不要忘了开启慢查询日志。我遇到过最典型的案例商品列表页排序字段没加索引导致查询要全表扫描前端页面直接超时。打开慢查询日志后发现那条SQL跑了2.8秒加了个联合索引降到30毫秒立刻解决。MySQL还有EXPLAIN工具执行计划里typeALL就代表全表扫描要警惕。5. 常见问题排查清单与避坑实录这部分是我这两年在这个项目上实际踩坑的记录一条一条都是真金白银换来的经验。整理成速查表方便你遇到问题时直接对照。5.1 Django和Flask共用数据模型带来的同步问题因为两个框架连接同一个库最容易踩的坑是模型不一致。Django迁移改了表结构Flask这边的SQLAlchemy模型没有同步更新查询时用了新字段直接报错。解决办法不是手动去两边改代码而是统一一个模型来源把核心表模型定义成独立的Python模块Django和Flask都去引用这个模块或者干脆Flask这边只读视图和原生SQL减少对模型同步的依赖。5.2 JWT Token过期但前端不知道出现页面跳转登录死循环Token过期后接口返回401前端在每次Ajax请求的拦截器里判断所有401响应统一跳转登录页。听起来很简单但有个情况经常坑人登录页本身也发请求这个请求如果也返回401就会无限跳转。我的做法是给登录和注册的接口URL加白名单不拦截这些请求。5.3 支付回调乱序导致订单状态被修改为已取消用户下单后马上支付支付回调到达之前正好超过30分钟Celery定时任务把订单取消并释放了库存紧接着支付回调到了发现订单是已取消状态直接跳过处理。虽然没造成资金损失但订单数据很乱。处理方式是回调判断如果订单已取消需要主动发起退款并把状态标记为“已取消已退款”。这个逻辑漏掉就是资损事故必须补全。5.4 分页深翻页的性能杀手运营后台经常会有人翻到第10000页SQL的offset会越来越慢。解决方案是不再用传统的offset/limit分页而是改成基于游标的分页按id排序前端传last_id后端只查id last_id limit 20。尤其适合商品评论和订单流水这类按时间递增的表。接口参数从page改成last_id兼容起来也不是很难。5.5 数据库连接池被耗尽流量稍大时如果代码里有任何地方查询完没关闭连接连接不会被释放等连接池满了所有请求都会卡住。排查方法是在数据库层看活跃连接数和Sleep连接数如果是Django检查是否在视图函数里手动创建了连接而没有close()。生产环境中用短连接还好长连接更要注意归还。6. 上线前必须做的压测、监控和安全加固这部分内容写给我自己也是提醒别等线上出了事故才后悔没早做。压测工具我推荐用最简单的locust写个Python脚本就能模拟并发用户。至少压出三个数据系统能支撑的最大QPS、下单接口的P99延迟、数据库的连接使用率。我常用的压测场景是“200个用户同时点击秒杀按钮”压一遍下来哪个接口有问题会立刻暴露。压测发现下单接口只能跑20QPS怎么调优先看是不是select_for_update()锁等待太严重再检查是不是事务范围过大把不必要的数据写入也包在事务里锁时间变长吞吐自然低。监控指标重点关注接口错误率、请求处理时长、Redis内存使用量、MySQL活跃连接数。这些数据用开源监控工具就能看。告警规则别设太敏感比如错误率超过1%连续2分钟才告警否则日志半夜疯狂发消息没人会认真看。安全方面有几件事是底线。用户密码必须加密存储这是常识所有接口必须做参数校验尤其是下单接口的sku_id和count防止负数绕过价格计算接口要做访问频率限制用Redis实现简单的令牌桶算法防止恶意刷接口后台页面要加IP访问白名单或者二次验证。还有文件上传商品图片上传的扩展名和内容都要校验不能只改后缀就放过否则恶意文件可能直接穿透服务。最后分享一次实测经历项目刚上线的第一周我们搞了一次促销活动把主推商品的价格标示错了一位结果用户疯狂下单1小时涌进来上万笔订单。支付回调正常但Celery的异步任务全部堆积在队列里超时取消任务的延迟越来越大导致大量订单状态混乱。当时我人还在外面收到告警之后只能远程连上服务器看日志。排查下来发现是订单状态更新和队列消费的资源配置不合理——回调任务默认走了低优先级队列被大量短信通知任务堵住了。幸好活动前在代码层做了幂等处理订单状态没有被覆盖最终数据没有乱但这事让我明白一个道理不停机这些优化都不算数系统一定要有降级预案。后来我调整了队列权重支付回调走独立的高优先级队列短信邮件这些延迟个几秒完全能接受。再后来做了一次全链路压测把下单、支付、取消、库存回补完整跑了一遍才发现不少隐藏问题当时在测试环境根本测不出来。所以最后给大家两个实在的建议第一任何电商系统都要有完整的订单状态日志任何异常都能顺着日志追踪到底第二上线后的第一周不要睡踏实觉多盯监控多看告警发现问题立刻修别等“周一再说”。这种项目最大的收获不是代码本身而是你终于能体会到“流量一来代码里每个细节都在决定系统生死”的感觉。