news 2026/10/27 19:07:10

中小项目避免过度设计:并发模型、连接池与缓存实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中小项目避免过度设计:并发模型、连接池与缓存实战

先把话说在前面:这篇文章不聊感情话题,聊的是工程里一种跟"怕结不了婚"性质很像的错觉——总觉得自己手上这套系统明天就会被流量冲垮,于是提前把能上的复杂方案全上一遍:分库分表、消息队列、微服务拆分、多级缓存,一个都不落。结果系统上线一年,日活还没到四位数,运维复杂度倒是先翻了三倍。

"不要怕结不了婚,你没那么好命"这句话放到技术语境里,其实是一句很务实的提醒:绝大多数业务在可见的周期内,根本不会遇到你想象中的那种量级。与其担心未来的洪水,不如先把当下的地基打牢。本文就围绕"中小项目如何避免过度设计、如何用最低成本把性能做够"这条主线,从并发模型讲到连接池、缓存、索引,再给出一套可以直接跑起来的对比实验。不管你是刚接手老项目的后端同学,还是准备做技术选型的负责人,读完都能拿到一套可落地的判断标准。

1. 背景与核心概念

1.1 为什么"怕扛不住"是最常见的过度设计起点

做技术选型时最容易被忽略的一点是:性能问题几乎从来不是"均匀发生"的,它是被特定业务场景、特定时间点、特定数据量触发出来的。一个内部管理系统,用户总数两百人,峰值并发可能就十几个;一个 To C 的小程序,如果没有投放和裂变,日活也很难在三个月内冲到十万。可现实是,很多团队在项目立项阶段就按"百万并发"的假设去搭架构,理由是"以后肯定要用到"。

这种心态的代价不在于多写了多少代码,而在于把系统的可维护性提前透支了。一旦拆成微服务,你就得处理服务注册发现、链路追踪、分布式事务、跨服务数据一致性;一旦引入消息队列,你就得面对消息堆积、重复消费、顺序性、死信队列;一旦上了分库分表,所有跨表的查询、聚合、分页都要重写。这些都不是"以后用得上"的储备,而是"现在就要还的债"。

所以本文想传达的核心判断很简单:先量化,再设计。你的接口 QPS 到底是多少,慢在哪一层,是 CPU 打满还是 IO 等待,这些没有实测数据之前,任何架构上的"提前准备"都只是猜测。下面几节会告诉你如何用很小的成本把这件事量清楚。

1.2 三个典型的过度设计信号

在真实项目里,过度设计往往有很明显的信号,识别出来就能省下大量返工。第一个信号是"为不存在的场景写抽象层":比如业务里只有一种支付渠道,却先设计了一套支付网关抽象、策略工厂、插件注册机制,最后发现第二家渠道三年后才接入,中间所有改动都在这一层上绕圈。第二个信号是"用基础设施解决代码问题":一个慢查询本该加索引,却先上了 Redis 缓存;一个循环里发起的 N 次数据库调用本该改批量查询,却先加了连接池参数。

第三个信号最隐蔽,也最常见:把"技术先进性"当成选型依据,而不是把"团队承受力"当成依据。一套优秀的分布式方案,如果团队里没人排过它的故障,那它对这个团队来说就是负资产。线上出问题时,能快速定位、快速回滚的方案,永远比理论性能更高的方案更值得选。

信号典型表现更务实的做法
为不存在的场景写抽象只有一种实现却先做插件化直接写具体实现,出现第二个实现时再抽
用基础设施绕过代码问题慢查询先加缓存而不是加索引先看执行计划,再决定缓存
以技术先进性做选型按大厂架构照搬按团队排障能力和数据量选型

1.3 本文涉及的技术范围

为了让讨论不停留在概念层面,本文会用 Python 生态做一套完整的可运行实验:用 Flask 写同步阻塞版本,用 FastAPI 写异步非阻塞版本,再写一个基于线程池的压测脚本,把同一段"模拟慢 IO"的接口在两种模型下的表现量出来。选 Python 的好处是代码短、依赖少、实验成本低,但结论对所有语言都成立,因为背后的东西是操作系统层面的阻塞与非阻塞,跟语言无关。

