做网站主机,服装公司电子商务网站建设策划书,好看的网站链接,wordpress ddos攻击除了Spring的依赖注入仅解决控制反转问题的1/5之外#xff0c;Spring Reactive还基于事件循环。 尽管还有其他流行的事件循环驱动解决方案#xff08;NodeJS#xff0c;Nginx#xff09;#xff0c;但单线程事件循环是每个请求线程#xff08;线程池#xff09;朝另一个… 除了Spring的依赖注入仅解决控制反转问题的1/5之外Spring Reactive还基于事件循环。 尽管还有其他流行的事件循环驱动解决方案NodeJSNginx但单线程事件循环是每个请求线程线程池朝另一个方向摆动。 在事件循环与每个请求线程竞争的情况下是否没有某种模式可以使它们成为基础 好吧实际上是的 但是在开始之前让我们看一下有关事件循环和每个请求线程的问题。 如果您对该解决方案更感兴趣则可以跳过接下来的两个部分。 螺纹连接问题 事件循环 首先“线程耦合” 为什么要担心 对于事件循环来说单线程本质要求所有I / O都必须异步进行。 如果需要阻止数据库或HTTP调用它将阻止单个事件循环线程并支撑系统。 这种限制本身就是一个很大的耦合问题因为要使Reactive将所有I / O耦合到异步状态。 这意味着不再需要像JPA这样的ORM来简化对数据库的访问因为JPA需要阻止数据库调用。 是的以前在应用程序中删除了40-60的样板代码的东西现在已经不可用了请重新写一遍 除了决定使用响应式模式的限制性I / O之外还限制了使用多个处理器的能力因为只有一个线程。 好的反应式引擎的实例已复制到每个CPU但是它们不能共享状态。 在两个事件循环之间共享状态的多线程含义很困难。 响应式编程非常困难更不用说向其中添加多线程了。 是的事件循环之间的通信可以通过事件进行。 但是使用此方法在事件循环之间使共享状态的重复副本保持同步会产生一些可以避免的问题。 基本上您会被告知要设计您的反应性系统以免发生这种情况。 因此您被卡在一个线程上。 所以呢 好吧如果您执行计算量大的操作例如安全密码学JWT则会产生调度问题。 通过在单个线程上必须先完成此操作然后才能执行其他任何操作。 使用多个线程操作系统可以在时间上切入其他线程以处理其他占用较少CPU资源的请求。 但是您只有一个线程因此所有可爱的操作系统线程调度现在都丢失了。 在维修其他任何东西之前您都不得不等待昂贵的CPU密集型操作完成。 哦请忽略这些问题 我们开发人员喜欢性能。 响应式的所有目的都是为了提高性能和改善可伸缩性。 较少的线程可以减少开销从而提高吞吐量。 好的是的我将拥有性能更好的生产系统从而可能降低硬件成本。 但是由于来自单线程事件循环的耦合限制构建和增强该生产系统的速度将大大降低。 更不用说必须重写算法才能避免占用CPU。 与缺乏足够的云硬件供应相比由于开发人员稀缺因此争论规模成本可能仅适用于那些罕见的大型系统。 我们会做出很多反应。 这可能是因为我们还没有充分考虑过这一点。 因此可能是为什么Reactive框架警告不要更改整个销售。 它们通常指示响应模式仅适用于较小且较不复杂的系统。 每个请求线程线程池 另一方面每个请求线程模式例如Servlet 2.x使用线程池来处理扩展。 它们分配一个线程来服务请求并通过具有多个通常是池化的线程进行扩展。 我们可能会读到许多文章称Reactive超出了每个请求线程的规模限制但是每个请求线程的主要问题实际上不是性能也不是规模。 每个请求线程的问题在您的应用程序中更为宽松实际上会污染整个体系结构。 要查看此问题只需看一下调用方法 Response result object.method(identifier); 该方法的实现应如下 Inject Connection connection; Inject HttpClient client; public Result method(Long identifier) { // Retrieve synchronous database result ResultSet resultSet connection.createStatement() .executeQuery( some SQL where id identifier); resultSet.next(); String databaseValue resultSet.getString( value ); // Retrieve synchronous HTTP result HttpResponse response client.send( some URL/ databaseValue); // Return result requiring synchronous results to complete return new Result(response.getEntity()); } 这给请求的线程带来了一个耦合问题可能会污染整个体系结构。 是的您刚刚在请求线程上放置了一个耦合到其他系统。 当数据库调用是同步的时HTTP调用也迫使下游系统同步响应。 我们不能将HTTP调用更改为异步调用因为请求线程希望继续执行从该方法返回的结果。 与请求线程的这种同步耦合不仅限制了调用还限制了下游系统必须提供同步响应。 因此每个请求线程的线程耦合可能会污染您的其他系统甚至可能污染整个体系结构。 难怪同步HTTP调用的REST微服务模式如此流行 这是一种迫使自己自上而下地在系统上的模式。 听起来像每个请求线程和Reactive在强制一切自上而下支持自己方面都持有相同的观点。 支持I / O的线程 总之问题如下。 单线程事件循环 仅将您耦合到异步通信不再提供简单的JPA代码 只是避免了多线程因为从事件队列执行事件的两个线程会产生大量的同步问题可能会降低解决方案的速度并导致难以为最好的开发人员编写的并发错误 失去了线程调度的优势即操作系统已花费大量精力进行优化 而按请求线程解决方案 仅将您耦合到同步通信因为可以立即看到结果不久后不会通过回调 由于管理更多的线程因此具有较高的开销单线程事件循环因此可伸缩性较差 实际上可以考虑从同步通信每个请求线程到异步通信单线程事件循环之间的线程池和响应式单线程之间的钟摆摆动。 剩下的问题实际上是专门为支持每种类型的通信而构建的线程模型的实现约束。 加上同步通信在下游系统上造成的耦合这种摆动到异步通信的举动并不是一件坏事。 所以问题是为什么我们被迫只选择一种沟通方式 为什么我们不能同时使用同步和异步通信样式 好吧我们不能将异步调用放入同步方法调用中。 没有机会进行回调。 是的我们可以阻止在回调中等待但是Reactive会认为自己在规模上具有优势因为其中涉及额外的线程开销。 因此我们需要异步代码来允许同步调用。 但是我们不能将同步调用放入事件循环中因为它会中断事件循环线程。 因此我们需要额外的线程来进行同步调用以允许事件循环线程继续进行其他事件。 反应性就是答案。 使用调度程序 Mono blockingWrapper Mono.fromCallable(() - { return /* make a remote synchronous call */ }).subscribeOn(Schedulers.elastic()); 来自http://projectreactor.io/docs/core/release/reference/#faq.wrap-blocking的代码 是的现在我们可以在事件循环中进行同步调用了。 问题解决了很好。 好吧如果您可以相信已将所有同步调用正确包装在Callables中则会对其进行排序。 弄错了那么您就阻塞了事件循环线程并暂停了应用程序。 至少在多线程应用程序中只有特定请求受苦而不是整个应用程序受苦。 无论如何对我而言这似乎比实际解决问题更多的工作。 哦等等一切都需要自下而上地进行反应这样才能解决此问题。 只是不要阻塞呼叫而是将所有驱动程序和整个技术堆栈更改为Reactive。 总体而言“以一种仅与我们集成的方式改变一切以适合我们的方式”似乎非常接近技术供应商的锁定-无论如何我认为。 因此我们可以考虑一个允许同步调用并且不非常依赖开发人员正确实现的解决方案吗 为什么是 反转螺纹联轴器 异步通信驱动的Reactive单线程事件循环不好意思被认为是正确的解决方案。 开发人员使用调度程序解决了同步通信。 在这两种情况下Reactive函数都使用为其指定的线程来运行 异步函数与事件循环的线程一起执行 通过调度程序中的线程执行的同步功能 函数执行线程的控制在很大程度上取决于开发人员能否正确执行。 开发人员有足够的精力专注于构建代码以满足功能要求。 现在开发人员密切参与了应用程序的线程处理每请求线程总是从开发人员那里某种程度上抽象出来的。 对线程的这种亲密关系大大增加了构建任何Reactive的学习曲线。 另外当开发人员在凌晨2点将其拔出时他们会松开很多头发以使代码在该截止日期或生产修复中正常工作。 那么我们可以从必须正确执行线程的工作中删除开发人员吗 更重要的是我们在哪里控制选择线程 让我们看一个简单的事件循环 public interface AsynchronousFunction { void run(); } public void eventLoop() { for (;;) { AsynchronousFunction function getNextFunction(); function.run(); } } 好吧我们唯一可以控制的对象就是异步函数本身。 使用Executor指定线程我们可以如下增强事件循环 public interface AsynchronousFunction { Executor getExecutor(); void run(); } public void eventLoop() { for (;;) { AsynchronousFunction function getNextFunction(); function.getExecutor().execute(() - function.run()); } } 现在这允许异步函数指定其所需的线程如下所示 通过同步执行器使用事件循环线程getExecutor{returnrunnable- runnable.run; } 通过线程池支持的Executor使用单独的线程进行同步调用getExecutor{return Executors.newCachedThreadPool; } 控件被反转以便开发人员不再负责指定线程。 该函数现在指定用于执行自身的线程。 但是我们如何将执行程序与功能关联 我们使用控制反转的ManagedFunction public interface ManagedFunction { void run(); } public class ManagedFunctionImpl implements ManagedFunction, AynchronousFunction { Inject P1 p1; Inject P2 p2; Inject Executor executor; Override public void run() { executor.execute(() - implementation(p1, p2)); } private void implementation(P1 p1, P2 p2) { // Use injected objects for functionality } } 请注意仅包含相关的ManagedFunction详细信息。 请参阅耦合控件的反转以获取ManagedFunction的更多详细信息。 通过使用ManagedFunction我们可以将Executor与增强事件循环的每个函数相关联。 实际上由于Executor封装在ManagedFunction中因此我们可以返回到原始事件循环。 因此现在不再需要开发人员使用调度程序因为ManagedFunction负责使用哪个线程来执行函数的逻辑。 但这只是将开发人员从代码正确配置到配置的问题。 在为函数指定正确的线程执行程序时如何减少开发人员的错误 确定执行线程 ManagedFunction的一个属性是所有对象都被依赖注入。 除非注入了依赖项否则没有对系统其他方面的引用强烈建议不要使用静态引用。 因此ManagedFunction的依赖关系注入元数据提供了ManagedFunction使用的所有对象的详细信息。 了解函数使用的对象有助于确定函数的异步/同步性质。 要将JPA与数据库一起使用需要一个Connection或DataSource对象。 要对微服务进行同步调用需要HttpClient对象。 如果ManagedFunction不需要这些则可以安全地考虑没有进行阻塞通信。 换句话说如果ManagedFunction没有注入HttpClient则它将无法进行HttpClient同步阻塞调用。 因此可以安全地由事件循环线程执行ManagedFunction而不会暂停整个应用程序。 因此我们可以识别一组依赖关系这些依赖关系指示ManagedFunction是否需要由单独的线程池执行。 我们知道系统中的所有依赖项因此可以将它们分类为异步/同步。 或更恰当地说是否可以在事件循环线程上安全使用依赖项。 如果依赖关系不安全则需要该依赖关系的ManagedFunctions由单独的线程池执行。 但是什么线程池 我们只使用一个线程池吗 好吧响应式调度程序可以灵活地为涉及阻塞调用的各种功能使用/重用不同的线程池。 因此在使用多个线程池时我们需要类似的灵活性。 我们通过将线程池映射到依赖项来使用多个线程池。 好的这有点使您动脑了。 因此让我们用一个例子来说明 public class ManagedFunctionOne implements ManagedFunction { // No dependencies // ... remaining omitted for brevity } public class ManagedFunctionTwo implements ManagedFunction { Inject InMemoryCache cache; // ... } public class ManagedFunctionThree implements ManagedFunction { Inject HttpClient client; // ... } public class ManagedFunctionFour implements ManagedFunction { Inject EntityManager entityManager; // meta-data also indicates transitive dependency on Connection // ... } 现在我们具有以下线程配置 相依性 线程池 HttpClient 线程池一 连接 线程池二 然后我们使用依赖关系将ManagedFunctions映射到线程池 托管功能 相依性 执行者 ManagedFunctionOne ManagedFunctionTwo 线程池表中没有 事件循环线程 ManagedFunction3 HttpClient 线程池一 托管功能四 连接作为EntityManager的传递依赖项 线程池二 线程池执行器用于ManagedFunction的决定现在只是映射配置。 如果某个依赖项调用了阻塞调用它将被添加到线程池映射中。 使用此依赖项的ManagedFunction将不再在事件线程循环上执行从而避免了应用程序暂停。 此外大大减少了丢失阻塞呼叫的可能性。 由于对依赖项进行分类相对容易因此遗漏阻塞调用的机会较小。 另外如果缺少依赖项则仅是对线程池映射的配置更改。 它是固定的无需更改代码。 随着应用程序的成长和发展它特别有用。 这与要求代码更改和开发人员需要认真思考的反应式调度程序不同。 由于现在由框架而不是应用程序代码控制执行ManagedFunction的执行线程因此它有效地反转了对执行线程的控制。 开发人员代码不再线程化。 框架根据ManagedFunctions的依赖关系特性对其进行配置。 办公楼层 从理论上讲这一切都很好但是请向我展示工作代码 OfficeFloor http://officefloor.net 是本文讨论的线程控制模式反转的实现。 我们发现框架的线程模型过于僵化导致变通例如Reactive Scheduler。 我们正在寻找基础模式来创建不需要这种解决方法的框架。 可以在教程中找到代码示例我们重视所有反馈。 请注意尽管OfficeFloor遵循线程控制的反转但考虑其他方面例如依赖关系上下文变异状态线程局部变量线程亲和力背压和减少的锁定以提高性能其实际的线程模型更为复杂。 但是这些是其他文章的主题。 但是正如本文所强调的OfficeFloor应用程序的线程是基于依赖关系映射的简单配置文件。 结论 线程的控制权反转允许函数指定它自己的线程。 由于线程是由注入的Executor控制的因此该模式称为Thread Injection 。 通过允许注入线程的选择由配置而不是代码确定。 这使开发人员免于将线程编码到应用程序中的潜在容易出错的错误任务。 线程注入的另一个好处是可以根据应用程序运行的计算机来定制线程映射配置。 在具有许多CPU的计算机上可以配置更多线程池以利用操作系统的线程调度。 在较小的计算机例如嵌入式计算机上可以更多地重用线程池对于单用途应用程序甚至有可能不使用这些线程池因为它们可以容忍阻塞以减少线程计数。 这将不会对应用程序进行任何代码更改而只需进行配置更改。 此外可能占用事件循环的计算量大的功能也可以移至单独的线程池。 只需在线程池映射中添加此计算的依赖项所有进行该计算的ManagedFunctions现在就不会占用事件循环线程。 线程注入的灵活性不仅仅是支持同步/异步通信。 由于线程注入全部由配置驱动因此不需要更改代码。 实际上开发人员根本不需要任何线程编码。 这是反应式调度程序无法提供的。 因此问题是您是否想将自己绑定到单线程事件循环而这实际上只是异步I / O的单一目的实现 还是您想使用更灵活的东西 翻译自: https://www.javacodegeeks.com/2019/04/spring-reactive-already-obsolete-inversion-thread-coupling.html