STM

软件事务性存储(STM)是一种允许任意组合原子操作的技术。它是数据库系统中事务管理的类似物。 STM可以原子方式执行多组内存操作。该 API 受到 Haskell 的 STM 库 启发,尽管 ZIO 中的实现完全不同。

与传统的低级锁定机制相比,STM具有许多优点:

  • 组合性
  • 避免死锁
  • 异常或超时的时候自动回滚
  • 避免由并发和锁粒度带来的压力

传统的使用锁的并发程序的主要问题在于难以实现组合,各自正确的代码片段组合在一起时可能会失败,因为当越来越多的锁被引入代码时,获取和释放锁的顺序不当很容易导致死锁。

事务性存储消除了许多基于底层锁的低级编程带来的困难,因为事务性数据结构是无锁的。

事务性数据结构

STM事务中可以有多种事务性数据结构:

由于STM非常重视组合性,因此您可以在这些数据结构上建立并定义自己的并发数据结构。例如,您可以使用 TRef,TMap 和 TQueue 构建事务优先级队列。

STM 数据类型

STM[E, A] 表示可以以事务性的方式执行,并得到失败 E 或成功 A 的 effect。它 有一种更强大的变体 ZSTM[R, E, A],它支持环境类型 R,就如同 ZIO[R, E, A]STM(和它的变体ZSTM)数据类型不如 ZIO[R, E, A] 数据类型强大,这是因为它不允许您执行任意的 effect。因为 STM 内的动作可以执行任意次数(也可以回滚),所以在内存性事务中只能执行 STM 操作和纯的计算。STM 操作也不能在事务之外执行,因此您不能在 STM.atomical 的保护范围之外意外地读写事务数据结构(或不明确地提交事务)。例如:

import zio._
import zio.stm._

def transferMoney(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] =
  for {
    senderBal <- from.get
    _         <- if (senderBal < amount) STM.fail("Not enough money")
                 else STM.unit
    _         <- from.update(existing => existing - amount)
    _         <- to.update(existing => existing + amount)
    recvBal   <- to.get
  } yield recvBal

val program: IO[String, Long] = for {
  sndAcc  <- STM.atomically(TRef.make(1000L))
  rcvAcc  <- STM.atomically(TRef.make(0L))
  recvAmt <- STM.atomically(transferMoney(sndAcc, rcvAcc, 500L))
} yield recvAmt

transferMoney 描述了发送方和接收方之间的原子事务过程。如果发件人帐户中没有足够的资金,交易将失败。这意味着个人帐户在自动完成借贷交易过程中,如果事务在中间失败,则整个过程将被回滚,并且看起来什么也没有发生过一样。在这里,我们看到 STM effect 通过 for-comprehension 按顺序来组合,并且通过 STM.atomical(或在任何单个 STM effect上调用 commit)将 STM effect 转换为可以执行的 ZIO effect。通过使用 STM.atomically(或 commit),程序员可以将 STM.atomically 中的各个操作视为不可分割,从而将它整体上看待成一个原子事务。

错误

STM 就像 ZIO 一样通过错误通道来支持错误输出。在 transferMoney 中,我们看到了一个产生错误的示例(STM.fail)。 STM 中的错误具有中止的语义:如果原子事务遇到错误,则该事务将回滚并且无效。

重试

STM.retry 是阻塞状态下的事务实现组合的关键。例如,如果我们要等待汇款人的账户有足够的钱时进行汇款(而不是立即失败),我们就可以用 STM.retry 来代替:

def transferMoneyNoMatterWhat(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] =
  for {
    senderBal <- from.get
    _         <- if (senderBal < amount) STM.retry else STM.unit
    _         <- from.update(existing => existing - amount)
    _         <- to.update(existing => existing + amount)
    recvBal   <- to.get
  } yield recvBal

STM.retry 将重试整个事务,直到成功为止(而不是像前面的示例一样失败)。但是请注意,仅当其下的事务数据结构发生改变时,重试才会开始。STM.retry 组合器还有许多其他变体,例如 STM.check。您可以用 STM.check(senderBal < amount) 替换,而不是 if (senderBal < amount) STM.retry else STM.unit

选项性组合

STM 事务一般按顺序组合在一起,依次执行两个STM effect。然而,STM 事务也可以通过 orTry 选项性地组合在一起,只要其中一个执行通过即可。假设我们有两个 STM effect sAsB,则我们可以以 sA orTry sB 的形式组合这两个 effect。事务将首先尝试运行 sA,如果无效需要重试,则 sA 被放弃,转而运行 sB,现在,如果 sB 也需要重试,则尝试重试整个过程,但是在这之前,它会等待被 sAsB 调用的事务数据结构发生变换。使用 orTry 是一种优雅的技术,可用于确定 STM 事务是否需要阻塞。例如,我们可以用 orTry 来将(retry 版本的) transferMoneyNoMatterWhat 转变为会立刻失败的 STM 事务,如果发件人没有足够的钱,则该交易将立即失败,而无需重试:

def transferMoneyFailFast(from: TRef[Long], to: TRef[Long], amount: Long): STM[String, Long] =
    transferMoneyNoMatterWhat(from, to, amount) orTry STM.fail("Sender does not have enough of money")

因为 orTry 的语义,事务将因为没有钱而立即失败。

Leave a Reply
Your email address will not be published.
*
*

BACK TO TOP