⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

4.4.9 数据如何保证一致——事务处理:数据一致性保证

一句话破题

事务让多个操作要么全部成功、要么全部回滚——这是保证数据一致性的核心机制。

Prisma 事务类型

类型用途特点
顺序事务多个独立操作简单,无法在操作间加逻辑
交互式事务有依赖的操作灵活,可以在操作间加业务逻辑

顺序事务

typescript
// 多个操作作为数组传入,按顺序执行
const [user, post] = await prisma.$transaction([
  prisma.user.create({
    data: { email: 'user@example.com', name: '用户' }
  }),
  prisma.post.create({
    data: { title: '文章', authorId: 'xxx' }
  })
])

适用场景:多个独立操作需要原子性执行

交互式事务

typescript
// 可以在事务中编写业务逻辑
const result = await prisma.$transaction(async (tx) => {
  // 1. 查询当前余额
  const account = await tx.account.findUnique({
    where: { userId: senderId }
  })
  
  // 2. 业务逻辑判断
  if (!account || account.balance < amount) {
    throw new Error('余额不足')
  }
  
  // 3. 扣减发送方余额
  await tx.account.update({
    where: { userId: senderId },
    data: { balance: { decrement: amount } }
  })
  
  // 4. 增加接收方余额
  await tx.account.update({
    where: { userId: receiverId },
    data: { balance: { increment: amount } }
  })
  
  // 5. 记录交易日志
  const transaction = await tx.transaction.create({
    data: {
      senderId,
      receiverId,
      amount,
      type: 'TRANSFER'
    }
  })
  
  return transaction
})

事务配置选项

typescript
await prisma.$transaction(
  async (tx) => {
    // ...
  },
  {
    maxWait: 5000,      // 等待获取事务的最长时间(毫秒)
    timeout: 10000,     // 事务执行的最长时间(毫秒)
    isolationLevel: 'Serializable'  // 隔离级别
  }
)

隔离级别

级别说明
ReadUncommitted最低隔离,可能读到未提交的数据
ReadCommitted只读已提交的数据(PostgreSQL 默认)
RepeatableRead同一事务内多次读取结果一致
Serializable最高隔离,完全串行化

常见事务场景

场景一:创建订单 + 扣减库存

typescript
async function createOrder(userId: string, items: OrderItem[]) {
  return prisma.$transaction(async (tx) => {
    // 检查并扣减库存
    for (const item of items) {
      const product = await tx.product.findUnique({
        where: { id: item.productId }
      })
      
      if (!product || product.stock < item.quantity) {
        throw new Error(`商品 ${item.productId} 库存不足`)
      }
      
      await tx.product.update({
        where: { id: item.productId },
        data: { stock: { decrement: item.quantity } }
      })
    }
    
    // 创建订单
    const order = await tx.order.create({
      data: {
        userId,
        items: {
          create: items.map(item => ({
            productId: item.productId,
            quantity: item.quantity,
            price: item.price
          }))
        }
      }
    })
    
    return order
  })
}

场景二:用户注册 + 初始化数据

typescript
async function registerUser(data: RegisterData) {
  return prisma.$transaction(async (tx) => {
    // 创建用户
    const user = await tx.user.create({
      data: {
        email: data.email,
        name: data.name
      }
    })
    
    // 创建用户配置
    await tx.userSettings.create({
      data: {
        userId: user.id,
        theme: 'light',
        notifications: true
      }
    })
    
    // 创建初始钱包
    await tx.wallet.create({
      data: {
        userId: user.id,
        balance: 0
      }
    })
    
    return user
  })
}

嵌套写入 vs 事务

嵌套写入(Prisma 自动使用事务):

typescript
// 这已经是原子操作
const user = await prisma.user.create({
  data: {
    email: 'user@example.com',
    profile: {
      create: { bio: '...' }
    },
    posts: {
      create: [
        { title: '文章1' },
        { title: '文章2' }
      ]
    }
  }
})

何时需要显式事务

  • 需要在操作间加业务逻辑
  • 需要跨多个顶级操作
  • 需要配置事务选项

错误处理

typescript
try {
  await prisma.$transaction(async (tx) => {
    // ...
  })
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    // Prisma 错误
    if (error.code === 'P2002') {
      // 唯一约束冲突
    }
  }
  throw error
}

避坑指南

  1. 事务要短:长事务会锁定资源,影响并发

  2. 不要在事务中调用外部服务

    typescript
    // 错误示例
    await prisma.$transaction(async (tx) => {
      await tx.order.create({ ... })
      await sendEmail()  // 外部调用可能失败或很慢
    })
    
    // 正确:先事务,后调用
    const order = await prisma.$transaction(async (tx) => {
      return tx.order.create({ ... })
    })
    await sendEmail()  // 事务成功后再发邮件
  3. 处理死锁:设置合理的超时时间

本节小结

  • 顺序事务用于简单的多操作原子执行
  • 交互式事务用于需要业务逻辑的场景
  • 嵌套写入自动使用事务,简单场景不需要显式事务
  • 事务要短,避免在事务中调用外部服务