商城有两套需求:前台给买家看,后台给运营用。功能和性能特征都不一样。 可行性分析不是「能不能做」,是算清技术、时间、钱三笔账。 容量估算的意义是判断瓶颈在不在数据库上,不是算出精确的 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 秒实验
- 把详情页的查询从
SELECT *改成只取三列,看耗时变多少——多取的列也要成本。 - 把列表页的
LIMIT 20改成LIMIT 200,看单次耗时是不是按比例涨。 - 在估算表里加一行「首页(聚合五条查询)」,把单次耗时乘 5 再算 QPS。
常见误区
- 把「单核理论 QPS」当成「能扛的并发数」。它只是这条 SQL 的天花板,不含网络、渲染、锁等待。 差一个数量级很正常。
- 把可行性分析写成「技术上可行」。不给出「谁来做、做多久、卡在哪」的答案,等于没做。 至少要说清哪一块要现学。
- 前后台用同一套代码、同一个优化标准。前台要快、后台要稳,两者的着力点相反。 混在一起写,结果两边都不达标。
自测三问(点开对答案)
1. 前后台需求的差别主要在哪一列? —— 「慢一点能忍吗」。 它决定这段代码值不值得优化,比访问占比更直接——不能忍的地方才需要优化,能忍的地方只要不错。 2. 单核理论 QPS 怎么算?它的前提有哪些? —— 1000 除以单次毫秒数。前提是单核单连接、不算网络往返、数据在内存、不算模板渲染。 四个前提缺一个,实际值就会掉一截。 3. 算出万级 QPS 之后,该做什么结论? —— 结论是「数据库不是瓶颈」,别在这加缓存;下一步该去看页面渲染和连接数。 如果算出来只有几十,那才轮到索引、缓存、分表。需求清楚了,下一篇做系统设计:功能结构图先定「有哪些模块」,目录结构跟着定, 然后处理一个具体的设计难题——订单状态怎么管才不出乱账。
- ☐ (本篇完,下一篇 24.3 系统设计)