软件事务性存储(STM)是一种允许任意组合原子操作的技术。它是数据库系统中事务管理的类似物。 STM可以原子方式执行多组内存操作。该 API 受到 Haskell 的 STM 库 启发,尽管 ZIO 中的实现完全不同。
与传统的低级锁定机制相比,STM具有许多优点:
- 组合性
- 避免死锁
- 异常或超时的时候自动回滚
- 避免由并发和锁粒度带来的压力
传统的使用锁的并发程序的主要问题在于难以实现组合,各自正确的代码片段组合在一起时可能会失败,因为当越来越多的锁被引入代码时,获取和释放锁的顺序不当很容易导致死锁。
事务性存储消除了许多基于底层锁的低级编程带来的困难,因为事务性数据结构是无锁的。
事务性数据结构
STM事务中可以有多种事务性数据结构:
- TRef: 一个对不可变量的可变引用
- TPromise: 一个只能设置一次的可变引用
- TArray: 一个可变引用的数组
- TMap: 一个可变的 map
- TQueue: 一个可变的队列
- TSet: 一个可变的集合
- TSemaphore: 信号量
- TReentrantLock: 重入锁
由于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 sA
和 sB
,则我们可以以 sA orTry sB
的形式组合这两个 effect。事务将首先尝试运行 sA
,如果无效需要重试,则 sA
被放弃,转而运行 sB
,现在,如果 sB 也需要重试,则尝试重试整个过程,但是在这之前,它会等待被 sA
或 sB
调用的事务数据结构发生变换。使用 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
的语义,事务将因为没有钱而立即失败。