🛢️ 第24章 Struts2+Spring+Hibernate+MySQL实现网络商城
📄 一句话概括

商城有两套需求:前台给买家看,后台给运营用。功能和性能特征都不一样。 可行性分析不是「能不能做」,是算清技术、时间、钱三笔账。 容量估算的意义是判断瓶颈在不在数据库上,不是算出精确的 QPS。

📝 本章和前面几章的关系

教材 24.2 讲需求分析和可行性分析,是标准的软件工程写法。 这一篇只留两件对写代码有直接影响的事:前后台需求的差别、以及怎么估一次查询能撑多少。 需求分析里最容易被跳过的一步是「谁在用」——同一个功能,访客用和运营用,写法完全不同。 可行性分析里最容易被写成空话的是「技术可行性」,它其实是一个可以算的数。 演示里把列表页和详情页各量了一遍,再拿 1000 除以耗时,得到单核的理论上限。

一、前台和后台是两套需求

模块 谁在用 访问占比 慢一点能忍吗
商品列表/搜索 所有访客 最大 不能,一秒就关
商品详情 浏览者 大 不能
下单 下单的人 小 能忍两三秒
后台管理 运营几个人 极小 能忍

这张表最有用的不是「访问占比」,是最后一列。 「能不能忍」决定了这段代码值不值得优化:后台一天几十次点击,写得再快也没人感知; 列表页一天几十万次,慢一秒就是几十万次抱怨。 「后台能忍」还有个副作用:后台可以写得粗糙,但不能写错。 后台的一个误操作会改掉几千条前台数据,而前台的一次卡顿只是让一个人多等一秒。 所以后台多写校验,少写优化。

二、可行性分析:三笔账

账 算什么 算不清的后果
技术 有没有现成的轮子、团队会不会用 中途换技术栈,等于重做
时间 每个模块几天,加起来是几周 上线日期靠拍脑袋,最后靠加班补
钱 服务器、带宽、短信接口按量计费 用户一多,账单比收入涨得快

三笔账里,最容易被糊过去的是「技术」——因为「会不会」是个模糊的判断。 把它变成一个具体问题就好判断了:这个功能,我在别的项目里写过一模一样的吗? 写过,时间是上次的一点二倍;没写过,就得先做一个最小可跑的样例。 「先做样例」这一步省不掉:它是唯一能把「不会」变成「要多久」的办法。 样例不用好看,能跑通一次就行——它证明的是「这条路走得通」,不是「这个功能做完了」。

三、一次查询能撑多少

算法很简单:一次查询花 t 毫秒,那单核一秒最多做 1000 除以 t 次。 这个数有几个前提,说清前提比算出数更重要:

前提 实际情况差在哪
单核单连接 真实是几十上百个连接抢同一批 CPU
不算网络 一次跨机房往返就是几毫秒,比查询本身还长
数据在内存 冷数据要读盘,第一次访问慢十倍
不算渲染 一个页面拼模板常常比查库还慢

所以「一万五千 QPS」这种数,不是「能扛一万五千人同时在线」,是「这条 SQL 本身不慢」。 它的真正用途是【排除】:单次查询只有零点零几毫秒,打三折再除以十也还有一千多, 那就说明数据库不是瓶颈,别再往这里加缓存。 反过来,如果单次已经是几十毫秒,那才轮到索引、缓存、分表这些手段上场。 先判断瓶颈在哪,再选手段——顺序反了,再多优化也是白忙。

四、动手 Demo:前台后台两套需求,和一次容量估算

这一段把前台后台的访问特征列成一张表,然后给商品表建上 cat_id 索引, 分别量「列表页取 20 条」和「详情页取 1 条」,再用 1000 除以耗时算出单核理论 QPS。

import sqlite3, time

c = sqlite3.connect(u"mall.db", isolation_level=None)
c.execute(u"CREATE TABLE IF NOT EXISTS tb_goods (id INTEGER PRIMARY KEY,"
          u" gname TEXT, cat_id INTEGER, price REAL, stock INTEGER)")
c.execute(u"BEGIN")
c.executemany(u"INSERT INTO tb_goods VALUES (?,?,?,?,?)",
              [(i, u"商品%05d" % i, i % 20 + 1, 10.0 + i % 900, i % 500)
               for i in range(1, 20001)])
c.execute(u"COMMIT")
c.execute(u"CREATE INDEX IF NOT EXISTS i_cat ON tb_goods(cat_id)")
c.execute(u"ANALYZE")

def timed(sql, args=(), n=5):
    best = 1e9
    for _ in range(n):
        t = time.perf_counter()
        c.execute(sql, args).fetchall()
        best = min(best, time.perf_counter() - t)
    return best * 1000

