關於DeferredResult的思考
使用SpringBoot搭建web程式,裡面內建了tomcat,一般都不會關心內部實現機制,上來就可以寫程式,並且可以跑起來。但是是思考了每次的請求是如何工作的。
簡單的來講就是tomcat是將每次請求都將封裝成一個Servlet,該Servlet來執行完業務邏輯程式碼,然後再有tomcat將資訊返回給呼叫方。每個Servlet是同步的。即在該servlet的業務邏輯做完了然後才釋放掉該Servlet。
但是servlet3提供了一個非同步的機制,即每次請求過來之後,可以先釋放該請求,但是會儲存一些資訊。業務邏輯由程式其他執行緒來處理,處理完成後將其值設定到DeferredResult裡面。然後再由容器將返回值返回給前端。
這樣做的好處:實現出現請求與業務IO分開,程式能夠處理更多的請求。
網上可以找到其他例子來學習DeferredResult是如何執行的,即在請求內部開啟一個執行緒來處理
@GetMapping public DeferredResult<String> queryDevice(){ DeferredResult<String> def = new DeferredResult<>(); new Thread(()->{ //處理業務邏輯 def.setResult("處理後的結果"); }).start(); return def; }
這樣很好理解,但是不能這樣做,為什麼,因為每一次執行緒的建立銷燬是消耗資源的,這樣頻繁的建立和銷燬非常影響效能。這個時候,可以提使用執行緒池來處理,對是可以這樣做的。是的,可以這樣做,但是需要考慮到,在某一時刻,可能會產生幾千個執行緒,這樣是非常多的,如果加上tomcat建立的Servlet執行緒數,那確實挺消耗資源的。
上面已經有了一個可行的方案,這裡提供我的一個思考,該思考是Java8新特性之後常用到的一個。
下面有三個類:
public abstract class Actor { public enum ActorType { ITC,/* 立刻消費. */ BLOCKING;/* 阻塞.*/ } /** * actor型別. */ public ActorType type; /** * actor名. */ public String name; public Actor(ActorType type) { this.type = type; this.name = this.getClass().getSimpleName(); } /** * 任務消費 */ public void future(Consumer<Void> c) { if (this.type.ordinal() == ActorType.BLOCKING.ordinal()) {//阻塞 ((ActorBlocking) this).push(c); return; } else { Misc.exeConsumer(c, null); } } }
public class ActorBlocking extends Actor { /** * 等待處理的Consumer. */ private ConcurrentLinkedQueue<Consumer<Void>> cs = new ConcurrentLinkedQueue<>(); /** * 擁有執行緒的個數 */ private int tc = 1; /** * cs的size */ private AtomicInteger size = new AtomicInteger(0); /** * 執行緒忙? */ public volatile boolean busy = false; public ActorBlocking() { super(ActorType.BLOCKING); } /** * 新增任務. */ public void push(Consumer<Void> c) { this.cs.add(c); this.size.incrementAndGet(); synchronized (this) {//通知執行緒消費資訊 this.notify(); } } /** * 執行緒忙? */ public boolean isBusy() { return this.busy; } /** * 佇列尺寸. */ public int size() { return this.size.get(); } public int getTc() { return tc; } public void setTc(int tc) { this.tc = tc < 1 ? 1 : tc; } /** * 啟動執行緒 */ protected void start() { ActorBlocking ab = this; ExecutorService ex = Executors.newFixedThreadPool(this.tc);//建立執行緒池 for (int i = 0; i < tc; i++) { ex.execute(() -> { while (true) { ab.run(); } }); } } /** * 搶佔式消費任務 */ private void run() { Consumer<Void> c = this.cs.poll(); if (c == null) { synchronized (this) { try { this.wait(); } catch (InterruptedException e) { } } c = this.cs.poll(); } if (c != null) /* 搶佔式. */ { this.size.decrementAndGet(); this.busy = true; Misc.exeConsumer(c, null); this.busy = false; } } }
@Component public class AppActorBlocking extends ActorBlocking { //可以設定CPU*2的 private int threadSize = 4; public AppActorBlocking() { this.setTc(threadSize);//設定執行緒數量 this.start(); } }
該方法是工具Misc類總的方法:
/** * 執行Consumer並將異常化解在內部. */ public static final <T> boolean exeConsumer(Consumer<T> c, T t) { try { c.accept(t); return true; } catch (Exception e) { if (logger.isWarnEnabled()) { logger.warn("{}", Misc.trace(new Throwable())); } if (logger.isWarnEnabled()) { logger.warn("t: {}, e: {}", t, Misc.trace(e)); } return false; } }
如何呼叫:
@Autowired public AppActorBlocking appBlocking; public void method(){ appBlocking.future(v->{ //處理邏輯程式碼 }); }
上面的程式碼理解是所有的業務邏輯都是一個個Task,每一次請求過來,那麼我就將業務邏輯程式碼生成一個Task,放入到佇列中,然後由執行緒去取其中的任務來消費。
這裡僅僅是換了一個思路,不是由執行緒池去建立執行緒來處理,而是建立幾個執行緒,然後搶佔式的去消費任務,而過來的每次請求,都會放入到佇列中。
DeferredResult的非同步處理能夠提升一些伺服器的效能,處理更多的連線數,但是一個WEB程式,處理連線數還與內建預設的tomcat相關(SpringBoot下還有其他容器),即tomcat預設的處理最大連線數為200,除了最大連線數,還有一個tomcat的最大處理執行緒數,如果該處設定小了,那麼併發也一定會小,在設定這些之外,需要設定一個等待佇列的大小,總有一些請求是不能被處理的,但又不能拒絕掉,否則使用者體驗特別不好,那麼就進入到等待佇列中,等tomcat有空閒的執行緒再來處理等待佇列中的執行緒。
至於什麼時候用到該DeferredResult,如果是訪問量不大的程式,如管理系統,沒必要使用到這個,畢竟沒有訪問量,反而增大了開發量,但是如果做了很好的封裝,那麼就沒關係了,這個就考量各自程式設計師的水平了。