Spring的反应式/命令式关系数据库的事务


Spring Framework最近公布了对反应性事务管理的支持。让我们深入了解一下这对于R2DBC(SQL数据库访问的反应规范)是如何工作的。
事务管理是一种模式,而不是特定于技术。从这个角度来看,它的属性和运行时行为是实现技术的一个功能。
从数据库的角度来看,命令式和响应式事务的工作方式相同。从Java的角度来看,强制事务和被动事务之间存在一些差异。

让我们先看看命令式交易。

命令式交易
在命令性事务中,更具体地说是面向方面的事务管理,例如拦截器,事务状态通常对代码是透明的。根据底层API,我们可以从某个地方获取事务状态和事务绑定资源。这个地方通常存在于ThreadLocal存储中。命令式事务假设您的代码的所有事务性工作都是相同的Thread。

命令事务的另一个方面是在@Transactional事务正在进行时将所有上下文数据都保留在方法中,像JPA这样的工具允许通过Java 8进行结果Stream.流式传输,无论如何,流式传输需要一个封闭的@Transactional方法。事务正在进行时,任何事务中数据都不会离开方法这个作用域。

我指出这两个问题因为它们在反应式事务中的表现不同。

资源绑定
在继续进行反应式事务之前,我们需要提高对事务状态的理解。事务状态通常包括事务状态(已启动,已提交,已回滚)和绑定到该事务的对应资源。
事务性资源(例如数据库连接)通常将其事务进程绑定到底层传输连接。在大多数情况下,这是TCP连接。在数据库连接使用多路复用的情况下,状态绑定到会话对象。在极少数情况下,数据库操作接受事务或会话标识符。因此,我们假设,我们将连接绑定到事务以包含最低能力的方法,因为事务状态通常不能跨连接移植。

反应性事务
使用反应式编程时,我们希望在使用事务时和传统应用是一样的便利(使用相同的编程模型),同样是使用基于注释@Transactional的事务标注的方法。回到事务管理只是一种模式的概念,我们唯一需要更换的是技术。

反应事务不再将其事务状态绑定ThreadLocal,而是绑定到订阅者上下文。这是与特定执行路径相关联的上下文。或者换句话说:实现的每个反应序列都会获得与其他执行隔离的订阅者上下文。这已经是强制性事务的第一个区别。

第二个区别是从@Transactional方法中逃逸的数据。使用Reactive Streams进行反应式编程几乎都是通过函数反应式操作者进行数据流和数据流传输。与异步API相比,这也是一个主要优势,Publisher发布者只要获得数据库驱动程序的解码,就会发出第一个元素,而不是在在Future完成之前等待最后一个数据包到达。

反应性事务包含了这一事实:与命令式事务类似,事务在实际工作之前就启动了。当我们根据事务工作生成数据时,数据流就会在事务处于活动状态时流过发布者Publisher。这意味着数据逃离了@Transactional标注的方法。我们将意识到@Transactional方法只是反应序列中的标记,我们不关注方法内部了; 我们宁愿只观察订阅和完成时发生的影响。
如果在事务处理期间发生任何错误,我们可能会留下在事务处理回滚时在事务中处理的数据。这是您的应用程序需要考虑的事项。按意图进行的反应性事务管理不会延迟发射,不会忽略流式属性。应用程序中的原子性权重比流​​式传输更重,因此您可以在应用程序中处理这些内容。

堵塞
从Java角度来看,使用R2DBC进行反应性数据库访问是完全无阻塞的。所有I / O都使用非阻塞套接字。所以从R2DBC得到的是I / O不再会阻塞你的线程。但是,反应式关系数据库驱动程序符合数据库通信协议并遵守数据库行为。虽然我们不再占用堵塞一个线程,但我们仍然占用数据库连接,因为这是RDBMS的工作方式 - 按命令发送命令。有些数据库允许稍微优化,称为流水线操作。在流水线操作模式下,驱动程序不断向连接发送命令,而无需等待上一个命令完成。
通常,在以下情况下可以释放连接:

  1. 声明(多个声明)已完成
  2. 应用程序事务已完成

我们仍然可以观察到阻止连接的锁定。

数据库锁
根据您使用的数据库,您可以观察MVCC行为或堵塞行为,这通常是事务锁定。对于命令式SQL数据库事务,我们通常最终得到两个(b)锁:

  • 应用程序线程被I / O阻止
  • 数据库持有一个锁

我们的应用程序只有在数据库释放锁定时才能进行。释放锁也会解除对应用程序线程的阻塞。由于非阻塞I / O,使用反应式数据库集成不再堵塞应用程序线程。数据库锁行为仍然存在。我们最终还是使用阻塞的数据库连接,但不会阻止两个资源了(另外一个是应用线程)。
从Java的角度来看,TCP连接很便宜。
由于SQL数据库的工作方式,我们仍然获得强大的一致性保证


ACID标准的数据库
SQL数据库和被动反应有三种观点:

  • 锁定:在谈论反应式时,SQL数据库不是最好的持久性机制。许多数据库在运行更新时执行内部锁定,因此并发访问受限。一些数据库应用MVCC,允许进度,锁定影响较小。在任何情况下,大量使用的用例可能不太适合您的反应应用程序,因为对于传统的SQL数据库,这可能会产生可伸缩性瓶颈。
  • 可伸缩性:SQL数据库通常比NoSQL更糟糕,您可以再放置50台计算机来扩展您的集群。使用像RedShift,CockroachDB,Yugabyte这样的新SQL数据库,我们可以以不同的方式扩展,并且比传统的SQL数据库更好。
  • 游标:许多SQL数据库在其有线协议中都具有反应功能。这通常类似于chunked fetching。在运行查询时,反应驱动程序可以通过获取少量结果来读取游标的结果,以免压垮驱动程序。一旦读取了第一行,驱动程序就可以将该行向下发送给其使用者并继续下一行。处理完块后,驱动程序可以开始处理下一个块。如果订阅被取消,驱动程序将停止从光标读取并释放它。这是一个非常强大的设计。

真的有任何性能优势吗?
您不会因为吞吐量而选择反应式,只是为了扩展性。一些影响会影响完全基于背压的吞吐量。背压是Subscriber订阅者一次可以处理多少条目的概念。
当前一个数据完成处理时,命令式驱动程序通常会获取下一个数据块。堵塞驱动程序底层连接和线程,直到数据库回复(命令性获取模型,请求之间的白色区域是延迟)。
在资源使用方面,反应式驱动程序不会阻止线程。总而言之,它们在物化过程中带有GC友好的执行模型。在生成流期间,GC压力会增加。

结论
您已经了解了命令式和反应式数据库属性。事务管理需要以与反应式代码不同的方式在命令式流中实现。实现中的更改反映了稍微不同的运行时行为,尤其是涉及数据转义时。通过改变延迟和资源使用情况的性能配置文件,您可以获得相同的强一致性保证。