全栈开发宝典
从需求到上线的一套完整方法论:选型、编码、数据、工程、安全、架构、性能。可离线单文件查阅,按图索骥即可。
怎么用这份宝典
- 当字典查:左上角搜索框输入关键词(JWT、索引、Docker、N+1…),无关章节自动淡出。
- 当规范用:带 清单 标记的段落可直接作为 Code Review 检查项。
- 当路线图:第 11 章给出从初级到架构师的能力阶梯与 6 个练手项目。
贯穿全篇的六条铁律
1. 先跑通再优化
任何优化都建立在「有一条能验证的基准」之上。没有测量的优化是玄学。
2. 简单优先
能用一个进程解决,就不要上微服务;能用 SQL 解决,就不要引中间件。复杂度是最大的技术债。
3. 边界防御
所有跨越信任边界的输入都是敌人:用户输入、第三方响应、数据库返回、配置文件。
4. 失败是常态
网络会超时、磁盘会满、依赖会挂。写代码时先问:它挂了会怎样?重试、超时、熔断、降级四件套。
5. 可观测优先
没有日志和指标的系统等于盲盒。上线前先想清楚:出问题时我靠什么定位。
6. 自动化一切重复
格式化、检查、测试、构建、部署——任何重复三次以上的动作都该被脚本化。
01全栈能力地图
全栈不是「什么都会一点」,而是能以端到端视角交付并负责一个完整系统。下面这张图是能力的分层结构。
系统分层与能力项
L0 · 基础内功
数据结构与算法、计算机网络(TCP/HTTP/TLS)、操作系统(进程/线程/IO)、SQL、Git、Linux 命令行。决定上限
L1 · 前端
HTML/CSS 布局、TypeScript、框架(React/Vue)、状态管理、构建工具、性能(Core Web Vitals)、可访问性。
L2 · 后端
HTTP 语义、REST/RPC、分层架构、认证授权、校验、错误处理、任务队列、文件上传、第三方集成。
L3 · 数据
关系建模、索引与执行计划、事务与隔离级别、缓存策略、搜索引擎、迁移与扩容。
L4 · 工程
代码规范、测试金字塔、CI/CD、容器化、环境管理、可观测性(日志/指标/链路)、发布策略。
L5 · 架构
服务拆分边界、一致性与分布式事务、事件驱动、容量规划、高可用与容灾、成本治理。
一次请求的全生命周期(排查问题时的心智模型)
DNS 解析 → TCP 握手 → TLS 握手 → 发送 HTTP 请求
→ CDN/网关(限流、鉴权、路由)
→ 应用进程(反序列化 → 参数校验 → 鉴权 → 业务编排)
→ 缓存查询(命中则返回)
→ 数据库查询(事务 / 锁 / 索引)
→ 渲染响应 → 网络回传 → 浏览器解析 → 首屏渲染 → 交互就绪
★ 每一步都可能成为瓶颈,定位时按序逐层排除,而不是猜测。02技术选型决策
选型的本质不是选「最好的」,而是选团队能驾驭、生态够成熟、出问题查得到的。下面是主流场景的推荐矩阵。
按场景选型
| 场景 | 首选 | 备选 | 选型要点 |
|---|---|---|---|
| 内容/营销站、SEO 优先 | Next.js / Nuxt(SSR+SSG) | Astro | 静态化程度越高,成本与延迟越低 |
| 后台管理 / 中台系统 | React + Ant Design / Vue + Element Plus | Svelte | 组件库成熟度 > 框架新鲜度 |
| 中大型业务后端 | NestJS(TS,强约定) | Fastify / Express | 团队规模大时,约定优于灵活 |
| 轻量 BFF / 边缘函数 | Hono / Elysia | Express | 冷启动与包体积敏感场景 |
| 高并发实时 | Go / Rust | Node + Cluster | CPU 密集别用 Node 单线程 |
| 数据密集 / 报表 | Python(FastAPI + Polars) | Node | 生态决定效率 |
| 企业级后端 / 金融电信 | Java Spring Boot 3 | Go | 强类型、生态成熟、事务与中间件支持最全、人才供给多 |
| AI / LLM 应用 | Python(FastAPI + vLLM/LangChain) | Node | 模型与数据生态几乎全在 Python 侧 |
| 主数据库 | PostgreSQL | MySQL 8 | Pg 的 JSONB/CTE/扩展基本无短板 |
| 缓存 / 会话 / 队列 | Redis | Valkey | 注意持久化与内存淘汰策略 |
| 搜索 | Elasticsearch / OpenSearch | Pg 全文检索(小规模) | 数据量 < 百万且需求简单,别上 ES |
| 文件存储 | S3 协议对象存储 | MinIO(自建) | 永远不要存本地磁盘 |
| 部署 | Docker + Compose(小)→ K8s(大) | Serverless | K8s 的运维成本常被低估 |
决策树:我该不该引入 XX?
1. 现有方案是否已经无法解决? (是 → 继续,否 → 停)
2. 问题能否用 20 行代码解决? (能 → 自己写,不引依赖)
3. 引入的维护成本谁承担? (无人承担 → 停)
4. 是否有降级/回滚方案? (无 → 先设计方案)
5. 团队是否有 1 人以上真正熟悉?(无 → 先做技术验证 Spike)
★ 依赖的隐性成本 = 学习成本 + 升级成本 + 故障排查成本 + 安全补丁成本版本与环境基线建议
| 项 | 建议 | 说明 |
|---|---|---|
| Node.js | 使用 LTS(偶数版本) | 生产环境永不追 newest;用 .nvmrc 锁定 |
| 包管理器 | pnpm(省磁盘、快、严格依赖) | 必须提交 lockfile,CI 用 frozen-lockfile |
| TypeScript | strict: true | 新项目不开 strict 等于白用 TS |
| 数据库 | 与云厂商 LTS 保持一致 | 大版本升级单独排期 |
03前端工程
前端的第一性原理:把数据准确、快速、无障碍地呈现给用户,并可靠地收集用户意图。框架会变,这三点不会。
3.1 现代 CSS 布局速查
90% 的布局需求用 Flex + Grid 就能解决,不需要引 UI 框架的栅格系统。
Flex:一维排列
沿主轴排布,适合导航栏、按钮组、垂直居中。gap 替代 margin 拼接。
Grid:二维网格
行列同时控制,适合卡片墙、后台框架、不规则布局。
/* —— 万能居中 —— */
.center { display:grid; place-items:center; }
.center-x { display:flex; justify-content:center; }
.holy { display:flex; align-items:center; justify-content:space-between; }
/* —— 经典后台骨架:头部 + 侧栏 + 内容 —— */
.layout{
display:grid; height:100dvh;
grid-template-columns:240px 1fr;
grid-template-rows:56px 1fr;
grid-template-areas:"header header" "side main";
}
.layout>header{grid-area:header} .layout>aside{grid-area:side;overflow:auto}
.layout>main{grid-area:main;overflow:auto}
/* —— 自适应卡片墙(不用媒体查询) —— */
.cards{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(260px,1fr))}
/* —— 底部吸附:内容不足时 footer 仍在底部 —— */
.page{display:flex;flex-direction:column;min-height:100dvh}
.page>main{flex:1}
/* —— 单行省略 / 多行省略 —— */
.ell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ell-2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* —— 现代响应式:容器查询(按父容器而非视口) —— */
.wrap{container-type:inline-size}
@container (max-width:480px){ .card{grid-template-columns:1fr} }
/* —— 深浅色 —— */
:root{--bg:#fff;--fg:#111}
@media(prefers-color-scheme:dark){:root{--bg:#0b0e14;--fg:#dbe2ee}}min-width:0 — Flex/Grid 子项默认 min-width:auto,长文本会撑破容器,需显式设 0 才能正常省略号截断;② 100vh 在移动端含地址栏高度,改用 100dvh;③ 滚动容器要同时给父级设 min-height:0。3.2 TypeScript 核心
TS 的价值不在「加类型注解」,而在用类型系统表达业务约束,让非法状态不可表示。
// 1) 判别联合:让非法状态无法构造
type Result<T> =
| { ok:true; data:T }
| { ok:false; code:'NOT_FOUND'|'FORBIDDEN'; msg:string };
function handle(r:Result<User>){
if(r.ok){ console.log(r.data.name); } // 此处 data 已收窄
else { console.log(r.code, r.msg); }
}
// 2) 泛型约束 + keyof:类型安全的取值
function pluck<T,K extends keyof T>(obj:T, keys:K[]):Pick<T,K>{
return keys.reduce((a,k)=>(a[k]=obj[k],a), {} as Pick<T,K>);
}
// 3) 常用工具类型
// Partial 全部可选 | Required 全部必填 | Pick 挑字段 | Omit 排除字段
// Record<K,V> 映射 | ReturnType 取函数返回 | Awaited 解 Promise
// NonNullable 去空 | Exclude/Extract 联合运算
type UserDTO = Omit<User,'passwordHash'|'salt'> & { roleName:string };
// 4) satisfies:校验字面量结构但保留精确类型(比 as 安全)
const routes = {
home:'/', login:'/login', user:'/u/:id'
} satisfies Record<string,string>;
// 5) 类型守卫:把 unknown 收窄成具体类型
function isUser(x:unknown):x is User{
return typeof x==='object' && x!==null && 'id' in x && 'email' in x;
}
// 6) 模板字面量类型:约束字符串格式
type EventName = `on${Capitalize<'click'|'change'>}`; // 'onClick'|'onChange'
// 7) 运行时校验:类型在编译后被擦除,边界处必须 zod 校验
import { z } from 'zod';
const CreateUser = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(['admin','user']).default('user'),
});
type CreateUserInput = z.infer<typeof CreateUser>; // 类型由 schema 推导as 掩盖错误——那是把编译期问题推迟到运行时;② 不要写 any,至少用 unknown 强制检查;③ 枚举优先用字符串联合字面量而非 enum(enum 会生成运行时代码且类型不兼容)。3.3 React 核心
心智模型
UI = f(state)。你描述的是「在某个状态下 UI 应该长什么样」,而不是「如何一步步把 DOM 改成目标样子」。
// ✅ 声明式:状态变,UI 自动变
const [list,setList]=useState<User[]>([]);
const [q,setQ]=useState('');
// 派生数据直接算,不要额外存 state(避免状态不同步)
const filtered = useMemo(()=>list.filter(u=>u.name.includes(q)),[list,q]);
// ❌ 反模式:把派生数据放进 state + useEffect 同步
// const [filtered,setFiltered]=useState([]);
// useEffect(()=>setFiltered(list.filter(...)),[list,q]); // 多一次渲染且易出 bug
// ✅ 请求数据的标准写法(含取消 + 竞态处理)
useEffect(()=>{
const ac = new AbortController();
setLoading(true);
fetch(`/api/users?q=${q}`,{signal:ac.signal})
.then(r=>r.json())
.then(d=>setList(d.items))
.catch(e=>{ if(e.name!=='AbortError') setError(e); })
.finally(()=>setLoading(false));
return ()=>ac.abort(); // 关键:清理,避免后发先至的响应覆盖
},[q]);Hooks 要点
| Hook | 用途 | 典型坑 |
|---|---|---|
useState | 组件内状态 | 异步更新批处理:用 setX(v=>v+1) 而非 setX(x+1) |
useEffect | 与外部系统同步 | 依赖数组写不全 → 闭包过期;务必返回清理函数 |
useMemo | 缓存昂贵计算 | 计算成本 < 比较成本时是负优化 |
useCallback | 稳定函数引用 | 仅当传给 memo 子组件或作为 effect 依赖时才需要 |
useRef | 保存不触发渲染的值 / DOM 引用 | 不要在渲染期间读写 ref.current |
useReducer | 复杂状态机 | 状态逻辑多且相互关联时优于多个 useState |
状态管理选型
| 方案 | 适用 | 说明 |
|---|---|---|
| useState / useReducer | 组件内、局部 | 默认选择,能不提升就不提升 |
| Context | 低频全局(主题、语言、当前用户) | 值变化会让所有消费者重渲染,别放高频数据 |
| URL(searchParams) | 筛选条件、分页、弹窗 | 可分享可回退,被严重低估的方案 |
| 服务端状态库(TanStack Query / SWR) | 所有「来自服务端的数据」 | 强烈推荐缓存、重试、失效、乐观更新全包 |
| Zustand / Jotai | 少量客户端全局状态 | 轻量,替代 Redux 的现代选择 |
| Redux Toolkit | 大型团队、需时间旅行调试 | 样板代码仍多,非必要不引入 |
错误边界
class ErrorBoundary extends React.Component<
{fallback:React.ReactNode; children:React.ReactNode},
{hasError:boolean}>
{
state = { hasError:false };
static getDerivedStateFromError(){ return { hasError:true }; }
componentDidCatch(err:Error, info:React.ErrorInfo){
reportError(err, info.componentStack); // 上报 Sentry 等
}
render(){ return this.state.hasError ? this.props.fallback : this.props.children; }
}
// 用法:包在路由级或模块级,避免一个组件崩溃白屏整页3.4 Next.js 实战(App Router)
npx create-next-app@latest app --ts --app --tailwind --eslint --src-dir
# 目录约定
app/
layout.tsx # 根布局(必须含 html/body)
page.tsx # 路由页面
loading.tsx # 该段的 Suspense 兜底
error.tsx # 该段的错误边界('use client')
not-found.tsx # 404
route.ts # Route Handler(API)
[id]/page.tsx # 动态段
(group)/ # 路由分组,不影响 URL
@modal/ # 平行路由
middleware.ts # 请求前拦截(鉴权/重写/国际化)// Server Component(默认):在服务端执行,可直接查库
export default async function Page({ searchParams }:{searchParams:{page?:string}}){
const page = Number(searchParams.page ?? 1);
const items = await db.post.findMany({ take:20, skip:(page-1)*20 });
return <PostList items={items} />;
}
// 缓存策略(Next 14+)
export const revalidate = 60; // ISR:60 秒重新生成
export const dynamic = 'force-dynamic'; // 每次请求都渲染
const r = await fetch(url,{ next:{ revalidate:300, tags:['posts'] } });
revalidateTag('posts'); // 写操作后主动失效
// Client Component:需要交互/浏览器 API 时加 'use client'
'use client';
export function Like({ id }:{id:string}){
const [n,setN]=useState(0);
return <button onClick={()=>setN(n+1)}>{n}</button>;
}
// Route Handler
export async function GET(req:Request){
const { searchParams } = new URL(req.url);
return Response.json({ ok:true, q:searchParams.get('q') });
}useState/useEffect、事件、浏览器 API 时才下沉为 Client Component,且把 'use client' 尽量推到组件树的叶子节点。传给 Client 组件的 props 必须可序列化(不能传函数、Date 需注意)。3.5 前端性能优化
Core Web Vitals 目标值
| 指标 | 含义 | 良好 | 常用手段 |
|---|---|---|---|
| LCP | 最大内容绘制(首屏速度) | < 2.5s | SSR/SSG、图片预加载、CDN、压缩、减少阻塞资源 |
| INP | 交互到下一帧的延迟 | < 200ms | 拆分长任务、防抖、Web Worker、减少重渲染 |
| CLS | 累积布局偏移 | < 0.1 | 给图片/广告位设宽高、字体 font-display:swap + 尺寸预留 |
| TTFB | 首字节时间 | < 800ms | 边缘缓存、数据库优化、连接复用 |
优化清单
- 路由级代码分割(
React.lazy/ Next 自动分割),首屏 JS ≤ 200KB gzip - 图片:用 WebP/AVIF、
next/image自动尺寸、loading="lazy"、首屏图priority - 字体:子集化、自托管、
preload关键字体、font-display:swap - 长列表虚拟化(> 1000 行用 react-window / TanStack Virtual)
- 防抖节流:
resize、scroll、搜索输入(250ms 防抖) - 缓存头:静态资源
Cache-Control: public, max-age=31536000, immutable+ 内容哈希文件名 - 开启 Brotli/gzip、HTTP/2、移除未使用依赖(
npx depcheck) - 用
view-transition/ CSS transform 动画,避免触发 layout 的属性(width/top/left)
// 防抖 + 节流的正确实现
export function debounce<T extends (...a:any[])=>void>(fn:T, wait=250){
let t:ReturnType<typeof setTimeout>;
return (...args:Parameters<T>)=>{ clearTimeout(t); t=setTimeout(()=>fn(...args),wait); };
}
export function throttle<T extends (...a:any[])=>void>(fn:T, wait=100){
let last=0;
return (...args:Parameters<T>)=>{
const now=Date.now();
if(now-last>=wait){ last=now; fn(...args); }
};
}
// 长任务让出主线程
const yieldToMain = ()=>new Promise(r=>setTimeout(r,0));04后端服务
后端的核心职责:在不可信的输入和不可靠的依赖之上,正确地持久化业务状态。
4.1 框架与分层
推荐分层(依赖只能向下)
Controller / Router → 解析协议、参数校验、组装响应(不含业务规则)
↓
Service / UseCase → 业务编排、事务边界、权限判定(不含 HTTP 概念)
↓
Repository / DAO → 数据访问、SQL、缓存读写(不含业务规则)
↓
Infrastructure → DB / Redis / MQ / 第三方 SDK
★ 铁律:上层可依赖下层,下层绝不反向依赖上层。
★ Service 层不出现 req/res 对象,否则无法被 CLI/队列/测试复用。
★ 跨层异常统一由最外层(全局异常过滤器)转换为 HTTP 响应。// —— 统一成功/失败响应 ——
type Ok<T> = { code:0; data:T; traceId:string };
type Err = { code:number; message:string; traceId:string };
// —— 业务异常(带 HTTP 语义但不依赖框架)——
export class BizError extends Error {
constructor(public status:number, message:string, public code=status){
super(message);
}
}
// service/user.service.ts —— 纯业务,可单测
export class UserService {
constructor(private repo:UserRepo, private mailer:Mailer){}
async register(input:CreateUserInput){
if(await this.repo.existsByEmail(input.email))
throw new BizError(409,'邮箱已注册');
const hash = await argon2.hash(input.password); // 绝不明文存密码
const user = await this.repo.create({...input, passwordHash:hash});
await this.mailer.sendWelcome(user.email); // 副作用放最后,失败不阻断主流程
return sanitize(user); // 出参脱敏
}
}
// —— 全局错误处理(Express 中间件)——
app.use((err:Error,req:Request,res:Response,next:NextFunction)=>{
const traceId = req.traceId;
if(err instanceof BizError){
return res.status(err.status).json({code:err.code,message:err.message,traceId});
}
logger.error({err,traceId,url:req.url},'unhandled');
res.status(500).json({code:500,message:'服务器内部错误',traceId}); // 不泄漏堆栈
});参数校验(边界防御第一道关)
const PageQuery = z.object({
page: z.coerce.number().int().min(1).max(10000).default(1),
size: z.coerce.number().int().min(1).max(100).default(20), // 必须限制上限!
sort: z.enum(['createdAt','name']).default('createdAt'),
order: z.enum(['asc','desc']).default('desc'),
});
app.get('/users', async (req,res)=>{
const q = PageQuery.parse(req.query); // 校验失败自动抛 400
res.json(await userService.list(q));
});4.2 REST API 设计规范
| 要点 | 正确做法 | 错误做法 |
|---|---|---|
| 资源命名 | 复数名词 /users、/users/1/orders | 动词 /getUser、/createOrder |
| 动作表达 | 用 HTTP 方法:GET/POST/PUT/PATCH/DELETE | 全部 POST + 路径里写动作 |
| 非 CRUD 动作 | 子资源动词化 POST /orders/1/cancel | POST /cancelOrder |
| 分页 | ?page=1&size=20 或游标 ?cursor=xxx&size=20 | 一次性返回全部 |
| 状态码 | 200/201/204/400/401/403/404/409/422/429/500 | 一律 200 + body 里放 code |
| 版本 | URL 前缀 /api/v1 或 Header(二选一,全局统一) | 混用 |
| 错误体 | {code,message,details?,traceId} | 返回 HTML 堆栈 |
状态码速查
| 码 | 语义 | 典型场景 |
|---|---|---|
| 200 | 成功,返回体有内容 | 查询、更新成功 |
| 201 | 已创建 | POST 创建成功,响应带 Location 头 |
| 204 | 成功但无返回体 | DELETE |
| 400 | 请求本身有问题(参数/格式) | 校验失败 |
| 401 | 未认证(不知道你是谁) | token 缺失/过期 |
| 403 | 已认证但无权限 | 普通用户访问管理接口 |
| 404 | 资源不存在 | ID 不存在(也可对无权限资源返回 404 防探测) |
| 409 | 状态冲突 | 唯一键冲突、重复提交 |
| 422 | 语义正确但业务校验不通过 | 余额不足 |
| 429 | 限流 | 需带 Retry-After 头 |
| 500 | 服务端未知错误 | 必须打日志 + traceId,不暴露细节 |
Idempotency-Key,服务端唯一索引去重或缓存首次结果)。支付、下单、发券接口不做幂等 = 定时炸弹。app.post('/orders', async (req,res,next)=>{
const key = req.header('Idempotency-Key');
if(!key) return res.status(400).json({message:'缺少 Idempotency-Key'});
const cacheKey = `idem:${req.userId}:${key}`;
const cached = await redis.get(cacheKey);
if(cached) return res.json(JSON.parse(cached)); // 重复请求:直接回放
const result = await orderService.create(req.userId, req.body);
await redis.set(cacheKey, JSON.stringify(result), 'EX', 86400);
res.status(201).json(result);
});4.3 认证与授权
| 维度 | Session + Cookie | JWT |
|---|---|---|
| 状态 | 服务端有状态(存 Redis) | 无状态(自包含) |
| 撤销 | 即时(删 Redis 即可) | 困难(需黑名单/短 TTL + 刷新令牌) |
| 跨域/移动端 | 需处理 SameSite/CORS | 友好(放 Authorization 头) |
| XSS 风险 | 低(HttpOnly Cookie 不可被 JS 读) | 高(存 localStorage 会被窃取) |
| CSRF 风险 | 需防护(SameSite=Lax + CSRF Token) | 低(不自动携带) |
| 建议 | Web 后台首选 | 第三方/网关/短时效场景 |
// 密码存储:argon2id > bcrypt(cost 12) > scrypt。禁止 MD5/SHA1/裸 SHA256
import argon2 from 'argon2';
const hash = await argon2.hash(pwd, { type:argon2.argon2id });
const ok = await argon2.verify(hash, pwd);
// JWT 签发:只放必要声明,别塞敏感信息(payload 仅 base64,可解码)
const access = jwt.sign({ sub:user.id, role:user.role, tv:user.tokenVersion },
process.env.JWT_SECRET!, { expiresIn:'15m' });
// 鉴权中间件(RBAC)
function requireRole(...roles:Role[]){
return (req,res,next)=>{
if(!req.user) return res.status(401).end();
if(!roles.includes(req.user.role)) return res.status(403).end();
next();
};
}
app.delete('/admin/users/:id', requireRole('admin'), handler);
// ★ 越权防护:查询必须带上归属条件,不能只查 id
await db.post.deleteMany({ where:{ id, authorId:req.user.id } }); // ✅ 而非 delete({where:{id}})alg:none 或不校验算法(必须显式指定 algorithms:['HS256']);④ 密码重置令牌可重复使用、不过期;⑤ 登录接口无限速(必须加失败次数限制 + 验证码)。4.4 异步任务与消息
凡是不需要用户在本次请求中等结果的事,都应该异步化:发邮件、生成报表、图片处理、对账、推送。
const emailQ = new Queue('email',{ connection:redis });
// 生产者
await emailQ.add('welcome', { userId }, {
attempts:3, // 重试次数
backoff:{ type:'exponential', delay:2000 }, // 指数退避
removeOnComplete:{ count:1000 },
removeOnFail:{ age:7*86400 },
});
// 消费者(独立进程)
new Worker('email', async job=>{
if(job.name==='welcome') await mailer.sendWelcome(job.data.userId);
}, { connection:redis, concurrency:10,
limiter:{ max:100, duration:60_000 } }); // 限流保护下游
// 定时任务(也可用 cron / 数据库锁保证单实例)
await emailQ.upsertJobScheduler('digest', { pattern:'0 9 * * *' }, { name:'digest' });4.5 实时通信与外部集成
| 方案 | 方向 | 适用 |
|---|---|---|
| 轮询 | C→S | 简单状态刷新,间隔 > 10s,能不用就不用 |
| SSE(EventSource) | S→C 单向 | 推荐AI 流式输出、进度推送、通知。原生支持断线重连 |
| WebSocket | 双向 | 协同编辑、IM、实时游戏。需自己处理心跳/重连/鉴权 |
res.writeHead(200,{
'Content-Type':'text/event-stream',
'Cache-Control':'no-cache, no-transform',
'Connection':'keep-alive',
'X-Accel-Buffering':'no', // 关键:禁止 Nginx 缓冲
});
for await (const chunk of stream){
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
res.write('event: done\ndata: {}\n\n'); res.end();调用第三方 API 的四件套
// ① 超时(默认无超时 = 连接泄漏)② 重试(仅幂等 + 仅网络/5xx)
// ③ 熔断(连续失败快速失败)④ 降级(返回兜底数据)
const api = axios.create({ baseURL, timeout:3000 }); // 连接+读取超时
async function callWithRetry<T>(fn:()=>Promise<T>, n=3):Promise<T>{
let lastErr;
for(let i=0;i<n;i++){
try{ return await fn(); }
catch(e){
lastErr = e;
const retryable = !e.response || e.response.status >= 500; // 4xx 不重试
if(!retryable || i===n-1) break;
await sleep(2**i * 300 + Math.random()*200); // 指数退避 + 抖动防惊群
}
}
throw lastErr;
}05数据库与存储
数据库是系统里最难替换、最容易成为瓶颈的部分。建模阶段偷的懒,会用十倍的运维代价偿还。
5.1 建模原则
| 决策 | 建议 | 理由 |
|---|---|---|
| 主键 | BIGSERIAL 或 UUIDv7(时间有序) | UUIDv4 随机写入导致 B+ 树页分裂,性能差 |
| 时间 | timestamptz(Pg)/ datetime(3)(MySQL) | 永远用 UTC 存储,展示层转时区 |
| 金额 | DECIMAL(18,2) 或「分」为单位的 BIGINT | 禁用 float浮点有精度误差 |
| 字符串 | 有明确上限用 VARCHAR(n),长文本 TEXT | Pg 中两者性能一致 |
| 布尔/状态 | 状态多时用 SMALLINT/枚举表而非多个 bool | 避免「已支付且已取消」的非法组合 |
| JSON | Pg JSONB(可建 GIN 索引) | 仅用于「结构不定的扩展属性」,核心字段必须列式 |
| 软删除 | deleted_at + 部分唯一索引 | 见下方注意 |
| 审计字段 | created_at / updated_at / created_by / updated_by | 排查问题时救命 |
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash TEXT NOT NULL,
role SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- 软删除下保证未删除记录唯一(Pg 部分索引)
CREATE UNIQUE INDEX uk_users_email ON users(email) WHERE deleted_at IS NULL;
-- 常用组合查询索引:注意「区分度高的列放前面」
CREATE INDEX idx_users_role_created ON users(role, created_at DESC);
COMMENT ON COLUMN users.role IS '0=member,1=admin,2=superadmin';WHERE deleted_at IS NULL,忘一次就是数据泄漏;唯一约束要改写成部分索引;关联查询性能下降。只在「业务确实需要恢复/审计」时使用,并考虑历史表归档替代。5.2 索引与执行计划
最左前缀原则
-- 索引 (a, b, c) 能加速:
WHERE a=1 ✅
WHERE a=1 AND b=2 ✅
WHERE a=1 AND b=2 AND c=3 ✅
WHERE a=1 ORDER BY b ✅
WHERE b=2 ❌ 未用最左列
WHERE a=1 AND c=3 ⚠️ 只用到 a,c 部分无法定位
WHERE a>1 AND b=2 ⚠️ 范围列之后的列无法再用索引定位
-- 覆盖索引:查询列全在索引里,无需回表 → Using index
CREATE INDEX idx_cover ON orders(user_id, status) INCLUDE (amount);
-- 函数/表达式索引:别在列上包函数(会导致索引失效)
WHERE lower(email)='a@b.com' ❌
CREATE INDEX idx_lower_email ON users(lower(email)); ✅ 建表达式索引
-- 隐式转换陷阱:字符串列传数字、不同字符集/排序规则 JOIN → 索引失效
-- LIKE 前缀匹配可用索引,'%xxx' 不可用(改用全文检索/倒排)EXPLAIN 怎么看
| 关键字 | 含义 | 优化方向 |
|---|---|---|
Seq Scan | 全表扫描 | 大表上出现需警惕 → 加索引 / 限制返回范围 |
Index Scan | 走索引 + 回表 | 正常 |
Index Only Scan | 覆盖索引,不回表 | 最优 |
Bitmap Heap Scan | 多索引合并后回表 | 考虑建组合索引 |
rows 估算 ≫ 实际 | 统计信息过期 | 执行 ANALYZE 或调大统计采样 |
Nested Loop + 大表 | 驱动表行数大时灾难 | 改用 Hash Join / 加索引 |
Using filesort / temporary | 排序/分组未用索引 | 建符合排序顺序的索引 |
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id=42 ORDER BY id DESC LIMIT 20;
-- MySQL 慢查询定位
SET GLOBAL slow_query_log='ON'; SET GLOBAL long_query_time=1;
SELECT * FROM sys.statement_analysis ORDER BY avg_latency DESC LIMIT 10;
-- Pg 慢查询:pg_stat_statements(必装扩展)
SELECT calls, round(mean_exec_time::numeric,2) ms, query
FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;分页优化
-- ❌ 深分页:OFFSET 100000 要扫描并丢弃 10 万行
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;
-- ✅ 游标分页(keyset):利用索引直接定位
SELECT * FROM orders WHERE id < :last_id ORDER BY id DESC LIMIT 20;
-- ✅ 延迟关联:先查 ID 再回表,减少回表量
SELECT * FROM orders o JOIN
(SELECT id FROM orders ORDER BY id LIMIT 20 OFFSET 100000) t USING(id);5.3 事务与并发
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| Read Uncommitted | ✗可能 | ✗ | ✗ | 几乎不用 |
| Read Committed | ✅ | ✗ | ✗ | 多数数据库默认,够用 |
| Repeatable Read | ✅ | ✅ | ✗(Pg 实际可避免) | MySQL 默认 |
| Serializable | ✅ | ✅ | ✅ | 性能最低,用于强一致场景 |
-- 悲观锁:先锁后改,适合高冲突(库存、余额)
BEGIN;
SELECT * FROM sku WHERE id=1 FOR UPDATE; -- 行锁(必须走索引,否则锁表!)
UPDATE sku SET stock=stock-1 WHERE id=1 AND stock>=1;
COMMIT;
-- 乐观锁:版本号,适合低冲突(大多数业务)
UPDATE sku SET stock=stock-1, version=version+1
WHERE id=1 AND version=:expected_version;
-- 影响行数 0 → 说明被并发修改,重试或报错
-- 原子 upsert(避免「先查再插」的竞态)
INSERT INTO user_login(user_id, date, times) VALUES (1, CURRENT_DATE, 1)
ON CONFLICT (user_id, date) DO UPDATE SET times = user_login.times + 1;
-- 防死锁:固定加锁顺序(如都按 id 升序)、缩短事务、避免事务内做远程调用lock_timeout、statement_timeout);④ 出现死锁先看日志里的两条 SQL 加锁顺序。5.4 ORM 实战
- 必须开启 SQL 日志(开发环境),否则你永远不知道 ORM 生成了什么
- 警惕 N+1 查询:列表页 + 循环里查关联数据,10 行变 11 条 SQL
- 用
select明确字段,别默认SELECT * - 批量操作用
createMany/bulkInsert,别在循环里单条插入 - 迁移必须可回滚、向前兼容(先加字段 → 双写 → 切读 → 删旧字段)
- 连接池大小 ≈
CPU核数 * 2 + 磁盘数,不是越大越好(Pg 建议 10~30)
// ❌ N+1:每篇都查一次作者
const posts = await db.post.findMany();
for(const p of posts) p.author = await db.user.findUnique({where:{id:p.authorId}});
// ✅ include 一次性 join / 或 dataloader 批处理
const posts = await db.post.findMany({
select:{ id:true, title:true, author:{ select:{ id:true, name:true } } },
take:20, skip:(page-1)*20,
orderBy:{ createdAt:'desc' },
});
const [items, total] = await db.$transaction([
db.post.findMany({...}), db.post.count({where}),
]);
// 事务(隔离级别 + 超时)
await db.$transaction(async (tx)=>{
await tx.order.create({data});
await tx.sku.update({where:{id}, data:{stock:{decrement:1}}});
}, { isolationLevel:'ReadCommitted', timeout:5000 });5.5 Redis 缓存
数据结构选型
| 结构 | 典型场景 |
|---|---|
| String | 缓存对象(序列化 JSON)、计数器(INCR)、分布式锁 |
| Hash | 对象部分字段读写(购物车、用户配置) |
| List | 队列/栈、最新动态(LPUSH + LTRIM) |
| Set | 去重、标签、共同好友(SINTER)、抽奖 |
| ZSet | 排行榜、延迟队列(score=执行时间)、优先级队列 |
| Bitmap | 签到、DAU 统计(1 亿用户日活仅需 12MB) |
| HyperLogLog | UV 去重计数(0.81% 误差,12KB 固定内存) |
| Stream | 可靠消息队列(支持消费组、ACK、重试) |
缓存三大问题
穿透
查一个根本不存在的 key,每次都打到 DB。
解法:缓存空值(短 TTL)+ 布隆过滤器 + 参数校验拦截非法 ID。
击穿
热点 key 过期瞬间,海量请求直击 DB。
解法:互斥锁重建(singleflight)+ 逻辑过期(不设 TTL,后台异步刷新)+ 热点永不过期。
雪崩
大批 key 同时过期。
解法:TTL 加随机抖动 + 多级缓存 + 限流降级 + 提前预热。
// Cache-Aside 标准读路径(最常用)
async function getOrLoad<T>(key:string, ttl:number, load:()=>Promise<T>):Promise<T>{
const hit = await redis.get(key);
if(hit) return JSON.parse(hit);
// 单飞锁:同一 key 只有一个请求去回源,其余短暂等待
const lockKey = `lock:${key}`;
const got = await redis.set(lockKey,'1','EX',10,'NX');
if(!got){ await sleep(50); return getOrLoad(key,ttl,load); } // 递归等一等
try{
const data = await load();
const val = data === null ? '__NULL__' : JSON.stringify(data);
await redis.set(key, val, 'EX', ttl + Math.floor(Math.random()*ttl*0.2)); // 抖动防雪崩
return data;
} finally { await redis.del(lockKey); }
}
// 写路径:先更新 DB,再删除缓存(不是更新缓存!)
await db.user.update(...); await redis.del(`user:${id}`);
// 高一致要求:延迟双删(删 → 等 500ms → 再删)或订阅 binlog(Canal/Debezium)异步失效
// 分布式锁(Redlock 简化版,务必设置自动过期 + 校验随机值再删)
const val = crypto.randomUUID();
const ok = await redis.set(lockKey, val, 'EX', 30, 'NX');
// 释放时用 Lua 保证「判断 + 删除」原子性:
// if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 endmaxmemory-policy(通常 allkeys-lru),否则内存打满会写失败。数据库扩展路径(按顺序,不要跳步)
06工程化与 DevOps
工程化的目标:让「正确的做法」成为「最省力的做法」。靠人自觉遵守的规范一定会失效,靠工具强制的才会。
6.1 Git 协作规范
分支模型(中小团队推荐 GitHub Flow)
main ← 永远可部署,保护分支,禁止直推
└ feature/xxx ← 从 main 切出,PR 合并后删除
└ fix/xxx
└ release/v1.2 ← 需要并行维护多版本时才引入(Git Flow)
流程:切分支 → 小步提交 → 提 PR(需 1 人 Review + CI 全绿)→ Squash Merge → 删分支Conventional Commits
<type>(<scope>): <subject>
feat(user): 新增手机号登录
fix(order): 修复优惠券重复核销
perf(db): 订单列表深分页改游标分页
refactor(auth): 抽离权限校验中间件
docs(api): 补充分页参数说明
test(pay): 增加退款幂等用例
chore(deps): 升级 vite 到 5.4
revert: 回滚 feat(user): xxx
★ type: feat|fix|perf|refactor|docs|test|build|ci|chore|revert
★ 好处:自动生成 CHANGELOG、语义化版本号、一眼看懂改动意图常用操作
# 把当前分支的提交整理干净后再合入(个人分支用,公共分支禁用)
git rebase -i HEAD~3 # squash / reword / drop
git rebase main # 同步主干,避免「合并提交」污染历史
# 只把某次提交挪过来(线上热修常用)
git cherry-pick <commit>
# 撤销已 push 的错误提交(保留历史,安全)
git revert <commit>
# 手滑切走了分支,找回丢失的提交
git reflog → git checkout <hash>
# 临时保存工作区
git stash push -m "wip" / git stash pop
# 提交前自检:只看本次改动的 diff
git diff --staged.env、node_modules、构建产物;误提交后用 git filter-repo 清理并立即轮换密钥。6.2 测试金字塔
/E2E\ 10% 关键业务链路(登录→下单→支付),慢但覆盖真实链路
/------\
/ 集成测试 \ 30% API + DB + 缓存,最有性价比
/------------\
/ 单元测试 \ 60% 纯函数、业务规则,快(毫秒级)、无外部依赖
/----------------\
★ 经验:不要追求覆盖率数字,追求「改动后敢不敢发版」。
★ 有价值的测试:覆盖 bug 复现路径、边界条件、金额/权限/幂等等易错逻辑。describe('UserService.register', ()=>{
it('邮箱重复时抛 409', async ()=>{
const repo = { existsByEmail: vi.fn().mockResolvedValue(true),
create: vi.fn() } as unknown as UserRepo;
await expect(new UserService(repo, mailer).register(input))
.rejects.toMatchObject({ status:409 });
expect(repo.create).not.toHaveBeenCalled(); // 断言副作用未发生
});
});
// 集成测试:起真实 DB(Testcontainers)或独立 test schema,跑事务回滚
beforeEach(async ()=>{ await db.$executeRaw`BEGIN`; });
afterEach (async ()=>{ await db.$executeRaw`ROLLBACK`; });
// E2E(Playwright):只测关键路径
test('下单流程', async ({page}) =>{
await page.goto('/login');
await page.fill('[name=email]', 'a@b.com');
await page.click('button[type=submit]');
await expect(page).toHaveURL('/');
});6.3 CI/CD
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: corepack enable && pnpm install --frozen-lockfile
- run: pnpm lint && pnpm typecheck
- run: pnpm test --coverage
- run: pnpm build
- uses: docker/build-push-action@v6 # 仅 main 分支推送镜像
if: github.ref == 'refs/heads/main'
with: { push: true, tags: ghcr.io/org/app:${{ github.sha }} }发布策略
| 策略 | 做法 | 适用 |
|---|---|---|
| 滚动发布 | 逐台替换实例 | 常规,需保证接口前后兼容 |
| 蓝绿部署 | 两套环境切换流量 | 回滚最快(切回即可),成本高 |
| 金丝雀/灰度 | 先放 5% 流量,观察指标后放量 | 推荐核心业务 |
| 特性开关 | 代码上线但功能关闭,随时开启 | 解耦「部署」与「发布」 |
6.4 Docker
# —— 多阶段构建:产物镜像不含 devDependencies 与源码工具 ——
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build && pnpm prune --prod # 构建完再裁掉开发依赖
FROM node:22-alpine AS run
WORKDIR /app
ENV NODE_ENV=production
# ★ 用非 root 用户运行
RUN addgroup -S app && adduser -S app -G app
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
USER app
EXPOSE 3000
# ★ 用 exec 形式 + init 处理信号,保证优雅退出
CMD ["node","dist/main.js"]services:
app:
build: .
ports: ["3000:3000"]
env_file: .env
depends_on:
db: { condition: service_healthy }
rds: { condition: service_started }
restart: unless-stopped
db:
image: postgres:16-alpine
environment: { POSTGRES_PASSWORD: dev, POSTGRES_DB: app }
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL","pg_isready -U postgres"]
interval: 5s retries: 10
rds:
image: redis:7-alpine
command: ["redis-server","--maxmemory","256mb","--maxmemory-policy","allkeys-lru"]
volumes: { pgdata: {} }.dockerignore必须排除node_modules、.git、dist(否则构建上下文巨大且缓存失效)- Dockerfile 中变化少的层放前面(先 COPY lockfile 装依赖,再 COPY 源码),最大化命中缓存
- 容器必须优雅退出:监听
SIGTERM,关闭连接后再退出,否则滚动发布丢请求 - 健康检查(
/healthz)+ 就绪检查(/readyz)分开,前者看进程,后者看依赖 - 日志输出 stdout(不要写文件),由容器运行时收集
6.5 配置与可观测性
12-Factor 配置
# .env.example 提交到仓库(只有键,无值);.env 加入 .gitignore
DATABASE_URL=postgres://user:pass@localhost:5432/app
REDIS_URL=redis://localhost:6379
JWT_SECRET= # 必填,启动时校验,缺失直接 fail-fast
LOG_LEVEL=info
# 启动时校验配置,避免运行到一半才发现缺配置
const env = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
}).parse(process.env); // 校验失败进程立即退出并打印缺哪个变量可观测性三支柱
日志 Log
结构化 JSON,带 traceId/userId。禁止在日志输出密码、token、身份证、手机号(脱敏)。用 level 分级,生产只用 info 及以上。
指标 Metric
RED 黄金三指标:Rate(QPS)、Errors(错误率)、Duration(耗时 P50/P95/P99)。配告警阈值与 SLO。
链路 Trace
OpenTelemetry 自动埋点跨服务传播 traceparent,一个 traceId 串起全链路,定位慢在哪一跳。
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
redact: ['req.headers.authorization','password','idCard'], // 自动脱敏
base: { service:'api', version:APP_VERSION },
});
// 每个请求注入 traceId(中间件)
const traceId = req.header('x-trace-id') ?? crypto.randomUUID();
const log = logger.child({ traceId, userId:req.user?.id });
req.log = log; // 后续所有日志自动带上上下文
log.info({ rows:120, ms:35 }, 'query users');07安全防御
安全的第一原则:不信任任何来自边界外的数据。第二原则:默认拒绝,显式允许。
OWASP Top 10 防御对照
| 风险 | 防御手段 |
|---|---|
| A01 失效的访问控制(越权) | 每个查询带归属条件;服务端每次请求都校验权限(不依赖前端隐藏按钮);资源 ID 用 UUID 或校验归属;禁止遍历 ID |
| A02 加密失败 | 全站 HTTPS(HSTS);密码用 argon2id/bcrypt;敏感字段加密存储;禁止自研加密算法 |
| A03 注入 | 参数化查询/预编译(ORM 默认安全,但 $queryRaw 拼字符串就危险);禁止拼接 SQL/命令/LDAP |
| A04 不安全设计 | 威胁建模:画数据流图,标出信任边界;业务层面防刷(限购、风控、验证码) |
| A05 安全配置错误 | 关掉调试模式与目录列表;删除示例账号;最小化开放端口;安全响应头(CSP/X-Frame-Options/X-Content-Type-Options) |
| A06 易受攻击组件 | pnpm audit / Dependabot / SCA 扫描纳入 CI;及时升级 |
| A07 认证失败 | 登录限速 + 失败锁定;MFA;会话超时;密码强度策略;防凭据填充 |
| A08 数据完整性 | 校验第三方回调签名;锁定依赖版本(lockfile);CI 校验产物哈希 |
| A09 日志与监控不足 | 记录登录/权限变更/敏感操作审计日志;日志集中化且防篡改;告警 |
| A10 SSRF | 禁止用用户提供的 URL 直接请求;白名单域名 + 禁止内网 IP(含 169.254.169.254 元数据服务);禁止跟随重定向到内网 |
代码级防御速查
// 1) SQL 注入 —— 绝不用模板字符串拼 SQL
await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`; // ✅ 参数化
// await db.$queryRawUnsafe(`... '${email}'`); // ❌ 注入之门
// 2) XSS —— 框架默认转义;危险场景用 DOMPurify
// ❌ dangerouslySetInnerHTML={{__html: userInput}}
// ✅ 富文本:服务端清洗 + 前端 DOMPurify.sanitize() + CSP
// 3) CSRF —— SameSite=Strict Cookie + 双重提交 Token(同源校验 Origin)
res.cookie('sid', id, { httpOnly:true, secure:true, sameSite:'strict', maxAge:7*86400000 });
// 4) 文件上传 —— 校验类型用「魔数」而非扩展名;重命名;存对象存储;禁止执行权限
if(!['image/jpeg','image/png'].includes(mimeFromBuffer))
throw new BizError(400,'不支持的文件类型');
// 5) 敏感信息泄漏 —— 出参白名单,禁止直接吐 ORM 实体
const safe = (({id,name,email})=>({id,name,email}))(user); // ✅ 显式挑字段
// 6) 安全响应头(Helmet 一句话搞定)
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin限流(必做)
// Redis 滑动窗口限流(Lua 保证原子性)
-- KEYS[1]=key ARGV[1]=window(ms) ARGV[2]=limit
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - ARGV[1])
local cnt = redis.call('ZCARD', KEYS[1])
if cnt < tonumber(ARGV[2]) then
redis.call('ZADD', KEYS[1], now, now .. '-' .. math.random())
redis.call('PEXPIRE', KEYS[1], ARGV[1])
return 1
end
return 0
// 分层限流:网关(全局 IP 级)→ 应用(用户/接口级)→ 下游(防打爆依赖)
// 登录/短信/支付接口单独配置更严格阈值(如 5 次/分钟 + 图形验证码)audit 无高危;⑧ 默认账号已删除;⑨ 管理后台不暴露公网或加 IP 白名单 + MFA。08架构演进
架构不是设计出来的,是在约束下演进而来的。所有架构决策都是在回答:这个复杂度,值得吗?
8.1 从单体到微服务
| 形态 | 适用 | 优点 | 代价 |
|---|---|---|---|
| 单体 | 团队 < 5 人,业务早期 | 部署简单、事务简单、调试方便 | 代码耦合、发布互相阻塞 |
| 模块化单体(推荐) | 大多数公司的终点 | 保留单体优势,边界清晰,随时可拆 | 需要纪律维持模块边界 |
| 微服务 | 多团队并行、独立扩缩容需求 | 独立部署、技术异构、故障隔离 | 分布式事务、链路追踪、运维成本翻倍 |
只因为「代码乱」而拆,只会得到一堆分布式单体——更乱,且更难调试。
8.2 DDD 战术速览
| 概念 | 含义 | 落地 |
|---|---|---|
| 实体 Entity | 有唯一标识、有生命周期 | Order(靠 id 区分) |
| 值对象 Value Object | 无 id,靠属性值相等 | Money(100,'CNY')、Address |
| 聚合根 Aggregate Root | 一致性边界,外部只能通过它修改内部 | Order 聚合内含 OrderItem |
| 仓储 Repository | 聚合的持久化抽象 | OrderRepo.save(order) |
| 领域服务 | 跨聚合的业务逻辑 | TransferService |
| 领域事件 | 已发生的事实,触发后续动作 | OrderPaid |
| 限界上下文 | 术语与模型的边界(拆分依据) | 订单上下文 vs 物流上下文 |
modules/order/ 内自带 controller/service/repo,而不是全局 controllers/ + services/),就已经获得 80% 的收益。8.3 事件驱动与消息
// 领域事件:发布(在事务内写 outbox 表,保证不丢)
await db.$transaction(async tx=>{
const order = await tx.order.create({data});
await tx.outbox.create({ data:{
topic:'order.paid',
payload:{ orderId:order.id, amount:order.amount },
}});
});
// 独立 Relay 进程轮询 outbox → 投递 MQ → 标记已发送(至少一次)
// 消费端必须幂等(消息会重复投递)
async function onOrderPaid(evt){
const exists = await db.processedEvent.findUnique({where:{eventId:evt.id}});
if(exists) return; // 已处理过,直接返回
await db.$transaction(async tx=>{
await tx.processedEvent.create({data:{eventId:evt.id}}); // 去重表唯一索引
await tx.inventory.deduct(evt.payload);
});
}8.4 一致性方案对比
| 方案 | 一致性 | 复杂度 | 适用 |
|---|---|---|---|
| 本地事务 | 强一致 | 低 | 单库,首选 |
| Outbox + MQ | 最终一致 | 中 | 跨服务首选本地事务保证事件不丢 |
| Saga(编排/协同) | 最终一致 | 高 | 长事务,需补偿(下单→扣库存→支付→发货) |
| TCC(Try/Confirm/Cancel) | 准强一致 | 很高 | 金融级,需预留资源 |
| 2PC/XA | 强一致 | 高且性能差 | 基本不用(锁资源时间长) |
8.5 高并发三板斧 + 容量估算
缓存
减少计算与 IO:本地缓存 → 分布式缓存 → CDN → 浏览器缓存。命中率与一致性权衡。
异步
削峰填谷:请求进队列,消费者按能力处理。把「同步等待」变成「稍后通知」。
分流
水平扩展无状态服务;读写分离;分库分表;按用户/地域分片;限流降级保核心。
| 量级参考 | 数值 | 含义 |
|---|---|---|
| 单机 Nginx | ~5 万并发连接 | 静态转发几乎无瓶颈 |
| Node 服务(纯 IO) | 1~3k QPS/核 | CPU 密集任务会骤降 |
| PostgreSQL 单实例 | 简单查询 ~1万 QPS | 写受 WAL 刷盘限制,约几千 TPS |
| Redis 单实例 | ~10 万 QPS | 受网络与命令复杂度影响 |
| 估算公式 | QPS = 日活 × 人均请求数 / 86400 × 峰值系数(3~5) | 日活 10 万 × 20 次 ≈ 23 QPS 均值,峰值 ~100 QPS |
09性能清单
优化顺序永远:测量 → 定位瓶颈 → 改 → 复测。凭感觉优化是最大的时间浪费。
前端
- 首屏 JS ≤ 200KB gzip
- 图片 WebP/AVIF + 响应式尺寸
- 路由级代码分割
- 长列表虚拟化
- 静态资源长缓存 + 内容哈希
- 避免布局抖动(预留尺寸)
后端
- 消除 N+1 查询
- 批量接口代替循环单查
- 连接池合理配置
- 耗时操作异步化
- 响应开启压缩
- 加超时与熔断
数据
- 慢查询全量治理
- 索引覆盖高频查询
- 深分页改游标
- 大字段拆表
- 冷热数据分离归档
- 避免 SELECT *
压测方法
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [ {duration:'1m',target:50}, {duration:'3m',target:50}, {duration:'1m',target:0} ],
thresholds: { http_req_duration:['p95<500'], http_req_failed:['rate<0.01'] },
};
export default function(){
const r = http.get('https://api.example.com/users',{headers:{Authorization:`Bearer ${TOKEN}`}});
check(r, { 'status 200': (x)=>x.status===200 });
sleep(1);
}
// k6 run --vus 50 --duration 5m script.js
// ★ 压测要渐增,观察拐点(QPS 不再涨而延迟飙升处即容量上限)10Python 全栈
Python 的定位:胶水层与数据/AI 侧的首选。用它的强项(生态、表达力、数据处理),避开它的弱项(CPU 密集、全局解释器锁)。
10.1 选型与工程结构
| 框架 | 适用 | 要点 |
|---|---|---|
| FastAPI(推荐) | API 服务、微服务、AI 服务 | 原生 async、Pydantic 校验、自动生成 OpenAPI,性能接近 Node |
| Django | 内容/后台/中台系统 | 自带 ORM、Admin、权限、迁移,"开箱即用全家桶",适合快速交付 |
| Flask | 小工具、内部服务 | 极简,但大型项目需自己组装,新项目一般直接上 FastAPI |
| 数据/AI | Polars / Pandas / NumPy | 报表、ETL、模型服务 |
# 安装 uv(替代 pip/venv/pipx/poetry 的一体化工具,快 10~100 倍)
curl -LsSf https://astral.sh/uv/install.sh | sh
uv python install 3.12 # 管理解释器版本
uv venv && source .venv/bin/activate
uv add fastapi uvicorn[standard] sqlalchemy asyncpg pydantic-settings
uv add --dev pytest ruff mypy httpx
uv lock && uv sync --frozen # 锁文件 + CI 复现安装
uv run pytest # 自动在虚拟环境中执行
# 传统方式(pyproject.toml 同样适用)
python -m venv .venv && pip install -r requirements.txt
pip freeze > requirements.txt # 建议改用 pip-compile / uv lockapp/
main.py # 应用入口、装配路由与中间件
core/
config.py # pydantic-settings 读取环境变量(带校验)
security.py # 密码哈希、JWT
deps.py # 依赖注入:get_db / get_current_user
api/v1/
routers/user.py # 路由(薄,只做参数与响应)
domain/ # 业务规则(不依赖框架)
models/ # SQLAlchemy ORM 模型
schemas/ # Pydantic DTO(入参/出参)
services/ # 业务编排
repositories/ # 数据访问
tests/
alembic/ # 数据库迁移
pyproject.toml10.2 类型与代码风格
from dataclasses import dataclass
from typing import Annotated, Literal, TypeAlias, Self
from pydantic import BaseModel, Field, EmailStr, field_validator
# 1) 现代类型写法(3.10+):X | None 取代 Optional[X]
def find(uid: int) -> User | None: ...
# 2) 类型别名与字面量约束
Role: TypeAlias = Literal["admin", "user", "guest"]
# 3) Pydantic v2:运行时校验 + 自动文档(唯一可信的边界校验)
class CreateUserIn(BaseModel):
model_config = {"extra": "forbid"} # 拒绝未知字段,防字段误传
email: EmailStr
password: str = Field(min_length=8, max_length=72) # bcrypt 上限 72 字节
role: Role = "user"
@field_validator("password")
@classmethod
def strong(cls, v: str) -> str:
if v.isalpha(): raise ValueError("密码需包含数字")
return v
class UserOut(BaseModel):
model_config = {"from_attributes": True} # 允许从 ORM 对象构造
id: int
email: EmailStr
# ★ 出参必须单独定义 schema,绝不直接返回 ORM 对象(会泄漏 password_hash)
# 4) 工具链配置(pyproject.toml 片段)
# [tool.ruff] line-length = 100 ; select = ["E","F","I","UP","B","SIM"]
# [tool.mypy] strict = true
# ruff check . && ruff format . (替代 flake8+black+isort,快 100 倍)10.3 FastAPI 实战
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException, status, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db() # 启动时初始化连接池
yield
await engine.dispose() # 关闭时优雅释放
app = FastAPI(lifespan=lifespan, title="API", version="1.0.0")
# —— 依赖注入:数据库连接按请求作用域自动开关 ——
async def get_db() -> AsyncIterator[AsyncSession]:
async with SessionLocal() as s:
try:
yield s
await s.commit()
except Exception:
await s.rollback(); raise
DbDep = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)] # 鉴权依赖可复用
@app.post("/users", response_model=UserOut, status_code=status.HTTP_201_CREATED)
async def create_user(payload: CreateUserIn, db: DbDep):
if await user_repo.exists_by_email(db, payload.email):
raise HTTPException(409, "邮箱已注册")
return await user_service.register(db, payload)
@app.get("/users", response_model=list[UserOut])
async def list_users(
page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), db: DbDep = None
): ...
# —— 全局异常处理:统一错误体 + traceId ——
@app.exception_handler(Exception)
async def unhandled(request: Request, exc: Exception):
trace_id = request.headers.get("x-trace-id", uuid4().hex)
logger.exception("unhandled", extra={"trace_id": trace_id})
return JSONResponse(status_code=500,
content={"code": 500, "message": "服务器内部错误", "traceId": trace_id})
# ★ 关键性能规则:
# - def(同步)路由会自动跑线程池,async def 里绝不能调用阻塞 IO
# - async def 中调用 requests/time.sleep/同步 ORM = 阻塞整个事件循环,全站卡顿
# - 阻塞库要么改成 def 路由,要么用 run_in_executor / 换 httpx、asyncpg10.4 SQLAlchemy 2.0 与迁移
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, selectinload
from sqlalchemy import select, func
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(unique=True, index=True)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
# 2.0 风格查询(不再是 Query API)
stmt = select(User).where(User.email == email).limit(1)
user = (await db.execute(stmt)).scalar_one_or_none()
# ★ N+1 治理:用 selectinload / joinedload 预加载
stmt = select(Post).options(selectinload(Post.author)).limit(20) # ✅ 2 条 SQL
# for p in posts: p.author ❌ 循环里触发 20 条查询
# 分页(游标优于 offset)
stmt = select(Post).where(Post.id < cursor).order_by(Post.id.desc()).limit(20)
# 事务:session 默认以 commit 结束;嵌套用 savepoint
async with db.begin():
...
# Alembic 迁移
# uv add alembic && alembic init alembic
# alembic revision --autogenerate -m "add users" (必须人工审阅生成的脚本!)
# alembic upgrade head / downgrade -1NullPool 之外要配 pool_size(默认 5)与 max_overflow;pool_pre_ping=True 防止数据库重启后的失效连接。连接数 ≈ workers × pool_size,别超过数据库 max_connections。10.5 异步与并发
| 场景 | 方案 |
|---|---|
| IO 密集(HTTP、DB、文件) | asyncio + async 驱动(httpx / asyncpg / aiofiles),单进程可扛数千并发 |
| CPU 密集(图像处理、加密、模型推理) | multiprocessing / ProcessPoolExecutor,或把任务丢给 Celery worker 独立进程 |
| 混合 | run_in_executor 把阻塞调用丢线程池,避免卡死事件循环 |
| 后台任务 | Celery(重) / RQ、Arq(轻) / FastAPI BackgroundTasks(仅轻量、进程内、不保证不丢) |
# 并发抓取:Semaphore 限制并发,防止打爆下游
async def fetch_all(urls: list[str]):
sem = asyncio.Semaphore(10)
async with httpx.AsyncClient(timeout=3.0) as client:
async def one(u: str):
async with sem:
r = await client.get(u)
r.raise_for_status()
return r.json()
return await asyncio.gather(*map(one, urls), return_exceptions=True)
# CPU 密集:交给进程池
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(process_pool, heavy_compute, data)
# ★ GIL 真相:3.13 起可选 free-threading,但生态未就绪。
# 生产高并发仍以「多进程 + 异步 IO」为主:uvicorn --workers N10.6 测试
import pytest
from httpx import AsyncClient, ASGITransport
@pytest.fixture
async def client(db_session) -> AsyncClient:
app.dependency_overrides[get_db] = lambda: db_session # 替换依赖
async with AsyncClient(transport=ASGITransport(app), base_url="http://t") as c:
yield c
app.dependency_overrides.clear()
@pytest.mark.parametrize("email,expect", [("a@b.com",201), ("bad",422), (None,422)])
async def test_create_user(client, email, expect):
r = await client.post("/users", json={"email": email, "password": "Ab123456"})
assert r.status_code == expect
# 每个用例独立事务并回滚,保证互相隔离
@pytest.fixture
async def db_session():
async with engine.connect() as conn:
ts = await conn.begin()
yield AsyncSession(bind=conn)
await ts.rollback()
# uv run pytest -q --cov=app --cov-report=term-missing10.7 部署与典型坑
# 生产运行:gunicorn 管进程 + uvicorn worker 管协程
gunicorn app.main:app -k uvicorn.workers.UvicornWorker \
--workers 4 --worker-connections 1000 --timeout 60 --graceful-timeout 30 --bind 0.0.0.0:8000
# workers 建议 = CPU 核数(IO 密集可 2*核+1),不要盲目加大
# Dockerfile(uv 多阶段)
FROM python:3.12-slim AS build
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-editable # 依赖与源码分离,缓存友好
FROM python:3.12-slim
COPY --from=build --chown=app:app /app/.venv /app/.venv
COPY --chown=app:app ./app /app/app
USER app
ENV PATH="/app/.venv/bin:$PATH"
CMD ["gunicorn","app.main:app","-k","uvicorn.workers.UvicornWorker","--workers","4"]def f(x=[])(共享同一列表,用 None 哨兵);② 循环内定义 lambda/闭包捕获同一变量;③ except: 裸捕获吞掉 KeyboardInterrupt,务必指定异常类型;④ 循环 import(把公共依赖下沉到独立模块);⑤ 用 == 比较浮点数;⑥ datetime.now() 无时区(统一用 datetime.now(timezone.utc));⑦ 在 async 函数中调用阻塞 IO;⑧ 全局可变状态(多 worker 下不共享);⑨ 忘记 if __name__ == "__main__" 导致多进程启动异常;⑩ 依赖未锁版本,某天上游 breaking change 直接线上爆炸。11Java 全栈
Java 的主场:大型企业级系统、复杂业务、强事务与高并发后端。它的价值在于成熟的生态、强类型与可预测的性能表现。
11.1 技术栈与工程结构
| 层 | 推荐 | 说明 |
|---|---|---|
| JDK | JDK 17(LTS)或 21(LTS,含虚拟线程) | 新项目直接上 21;老系统 8/11 逐步迁移 |
| 框架 | Spring Boot 3.x | Jakarta EE 命名空间(jakarta.* 而非 javax.*) |
| 构建 | Maven(稳) / Gradle(快、灵活) | 单体多模块用 Maven,构建耗时敏感用 Gradle |
| 数据 | MyBatis-Plus(SQL 可控)或 JPA(快速 CRUD) | 见 11.4 对比 |
| 工具 | Lombok(谨慎)、MapStruct(DTO 映射)、Flyway(迁移) | MapStruct 编译期生成,性能远优于反射 BeanUtils |
com.example.app
├ common/ # 统一响应 R<T>、异常、工具、常量
│ ├ Result.java GlobalExceptionHandler.java
├ config/ # 配置类、拦截器、WebMvcConfigurer
├ security/ # 认证、鉴权、JWT 过滤器
└ modules/
└ order/ # ★ 按域聚合,而不是全局 controller/service/mapper 平铺
├ OrderController.java
├ OrderService.java / OrderServiceImpl.java
├ OrderMapper.java
├ entity/Order.java # 与表对应(PO)
├ dto/OrderCreateReq.java OrderVO.java
└ convert/OrderConvert.java # MapStruct 接口11.2 语言要点(JDK 17+)
// 1) record:不可变 DTO,一行替代 getter/equals/hashCode/toString
public record UserVO(Long id, String email, String roleName) {}
// 2) 密封接口:限定实现范围,配合 switch 模式匹配可穷尽分支
public sealed interface PayResult permits Success, Failed, Pending {}
// 3) 模式匹配 + switch 表达式
String desc = switch (result) {
case Success s -> "成功 " + s.txnId();
case Failed f -> "失败 " + f.reason();
case Pending p -> "处理中";
}; // 密封接口无需 default,编译期保证穷尽
// 4) Optional 的正确用法:只用于「返回值」,不要做参数/字段
public Optional<User> findByEmail(String email) { ... }
findByEmail(e).map(User::getName).orElse("匿名"); // ✅
// Optional.ofNullable(x).get() ❌ 不如直接判空
// 5) 文本块(SQL/JSON 可读性救星)
String sql = """
SELECT id, email FROM users
WHERE created_at >= ? AND status = ?
""";
// 6) 异常:受检异常用于「调用方必须处理」,业务异常用自定义 RuntimeException
public class BizException extends RuntimeException {
private final int code;
public BizException(int code, String msg){ super(msg); this.code = code; }
}11.3 Spring Boot 实战
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor // 构造器注入(优于 @Autowired 字段注入,便于测试)
public class OrderController {
private final OrderService orderService;
@PostMapping
public Result<OrderVO> create(@Valid @RequestBody OrderCreateReq req) {
return Result.ok(orderService.create(req));
}
@GetMapping
public Result<PageResult<OrderVO>> page(
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(defaultValue = "20") @Max(100) int size) {
return Result.ok(orderService.page(page, size));
}
}
// 入参校验
public record OrderCreateReq(
@NotNull Long skuId,
@NotNull @Min(1) @Max(9999) Integer quantity,
@NotBlank @Size(max = 200) String remark) {}
// 统一响应 + 全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> onValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ":" + f.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.fail(400, msg);
}
@ExceptionHandler(BizException.class)
public Result<Void> onBiz(BizException e) { return Result.fail(e.getCode(), e.getMessage()); }
@ExceptionHandler(Exception.class)
public Result<Void> onErr(Exception e, HttpServletRequest req) {
String traceId = MDC.get("traceId");
log.error("unhandled uri={} traceId={}", req.getRequestURI(), traceId, e);
return Result.fail(500, "服务器内部错误"); // 不暴露堆栈
}
}private/final(无法被代理);② 同类内部方法自调用(未经过代理,最隐蔽——注入自己或用 AopContext);③ 异常被 try-catch 吞掉未抛出;④ 抛出的异常类型默认不回滚(只有 RuntimeException/Error 回滚,需 rollbackFor = Exception.class);⑤ 数据库引擎非 InnoDB;⑥ 事务方法内有耗时远程调用(设计问题锁持有时间过长,务必移出事务)。@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper mapper;
private final InventoryClient inventory;
@Transactional(rollbackFor = Exception.class, timeout = 5) // 明确回滚范围 + 超时
public OrderVO create(OrderCreateReq req) {
if (mapper.existsByBizNo(req.bizNo())) return mapper.getByBizNo(req.bizNo()); // 幂等
mapper.insert(Order.from(req));
// ❌ 禁止:远程调用放事务内(网络慢 → 行锁长期不释放 → 雪崩)
// inventory.deduct(req.skuId(), req.quantity());
return OrderConvert.INSTANCE.toVO(po);
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 独立事务:日志必须落库
public void audit(Long orderId, String action) { ... }
}11.4 数据层选型
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| MyBatis / MyBatis-Plus | SQL 完全可控、易优化、DBA 友好 | 需写 XML、改字段要同步 | 复杂查询、报表、国内主流 |
| JPA / Hibernate | 开发快、领域模型友好、自动 DDL | 复杂 SQL 难优化、易踩懒加载坑 | CRUD 为主的管理系统 |
| jOOQ | 类型安全的 SQL、功能最强 | 商业许可(部分 DB)、学习成本 | SQL 密集型系统 |
| JdbcTemplate | 极简、透明 | 样板代码多 | 轻量服务 |
// JPA 的 N+1:默认懒加载,循环访问关联对象触发 N 条 SQL
// ✅ 方案1:@EntityGraph 指定抓取
@EntityGraph(attributePaths = {"items"})
List<Order> findByUserId(Long userId);
// ✅ 方案2:JPQL fetch join
@Query("select distinct o from Order o left join fetch o.items where o.userId = :uid")
List<Order> findWithItems(@Param("uid") Long uid);
// ✅ 方案3:批量查询后内存组装(大数据量最稳)
// 先查 orders,再用 where order_id in (...) 一次性查 items,Map 分组回填
// MyBatis 防注入:#{} 预编译,${} 是字符串拼接(仅限动态表名/排序且需白名单)
// ✅ WHERE email = #{email} ❌ WHERE email = '${email}'
// 连接池 HikariCP(Spring Boot 默认)
spring.datasource.hikari.maximum-pool-size=20 # ≈ CPU*2+磁盘,非越大越好
spring.datasource.hikari.connection-timeout=3000 # 拿不到连接快速失败,别让线程堆积
spring.datasource.hikari.leak-detection-threshold=60000 # 检测连接泄漏
// 迁移 Flyway:V1__init.sql / V2__add_index.sql,只在 CI 中顺向执行11.5 并发与 JVM
// ❌ 禁止 Executors.newFixedThreadPool:队列无界 → OOM
// ✅ 手动创建 ThreadPoolExecutor,明确队列与拒绝策略
@Bean
public ThreadPoolExecutor bizExecutor() {
return new ThreadPoolExecutor(
8, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500), // 有界队列
new ThreadFactoryBuilder().setNameFormat("biz-%d").build(), // 命名便于排查
new ThreadPoolExecutor.CallerRunsPolicy()); // 反压:让调用方自己执行
}
// 并行编排
CompletableFuture<User> u = supplyAsync(() -> userSvc.get(id), pool);
CompletableFuture<List<Order>> o = supplyAsync(() -> orderSvc.list(id), pool);
return u.thenCombine(o, UserDetail::of).orTimeout(2, TimeUnit.SECONDS).join();
// JDK 21 虚拟线程:IO 密集场景可显著提升吞吐(无需池化,一个请求一个线程)
@Bean TomcatProtocolHandlerCustomizer<?> vt() {
return h -> h.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
// ★ 虚拟线程不解决 CPU 密集问题;synchronized 会 pin 住载体线程(改用 ReentrantLock)-Xms2g -Xmx2g # 堆大小固定,避免动态扩缩抖动(必须等值)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 # 大堆低延迟首选 G1;JDK17+ 可试 ZGC
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp # OOM 自动留现场
-Xlog:gc*:file=/logs/gc.log:time,uptime:filecount=5,filesize=50m
-Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8 # 时区与编码,必配
-XX:NativeMemoryTracking=summary # 排查堆外内存泄漏
# 线上排查三件套
jps -l # 找 pid
jstack <pid> > stack.txt # 线程栈(查死锁、线程池打满、CPU 飙高)
jmap -histo <pid> | head -30 # 对象直方图(查内存泄漏)
jstat -gcutil <pid> 1000 # 每秒 GC 情况
arthas # 阿里开源,在线诊断神器(watch/trace/dashboard)11.6 测试与可观测
@SpringBootTest
@Testcontainers // 起真实数据库/Redis,比 H2 更贴近生产
class OrderServiceTest {
@Container static PostgreSQLContainer<?> pg =
new PostgreSQLContainer<>("postgres:16-alpine");
@MockBean InventoryClient inventory; // 隔离外部依赖
@Autowired OrderService orderService;
@Test void 缺货时下单失败() {
when(inventory.deduct(anyLong(), anyInt())).thenReturn(false);
assertThrows(BizException.class, () -> orderService.create(req));
verify(orderMapper, never()).insert(any()); // 断言副作用未发生
}
}
// 可观测:Actuator + Micrometer + OpenTelemetry
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.endpoint.health.probes.enabled=true # K8s 就绪/存活探针
// 自定义业务指标(下单成功率、支付耗时)比 JVM 指标更有价值FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -B dependency:go-offline # 依赖层单独缓存
COPY src ./src
RUN mvn -B -DskipTests package
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# ★ 用 jar 分层(spring-boot 已支持 layertools),改代码只传应用层
COPY --from=build /app/target/app.jar app.jar
RUN adduser -S app && chown app app.jar
USER app
ENTRYPOINT ["java","-XX:+UseContainerSupport","-XX:MaxRAMPercentage=75",\
"-XX:+ExitOnOutOfMemoryError","-jar","/app.jar"]
# ★ 必加 UseContainerSupport:让 JVM 识别容器内存限制,否则按宿主机内存分配堆equals/hashCode 未同时重写 → HashMap/Set 行为异常;② BigDecimal 必须用字符串构造(new BigDecimal("0.1"),用 double 构造会有精度问题);③ SimpleDateFormat 非线程安全(用 DateTimeFormatter);④ ArrayList 在 foreach 中删除元素抛并发修改异常;⑤ 实体类直接作为出参 → 序列化循环引用 + 敏感字段泄漏(必须转 VO);⑥ 日志用 log.error("x"+e) 而非 log.error("x", e),丢失堆栈;⑦ Stream 中做副作用修改外部状态;⑧ @Async 与 @Transactional 自调用失效同源(代理问题)。12速查表 Cheat Sheet
Git
git switch -c feat/x # 新建并切换分支
git restore --staged file # 取消暂存
git restore file # 丢弃工作区修改
git commit --amend --no-edit # 补进上一次提交
git rebase -i HEAD~3 # 交互式整理提交
git log --oneline --graph -20 # 图形化看历史
git diff main...HEAD # 看分支相对 main 的全部改动
git bisect start/bad/good # 二分定位引入 bug 的提交
git worktree add ../hotfix main # 多工作区并行开发Docker
docker compose up -d --build # 构建并后台启动
docker compose logs -f app --tail 100 # 跟踪日志
docker compose exec db psql -U postgres
docker system df / docker system prune -a # 查看/清理磁盘占用
docker stats # 实时资源占用
docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t app .
docker image history app:latest # 分析镜像层体积
COPY --chown=app:app ... # 避免 root 权限问题Linux / 排查
# 端口与连接
lsof -i :3000 # 谁占用了端口
ss -lntp # 监听端口
ss -s # 连接总数统计(查 TIME_WAIT 堆积)
# 资源
top / htop # CPU 内存
iostat -x 1 # 磁盘 IO(%util 接近 100 说明磁盘瓶颈)
vmstat 1 # 上下文切换、swap
df -h / du -sh *|sort -h # 磁盘空间
# 日志与进程
journalctl -u app -f --since "10 min ago"
tail -f app.log | grep --color ERROR
strace -p <pid> # 跟踪系统调用(慎用,影响性能)
kill -TERM <pid> # 优雅退出;-9 是最后手段SQL
-- 窗口函数:分组内排名 / 去重取最新一条
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn
FROM orders
) t WHERE rn = 1;
-- CTE:可读性 + 递归查询(组织树、菜单树)
WITH RECURSIVE tree AS (
SELECT id,name,parent_id FROM dept WHERE parent_id IS NULL
UNION ALL
SELECT d.id,d.name,d.parent_id FROM dept d JOIN tree t ON d.parent_id=t.id
) SELECT * FROM tree;
-- 分组统计 + 条件聚合
SELECT date_trunc('day',created_at) d,
count(*) total,
count(*) FILTER (WHERE status=1) paid -- Pg 的条件聚合
FROM orders GROUP BY 1 ORDER BY 1 DESC;
-- 批量 upsert / 去重插入
INSERT INTO tag(name) VALUES ('a'),('b') ON CONFLICT (name) DO NOTHING;Python
uv venv && source .venv/bin/activate # 虚拟环境(uv 比 pip 快 10~100 倍)
uv add fastapi / uv add --dev pytest ruff mypy
uv run python -m app.main # 免激活执行
pip install -e . / pip-compile pyproject.toml
uv run pytest -q --cov=app --cov-report=term-missing # 测试 + 覆盖率
uv run pytest -k "user and not slow" -x -vv # 过滤 + 首次失败即停
ruff check . --fix && ruff format . # lint + 格式化
mypy app # 静态类型检查
python -m cProfile -s cumulate app.py # 性能剖析
python -m py_compile / python -X importtime -c "import app" # 查慢导入
pipdeptree / uv tree # 依赖树,查冲突来源Java / JVM
# Maven
mvn -B clean package -DskipTests # 打包(-B 非交互,CI 必加)
mvn -B dependency:tree # 依赖树,查冲突
mvn -B dependency:go-offline # 预下载依赖(Docker 分层缓存用)
mvn -B versions:display-dependency-updates # 检查可升级依赖
mvn -T 1C clean install # 并行构建多模块
# Gradle
./gradlew bootJar ./gradlew test --tests '*OrderServiceTest'
./gradlew dependencies --configuration runtimeClasspath
# 运行时排查
jps -l # 列出 Java 进程
jstack <pid> | grep -A20 "BLOCKED" # 查死锁 / 线程阻塞
jmap -histo <pid> | head -30 # 对象直方图,定位内存泄漏
jstat -gcutil <pid> 1000 # 每秒 GC 概况
jcmd <pid> VM.native_memory summary # 堆外内存
java -XX:+PrintFlagsFinal -version | grep HeapSize # 查看默认堆大小Nginx 关键配置
# 反向代理(必须带这些头,否则后端拿不到真实信息)
location /api/ {
proxy_pass http://app:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s; proxy_read_timeout 60s;
}
# SPA history 路由
location / { try_files $uri $uri/ /index.html; }
# 静态资源长缓存
location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; }
# 限流(需先在 http 块定义 limit_req_zone)
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ { limit_req zone=api burst=20 nodelay; }
# SSE 必须关缓冲
proxy_buffering off; proxy_cache off; chunked_transfer_encoding on;TypeScript / Node 一行流
# 项目体检
npx depcheck # 找未使用依赖
npx madge --circular src # 找循环依赖
npx knip # 找未使用文件/导出/类型
pnpm audit --audit-level=high # 安全漏洞
pnpm why <pkg> # 谁依赖了它
pnpm dlx npm-check-updates -i # 交互式升级依赖
# 排查
node --inspect-brk dist/main.js # 断点调试
node --cpu-prof --heap-prof app.js # 生成性能剖析文件
npx clinic doctor -- node app.js # 自动诊断性能问题13成长路线与练手项目
能力阶梯
| 阶段 | 标志 | 核心任务 |
|---|---|---|
| 初级(0~1 年) | 能在指导下完成明确需求 | 写出可读代码、熟练 Git/Debug、理解所用框架的约定 |
| 中级(1~3 年) | 独立负责一个模块端到端交付 | 数据库建模、API 设计、写测试、定位线上问题 |
| 高级(3~5 年) | 负责一个子系统,主导技术方案 | 性能调优、架构权衡、带人、把不确定性变成计划 |
| 架构师(5 年+) | 为整个系统的演进负责 | 业务抽象、技术选型、治理复杂度、控制成本与风险 |
六个练手项目(按难度递增,每个都能覆盖全栈链路)
① 个人博客
SSR 渲染 + Markdown + 标签/搜索 + 评论。练:路由、SEO、CRUD、部署。
② 待办/看板协作
多用户 + 拖拽排序 + 实时同步(SSE/WebSocket)。练:鉴权、权限、实时、乐观更新。
③ 电商下单
商品/购物车/库存/订单/支付回调。练:事务、锁、幂等、状态机、超时关单。
④ 短链 + 统计
短码生成 + 高并发跳转 + UV/PV 统计。练:缓存、布隆过滤器、Bitmap、限流。
⑤ 多租户 SaaS
租户隔离 + 订阅计费 + RBAC + 审计日志。练:数据隔离方案、权限模型、计费状态机。
⑥ IM / 协同编辑
消息可靠投递 + 已读回执 + CRDT 协同。练:长连接、消息序号、冲突解决。
高效学习习惯
- 带着问题学:先写代码遇到瓶颈,再去看原理,记忆最牢
- 读源码 > 读博客:从 500 行的小库开始(如 clsx、mitt、p-limit)
- 输出倒逼输入:写技术方案、做内部分享、写复盘文档
- 建立自己的代码片段库:把重复三次的代码沉淀为模板/脚手架
- 复盘线上事故:每一个 bug 都问「为什么没在更早的阶段被发现」
14面试高频考点
浏览器 / 网络
输入 URL 到页面展示发生了什么?· 三次握手与四次挥手 · HTTPS/TLS 握手过程 · 跨域与 CORS 预检 · 强缓存与协商缓存 · 事件循环与宏/微任务 · 浏览器渲染流水线(回流/重绘)
JavaScript / TS
闭包与作用域链 · 原型链 · Promise 与 async/await 执行顺序 · 深浅拷贝 · 防抖节流 · 事件委托 · this 绑定 · 泛型与类型收窄
React
虚拟 DOM 与 Diff · Fiber 架构 · Hooks 原理与闭包陷阱 · 批处理与并发特性 · 受控/非受控组件 · 合成事件 · key 的作用 · 状态管理选型
Node / 后端
事件循环六阶段 · 单线程如何应对并发 · 流与背压 · 中间件机制 · 进程守护与集群 · JWT vs Session · REST 幂等 · 限流算法(固定/滑动窗口、令牌桶、漏桶)
数据库
索引数据结构为何是 B+ 树 · 最左前缀 · 聚簇 vs 非聚簇索引 · MVCC 实现 · 隔离级别与幻读 · 慢 SQL 优化思路 · 分库分表与全局 ID(雪花算法)
系统设计
如何设计一个短链/秒杀/IM/抢红包/限流系统 · 一致性哈希 · 缓存三大问题 · 分布式锁 · 分布式 ID · CAP 与 BASE · 消息队列如何保证不丢不重
Python
GIL 与多线程 · 生成器与迭代器 · 装饰器/上下文管理器 · 深浅拷贝 · 可变默认参数 · asyncio 原理与事件循环 · 元类与描述符 · 内存管理与垃圾回收(引用计数+分代)
JVM / Java
类加载与双亲委派 · 内存区域与 OOM 场景 · GC 算法与 G1/ZGC · 线程池参数与拒绝策略 · synchronized vs ReentrantLock(AQS)· volatile 与 happens-before · ThreadLocal 内存泄漏 · Spring 循环依赖与事务失效
写在最后
全栈的终点不是「什么都会写」,而是能在不确定性中做出合理取舍,并对结果负责。技术会过时,但以下能力不会:拆解复杂问题的能力、快速定位根因的能力、把模糊需求变成可交付方案的能力、以及与人和协作的能力。
保持好奇,保持怀疑,把每一次线上事故都变成下一次的底气。