javascript
Spring Reactive已经过时了吗? 螺纹连接反转
除了Spring的依賴注入僅解決控制反轉(zhuǎn)問題的1/5之外,Spring Reactive還基于事件循環(huán)。 盡管還有其他流行的事件循環(huán)驅(qū)動(dòng)解決方案(NodeJS,Nginx),但單線程事件循環(huán)是每個(gè)請(qǐng)求線程(線程池)朝另一個(gè)方向擺動(dòng)。 在事件循環(huán)與每個(gè)請(qǐng)求線程競(jìng)爭的情況下,是否沒有某種模式可以使它們成為基礎(chǔ)? 好吧,實(shí)際上是的!
但是在開始之前,讓我們看一下有關(guān)事件循環(huán)和每個(gè)請(qǐng)求線程的問題。 如果您對(duì)該解決方案更感興趣,則可以跳過接下來的兩個(gè)部分。
螺紋連接問題
事件循環(huán)
首先,“線程耦合”? 為什么要擔(dān)心? 對(duì)于事件循環(huán)來說,單線程本質(zhì)要求所有I / O都必須異步進(jìn)行。 如果需要阻止數(shù)據(jù)庫或HTTP調(diào)用,它將阻止單個(gè)事件循環(huán)線程并支撐系統(tǒng)。 這種限制本身就是一個(gè)很大的耦合問題,因?yàn)橐筊eactive將所有I / O耦合到異步狀態(tài)。 這意味著不再需要像JPA這樣的ORM來簡化對(duì)數(shù)據(jù)庫的訪問(因?yàn)镴PA需要阻止數(shù)據(jù)庫調(diào)用)。 是的,以前在應(yīng)用程序中刪除了40-60%的樣板代碼的東西現(xiàn)在已經(jīng)不可用了(請(qǐng)重新寫一遍!)
除了決定使用響應(yīng)式模式的限制性I / O之外,還限制了使用多個(gè)處理器的能力,因?yàn)橹挥幸粋€(gè)線程。 好的,反應(yīng)式引擎的實(shí)例已復(fù)制到每個(gè)CPU,但是它們不能共享狀態(tài)。 在兩個(gè)事件循環(huán)之間共享狀態(tài)的多線程含義很困難。 響應(yīng)式編程非常困難,更不用說向其中添加多線程了。 是的,事件循環(huán)之間的通信可以通過事件進(jìn)行。 但是,使用此方法在事件循環(huán)之間使共享狀態(tài)的重復(fù)副本保持同步會(huì)產(chǎn)生一些可以避免的問題。 基本上,您會(huì)被告知要設(shè)計(jì)您的反應(yīng)性系統(tǒng),以免發(fā)生這種情況。
因此,您被卡在一個(gè)線程上。 所以呢? 好吧,如果您執(zhí)行計(jì)算量大的操作(例如安全密碼學(xué)(JWT)),則會(huì)產(chǎn)生調(diào)度問題。 通過在單個(gè)線程上,必須先完成此操作,然后才能執(zhí)行其他任何操作。 使用多個(gè)線程,操作系統(tǒng)可以在時(shí)間上切入其他線程,以處理其他占用較少CPU資源的請(qǐng)求。 但是,您只有一個(gè)線程,因此所有可愛的操作系統(tǒng)線程調(diào)度現(xiàn)在都丟失了。 在維修其他任何東西之前,您都不得不等待昂貴的CPU密集型操作完成。
哦,請(qǐng)忽略這些問題! 我們開發(fā)人員喜歡性能。 響應(yīng)式的所有目的都是為了提高性能和改善可伸縮性。 較少的線程可以減少開銷,從而提高吞吐量。 好的,是的,我將擁有性能更好的生產(chǎn)系統(tǒng),從而可能降低硬件成本。 但是,由于來自單線程事件循環(huán)的耦合限制,構(gòu)建和增強(qiáng)該生產(chǎn)系統(tǒng)的速度將大大降低。 更不用說,必須重寫算法才能避免占用CPU。 與缺乏足夠的云硬件供應(yīng)相比,由于開發(fā)人員稀缺,因此爭論規(guī)模成本可能僅適用于那些罕見的大型系統(tǒng)。
我們會(huì)做出很多反應(yīng)。 這可能是因?yàn)槲覀冞€沒有充分考慮過這一點(diǎn)。 因此,可能是為什么Reactive框架警告不要更改整個(gè)銷售。 它們通常指示響應(yīng)模式僅適用于較小且較不復(fù)雜的系統(tǒng)。
每個(gè)請(qǐng)求線程(線程池)
另一方面,每個(gè)請(qǐng)求線程模式(例如Servlet 2.x)使用??線程池來處理擴(kuò)展。 它們分配一個(gè)線程來服務(wù)請(qǐng)求,并通過具有多個(gè)(通常是池化的)線程進(jìn)行擴(kuò)展。
我們可能會(huì)讀到許多文章稱Reactive超出了每個(gè)請(qǐng)求線程的規(guī)模限制,但是每個(gè)請(qǐng)求線程的主要問題實(shí)際上不是性能,也不是規(guī)模。 每個(gè)請(qǐng)求線程的問題在您的應(yīng)用程序中更為寬松,實(shí)際上會(huì)污染整個(gè)體系結(jié)構(gòu)。
要查看此問題,只需看一下調(diào)用方法:
Response result = object.method(identifier);該方法的實(shí)現(xiàn)應(yīng)如下:
@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()); }這給請(qǐng)求的線程帶來了一個(gè)耦合問題,可能會(huì)污染整個(gè)體系結(jié)構(gòu)。 是的,您剛剛在請(qǐng)求線程上放置了一個(gè)耦合到其他系統(tǒng)。
當(dāng)數(shù)據(jù)庫調(diào)用是同步的時(shí),HTTP調(diào)用也迫使下游系統(tǒng)同步響應(yīng)。 我們不能將HTTP調(diào)用更改為異步調(diào)用,因?yàn)檎?qǐng)求線程希望繼續(xù)執(zhí)行從該方法返回的結(jié)果。 與請(qǐng)求線程的這種同步耦合不僅限制了調(diào)用,還限制了下游系統(tǒng)必須提供同步響應(yīng)。 因此,每個(gè)請(qǐng)求線程的線程耦合可能會(huì)污染您的其他系統(tǒng),甚至可能污染整個(gè)體系結(jié)構(gòu)。 難怪同步HTTP調(diào)用的REST微服務(wù)模式如此流行! 這是一種迫使自己自上而下地在系統(tǒng)上的模式。 聽起來像每個(gè)請(qǐng)求線程和Reactive在強(qiáng)制一切自上而下支持自己方面都持有相同的觀點(diǎn)。
支持I / O的線程
總之,問題如下。
單線程事件循環(huán):
- 僅將您耦合到異步通信(不再提供簡單的JPA代碼)
- 只是避免了多線程,因?yàn)閺氖录?duì)列執(zhí)行事件的兩個(gè)線程會(huì)產(chǎn)生大量的同步問題(可能會(huì)降低解決方案的速度,并導(dǎo)致難以為最好的開發(fā)人員編寫的并發(fā)錯(cuò)誤)
- 失去了線程調(diào)度的優(yōu)勢(shì),即操作系統(tǒng)已花費(fèi)大量精力進(jìn)行優(yōu)化
而按請(qǐng)求線程解決方案:
- 僅將您耦合到同步通信(因?yàn)榭梢粤⒓纯吹浇Y(jié)果;不久后不會(huì)通過回調(diào))
- 由于管理更多的線程,因此具有較高的開銷(單線程事件循環(huán)),因此可伸縮性較差
實(shí)際上,可以考慮從同步通信(每個(gè)請(qǐng)求線程)到異步通信(單線程事件循環(huán))之間的線程池和響應(yīng)式單線程之間的鐘擺擺動(dòng)。 剩下的問題實(shí)際上是專門為支持每種類型的通信而構(gòu)建的線程模型的實(shí)現(xiàn)約束。 加上同步通信在下游系統(tǒng)上造成的耦合,這種擺動(dòng)到異步通信的舉動(dòng)并不是一件壞事。
所以問題是,為什么我們被迫只選擇一種溝通方式? 為什么我們不能同時(shí)使用同步和異步通信樣式?
好吧,我們不能將異步調(diào)用放入同步方法調(diào)用中。 沒有機(jī)會(huì)進(jìn)行回調(diào)。 是的,我們可以阻止在回調(diào)中等待,但是Reactive會(huì)認(rèn)為自己在規(guī)模上具有優(yōu)勢(shì),因?yàn)槠渲猩婕邦~外的線程開銷。 因此,我們需要異步代碼來允許同步調(diào)用。
但是,我們不能將同步調(diào)用放入事件循環(huán)中,因?yàn)樗鼤?huì)中斷事件循環(huán)線程。 因此,我們需要額外的線程來進(jìn)行同步調(diào)用,以允許事件循環(huán)線程繼續(xù)進(jìn)行其他事件。
反應(yīng)性就是答案。 使用調(diào)度程序:
Mono blockingWrapper = Mono.fromCallable(() -> { return /* make a remote synchronous call */ }).subscribeOn(Schedulers.elastic());來自http://projectreactor.io/docs/core/release/reference/#faq.wrap-blocking的代碼
是的,現(xiàn)在我們可以在事件循環(huán)中進(jìn)行同步調(diào)用了。 問題解決了(很好)。
好吧,如果您可以相信已將所有同步調(diào)用正確包裝在Callables中,則會(huì)對(duì)其進(jìn)行排序。 弄錯(cuò)了,那么您就阻塞了事件循環(huán)線程并暫停了應(yīng)用程序。 至少在多線程應(yīng)用程序中,只有特定請(qǐng)求受苦,而不是整個(gè)應(yīng)用程序受苦。
無論如何,對(duì)我而言,這似乎比實(shí)際解決問題更多的工作。 哦,等等,一切都需要自下而上地進(jìn)行反應(yīng),這樣才能解決此問題。 只是不要阻塞呼叫,而是將所有驅(qū)動(dòng)程序和整個(gè)技術(shù)堆棧更改為Reactive。 總體而言,“以一種僅與我們集成的方式改變一切以適合我們的方式”似乎非常接近技術(shù)供應(yīng)商的鎖定-無論如何,我認(rèn)為。
因此,我們可以考慮一個(gè)允許同步調(diào)用并且不非常依賴開發(fā)人員正確實(shí)現(xiàn)的解決方案嗎? 為什么是!
反轉(zhuǎn)螺紋聯(lián)軸器
異步通信驅(qū)動(dòng)的Reactive單線程事件循環(huán)(不好意思)被認(rèn)為是正確的解決方案。 開發(fā)人員使用調(diào)度程序解決了同步通信。 在這兩種情況下,Reactive函數(shù)都使用為其指定的線程來運(yùn)行:
- 異步函數(shù)與事件循環(huán)的線程一起執(zhí)行
- 通過調(diào)度程序中的線程執(zhí)行的同步功能
函數(shù)執(zhí)行線程的控制在很大程度上取決于開發(fā)人員能否正確執(zhí)行。 開發(fā)人員有足夠的精力專注于構(gòu)建代碼以滿足功能要求。 現(xiàn)在,開發(fā)人員密切參與了應(yīng)用程序的線程處理(每請(qǐng)求線程總是從開發(fā)人員那里某種程度上抽象出來的)。 對(duì)線程的這種親密關(guān)系大大增加了構(gòu)建任何Reactive的學(xué)習(xí)曲線。 另外,當(dāng)開發(fā)人員在凌晨2點(diǎn)將其拔出時(shí),他們會(huì)松開很多頭發(fā),以使代碼在該截止日期或生產(chǎn)修復(fù)中正常工作。
那么我們可以從必須正確執(zhí)行線程的工作中刪除開發(fā)人員嗎? 更重要的是,我們?cè)谀睦锟刂七x擇線程?
讓我們看一個(gè)簡單的事件循環(huán):
public interface AsynchronousFunction { void run(); } public void eventLoop() { for (;;) { AsynchronousFunction function = getNextFunction(); function.run(); } }好吧,我們唯一可以控制的對(duì)象就是異步函數(shù)本身。 使用Executor指定線程,我們可以如下增強(qiáng)事件循環(huán):
public interface AsynchronousFunction { Executor getExecutor(); void run(); } public void eventLoop() { for (;;) { AsynchronousFunction function = getNextFunction(); function.getExecutor().execute(() -> function.run()); } }現(xiàn)在,這允許異步函數(shù)指定其所需的線程,如下所示:
- 通過同步執(zhí)行器使用事件循環(huán)線程:getExecutor(){return(runnable)-> runnable.run(); }
- 通過線程池支持的Executor使用單獨(dú)的線程進(jìn)行同步調(diào)用:getExecutor(){return Executors.newCachedThreadPool(); }
控件被反轉(zhuǎn),以便開發(fā)人員不再負(fù)責(zé)指定線程。 該函數(shù)現(xiàn)在指定用于執(zhí)行自身的線程。
但是,我們?nèi)绾螌?zhí)行程序與功能關(guān)聯(lián)?
我們使用控制反轉(zhuǎn)的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 } }請(qǐng)注意,僅包含相關(guān)的ManagedFunction詳細(xì)信息。 請(qǐng)參閱(耦合)控件的反轉(zhuǎn)以獲取ManagedFunction的更多詳細(xì)信息。
通過使用ManagedFunction,我們可以將Executor與增強(qiáng)事件循環(huán)的每個(gè)函數(shù)相關(guān)聯(lián)。 (實(shí)際上,由于Executor封裝在ManagedFunction中,因此我們可以返回到原始事件循環(huán))。
因此,現(xiàn)在不再需要開發(fā)人員使用調(diào)度程序,因?yàn)镸anagedFunction負(fù)責(zé)使用哪個(gè)線程來執(zhí)行函數(shù)的邏輯。
但這只是將開發(fā)人員從代碼正確配置到配置的問題。 在為函數(shù)指定正確的線程(執(zhí)行程序)時(shí),如何減少開發(fā)人員的錯(cuò)誤?
確定執(zhí)行線程
ManagedFunction的一個(gè)屬性是所有對(duì)象都被依賴注入。 除非注入了依賴項(xiàng),否則沒有對(duì)系統(tǒng)其他方面的引用(強(qiáng)烈建議不要使用靜態(tài)引用)。 因此,ManagedFunction的依賴關(guān)系注入元數(shù)據(jù)提供了ManagedFunction使用的所有對(duì)象的詳細(xì)信息。
了解函數(shù)使用的對(duì)象有助于確定函數(shù)的異步/同步性質(zhì)。 要將JPA與數(shù)據(jù)庫一起使用,需要一個(gè)Connection(或DataSource)對(duì)象。 要對(duì)微服務(wù)進(jìn)行同步調(diào)用,需要HttpClient對(duì)象。 如果ManagedFunction不需要這些,則可以安全地考慮沒有進(jìn)行阻塞通信。 換句話說,如果ManagedFunction沒有注入HttpClient,則它將無法進(jìn)行HttpClient同步阻塞調(diào)用。 因此,可以安全地由事件循環(huán)線程執(zhí)行ManagedFunction,而不會(huì)暫停整個(gè)應(yīng)用程序。
因此,我們可以識(shí)別一組依賴關(guān)系,這些依賴關(guān)系指示ManagedFunction是否需要由單獨(dú)的線程池執(zhí)行。 我們知道系統(tǒng)中的所有依賴項(xiàng),因此可以將它們分類為異步/同步。 或更恰當(dāng)?shù)卣f,是否可以在事件循環(huán)線程上安全使用依賴項(xiàng)。 如果依賴關(guān)系不安全,則需要該依賴關(guān)系的ManagedFunctions由單獨(dú)的線程池執(zhí)行。 但是什么線程池?
我們只使用一個(gè)線程池嗎? 好吧,響應(yīng)式調(diào)度程序可以靈活地為涉及阻塞調(diào)用的各種功能使用/重用不同的線程池。 因此,在使用多個(gè)線程池時(shí),我們需要類似的靈活性。
我們通過將線程池映射到依賴項(xiàng)來使用多個(gè)線程池。 好的,這有點(diǎn)使您動(dòng)腦了。 因此,讓我們用一個(gè)例子來說明:
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 // ... }現(xiàn)在,我們具有以下線程配置:
| 相依性 | 線程池 |
| HttpClient | 線程池一 |
| 連接 | 線程池二 |
然后,我們使用依賴關(guān)系將ManagedFunctions映射到線程池:
| 托管功能 | 相依性 | 執(zhí)行者 |
| ManagedFunctionOne, ManagedFunctionTwo | (線程池表中沒有) | 事件循環(huán)線程 |
| ManagedFunction3 | HttpClient | 線程池一 |
| 托管功能四 | 連接(作為EntityManager的傳遞依賴項(xiàng)) | 線程池二 |
線程池(執(zhí)行器)用于ManagedFunction的決定現(xiàn)在只是映射配置。 如果某個(gè)依賴項(xiàng)調(diào)用了阻塞調(diào)用,它將被添加到線程池映射中。 使用此依賴項(xiàng)的ManagedFunction將不再在事件線程循環(huán)上執(zhí)行,從而避免了應(yīng)用程序暫停。
此外,大大減少了丟失阻塞呼叫的可能性。 由于對(duì)依賴項(xiàng)進(jìn)行分類相對(duì)容易,因此遺漏阻塞調(diào)用的機(jī)會(huì)較小。 另外,如果缺少依賴項(xiàng),則僅是對(duì)線程池映射的配置更改。 它是固定的,無需更改代碼。 隨著應(yīng)用程序的成長和發(fā)展,它特別有用。 這與要求代碼更改和開發(fā)人員需要認(rèn)真思考的反應(yīng)式調(diào)度程序不同。
由于現(xiàn)在由框架(而不是應(yīng)用程序代碼)控制執(zhí)行ManagedFunction的執(zhí)行線程,因此它有效地反轉(zhuǎn)了對(duì)執(zhí)行線程的控制。 開發(fā)人員代碼不再線程化。 框架根據(jù)ManagedFunctions的依賴關(guān)系特性對(duì)其進(jìn)行配置。
辦公樓層
從理論上講,這一切都很好,但是請(qǐng)向我展示工作代碼!
OfficeFloor( http://officefloor.net )是本文討論的線程控制模式反轉(zhuǎn)的實(shí)現(xiàn)。 我們發(fā)現(xiàn)框架的線程模型過于僵化,導(dǎo)致變通,例如Reactive Scheduler。 我們正在尋找基礎(chǔ)模式來創(chuàng)建不需要這種解決方法的框架。 可以在教程中找到代碼示例,我們重視所有反饋。
請(qǐng)注意,盡管OfficeFloor遵循線程控制的反轉(zhuǎn),但考慮其他方面(例如,依賴關(guān)系上下文,變異狀態(tài),線程局部變量,線程親和力,背壓和減少的鎖定以提高性能),其實(shí)際的線程模型更為復(fù)雜。 但是,這些是其他文章的主題。 但是,正如本文所強(qiáng)調(diào)的,OfficeFloor應(yīng)用程序的線程是基于依賴關(guān)系映射的簡單配置文件。
結(jié)論
線程的控制權(quán)反轉(zhuǎn)允許函數(shù)指定它自己的線程。 由于線程是由注入的Executor控制的,因此該模式稱為Thread Injection 。 通過允許注入,線程的選擇由配置而不是代碼確定。 這使開發(fā)人員免于將線程編碼到應(yīng)用程序中的潛在容易出錯(cuò)的錯(cuò)誤任務(wù)。
線程注入的另一個(gè)好處是可以根據(jù)應(yīng)用程序運(yùn)行的計(jì)算機(jī)來定制線程映射配置。 在具有許多CPU的計(jì)算機(jī)上,可以配置更多線程池以利用操作系統(tǒng)的線程調(diào)度。 在較小的計(jì)算機(jī)(例如嵌入式計(jì)算機(jī))上,可以更多地重用線程池(對(duì)于單用途應(yīng)用程序,甚至有可能不使用這些線程池,因?yàn)樗鼈兛梢匀萑套枞詼p少線程計(jì)數(shù))。 這將不會(huì)對(duì)應(yīng)用程序進(jìn)行任何代碼更改,而只需進(jìn)行配置更改。
此外,可能占用事件循環(huán)的計(jì)算量大的功能也可以移至單獨(dú)的線程池。 只需在線程池映射中添加此計(jì)算的依賴項(xiàng),所有進(jìn)行該計(jì)算的ManagedFunctions現(xiàn)在就不會(huì)占用事件循環(huán)線程。 線程注入的靈活性不僅僅是支持同步/異步通信。
由于線程注入全部由配置驅(qū)動(dòng),因此不需要更改代碼。 實(shí)際上,開發(fā)人員根本不需要任何線程編碼。 這是反應(yīng)式調(diào)度程序無法提供的。
因此,問題是,您是否想將自己綁定到單線程事件循環(huán),而這實(shí)際上只是異步I / O的單一目的實(shí)現(xiàn)? 還是您想使用更靈活的東西?
翻譯自: https://www.javacodegeeks.com/2019/04/spring-reactive-already-obsolete-inversion-thread-coupling.html
總結(jié)
以上是生活随笔為你收集整理的Spring Reactive已经过时了吗? 螺纹连接反转的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑gta5怎么找设置(GTA5电脑设置
- 下一篇: 平板wifi快捷键大全(手机wifi快捷