事务与并发控制
问题
什么是数据库事务?四种隔离级别分别解决什么问题?MVCC 是什么?
答案
ACID 特性
| 特性 | 说明 | 实现 |
|---|---|---|
| Atomicity 原子性 | 事务中操作全部成功或全部回滚 | undo log |
| Consistency 一致性 | 事务前后数据状态一致 | 其他三者保证 |
| Isolation 隔离性 | 并发事务互不干扰 | 锁 + MVCC |
| Durability 持久性 | 事务提交后数据永久保存 | redo log |
并发问题
| 问题 | 说明 | 示例 |
|---|---|---|
| 脏读 | 读到未提交的数据 | A 未提交的修改被 B 读到 |
| 不可重复读 | 同事务两次读不一致 | A 两次读之间 B 修改了数据 |
| 幻读 | 同条件两次查结果行数不同 | A 两次查之间 B 插入了新行 |
四种隔离级别
| 级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| Read Uncommitted | ❌ | ❌ | ❌ | 最高 |
| Read Committed(RC) | ✅ | ❌ | ❌ | 高 |
| Repeatable Read(RR) | ✅ | ✅ | ❌* | 中 |
| Serializable | ✅ | ✅ | ✅ | 最低 |
MySQL 默认
MySQL InnoDB 默认使用 Repeatable Read,但通过 MVCC + 间隙锁解决了大部分幻读问题。PostgreSQL 默认使用 Read Committed。
MVCC 多版本并发控制
MVCC 核心:每行数据保存多个版本,通过 Read View(快照)决定事务能看到哪个版本,实现读不加锁,读写不冲突。
Node.js 中使用事务
transaction.ts
// Prisma 事务
async function transfer(fromId: string, toId: string, amount: number) {
await prisma.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
});
if (from.balance < 0) {
throw new Error('余额不足'); // 自动回滚
}
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
});
});
}
// TypeORM 事务
await dataSource.transaction(async (manager) => {
await manager.save(order);
await manager.save(orderItems);
await manager.update(Product, productId, { stock: () => 'stock - 1' });
});
常见面试问题
Q1: 乐观锁和悲观锁的区别?
答案:
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思路 | 先加锁再操作 | 先操作再检查冲突 |
| 实现 | SELECT ... FOR UPDATE | 版本号 / CAS |
| 适用 | 写多读少、冲突频繁 | 读多写少、冲突较少 |
-- 悲观锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 乐观锁(版本号)
UPDATE accounts SET balance = 200, version = version + 1
WHERE id = 1 AND version = 5;
Q2: 数据库死锁怎么解决?
答案:
- 多个事务按相同顺序访问资源
- 设置锁等待超时
- InnoDB 会自动检测并回滚一个事务
- 减小事务范围,降低锁冲突
Q3: RR 级别下如何解决幻读?
答案:
InnoDB 用间隙锁(Gap Lock) 锁住索引之间的间隙,阻止其他事务在间隙中插入新行。