后面还会涉及数据库连接池的参数含义、索引失效的几种写法、缓存的三条基本纪律,以及一份上线前的选型清单。所有示例都标注了文件路径,可以按目录结构直接复制运行。

2. 环境准备与版本说明

2.1 运行环境与依赖清单

本文的示例在 Linux 和 macOS 上都能跑,Windows 下用 PowerShell 也可以,只是启动命令里的路径分隔符需要改成反斜杠。Python 建议使用当前主流的 3.x 版本,具体小版本号请以你本机python --version的输出为准,本文不锁定具体版本号,因为 Flask、FastAPI、uvicorn 这几个库的小版本迭代较快,锁死版本反而容易在安装时踩坑。

依赖一共四个:flask用来写同步版本,fastapi和uvicorn用来写异步版本,requests用来写压测脚本。如果你还想验证缓存版本,再加一个redis客户端即可,但没有 Redis 也不影响主体实验,因为第三节会用进程内字典做一个简化的缓存演示。

python3 --version python3 -m venv venv source venv/bin/activate pip install flask fastapi uvicorn requests

创建虚拟环境的目的是隔离依赖,避免和你机器上其他项目冲突。这一步看起来可有可无,但如果你同时维护几个 Python 项目,不隔离依赖几乎一定会遇到某个库被升级后旧项目跑不起来的问题。

2.2 项目目录结构

实验代码分成三个独立的应用目录和一个工具目录,彼此不共享代码,这样你可以单独启动任意一个版本,不会因为互相导入而混淆。

perf-demo/ ├── sync_app/ │ └── app.py ├── async_app/ │ └── main.py ├── cache_app/ │ └── main.py └── tools/ └── bench.py

这个结构刻意做得扁平,没有__init__.py、没有包层级,因为我们只是要验证并发模型差异,不需要模拟真实项目的复杂结构。等实验结论清楚了,再把它套进你实际项目的分层里。

2.3 启动方式与端口约定

三个版本分别占用 8000、8001、8002 端口,压测脚本通过--url参数指定目标,所以可以逐个启动、逐个测试,也可以同时启动做横向对比。

# 终端 1:同步版本 python sync_app/app.py # 终端 2:异步版本 uvicorn main:app --app-dir async_app --host 127.0.0.1 --port 8001 # 终端 3:缓存版本 uvicorn main:app --app-dir cache_app --host 127.0.0.1 --port 8002

--app-dir是 uvicorn 提供的参数,用来指定应用模块所在目录,这样就不必先cd进去。如果你更习惯进入目录再启动,直接cd async_app && uvicorn main:app --port 8001效果一样。

3. 核心原理拆解:同步、异步与并发模型

3.1 阻塞与非阻塞到底差在哪

理解并发模型,最关键的区分不是"快和慢",而是"等待期间这个执行单元在干什么"。同步阻塞模型里,一个请求进来占用一个线程,线程发起 IO(查数据库、调第三方接口、读文件)后会挂起等待,等待期间它什么也做不了,但它仍然占着内存和线程资源。异步非阻塞模型里,协程发起 IO 后会把控制权交还事件循环,事件循环接着去处理别的协程,等 IO 完成再回来继续。

换句话说,同步模型用"更多线程"换吞吐,异步模型用"更少线程 + 更高单线程利用率"换吞吐。前者的问题在于线程是有成本的:每个线程默认占用一定栈空间,线程切换需要内核介入,线程数上千之后调度开销会急剧上升。后者的问题在于一旦代码里混入了阻塞调用(比如用了同步的数据库驱动),整个事件循环就会被卡住,异步的优势瞬间归零。

这也解释了一个常见现象:有人把 Flask 换成 FastAPI,接口还是async def,结果压测下来毫无提升。原因往往是他async def里面调的是同步的requests.get()或者同步 ORM,事件循环照样被阻塞。

3.2 进程、线程、协程的取舍

进程是资源隔离的最小单位,一个进程崩溃不影响另一个,但进程间的通信和数据共享成本高。线程共享进程内存,切换比进程轻,但需要处理竞态和锁。协程是用户态的轻量执行单元,切换成本极低,但它只在单个线程内并发,无法利用多核。

