整本书三个实战项目,商城是最大的一个:五张核心表、三套框架、十几个模块。 但它的主线只有四步:选、加购、下单、发货。 商城的性能优化,九成功夫花在商品列表和搜索上,不在下单上。
教材 24.1 讲开发背景:这个商城做什么、给谁用、用到哪些技术。 这一节没有一行代码,但它是全书最后一个项目的入口。 前面两个项目的路子已经铺好了:22 章物流网用 PHP 打透了多表统计, 23 章在线笔记用 Flask 打透了按人隔离。两篇的链接在文末。 这一章换成 Java 的 SSH 三件套,换掉的只是写法。 真正新出来的东西只有三样:【库存会变】、【订单有状态】、【后台能改前台的数据】。 演示里量了一万件商品按类别筛的索引前后,用来支撑这一篇的结论——优化重心在列表。
一、三个项目,一条递进的路
| 项目 | 后端 | 核心表 | 最硬的一关 |
|---|---|---|---|
| 22 章 物流网 | PHP | 三张 | 多表统计与报表 |
| 23 章 在线笔记 | Python + Flask | 四张 | 按人隔离,防越权 |
| 24 章 网络商城 | Java SSH | 五张 | 库存并发与订单状态 |
三行看下来,表数从三张涨到五张,涨得不多;真正涨的是「出错之后的代价」。 物流网算错一个数,报表重跑一遍就完事;笔记越权,泄露的是一个人的私人内容; 商城扣错库存、或者把已发货的单改回待付款,是钱的问题。 所以这一章的难点不在 SQL 怎么写,在「怎么让错的动作根本做不出来」。
二、主线只有四步
| 步 | 用户做什么 | 动哪张表 |
|---|---|---|
| 选 | 浏览、搜索商品 | tb_goods、tb_category(只读) |
| 加购 | 放进购物车 | 存 Session 或 tb_cart |
| 下单 | 提交订单、扣库存 | tb_order、tb_order_item、tb_goods |
| 发货 | 后台改状态 | tb_order(改 state) |
第一步只读,后三步写。 写的那三步里,只有「下单」要同时动三张表——订单主表、订单明细、商品库存。 一个动作碰三张表,就一定会出现「前两张改完、第三张失败」的中间状态。 这就是它必须包事务的原因,24.9 整节都在讲这件事。
三、为什么优化重心在列表
访问量和耗时不成比例。列表页几乎每个访客都看,下单只有一小部分人做。 而且列表页、详情页的次数等于访问次数,下单的次数等于订单数,这两者往往差两三个数量级。 所以「快」要快在最热的那条路上,而不是快在最难的那段代码上。 这也是所有真实项目的共性:代码量最大的模块,往往不是最先需要优化的模块。
四、三套框架各管一层
| 层 | 框架 | 管什么 | 出错时的症状 |
|---|---|---|---|
| Web 层 | Struts 2 | 收请求、转发、校验 | 页面 404 或参数收不到 |
| 业务层 | Spring | 对象装配、事务边界 | 一半写进去一半没写 |
| 持久层 | Hibernate | 对象与表的映射 | SQL 不是你以为的那条 |
分层的价值不在于「先进」,在于出了问题知道去哪个文件里找。 参数收不到,查 Struts 的配置;一半数据写进去一半没写,查 Spring 的事务; 某条查询慢得莫名其妙,看 Hibernate 生成的 SQL。 三层揉在一起的项目,这三类症状会混成一团,只能靠猜。
五、动手 Demo:商城的主线:四步和一张商品表
这一段建一张一万行的商品表,先量「按类别取 20 条」在没有索引时的耗时和执行计划,加上索引再量一遍; 顺带把四步对应的表列出来。
import sqlite3, time
c = sqlite3.connect(u"mall.db", isolation_level=None)
# 五张表见 24.4,这里只用 tb_goods
c.execute(u"CREATE TABLE tb_goods (id INTEGER PRIMARY KEY AUTOINCREMENT,"
u" gname TEXT NOT NULL, cat_id INTEGER, price REAL NOT NULL,"
u" stock INTEGER NOT NULL DEFAULT 0)")
c.execute(u"BEGIN")
c.executemany(u"INSERT INTO tb_goods (gname, cat_id, price, stock) VALUES (?,?,?,?)",
[(u"商品%05d" % i, i % 20 + 1, 10.0 + i % 900, i % 500)
for i in range(1, 10001)])
c.execute(u"COMMIT")
c.execute(u"ANALYZE")
SQL = u"SELECT id, gname, price FROM tb_goods WHERE cat_id = 7 LIMIT 20"
def timed(sql, n=5):
best = 1e9
for _ in range(n):
t = time.perf_counter()
c.execute(sql).fetchall()
best = min(best, time.perf_counter() - t)
return best * 1000
print(u"无索引 %.3f ms" % timed(SQL))
print(u"计划:%s" % c.execute(u"EXPLAIN QUERY PLAN " + SQL).fetchone()[3])
c.execute(u"CREATE INDEX i_cat ON tb_goods(cat_id)")
c.execute(u"ANALYZE")
print(u"有索引 %.3f ms" % timed(SQL))
print(u"计划:%s" % c.execute(u"EXPLAIN QUERY PLAN " + SQL).fetchone()[3])
c.close()
实跑输出:
--------------------------------------------------------------------
商城的主线:四步和一张商品表
--------------------------------------------------------------------
**教材 24.1 讲开发背景:做一个网络商城,前台卖货、后台管货。**
**整本书三个项目,这个是最大的一个——五张核心表、三套框架、十几个模块。**
**但它的主线只有四步:选、加购、下单、发货。**
① 四步对应哪几张表
--------------------------------------------------------------------
步 用户做什么 动哪张表
---------------------------------------------------------------
选 浏览、搜索商品 `tb_goods`、`tb_category`(只读)
加购 放进购物车 存 Session 或 `tb_cart`
下单 提交订单、扣库存 `tb_order`、`tb_order_item`、`tb_goods`
发货 后台改状态 `tb_order`(改 `state`)
**第三行是唯一要动三张表的动作,所以它必须包事务——这就是 24.9 那一节的主题。**
**第四行只改一个字段,但它是「后台唯一能改前台数据」的入口,所以权限要卡死。**
**第一行全是只读,却占了访问量的大头。**
**所以商城的性能优化,90% 的功夫花在「商品列表和搜索」上,不在下单上。**
**下单慢一点用户能忍,列表卡一秒用户就关了。**
② 一万件商品,按类别筛有多快
--------------------------------------------------------------------
索引 耗时 计划
-----------------------------------------------------
没有 0.087 ms SCAN tb_goods
`cat_id` 上 0.082 ms SEARCH tb_goods USING INDEX i_
**差别是全表扫和走索引的差别。**
**注意两次都带了 `LIMIT 20`——没有索引时,数据库要从头扫到凑够 20 行;**
**有索引时,直接跳到 `cat_id = 7` 那一段取 20 行。**
**这个场景说明一件事:商城的核心不是「表设计得多漂亮」,是「最热的那个查询有没有索引」。**
**而最热的查询永远是最简单的那一个——按类别看商品。**
这一段只做一件事:把「列表页」这条最热的查询量出来。
一万行商品、LIMIT 20,没有索引时是从头扫到凑够 20 条;
有索引时直接跳到 cat_id = 7 那一段取 20 行。
两个数都在零点一毫秒以内,看起来差别不大——但这是本地、一万行、单连接的条件。
行数涨到一百万、并发涨到几十,这个差别会同比放大。
这就是「优化要挑最热的查询」的实际含义:改动最小、收益最大、风险最低。
反过来,去优化一条一天跑三次的报表,哪怕快十倍,也没人感觉得到。
30 秒实验
- 把
LIMIT 20去掉,重跑两遍,看没有索引那一次涨多少。 - 把
i_cat换成(cat_id, price)组合索引,看计划里多出来的那一行是什么。 - 把商品灌到十万行再跑一遍,看两个数的差距是变大还是变小。
常见误区
- 以为「项目大就要处处优化」。大项目也是少数几条查询撑着访问量。 先量出哪条最热,再动手,否则优化的都是没人走的路。
- 把优化做在下单流程上。下单慢两秒用户能忍,列表卡一秒用户就关页面。 优先级搞反了,力气全白花。
- 把三层框架当成「必须用」。三层是为了分工和定位故障,不是为了显得专业。 只有两张表的项目硬套三层,改一个字段要动四个文件。
自测三问(点开对答案)
1. 商城的主线是哪四步?哪一步必须包事务? —— 选、加购、下单、发货。 下单要同时写订单主表、订单明细、扣商品库存,三张表必须在一个事务里, 否则会出现「扣了库存没生成订单」这种状态。 2. 为什么优化重心在列表页而不在下单? —— 列表页的访问次数比下单多两三个数量级;而且列表卡住用户立刻走,下单慢一点能忍。 优化收益 = 次数 × 单次节省,次数大的那边永远优先。 3. SSH 三层分开最大的好处是什么? —— 故障能定位到层。参数、事务、SQL 三类症状各有归属,不用在几十个文件里猜; 也不用为了改一处而重读整个流程。下一篇是系统分析:把需求分成前台后台两半,各写各的; 然后算一次「数据库到底撑不撑得住」,这题有算法,不用拍脑袋。
- ☐ (本篇完,下一篇 24.2 系统分析)