异步EJB只是一个Gi头吗?
在之前的文章( 此處和此處 )中,我展示了當服務器負載沉重時,創建非阻塞異步應用程序可以提高性能。 EJB 3.1引入了@Asynchronous批注,用于指定方法將在將來的某個時間返回其結果。 Javadocs聲明必須返回void或Future 。 以下清單顯示了使用此注釋的服務示例:
Service2.java
注釋位于第4行。該方法返回String類型的Future ,并在第10行通過將輸出包裝在AsyncResult 。 在客戶端代碼調用EJB方法時,容器將攔截該調用并創建一個任務,該任務將在另一個線程上運行,以便它可以立即返回Future 。 當容器然后使用另一個線程運行任務時,它將調用EJB的方法并使用AsyncResult來完成給定調用者的Future 。 即使看起來與Internet上所有示例中的代碼完全一樣,此代碼也存在一些問題。 例如, Future類僅包含用于獲取Future結果的阻塞方法,而不包含用于在回調完成時注冊回調的任何方法。 這將導致如下所示的代碼,當容器處于加載狀態時,這是很糟糕的:
客戶端程序
//type 1 Future<String> f = service.foo(s); String s = f.get(); //blocks the thread, but at least others can run //... do something useful with the string...//type 2 Future<String> f = service.foo(s); while(!f.isDone()){try {Thread.sleep(100);} catch (InterruptedException e) {...} } String s = f.get(); //... do something useful with the string...這種代碼是不好的,因為它導致線程阻塞,這意味著它們在這段時間內無法做任何有用的事情。 當其他線程可以運行時,需要進行上下文切換,這會浪費時間和精力(有關成本或我以前的文章的結果,請參見這篇出色的文章)。 像這樣的代碼會使已經處于負載狀態的服務器承受更大的負載,并停止運行。
那么是否有可能使容器異步執行方法,而編寫不需要阻塞線程的客戶端呢? 它是。 下面的清單顯示了一個servlet。
AsyncServlet2.java
@WebServlet(urlPatterns = { "/AsyncServlet2" }, asyncSupported = true) public class AsyncServlet2 extends HttpServlet {@EJB private Service3 service;protected void doGet(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {final PrintWriter pw = response.getWriter();pw.write("<html><body>Started publishing with thread " + Thread.currentThread().getId() + "<br>");response.flushBuffer(); // send back to the browser NOWCompletableFuture<String> cf = new CompletableFuture<>();service.foo(cf);// since we need to keep the response open, we need to start an async contextfinal AsyncContext ctx = request.startAsync(request, response);cf.whenCompleteAsync((s, t)->{try {if(t!=null) throw t;pw.write("written in the future using thread " + Thread.currentThread().getId()+ "... service response is:");pw.write(s);pw.write("</body></html>");response.flushBuffer();ctx.complete(); // all done, free resources} catch (Throwable t2) { ...第1行聲明Servlet支持異步運行-不要忘記這一點! 第8-10行開始將數據寫入響應,但有趣的位在第13行,其中調用了異步服務方法。 我們沒有將Future用作返回類型,而是向其傳遞了CompletableFuture ,它用于將結果返回給我們。 怎么樣? 第16行代碼將啟動異步servlet上下文,因此我們仍然可以在doGet方法返回后寫入響應。 從第17行開始,然后有效地在CompletableFuture上注冊了一個回調,一旦完成CompletableFuture并返回結果,該回調將被調用。 這里沒有阻塞代碼–沒有線程被阻塞,沒有線程被輪詢,等待結果! 在負載下,服務器中的線程數可以保持最少,從而確保服務器可以高效運行,因為需要較少的上下文切換。
服務實現如下所示:
Service3.java
@Stateless public class Service3 {@Asynchronouspublic void foo(CompletableFuture<String> cf) {// simulate some long running processThread.sleep(5000);cf.complete("bar");} }第7行確實很丑陋,因為它會阻塞,但假裝這是代碼調用大多數Web服務客戶端和JDBC驅動程序會阻塞的API調用在Internet或慢速數據庫中遠程部署的Web服務。 或者,使用異步驅動程序 ,當結果可用時,完成第9行所示的將來。然后向CompletableFuture發出信號,可以調用在先前清單中注冊的回調。
這不只是使用簡單的回調嗎? 這肯定是相似的,下面的兩個清單顯示了使用自定義回調接口的解決方案。
AsyncServlet3.java
@WebServlet(urlPatterns = { "/AsyncServlet3" }, asyncSupported = true) public class AsyncServlet3 extends HttpServlet {@EJB private Service4 service;protected void doGet(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { ...final AsyncContext ctx = request.startAsync(request, response);service.foo(s -> { ...pw.write("</body></html>");response.flushBuffer();ctx.complete(); // all done, free resources ...Service4.java
@Stateless public class Service4 {@Asynchronouspublic void foo(Callback<String> c) {// simulate some long running processThread.sleep(5000);c.apply("bar");}public static interface Callback<T> {void apply(T t);} }同樣,在客戶端中,絕對沒有任何阻塞。 但是,由于以下原因,使用CompletableFuture的AsyncServlet2和Service3類的早期示例更好些:
- CompletableFuture的API允許出現異常/失敗,
- CompletableFuture類提供用于異步執行回調和相關任務的方法,即在fork-join池中,以便整個系統使用盡可能少的線程運行,從而可以更有效地處理并發性,
- 可將CompletableFuture與其他對象結合使用,以便您可以注冊僅在多個CompletableFuture完成后才能調用的回調,
- 回調不會立即被調用,而是池中有限數量的線程按它們應運行的順序為CompletableFuture的執行提供服務。
在第一個清單之后,我提到異步EJB方法的實現存在一些問題。 除了阻塞客戶端之外,另一個問題是,根據EJB 3.1 Spec的 4.5.3章,客戶端事務上下文不會通過異步方法調用傳播。 如果要使用@Asynchronous批注創建兩個可以并行運行并在單個事務中更新數據庫的方法,則該方法將無效。 這在某種程度上限制了@Asynchronous注釋的使用。
使用CompletableFuture ,您可能認為可以在同一個事務上下文中并行運行多個任務,方法是先在EJB中啟動一個事務,然后創建多個可運行對象,然后使用runAsync方法運行它們,該方法在執行中運行它們池,然后注冊一個回調以使用allOf方法完成所有操作后allOf 。 但是您可能會因為多種原因而失敗:
- 如果您使用容器管理的事務,那么一旦導致事務開始的EJB方法將控制權返回給容器,事務將被提交-如果那時您的期貨還沒有完成,則您將不得不阻塞運行EJB方法的線程這樣它就等待并行執行的結果,而阻塞正是我們要避免的,
- 如果運行任務的單個執行池中的所有線程都被阻塞,等待它們的數據庫調用應答,那么您將有可能創建性能不佳的解決方案–在這種情況下,您可以嘗試使用非阻塞的異步驅動程序 ,但不能每個數據庫都有這樣的驅動程序,
- 一旦任務在不同的線程(例如執行池中的線程)上運行,線程本地存儲(TLS)就不再可用,因為正在運行的線程與將工作提交到執行池并進行設置的線程不同在提交工作之前將值存入TLS,
- 諸如EntityManager 類的資源不是線程安全的 。 這意味著你無法通過EntityManager成提交給池的任務,而每個任務需要得到它自己的保持EntityManager實例,而是創建EntityManager取決于TLS(見下文)。
讓我們通過以下代碼更詳細地考慮TLS,該代碼顯示了一種異步服務方法,該服務方法試圖做幾件事以測試允許的操作。
Service5.java
@Stateless public class Service5 {@Resource ManagedExecutorService mes;@Resource EJBContext ctx;@PersistenceContext(name="asdf") EntityManager em;@Asynchronouspublic void foo(CompletableFuture<String> cf, final PrintWriter pw) {//pw.write("<br>inside the service we can rollback, i.e. we have access to the transaction");//ctx.setRollbackOnly();//in EJB we can use EMKeyValuePair kvp = new KeyValuePair("asdf");em.persist(kvp);Future<String> f = mes.submit(new Callable<String>() {@Overridepublic String call() throws Exception {try{ctx.setRollbackOnly();pw.write("<br/>inside executor service, we can rollback the transaction");}catch(Exception e){pw.write("<br/>inside executor service, we CANNOT rollback the transaction: " + e.getMessage());}try{//in task inside executor service we CANNOT use EMKeyValuePair kvp = new KeyValuePair("asdf");em.persist(kvp);pw.write("...inside executor service, we can use the EM");}catch(TransactionRequiredException e){pw.write("...inside executor service, we CANNOT use the EM: " + e.getMessage());} ...第12行沒有問題,您可以回滾當容器調用EJB方法時在第9行自動啟動的事務。 但是該事務將不是可能由調用第9行的代碼啟動的全局事務。第16行也沒問題,您可以使用EntityManager寫入由第9行開始的事務內部的數據庫。顯示了在不同線程上運行代碼的另一種方式,即使用Java EE 7中引入的ManagedExecutorService 。但是,這在任何時候都依賴TLS時也都會失敗,例如,第22行和第31行會導致異常,因為在第9行啟動的事務無法定位,因為使用TLS進行定位,并且第21-35行的代碼使用與第19行之前的代碼不同的線程運行。
下一個清單顯示,第11-14行在CompletableFuture上注冊的完成回調也與第4-10行在不同的線程中運行,因為在第6行的回調之外啟動提交事務的調用將在第6行失敗再次參見圖13,因為第13行的調用在TLS中搜索當前事務,并且因為運行第13行的線程與運行第6行的線程不同,所以找不到事務。 實際上,下面的清單實際上有一個不同的問題:處理對Web服務器的GET請求的線程運行第JBAS010152: APPLICATION ERROR: transaction still active in request with status 0和11行,然后返回,此時JBoss日志JBAS010152: APPLICATION ERROR: transaction still active in request with status 0 –即使線程運行第13行可以找到該事務,它是否仍處于活動狀態或容器是否已將其關閉也值得懷疑。
AsyncServlet5.java
@Resource UserTransaction ut;@Override protected void doGet(HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {ut.begin(); ...CompletableFuture<String> cf = new CompletableFuture<>();service.foo(cf, pw); ...cf.whenCompleteAsync((s, t)->{...ut.commit(); // => exception: "BaseTransaction.commit - ARJUNA016074: no transaction!"}); }事務顯然依賴于線程和TLS。 但這不僅僅是依賴TLS的事務。 以JPA為例,該JPA被配置為直接在TLS中存儲會話(即與數據庫的連接) ,或者被配置為將該會話的范圍限定為當前的JTA事務 ,而該事務又依賴于TLS。 或以使用從EJBContextImpl.getCallerPrincipal提取的Principal進行安全檢查為例,該Principal調用AllowedMethodsInformation.checkAllowed ,然后調用使用TLS的CurrentInvocationContext并簡單地返回(如果在TLS中找不到上下文),而不是進行適當的權限檢查如第112行所示。
這些對TLS的依賴意味著,在使用CompletableFuture或Java SE fork-join池或其他線程池(無論是否由容器管理)時,許多標準Java EE功能將不再起作用。
為了對Java EE公平起見,我在這里所做的事情都按設計工作! 規范實際上禁止在EJB容器中啟動新線程。 我記得十多年前我曾經使用過舊版本的Websphere進行過一次測試-啟動一個線程會引發異常,因為該容器確實嚴格遵守規范。 這是有道理的:不僅因為線程數應由容器管理,還因為Java EE對TLS的依賴意味著使用新線程會導致問題。 從某種意義上講,這意味著使用CompletableFuture是非法的,因為它使用了不受容器管理的線程池(該池由JVM管理)。 使用Java SE的ExecutorService也是如此。 Java EE 7的ManagedExecutorService是一個特例-它是規范的一部分,因此您可以使用它,但是您必須了解這樣做的含義。 EJB上的@Asynchronous批注也是如此。
結果是可以在Java EE容器中編寫異步非阻塞應用程序,但是您確實必須知道自己在做什么,并且可能必須手動處理安全性和事務之類的事情,這確實是個問題。首先使用Java EE容器的原因。
那么是否有可能編寫一個容器來消除對TLS的依賴以克服這些限制? 的確如此,但是解決方案并不僅僅依賴于Java EE。 該解決方案可能需要更改Java語言。 許多年前,在依賴注入之前,我曾經寫過POJO服務,它在方法之間傳遞了JDBC連接,即作為服務方法的參數。 我這樣做是為了可以在同一事務內(即在同一連接上)創建新的JDBC語句。 我所做的與JPA或EJB容器所需要做的事情并沒有什么不同。 但是,現代框架沒有使用TLS作為顯式傳遞連接或用戶之類的東西的方式,而是將TLS作為集中存儲“上下文”的位置,即連接,事務,安全信息等。 只要您在同一線程上運行,TLS就是隱藏此類樣板代碼的好方法。 讓我們假裝TLS從未被發明過。 我們如何在不強制每種方法都將其作為參數的情況下傳遞上下文? Scala的implicit關鍵字是一種解決方案。 您可以聲明參數可以隱式定位,這使編譯器難以將其添加到方法調用中。 因此,如果Java SE引入了這樣的機制,則Java EE不需要依賴TLS,我們可以構建真正的異步應用程序,在該應用程序中,容器可以像今天一樣通過檢查注釋來自動處理事務和安全性! 也就是說,當使用同步Java EE時,容器會知道何時提交事務-在啟動事務的方法調用結束時。 如果您異步運行,則需要顯式關閉事務,因為容器不再知道何時執行此操作。
當然,保持不阻塞的需要以及因此不依賴TLS的需要在很大程度上取決于當前的方案。 我不認為我今天在這里描述的問題是當今的普遍問題,而是解決市場利基市場的應用程序所面臨的問題。 只需看一下Java EE優秀工程師目前正在提供的工作數量,而同步編程就是其中的標準。 但是我確實相信,規模更大的IT軟件系統將變得越來越多,它們處理的數據越多,阻塞API就會成為一個問題。 我還認為,當前硬件增長速度的放緩使這個問題更加復雜。 有趣的是,Java是否a)是否需要跟上異步處理的趨勢,以及b)Java平臺是否會采取行動來固定對TLS的依賴。
翻譯自: https://www.javacodegeeks.com/2015/08/is-asynchronous-ejb-just-a-gimmick.html
總結
以上是生活随笔為你收集整理的异步EJB只是一个Gi头吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ddos攻击(慢DDOS攻击)
- 下一篇: 安卓待机耗电比苹果快(安卓待机耗电)