所以实际选型通常是组合使用:多进程利用多核,进程内用线程或协程提高 IO 并发。Gunicorn 配多个 worker 进程、每个进程内部再跑协程,就是很典型的组合方式。理解这个层次关系,比记住"异步比同步快"有用得多,因为你调参的时候知道该调哪一层。

3.3 连接池与资源上限的必要性

即使你把应用层的并发做上去了,数据库那一侧仍然是有限资源。一个数据库实例能承受的连接数是有限的,每个连接都要占内存、占文件描述符。如果应用层无限制地创建连接,高峰时数据库会直接拒绝新连接,表现出来就是"连接超时"或者"too many connections"。

连接池的作用是把连接复用起来:池子里维持固定数量的连接,请求来了借一个,用完还回去。这样既避免了频繁建连的开销,也天然给并发加了一个上限,保护下游不被压垮。池子大小的设置是个权衡,太小会排队,太大会把数据库压垮,具体取值要结合数据库的最大连接数和应用的并发度来算。

3.4 常见误区

第一个误区是"并发数越高越好"。压测时把并发从 50 加到 500,QPS 反而下降,因为系统已经过载,大量时间花在上下文切换和排队上。真实的最优并发点通常比你想的低很多。

第二个误区是"平均响应时间能代表体验"。平均值会被少数极快的请求拉低,掩盖掉长尾。看 P95、P99 才有意义,因为用户感受到的卡顿来自那 5% 的慢请求。

第三个误区是"优化了应用层就完事了"。很多系统的瓶颈在数据库的慢查询上,应用层再怎么优化,一条没走索引的 SQL 照样能把整个接口拖到秒级。

4. 完整实战:同一接口的三种实现与压测对比

4.1 实验设计思路

我们设计一个非常典型的场景:接口需要调用一个耗时 200 毫秒的外部依赖(真实项目里可能是第三方 API、可能是慢查询、也可能是文件处理)。这个接口本身几乎没有 CPU 计算,属于典型的 IO 密集型任务。三个版本分别用同步阻塞、异步非阻塞、异步加缓存来实现,然后用同一个压测脚本打,观察吞吐量和延迟分布的变化。

这里要提前说明:本文不会给出"某某配置下 QPS 达到多少"这样的确定性结论,因为实际数值高度依赖你的机器核数、内存、Python 版本和系统负载。实验的价值在于让你看到趋势差异,并且掌握自己动手测量的方法。你在自己机器上跑出来的绝对数值,比任何文章里的数字都更有参考价值。

4.2 版本一:同步 Flask 实现

# 文件路径:sync_app/app.py import time from flask import Flask, jsonify app = Flask(__name__) def fetch_remote_data(): """模拟一次耗时 200ms 的外部 IO 调用""" time.sleep(0.2) return {"source": "remote", "value": 42} @app.route("/api/report") def report(): data = fetch_remote_data() return jsonify({"status": "success", "payload": data}) if __name__ == "__main__": # threaded=True 表示每个请求交给一个线程处理 app.run(host="127.0.0.1", port=8000, threaded=True)

这段代码的关键在time.sleep(0.2),它模拟的是阻塞式 IO,线程在这 200 毫秒里完全被占用。threaded=True让 Flask 用多线程处理请求,所以并发能力取决于线程池大小和系统能撑住多少线程,而不是 CPU 核数。当并发请求数超过线程处理能力时,新请求只能排队,表现出来就是延迟线性上升。

这里需要注意app.run()是开发服务器,只适合本地实验。它的默认线程模型和参数并不是为生产设计的,真实部署应该用 gunicorn 或 uwsgi。本文用它只是为了减少实验步骤,结论不受影响。

4.3 版本二:异步 FastAPI 实现

# 文件路径:async_app/main.py import asyncio from fastapi import FastAPI app = FastAPI() async def fetch_remote_data(): """用 asyncio.sleep 模拟非阻塞 IO""" await asyncio.sleep(0.2) return {"source": "remote", "value": 42} @app.get("/api/report") async def report(): data = await fetch_remote_data() return {"status": "success", "payload": data}

这个版本的差别就在await asyncio.sleep(0.2)。协程在这 200 毫秒里把控制权交还事件循环,事件循环可以去处理其他请求,所以单线程就能同时挂着成百上千个等待中的请求。前提是report必须写成async def,里面所有 IO 都必须是异步的。

