事务处理

要点

  • 事务保证一组操作要么全部成功,要么全部回滚
  • ACID 特性:原子性、一致性、隔离性、持久性
  • Prisma 和 Drizzle 都支持事务,API 略有不同
  • 隔离级别影响并发性能和数据一致性
  • 死锁是事务的常见问题,需要预防和检测
  • 分布式事务复杂,AI 项目通常避免使用

1. ACID 特性

事务的四个特性:

  1. 原子性(Atomicity):事务内的操作要么全部成功,要么全部回滚
  2. 一致性(Consistency):事务前后数据保持一致(满足约束)
  3. 隔离性(Isolation):并发事务互不干扰
  4. 持久性(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 uncommitted
  • read committed(默认)
  • repeatable read
  • serializable

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 预防

  1. 固定顺序:所有事务按相同顺序访问资源
  2. 缩短事务:减少事务持有锁的时间
  3. 超时检测:数据库会检测死锁并回滚其中一个事务

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 项目通常避免使用,而是采用最终一致性方案:

  1. Saga 模式:把长事务拆成多个本地事务,每个事务有对应的回滚操作
  2. 事件驱动:通过消息队列异步处理,保证最终一致性
  3. 补偿机制:失败时执行补偿操作,恢复数据

8. 事务的最佳实践

  1. 尽量短:减少事务持有锁的时间
  2. 避免用户交互:不要在事务里等待用户输入或外部 API 调用
  3. 捕获异常:事务失败时妥善处理
  4. 重试逻辑:死锁或超时可以重试
  5. 监控:记录事务执行时间、失败率

总结

事务保证一组操作的原子性和一致性。Prisma 和 Drizzle 都提供了事务 API,支持不同的隔离级别。

这一节涉及到的几个实践:

  1. ACID 特性:原子性、一致性、隔离性、持久性
  2. Drizzle 事务:回调式,支持嵌套
  3. Prisma 事务:回调式和批量式
  4. 隔离级别:Read Committed、Repeatable Read、Serializable
  5. 死锁:固定顺序、缩短事务、超时检测
  6. 乐观锁:版本号或时间戳检测冲突
  7. 分布式事务:避免使用,采用 Saga 或事件驱动

事务是保证数据一致性的关键机制,但也会带来性能开销。合理使用事务,避免过度使用。

下一篇看分页查询——cursor 分页、offset 分页、无限滚动。