t_list = timed(u"SELECT id, gname, price FROM tb_goods WHERE cat_id = ? LIMIT 20", (7,))
t_detail = timed(u"SELECT * FROM tb_goods WHERE id = ?", (123,))
print(u"列表页 %.3f ms -> %.0f QPS" % (t_list, 1000 / t_list))
print(u"详情页 %.3f ms -> %.0f QPS" % (t_detail, 1000 / t_detail))
c.close()

实跑输出:

--------------------------------------------------------------------
前台后台两套需求,和一次容量估算
--------------------------------------------------------------------

**教材 24.2 讲系统分析:需求分析和可行性分析。**
**商城的特殊之处在于它有「两套需求」:前台给买家看,后台给运营用。**
**两套需求的性能特征完全不同,这是设计时最容易忽略的一点。**

  ① 前台后台的差别不在功能,在访问比例
  --------------------------------------------------------------------
模块           谁在用      访问占比  慢一点能忍吗
---------------------------------------------------
商品列表/搜索  所有访客    最大      不能,一秒就关
商品详情       浏览者      大        不能
下单           下单的人    小        能忍两三秒
后台管理       运营几个人  极小      能忍
**这张表决定了优化的优先级:先保列表和详情,再谈别的。**
**而很多项目反过来做——先把下单流程打磨得滴水不漏,结果首页要三秒才出来。**
**「后台能忍」这一行还有个副作用:后台可以写得很粗糙,但不能写错。**
**因为后台的一个误操作会改掉几千条前台数据,而前台的一次卡顿只是让一个人多等一秒。**
**所以后台的代码要多写校验,少写优化。**

  ② 容量估算:一次查询能撑多少 QPS
  --------------------------------------------------------------------
查询             单次      单核理论上限
---------------------------------------
列表页(20 条)  0.084 ms  11862 QPS
详情页(1 条)   0.049 ms  20243 QPS
**「单核理论上限」是拿 1000 除以单次耗时算出来的,它是个上限,不是实际值。**
**因为一个页面不止一次查询,而且还有网络、模板渲染、GC 这些开销。**
**但这个数有用:它告诉你「瓶颈在不在数据库上」。**
**两个数都在一万到三万之间,说明对一个小商城来说,数据库根本不是瓶颈。**
**真正的瓶颈在「页面渲染」和「并发连接数」上,那些是应用服务器的事。**
**所以「要不要上缓存」这个问题,答案取决于真实访问量,不取决于「听说缓存能提速」。**

这一段量了两条最典型的查询:列表页取 20 条、详情页取 1 条。 两条都建了索引,量的是「最好情况」。 「单核理论上限」是拿 1000 除以毫秒数算的。 它是个天花板:真跑起来达不到,因为一台机器要同时处理别的连接、别的查询。 但天花板有用——它告诉你「离瓶颈还有多远」。这次两个数都在万级,说明对一个小商城来说, 数据库不是瓶颈。真正的瓶颈会出现在页面渲染和连接数上。 所以「要不要上缓存」这个问题,答案取决于真实访问量,不取决于「听说缓存能提速」。 没有实测的访问量,加缓存只是把一个不存在的瓶颈换了个地方。

30 秒实验

  1. 把详情页的查询从 SELECT * 改成只取三列,看耗时变多少——多取的列也要成本。
  2. 把列表页的 LIMIT 20 改成 LIMIT 200,看单次耗时是不是按比例涨。
  3. 在估算表里加一行「首页(聚合五条查询)」,把单次耗时乘 5 再算 QPS。

常见误区

  1. 把「单核理论 QPS」当成「能扛的并发数」。它只是这条 SQL 的天花板,不含网络、渲染、锁等待。 差一个数量级很正常。
  2. 把可行性分析写成「技术上可行」。不给出「谁来做、做多久、卡在哪」的答案,等于没做。 至少要说清哪一块要现学。
  3. 前后台用同一套代码、同一个优化标准。前台要快、后台要稳,两者的着力点相反。 混在一起写,结果两边都不达标。
自测三问(点开对答案) 1. 前后台需求的差别主要在哪一列? —— 「慢一点能忍吗」。 它决定这段代码值不值得优化,比访问占比更直接——不能忍的地方才需要优化,能忍的地方只要不错。 2. 单核理论 QPS 怎么算?它的前提有哪些? —— 1000 除以单次毫秒数。前提是单核单连接、不算网络往返、数据在内存、不算模板渲染。 四个前提缺一个,实际值就会掉一截。 3. 算出万级 QPS 之后,该做什么结论? —— 结论是「数据库不是瓶颈」,别在这加缓存;下一步该去看页面渲染和连接数。 如果算出来只有几十,那才轮到索引、缓存、分表。

需求清楚了,下一篇做系统设计:功能结构图先定「有哪些模块」,目录结构跟着定, 然后处理一个具体的设计难题——订单状态怎么管才不出乱账。

相关笔记