一个容易踩的坑:如果你在async def里调用了同步函数,比如requests.get(),那么事件循环会在这个调用处被完全阻塞,所有其他协程都得等着。这不是 FastAPI 的问题,而是你把阻塞代码放进了异步上下文。解决办法要么换成异步客户端,要么用run_in_executor把它丢到线程池里执行。

# 错误示范:async def 里调用同步阻塞代码 import requests from fastapi import FastAPI app = FastAPI() @app.get("/api/bad") async def bad(): # 这一行会阻塞整个事件循环 resp = requests.get("https://example.com", timeout=5) return {"length": len(resp.text)}

4.4 版本三:异步加进程内缓存

# 文件路径:cache_app/main.py import asyncio import time from fastapi import FastAPI app = FastAPI() _cache = {} CACHE_TTL = 10.0 async def fetch_remote_data(): await asyncio.sleep(0.2) return {"source": "remote", "value": 42} async def get_data_with_cache(): now = time.monotonic() item = _cache.get("report") if item and now - item["ts"] < CACHE_TTL: return item["data"], True data = await fetch_remote_data() _cache["report"] = {"data": data, "ts": now} return data, False @app.get("/api/report") async def report(): data, hit = await get_data_with_cache() return {"status": "success", "payload": data, "cached": hit}

这个版本的意义在于说明一件事:很多时候最大的性能提升不来自换并发模型,而来自"少做一次"。如果这份数据的变化频率远低于访问频率,缓存带来的收益会比异步大一个数量级。反过来,如果数据必须实时,那么缓存就只能在很短的时间窗口里用,收益有限。

注意这里用的是进程内字典,多进程部署时每个进程各存一份,缓存不一致是必然的。这在只读或者容忍短暂不一致的场景下没问题,但如果你需要强一致,就得换成 Redis 这类集中式缓存,同时增加一层序列化和网络开销。

4.5 压测脚本与运行方式

# 文件路径:tools/bench.py import argparse import statistics import time from concurrent.futures import ThreadPoolExecutor import requests def single_call(url: str, timeout: float) -> float: """发起一次请求,返回本次请求的耗时(秒)""" start = time.perf_counter() resp = requests.get(url, timeout=timeout) resp.raise_for_status() return time.perf_counter() - start def run(url: str, concurrency: int, total: int, timeout: float) -> None: latencies = [] wall_start = time.perf_counter() with ThreadPoolExecutor(max_workers=concurrency) as pool: futures = [pool.submit(single_call, url, timeout) for _ in range(total)] for future in futures: try: latencies.append(future.result()) except Exception as exc: # noqa: BLE001 print(f"request failed: {exc}") wall = time.perf_counter() - wall_start if not latencies: print("no successful request") return latencies.sort() ok = len(latencies) p50 = statistics.median(latencies) p95 = latencies[max(int(ok * 0.95) - 1, 0)] p99 = latencies[max(int(ok * 0.99) - 1, 0)] print( f"total={total} ok={ok} concurrency={concurrency} " f"wall={wall:.2f}s qps={ok / wall:.1f} " f"p50={p50 * 1000:.1f}ms p95={p95 * 1000:.1f}ms p99={p99 * 1000:.1f}ms" ) def main() -> None: parser = argparse.ArgumentParser(description="simple http benchmark") parser.add_argument("--url", required=True) parser.add_argument("--concurrency", type=int, default=50) parser.add_argument("--total", type=int, default=500) parser.add_argument("--timeout", type=float, default=10.0) args = parser.parse_args() run(args.url, args.concurrency, args.total, args.timeout) if __name__ == "__main__": main()

运行方式很简单,先启动被测服务,再打开另一个终端执行脚本:

python tools/bench.py --url http://127.0.0.1:8000/api/report --concurrency 50 --total 500 python tools/bench.py --url http://127.0.0.1:8001/api/report --concurrency 50 --total 500 python tools/bench.py --url http://127.0.0.1:8002/api/report --concurrency 50 --total 500

脚本里用线程池模拟并发客户端,每个线程发一次请求然后结束。这种压测方式比较粗糙,但足够看出趋势差异。真正做容量规划时,应该用 wrk、locust 这类专业工具,它们能维持稳定并发、能生成更真实的请求分布。

