事务处理
要点
- 事务保证一组操作要么全部成功,要么全部回滚
- ACID 特性:原子性、一致性、隔离性、持久性
- Prisma 和 Drizzle 都支持事务,API 略有不同
- 隔离级别影响并发性能和数据一致性
- 死锁是事务的常见问题,需要预防和检测
- 分布式事务复杂,AI 项目通常避免使用
1. ACID 特性
事务的四个特性:
- 原子性(Atomicity):事务内的操作要么全部成功,要么全部回滚
- 一致性(Consistency):事务前后数据保持一致(满足约束)
- 隔离性(Isolation):并发事务互不干扰
- 持久性(Durability):事务提交后数据永久保存
2. Drizzle 事务
import { db } from './lib/db'
import { users, conversations } from './schema'
await db.transaction(async (tx) => {
// 创建用户
const [user] = await tx.insert(users).values({
email: '[email protected]',
name: 'Alice',
}).returning()
// 创建对话
await tx.insert(conversations).values({
userId: user.id,
title: 'New Conversation',
model: 'gpt-4',
})
// 如果任何一步失败,整个事务回滚
})tx 是一个事务对象,用法和 db 相同。事务内的所有操作使用同一个数据库连接。
2.1 嵌套事务
Drizzle 支持嵌套事务(PostgreSQL):
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: '[email protected]', name: 'Alice' })
await tx.transaction(async (nestedTx) => {
// 嵌套事务
await nestedTx.insert(conversations).values({
userId: 'user-1',
title: 'Nested',
model: 'gpt-4',
})
})
})2.2 事务隔离级别
await db.transaction(async (tx) => {
// ...
}, { isolationLevel: 'serializable' })可选值:
read uncommittedread committed(默认)repeatable readserializable
3. Prisma 事务
3.1 回调式事务
import { prisma } from './lib/db'
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: '[email protected]', name: 'Alice' },
})
await tx.conversation.create({
data: {
userId: user.id,
title: 'New Conversation',
model: 'gpt-4',
},
})
})3.2 批量事务
多个操作同时执行,不需要依赖关系:
await prisma.$transaction([
prisma.user.create({
data: { email: '[email protected]', name: 'Alice' },
}),
prisma.conversation.create({
data: {
userId: 'user-1',
title: 'New Conversation',
model: 'gpt-4',
},
}),
])3.3 事务隔离级别
await prisma.$transaction(async (tx) => {
// ...
}, {
isolationLevel: 'Serializable',
maxWait: 5000, // 等待获取事务的最大时间
timeout: 10000, // 事务超时
})4. 隔离级别
4.1 Read Committed(默认)
一个事务只能看到其他事务已提交的数据。大部分场景够用。
4.2 Repeatable Read
一个事务内多次读取同一数据,结果一致。防止「不可重复读」。
4.3 Serializable
最严格的隔离级别。事务像是串行执行,完全隔离。性能最差。
4.4 选择建议
- 大部分场景:Read Committed(默认)
- 需要防止不可重复读:Repeatable Read
- 金融、库存等强一致性场景:Serializable
5. 死锁
死锁是指两个或多个事务互相等待对方释放锁,导致都无法继续。
5.1 示例
事务 A: UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;
事务 B: UPDATE users SET balance = balance - 50 WHERE id = 2;
UPDATE users SET balance = balance + 50 WHERE id = 1;
如果事务 A 和 B 同时执行,A 锁定了 id=1,B 锁定了 id=2。然后 A 等待 id=2 的锁,B 等待 id=1 的锁,形成死锁。
5.2 预防
- 固定顺序:所有事务按相同顺序访问资源
- 缩短事务:减少事务持有锁的时间
- 超时检测:数据库会检测死锁并回滚其中一个事务
5.3 处理
try {
await db.transaction(async (tx) => {
// ...
})
} catch (err) {
if (err.message.includes('deadlock')) {
// 重试或返回错误
}
}6. 乐观锁
乐观锁不实际加锁,而是通过版本号或时间戳检测冲突:
model User {
id String @id
name String
version Int @default(1)
}// 读取
const user = await prisma.user.findUnique({ where: { id: 'user-1' } })
// 更新时检查版本
await prisma.user.update({
where: { id: 'user-1', version: user.version },
data: {
name: 'Alice Smith',
version: { increment: 1 },
},
})如果版本不匹配(说明有其他事务已经修改),更新会失败。
7. 分布式事务
分布式事务涉及多个数据库或服务,实现复杂。AI 项目通常避免使用,而是采用最终一致性方案:
- Saga 模式:把长事务拆成多个本地事务,每个事务有对应的回滚操作
- 事件驱动:通过消息队列异步处理,保证最终一致性
- 补偿机制:失败时执行补偿操作,恢复数据
8. 事务的最佳实践
- 尽量短:减少事务持有锁的时间
- 避免用户交互:不要在事务里等待用户输入或外部 API 调用
- 捕获异常:事务失败时妥善处理
- 重试逻辑:死锁或超时可以重试
- 监控:记录事务执行时间、失败率
总结
事务保证一组操作的原子性和一致性。Prisma 和 Drizzle 都提供了事务 API,支持不同的隔离级别。
这一节涉及到的几个实践:
- ACID 特性:原子性、一致性、隔离性、持久性
- Drizzle 事务:回调式,支持嵌套
- Prisma 事务:回调式和批量式
- 隔离级别:Read Committed、Repeatable Read、Serializable
- 死锁:固定顺序、缩短事务、超时检测
- 乐观锁:版本号或时间戳检测冲突
- 分布式事务:避免使用,采用 Saga 或事件驱动
事务是保证数据一致性的关键机制,但也会带来性能开销。合理使用事务,避免过度使用。
下一篇看分页查询——cursor 分页、offset 分页、无限滚动。