Spring Framework最近宣布将提供对反应式事务管理的支持 。
让我们深入研究它对R2DBC(SQL数据库访问的反应式规范)如何工作。
事务管理是一种模式,而不是特定于技术的。 从这个角度来看,它的属性和运行时行为是实现技术的功能。
TL; DR:从数据库的角度来看,命令式和响应式事务的工作原理相同。 从Java的角度来看,命令式事务和反应式事务之间存在一些差异。
让我们先来看命令式交易。
命令性交易
在命令性事务中,更具体地说,在具有例如拦截器的面向方面的事务管理中,事务状态通常对于代码是透明的。 根据底层API,我们可以从某处获取事务状态和事务绑定资源。 这个地方通常生活在一个ThreadLocal
存储。 命令性事务假定代码的所有事务工作都在同一Thread
。
命令性事务的另一个方面是,在进行事务时,所有数据都保留在@Transactional
方法内。 诸如JPA之类的工具允许通过Java 8 Stream.
进行结果流传输Stream.
在任何情况下,流传输都需要使用@Transactional
方法。 事务正在进行时,任何事务数据都不能离开方法–数据不会逃逸。
我指出这两个问题是因为它们在被动事务中的行为不同。
资源绑定
在继续进行被动交易之前,我们需要提高对交易状态的理解。 事务状态通常由事务状态(启动,提交,回滚)和绑定到事务的资源组成。
事务资源,例如数据库连接,通常将其事务进度绑定到基础传输连接。 在大多数情况下,这是TCP连接。 如果数据库连接使用多路复用,则状态将绑定到会话对象。 在极少数情况下,数据库操作会接受事务或会话标识符。 因此,我们假设将连接绑定到事务以包含能力最低的方法,因为事务状态通常无法跨连接移植。
反应性交易
使用反应式编程时,我们希望在使用事务时应用相同级别的便利性(阅读:使用相同的编程模型),而在使用基于注释的事务划分时,最好使用@Transactional
方法。 回到交易管理只是一种模式的概念,我们唯一需要交换的就是技术。
反应性事务不再将其事务状态绑定到ThreadLocal
而是绑定到订户上下文。 那是与特定执行路径关联的上下文。 或者换句话说:每个具体化的反应性序列都具有与其他执行隔离的订户上下文。 这已经是命令式交易的第一个区别。
第二个区别是@Transactional
方法中的数据转义。
使用反应式流的反应式编程几乎全部涉及通过功能反应式运算符进行的数据流和数据流。 与异步API相比,这也是一个主要优点,即异步Publisher
者在数据库驱动程序解码后立即发出第一个元素,而不是在Future
完成之前等待最后一个数据包到达。
反应性交易包含了这一事实。 与命令式事务类似,事务在实际工作之前开始。 当我们通过事务工作产生数据时,数据在事务处于活动状态时流经Publisher
。 这意味着在活动事务期间数据会逸出我们的@Transactional
方法。 更详细地看,我们将认识到@Transactional
方法只是反应性序列中的标记。 我们在方法上考虑不多; 我们宁愿只观察订阅和完成过程中发生的影响。
如果在事务处理过程中发生任何错误,则在实际事务回滚的同时,我们可能会保留在事务内处理的数据。 这是您的应用程序中要考虑的事情。
通过意图进行的反应式事务管理不会延迟排放,而不会忽略流属性。 原子性在应用程序中的权重比流技术高,这是您可以在应用程序中处理的事情。 否则,您将获得反应式数据流的全部功能。
(B)锁定
从Java的角度来看,使用R2DBC进行的反应性数据库访问是完全非阻塞的。 所有I / O都使用非阻塞套接字进行。 因此,您从R2DBC获得的是I / O不再阻塞您的线程。 但是,反应性关系数据库驱动程序符合数据库通信协议并遵守数据库行为。
尽管我们不再占用Thread
,但仍然占有数据库连接,因为这是RDBMS的工作方式-通过命令发送命令。 一些数据库允许进行轻微的优化,称为流水线。 在流水线模式下,驱动程序将继续向连接发送命令,而无需等待上一个命令完成。
通常,在以下情况下可以释放连接:
- 一个语句(多个语句)已完成
- 申请交易完成
我们仍然可以观察到锁定会阻止连接。
数据库锁
根据您使用的数据库,您可以观察MVCC行为或阻止行为,通常是事务锁定。 对于命令式SQL数据库事务,我们通常以两个(b)锁结束:
- 应用程序线程被I / O阻止
- 数据库锁
仅当数据库释放其锁时,我们的应用程序才能继续运行。 释放锁还可以解除对应用程序线程的阻塞。
由于非阻塞的I / O,使用反应式数据库集成不再阻塞应用程序线程。 数据库锁定行为保持不变 。 而不是阻塞两个资源,我们最终得到了阻塞的数据库连接。
从Java的角度来看,TCP连接便宜。
由于SQL数据库是如何工作的,我们仍然可以获得强大的一致性保证。
符合ACID的数据库是否天生就不会发生反应?
关于SQL数据库和响应式有以下三种观点:
- 锁定:谈论反应式时,SQL数据库并不是最佳的持久性机制。 许多数据库在运行更新时执行内部锁定,因此并发访问受到限制。 某些数据库应用了MVCC ,从而使锁定效果更小。 无论如何,繁重的用例可能不太适合您的响应式应用程序,因为对于传统的SQL数据库,这可能会导致可伸缩性瓶颈。
- 可伸缩性:SQL数据库的伸缩性通常比NoSQL差,在NoSQL上,您可以再放置50台计算机来扩展群集。 借助RedShift,CockroachDB,Yugabyte之类的新SQL数据库,我们可以比传统SQL数据库进行不同的扩展和更好的方式。
- 游标:许多SQL数据库在其有线协议中都具有响应功能。 这通常类似于分块读取。 运行查询时,响应式驱动程序可以通过获取少量结果以使驱动程序不致从游标中读取结果。 读取第一行后,驱动程序就可以将该行向下发送给其使用者,然后继续进行下一行。 一旦处理了块,驱动程序就可以开始处理下一个块。 如果取消订阅,驱动程序将停止从游标读取并释放它。 这是一个非常强大的安排。
真的有性能优势吗?
性能是一个巨大的领域。 在本文的背景下,让我们关注资源使用和吞吐量。
您不需要对吞吐量做出反应。 您这样做是为了实现可伸缩性。
有些影响会完全基于背压影响吞吐量。 背压是指Subscriber
可以通过向其Publisher
者报告所请求的条目数来一次处理多少条目的概念。 知道应用程序需要多少行的背压允许被动驱动程序应用智能预取。
命令性驱动程序通常在前一个数据完成处理时获取下一个数据块。 阻塞驱动程序将阻塞基础连接和线程,直到数据库答复为止( 命令获取模型 ,请求之间的白色区域是等待时间)。
知道客户端需要多少数据,可以让反应性驱动程序在应用程序处理前一个数据块时提取下一个数据块( 反应式提取模型 ,其中将等待时间降至最低)。
就资源使用而言,反应性驱动程序不会阻塞线程。 一旦从网络流中解码出行,它们就会发出接收到的行。 总而言之,它们在实现过程中带有GC友好的执行模型。 在组装期间,GC压力增加。
结论
您已经了解了命令式和反应式数据库属性。 事务管理需要在命令性流程中实现,而与反应式代码不同。 实现的更改反映出运行时行为略有不同,尤其是在涉及数据转义时。 通过更改有关延迟和资源使用的性能配置文件,您可以获得相同的强大一致性保证。
注意:程序性交易管理是有意省略的,因为本文概述了交易管理的内部原理以及命令式交易与反应式交易之间的差异。
翻译自: https://www.javacodegeeks.com/2019/05/reactive-relational-database-transactions.html