4.6 结果解读与结论

你会观察到的典型现象是这样的:同步版本在并发数较低时表现正常,并发数提高之后 QPS 很快遇到天花板,延迟快速上升,因为线程数被打满后新请求只能排队;异步版本在同样并发下 QPS 明显更高,延迟分布更平缓,因为它用极少的线程挂住了大量等待中的请求;缓存版本在总请求数远大于数据种类时,延迟会出现数量级的下降。

但要特别注意,这些结论成立的前提是"接口确实是 IO 密集型"。如果你的接口是纯 CPU 计算,比如图片处理、加密解密、复杂算法,那异步带来的收益非常有限,甚至因为事件循环的单线程特性反而更差。这时候正确的做法是多进程并行,利用多核。

所以第一个落地建议是:先判断你的接口属于哪种负载类型,再选并发模型。判断方法很土但有效——在这个接口上跑一次性能分析,看时间花在 CPU 上还是等待上。如果大部分时间在等待,异步有收益;如果大部分时间在计算,加机器或改算法更直接。

5. 数据库与缓存层务实优化

5.1 连接池参数怎么定

连接池大小是最容易被拍脑袋决定的参数。常见的错误做法是设成几百,觉得越大越能扛。实际上数据库服务端能接受的连接数是有硬上限的,而且每个连接都会消耗内存。应用侧池子设得过大,只是把排队从应用挪到了数据库,最终还是超时。

一个实用的估算方法是:先用压测找出单个应用实例在最优并发下需要的连接数,再乘以实例数量,得到总需求,把这个数字控制在数据库最大连接数的 60% 以内,留出管理和监控连接的余量。如果算下来不够,说明你应该先优化 SQL 让单次查询更快,而不是继续加连接。

# 文件路径:config/db.yaml database: host: 127.0.0.1 port: 3306 name: demo pool: min_size: 5 max_size: 20 max_idle_seconds: 300 connect_timeout: 5 read_timeout: 10

这几个参数的含义需要逐行看清楚:min_size是常驻连接数,太小会导致突发流量时频繁建连;max_size是上限,超过就排队;max_idle_seconds控制空闲连接多久被回收;两个超时参数决定失败能多快暴露出来。超时值设得过大,故障时会长时间挂住请求;设得过小,正常的慢查询会被误杀。

5.2 索引失效的几个高频写法

索引失效是慢查询的头号原因,而且失效往往很隐蔽,SQL 看起来没什么问题。最常见的几种情况:在索引列上做函数运算,比如WHERE DATE(create_time) = '2024-01-01',数据库无法使用create_time上的索引,只能全表扫描;在索引列上做隐式类型转换,比如索引列是字符串类型,却传了数字;使用前导通配的模糊匹配,比如LIKE '%keyword'。

-- 不推荐:索引列上做函数运算,无法走索引 SELECT id, order_no FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 推荐:改成范围查询,可以走 create_time 上的索引 SELECT id, order_no FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';

除了改写,还要养成看执行计划的习惯。绝大多数数据库都提供了解释执行计划的命令,看重点是走的是全表扫描还是索引扫描、预估行数和实际行数差多少、有没有额外的排序和临时表。在改动 SQL 之前先看计划,在改动之后再对比一次,这是最稳妥的验证方式。

5.3 缓存的三条纪律

缓存确实是最有效的性能手段之一,但它同时是制造线上事故的高发区。第一条纪律是:只缓存能容忍短暂不一致的数据。商品列表、配置项、统计结果这类可以缓存,账户余额、库存数量这类必须实时的不适合直接缓存。

第二条纪律是:必须设过期时间,哪怕你觉得数据永远不会变。没有过期时间的缓存意味着一条脏数据可以永久生效,而且当数据来源变更时你没有任何自动修复的机会。过期时间长短取决于你能接受多久的旧数据,这个值要和业务方一起确认,而不是开发自己拍。

第三条纪律是:缓存击穿要有兜底。当热点 key 正好过期、大量请求同时穿透到数据库,很容易把数据库打垮。常见的做法是加互斥锁让只有一个请求去回源,或者对空结果做短暂缓存,避免无效查询反复穿透。

