全栈项目需要理解数据建模、索引、缓存和 ORM。本文整理关系型数据库、NoSQL、ORM 与 Prisma 实践。

关系型数据库

MySQL 与 PostgreSQL

关系型数据库用表、行、列和约束组织数据,适合结构清晰、事务要求高的业务。MySQL 使用广泛,生态成熟;PostgreSQL 类型系统、扩展能力和复杂查询能力强。

关系型数据库适合订单、用户、支付、权限等需要强一致性、事务、复杂查询和结构化约束的场景。

事务与 ACID

事务保证一组操作要么全部成功,要么全部失败。ACID 分别是原子性、一致性、隔离性、持久性。隔离级别越高,并发问题越少,但性能成本越高。

事务能保证关键业务状态一致,比如下单时扣库存、生成订单、写流水必须作为一个整体处理,不能只成功一半。

索引

索引本质是用额外数据结构加速查询。MySQL InnoDB 常用 B+Tree 索引。索引不是越多越好,写入和更新也要维护索引。

索引失效常见原因:对索引列做函数计算、隐式类型转换、前置模糊匹配、联合索引不满足最左前缀、选择性太差。

缓存与 NoSQL

Redis

Redis 是内存数据存储,常用于缓存、分布式锁、计数器、排行榜、会话和消息队列。常见数据结构有 String、Hash、List、Set、ZSet。

缓存穿透、击穿、雪崩的区别:

  • 穿透:查询不存在数据打到数据库,可用空值缓存和布隆过滤器。
  • 击穿:热点 key 失效瞬间大量请求打到数据库,可用互斥锁和逻辑过期。
  • 雪崩:大量 key 同时失效,可用随机过期时间和限流降级。

MongoDB

MongoDB 是文档数据库,数据以 BSON 文档存储,适合字段变化频繁、文档聚合读取明显的场景。但如果业务强事务、复杂 join 很多,关系型数据库更合适。

MongoDB 不是简单替代 MySQL,要看数据模型和查询方式。文档型、结构灵活、读写文档整体的场景适合 MongoDB;强关系和事务场景更适合关系型数据库。

ORM 与 Prisma

ORM 的价值

ORM 把数据库表映射成语言里的模型,减少手写 SQL,提升类型提示和迁移管理体验。但复杂查询、性能优化和事务边界仍需要理解 SQL。

ORM 的缺点:可能生成低效 SQL,隐藏查询成本,复杂查询表达不直观。工程里要能查看 SQL、分析慢查询,而不是完全依赖 ORM。

Prisma

Prisma 通过 schema 定义模型,生成类型安全的 client,并提供 migration 管理数据库结构。它适合 TypeScript 全栈项目。

Prisma 的类型安全体现在:模型字段、查询参数和返回值都能由 schema 推导,字段名写错或类型不匹配会在编译阶段暴露。

数据建模

表设计

表设计要关注主键、外键、唯一约束、字段类型、索引、冗余和扩展性。不要为了省事把所有数据塞进 JSON,也不要过度范式化导致查询复杂。

用户和角色权限通常拆用户表、角色表、权限表,以及用户角色关联表、角色权限关联表。这样支持多角色、多权限和后续扩展。

代码示例

B+Tree 索引为什么适合数据库

B+Tree 的非叶子节点只存 key 和指针,叶子节点存数据或主键,并且叶子节点有链表连接。这样树高更低,范围查询更友好,磁盘 I/O 更少。

1
2
3
4
5
6
7
8
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email VARCHAR(128) NOT NULL,
name VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uniq_email (email),
KEY idx_created_at (created_at)
);

InnoDB 的主键索引是聚簇索引,叶子节点存整行数据;普通二级索引叶子节点存主键值。通过二级索引查到主键后,再回到主键索引查整行,这叫回表。

联合索引与最左前缀

联合索引 (a, b, c) 可以支持按最左前缀的查询,比如 a、a,b、a,b,c。如果跳过 a 只查 b,通常无法充分利用该联合索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE INDEX idx_user_status_time
ON orders (user_id, status, created_at);

-- 能利用 user_id、status,并可用于 created_at 范围
SELECT *
FROM orders
WHERE user_id = 10
AND status = 'paid'
AND created_at >= '2026-01-01';

-- 难以充分利用该索引
SELECT *
FROM orders
WHERE status = 'paid';

联合索引字段顺序设计:优先考虑查询模式。常见做法是等值条件放前面,范围条件放后面,同时兼顾字段区分度、排序和覆盖索引。

Redis 分布式锁

Redis 锁必须保证加锁原子性、设置过期时间、释放时校验 owner,不能直接 del key。

1
2
3
4
5
// 加锁:SET lockKey requestId NX PX 30000
await redis.set(lockKey, requestId, {
NX: true,
PX: 30_000
})

释放锁需要 Lua 保证「判断并删除」原子:

1
2
3
4
5
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

Redis 锁过期时间要大于业务正常执行时间,并考虑续期机制。过短会导致锁提前释放,过长会影响故障恢复。高可靠场景还要评估 Redlock 或数据库事务方案。

Prisma 事务示例

下单这类操作要保证库存和订单一致,不能拆成独立请求随意执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
await prisma.$transaction(async tx => {
const product = await tx.product.findUnique({
where: { id: productId }
})

if (!product || product.stock <= 0) {
throw new Error('库存不足')
}

await tx.product.update({
where: { id: productId },
data: { stock: { decrement: 1 } }
})

await tx.order.create({
data: {
userId,
productId,
status: 'paid'
}
})
})

高并发扣库存可能需要数据库行锁、乐观锁版本号、唯一约束或队列削峰。