javascript
Spring Reactive已经过时了吗? 螺纹连接反转
除了Spring的依賴注入僅解決控制反轉問題的1/5之外,Spring Reactive還基于事件循環。 盡管還有其他流行的事件循環驅動解決方案(NodeJS,Nginx),但單線程事件循環是每個請求線程(線程池)朝另一個方向擺動。 在事件循環與每個請求線程競爭的情況下,是否沒有某種模式可以使它們成為基礎? 好吧,實際上是的!
但是在開始之前,讓我們看一下有關事件循環和每個請求線程的問題。 如果您對該解決方案更感興趣,則可以跳過接下來的兩個部分。
螺紋連接問題
事件循環
首先,“線程耦合”? 為什么要擔心? 對于事件循環來說,單線程本質要求所有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(){return(runnable)-> 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
總結
以上是生活随笔為你收集整理的Spring Reactive已经过时了吗? 螺纹连接反转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑gta5怎么找设置(GTA5电脑设置
- 下一篇: spring 异常捕获异常_跟踪异常–第