6. 常见问题与排查思路

实验和真实项目里遇到的问题,大多集中在下面几类。表格里给出了现象、原因和排查方向,但真正的排查一定要按顺序来,从最容易验证的地方开始。

问题现象常见原因解决思路
换异步框架后性能没提升async def里调用了同步阻塞代码全局搜索requests、同步 ORM、time.sleep,换成异步版本或放入线程池
压测 QPS 随并发上升反而下降系统已过载,时间花在上下文切换降低并发找拐点,先优化单次请求耗时
接口偶发超时,日志无明显异常连接池耗尽后请求排队监控池子的等待数,适当调整上限并排查慢查询
数据库报连接数超限应用实例数乘以池上限超过数据库承载计算总连接需求,优化 SQL,必要时加中间层池化
缓存更新后读到旧值缓存未失效或多实例本地缓存不一致写操作后主动删除缓存,跨实例场景改用集中式缓存
改了索引但查询还是慢索引列上有函数或隐式类型转换用执行计划确认是否真的走索引,改写查询条件

排查时最容易犯的错误是"看到 A 就改 A"。比如看到响应慢就去调大线程池,结果线程池变大后数据库更扛不住,反而更慢。正确的顺序是先定位瓶颈在哪一层:看应用 CPU 使用率、看连接池等待、看数据库慢查询日志、看下游接口耗时。找到真正的瓶颈再做改动,每次只改一个变量,改完立刻压测验证。

还有一个容易被忽略的点是压测环境本身。本地压测时,压测客户端和被压服务可能跑在同一台机器上,争抢 CPU 和网络资源,导致结论失真。有条件的话,把压测客户端放到另一台机器上,或者至少观察压测过程中的 CPU 占用,确认不是客户端先成为瓶颈。

7. 最佳实践与工程建议

7.1 选型前的量化清单

在决定引入任何复杂方案之前,先回答几个具体问题:当前接口的 QPS 峰值是多少?P95 延迟是多少?单次请求内部的时间分布是怎样的?数据库当前的连接数和慢查询数量是多少?未来半年预计的增长幅度是多少?这些问题如果答不上来,说明你还没有做选型的信息基础,需要先做测量。

接下来的判断就变得很清晰:如果单次请求慢在数据库,先加索引和优化 SQL,这通常能带来最大的收益;如果单次请求慢在下游调用,先看能不能并发或减少调用次数;如果确实存在高并发读、且数据可容忍短暂不一致,再考虑缓存;只有当以上都做完了还有压力,才轮到考虑架构层面的拆分。这个顺序不能颠倒,因为越往后的方案,成本和风险越高。

7.2 配置管理与可观测性

并发相关的参数极度依赖运行环境,硬编码进代码里一定会出问题。连接池大小、超时时间、缓存过期时间这些值应该放在配置文件或配置中心里,随环境变化。同时,这些参数一旦调整,必须能从监控上看到效果,否则调参就是盲调。

必要的监控指标至少包括:接口的 QPS 和 P50/P95/P99 延迟、错误率、线程池或协程池的活跃数、连接池的等待数和等待时长、慢查询数量。这些指标不需要一开始就做得很完整,但每一条都能对应一个具体的故障场景,缺哪一条就等于那个场景出问题时你是瞎的。

日志方面,请求耗时日志里至少要带上请求标识、耗时、是否命中缓存、下游调用耗时。线上排查时,是否命中缓存这个字段能帮你快速判断问题出在缓存层还是数据源层,省下大量时间。

7.3 上线与回滚

任何涉及并发模型、缓存策略、数据库结构的变更,都应该先在测试环境做完整压测,再走灰度。灰度期间要盯住延迟和错误率两个指标,一旦出现明显劣化就立刻回滚,不要在线上边观察边调参。

数据库相关变更尤其要谨慎。加索引在大表上可能锁表,需要选择业务低峰期并确认数据库的在线 DDL 能力;改 SQL 要先在测试环境用同样的数据量验证执行计划;涉及数据删除或批量更新时,必须先备份、先在小范围验证、并明确回滚方式。这些不是流程上的形式主义,而是过去很多事故换来的经验。

8. 学习路线与收尾建议

