Prisma transactions 快速食用指南

461 阅读8分钟

数据库知识

事务

Automic

Ensures that either all or none operations of the transactions succeed. The transaction is either committed successfully or aborted and rolled back.

事务的原子性(Atomicity)保证了事务内的所有操作要么全部成功(提交),要么全部失败(回滚)

Consistent

Ensures that the states of the database before and after the transaction are valid (i.e. any existing invariants about the data are maintained).

事务的原子性(Atomicity)保证了事务内的所有操作要么全部成功(提交),要么全部失败(回滚)。一致性则是原子性在数据状态层面的要求:如果事务成功提交,那么数据库状态应从一个有效状态转变为另一个有效状态;如果事务回滚,数据库应恢复到事务开始前的状态,确保数据始终保持一致性。

事务一致性示例

正常执行
  • 事务开始前:账户A余额为1000元,账户B余额为500元,数据库处于一致状态。

  • 事务执行

    • 开始一个转账事务。
    • 更新账户A的余额为900元A_balance - 100)。
    • 更新账户B的余额为600元B_balance + 100)。
  • 事务结束(提交) :转账事务成功提交,账户A余额为900元,账户B余额为600元。此时,数据库状态仍然满足账户余额非负的业务规则,保持了一致性。

异常处理
  • 事务开始前:账户A余额为1000元,账户B余额为500元,数据库处于一致状态。

  • 事务执行

    • 开始一个转账事务。
    • 更新账户A的余额为900元A_balance - 100)。
    • 在更新账户B余额时,由于某种原因(如网络中断、系统故障等)事务未能完成。
  • 事务结束(回滚) :由于事务未能成功完成,系统自动回滚事务。账户A的余额恢复为1000元,账户B的余额保持为500元。虽然转账操作未完成,但数据库状态仍回到了事务开始前的一致状态。

在这个例子中,事务的一致性保证了无论事务成功提交还是因故回滚,数据库状态始终满足业务规则(账户余额非负)。如果缺乏事务一致性保障,可能出现以下不一致情况:

  • 账户A扣款成功,账户B未能加款:账户A余额为900元,账户B余额仍为500元,导致总金额减少了100元,违反了资金守恒的业务规则。
  • 账户A、B均未完成更新:账户A余额为900元,账户B余额为500元,但转账事务未完成,导致资金状态混乱。

通过使用事务并确保其一致性,银行系统可以避免上述不一致情况,确保在并发环境下数据的正确性和完整性。

Isolated

Ensures that concurrently running transactions have the same effect as if they were running in serial.

事务隔离级别定义了不同事务之间数据访问的隔离程度,旨在防止因并发操作导致的数据不一致性问题。以下详细介绍四种常见的隔离级别,并通过例子进一步说明:

1. 读未提交(Read Uncommitted)

定义:最低级别的隔离,允许事务看到其他未提交事务对数据的修改。

问题:可能导致脏读(Dirty Read),即一个事务读取到另一个事务尚未提交(可能还会回滚)的数据。

例子

  • 事务A:开始更新某账户余额,将余额从1000元改为900元,但尚未提交。
  • 事务B(读未提交隔离级别):读取该账户余额,看到的是900元(脏数据)。
  • 事务A:由于某种原因回滚操作,账户余额恢复为1000元
  • 事务B:基于错误的余额900元进行后续计算或决策,导致数据不一致。

2. 读已提交(Read Committed)

定义:在一个事务内,两次执行相同的查询语句,由于其他事务对数据的修改并提交,导致两次查询结果不一致。

问题:可能导致不可重复读(Non-Repeatable Read),即一个事务在两次读取同一数据时,得到的结果不一致,因为其间另一事务对数据进行了修改并提交。

不可重复读示例

  • 事务A(可重复读隔离级别):第一次查询某账户余额,看到1000元
  • 事务B:提交一个转账操作,从该账户转出100元,账户余额变为900元
  • 事务A:再次查询同一账户余额,看到900元(与第一次读取结果不同)。

在这个例子中,事务A在两次查询同一账户余额时得到了不同的结果,这是典型的不可重复读问题。