沿着本文这条线继续往下走,建议按这个顺序补:先把操作系统层面的阻塞、非阻塞、多路复用这几个概念弄清楚,这是所有并发模型的地基;然后学一种异步框架的实际用法和它的坑,比如事件循环被同步代码阻塞的典型场景;再往后是数据库的连接管理、执行计划分析、索引设计;最后才是缓存架构和分布式相关的内容。

如果你现在手上正好有一个"感觉挺慢"的接口,最值得做的第一件事不是改架构,而是花半天时间把它量清楚:加一个耗时日志,把内部各个调用的时间打出来,用执行计划看一遍 SQL,用压测脚本跑一遍当前的基线。很多时候你会发现,性能问题只是两三条没走索引的 SQL,或者一个循环里发了几十次请求,改完之后方案都不用换。

过度设计的成本是持续的、隐性的,它体现在每次改动都要多理解一层抽象、每次故障都要多排查一个组件、每个新人都要多花一周才能上手。而性能优化的成本是一次性的、显性的,你能清楚知道花了多少时间换来了多少提升。把这两者的账算清楚,很多选型上的纠结自然就有答案了。代码都在上面,先把三个版本跑起来,看看你自己的机器上差异有多大。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 19:04:05

Eclipse+CDT+MinGW搭建C/C++开发环境与排错

简介&#xff1a;这份开发文档面向已有一定 Eclipse 使用经验、希望在 Windows 平台使用 Eclipse、CDT 与 MinGW 搭建 C/C 开发环境的学习者和开发者&#xff0c;重点解决软件准备、环境变量设置、CDT 参数配置及工程创建过程中容易卡壳的问题。压缩包内共 1 个 docx 文件&…

作者头像 李华
网站建设 2026/10/27 19:02:09

Qt无边框窗口实战:用Win32消息还原Aero Snap与DWM投影

简介&#xff1a;面向 Qt/C 桌面开发者的无边框窗口实战工程&#xff0c;聚焦 Windows 平台下将 Qt 框架与 WINAPI、DWM 接口结合&#xff0c;解决直接使用 FramelessWindowHint 后丢失系统拖拽、缩放和阴影等问题。工程覆盖标题栏图标、边缘缩放、窗口拖拽、Aero 阴影、最大最…

作者头像 李华
网站建设 2026/10/27 19:01:50

SigmaWin+ V7.02 USB驱动64位安装与连接排查

简介&#xff1a;面向使用 Yaskawa Σ 系列伺服驱动器、变频器等工业设备的自动化工程师、设备调试与维护人员&#xff0c;SigmaWin V7.02 的 64 位 USB 驱动安装包主要用于解决 64 位 Windows 主机与现场硬件建立稳定 USB 通信、正确识别设备并完成编程监控的问题。压缩包共 9…

作者头像 李华
网站建设 2026/10/27 19:00:01

基于微信小程序的餐饮线上点餐平台【附源码+文档】

&#x1f495;&#x1f495;作者&#xff1a; 米罗学长 &#x1f495;&#x1f495;个人简介&#xff1a;混迹java圈十余年&#xff0c;精通Java、小程序、数据库等。 &#x1f495;&#x1f495;各类成品Java毕设 。ssm&#xff0c;springboot&#xff0c;vue等项目&#xff0…

作者头像 李华
网站建设 2026/10/27 18:59:44

RHEL9 配置外网完整流程

RHEL9 配置外网完整流程&#xff08;虚拟机 / 物理机通用&#xff0c;按顺序来&#xff09;目标&#xff1a;先保证能 ping 公网 IP&#xff0c;再解决 DNS&#xff0c;最后 dnf 能拉包 前提&#xff1a;你的环境&#xff08;VMware/Proxmox/ 云主机&#xff09;网络模式要先对…

作者头像 李华
网站建设 2026/10/27 18:59:30

c语言中scanf函数

对于我们学习c语言&#xff0c;scanf函数是一项基础和十分重要的函数&#xff0c;但我们在使用该函数时可能会遇到些许问题&#xff0c;下面是一些基础问题及其解决方案首先是scanf的基础用法scanf 用来从键盘读数据。格式字符串里写“要读什么类型”&#xff0c;后面写“存到哪…

作者头像 李华