3. 可重复读(Repeatable Read)

  • 定义:在一个事务内,两次执行相同的范围查询(如SELECT ... WHERE ...),由于其他事务在这两次查询之间插入了满足查询条件的新数据并提交,导致第二次查询结果集中出现了第一次查询未曾出现的行(即“幻影行”)。

幻读示例

  • 事务A(可重复读隔离级别):第一次查询所有余额大于500元的账户,得到结果集R1
  • 事务B:提交一个新账户的开户操作,该账户余额为600元(满足事务A查询条件)。
  • 事务A:再次执行相同的查询(查询所有余额大于500元的账户),得到结果集R2。由于事务B插入的账户余额满足事务A的查询条件,理论上R2应该包含事务B插入的新账户,但实际情况取决于数据库的具体实现。在支持多版本并发控制(MVCC)的数据库(如MySQL InnoDB)中,事务A在开始时获取了一个数据快照,后续查询都将基于这个快照进行,因此即使事务B插入了新数据,事务A也不会看到这些新数据,避免了幻读现象。而在不使用MVCC的数据库中,可能会出现幻读,但通常会通过其他并发控制机制(如Next-Key Locks)来防止。

4. 串行化(Serializable)

定义:最高隔离级别,通过严格限制事务间的并行执行,确保事务按一定的顺序执行,如同单线程执行一样,完全避免脏读、不可重复读和幻读。

实现机制:通常通过(如在事务开始时对所有涉及的数据加锁至事务结束)或时间戳排序等手段强制事务串行化执行。

例子(假设采用锁机制):

  • 事务A(串行化隔离级别):开始更新某账户余额,锁定该账户。
  • 事务B:尝试更新同一账户余额,但由于事务A已锁定该账户,事务B必须等待事务A完成并释放锁。
  • 事务A:完成更新并提交,释放锁。
  • 事务B:现在可以获取锁并更新账户余额,事务之间不存在数据不一致性问题。

实现机制

为了实现不同的隔离级别,数据库系统通常采用以下一种或多种机制:

  • 锁(Locking) :通过在读取或修改数据时加锁,阻止其他事务对已锁定数据进行并发操作,直到锁被释放。包括行锁、页锁、表锁、间隙锁等不同粒度的锁。
  • 多版本并发控制(MVCC, Multi-Version Concurrency Control) :如Oracle、PostgreSQL、MySQL InnoDB等采用的机制。每个事务看到的是数据库的一个快照版本,不同事务可以同时读取同一数据的不同版本,从而避免锁定冲突。
  • 时间戳排序(Timestamp Ordering) :为事务分配时间戳,根据时间戳顺序执行事务,确保事务按特定顺序看到数据。

总结

尽管这些属性中的每一个都存在大量的模糊性和细微差别(例如,一致性实际上可以被视为应用程序级别的责任而非数据库属性,或者隔离通常是以更强或较弱的隔离级别来保证的),但总体而言,它们为开发人员在考虑数据库事务时对期望值的理解提供了一个很好的高级指导原则。

transaction

下面的部分基本上是(www.prisma.io/docs/orm/pr…

Prisma Client supports six different ways of handling transactions for three different scenarios:

image.png

technique 1. Nested writes

nested write lets you perform a single Prisma Client API call with multiple operations that touch multiple related records. For example, creating a user together with a post or updating an order together with an invoice. Prisma Client ensures that all operations succeed or fail as a whole.

// Create a new user with two posts in a
// single transaction
const newUser: User = await prisma.user.create({
  data: {
    email: 'alice@prisma.io',
    posts: {
      create: [
        { title: 'Join the Prisma Slack on https://slack.prisma.io' },
        { title: 'Follow @prisma on Twitter' },
      ],
    },
  },
})

The following example demonstrates a nested write with update:

// Change the author of a post in a single transaction
const updatedPost: Post = await prisma.post.update({
  where: { id: 42 },
  data: {
    author: {
      connect: { email: 'alice@prisma.io' },
    },
  },
})

Independent writes

Read, modify, write

In some cases you may need to perform custom logic as part of an atomic operation - also known as the read-modify-write pattern.

  • Read a value from the database
  • Run some logic to manipulate that value (for example, contacting an external API)
  • Write the value back to the database

technique 2. $transaction([]) API

technique 3. Batch operations

technique 4. Idempotent operations

technique 5. Optimistic concurrency control

technique 6. Interactive transactions