用AsyncEventBus實現單機的異步消息隊列

Java中同一應用內消息隊列的小玩法

背景

在某些特定的場景下,某些服務總有獨立於主鏈路,而又必須去做的事(簡稱爲下游鏈路):比如

  • 場景A:往數據庫中插入一條數據之後,下游鏈路需要同步的去處理消費(在同一應用中,就不考慮kafka等MQ了);
  • 場景B:系統中需要打印大量的log,log打印操作比較耗時但是在主鏈路中又高頻次;

場景A和場景B中,顯然當前主鏈路是不關注下游鏈路的處理結果的,需要立即返回。好處主要有:

  1. 降低系統RT值;
  2. 業務解耦合:避免下游鏈路影響上游

在線上環境,顯然不能採用隨時創建一個線程的方式來執行,從而需要一個全局線程池來操作,故而AsyncEventBus應運而生。

關鍵類,代碼舉例

  • Event:事件主體,提交和消費的數據載體
@Data
@AllArgsConstructor
public class MyEvent {
    private String name;
    private Integer age;
}
  • Listener:消費者,實際消費Event的地方
  1. 實現EventListener接口
  2. 接收到消息時,被調用的方法用@Subscribe註解
@Slf4j
public class MyEventListener implements EventListener {

	   /**
	    * 收到每條消息,實際上執行的方法:注意,需要這兩個註解
	    *
	    * @param event
	    */
	   @Subscribe
	   @AllowConcurrentEvents
	   public void listen(MyEvent event) {
	       try {
	           Thread.sleep(100);
	           log.info(" deal event "  + " " + JSON.toJSONString(event));
	       } catch (Exception e) {
	           log.error(" listen error " , e);
	       }
	   }
}

  • EventBus:控制中心
public class MyAsyncEventBus extends AsyncEventBus {
	
	   /**
	    * 在xml中實例化bean時,會調用此構造方法,將自定義的線程池交由AsyncEventBus使用
	    * 
	    * @param executor 
	    */
	   public MyAsyncEventBus(Executor executor) {
	       super(executor);
	   }
	
	   /**
	    * 在xml中實例化bean時,會調用此方法將自定義的Listener註冊進AsyncEventBus
	    * 
	    * @param eventListeners 
	    */
	   public synchronized void setEventListeners(Set<EventListener> eventListeners) {
	       
	       if (eventListeners != null) {
	           for (EventListener eventListener : eventListeners) {
	               // 調用父類AsyncEventBus的register方法,將Listeners註冊進EventBus中
	               this.register(eventListener);
	           }
	       }
	   }
	
	   /**
	    * 業務方調用此方法來提交事件,由AsyncEventBus分發至各個Listener,執行業務代碼
	    * 
	    * @param event 
	    */
	   @Override
	   public void post(Object event) {
	       try {
	           super.post(event);
	       } catch (Exception e) {
	           e.printStackTrace();
	       }
	   }
}

  • XML 方式配置bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName">

    <!-- 將自定義的MyEventListener 交由Spring託管 -->
    <bean id="myEventListener" class="com.jszhao.demo.bus.MyEventListener"/>

    <!-- 註冊一個線程池,供Bus使用 -->
    <bean id="eventExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10"/>
        <property name="maxPoolSize" value="20"/>
        <property name="queueCapacity" value="10000"/>
    </bean>

    <!-- 將自定義的EventBus 交由Spring託管 -->
    <!-- 實例化時,採用包含eventExecutor的構造方法,將前面生成的線程池注入 -->
    <!-- 實例化時,將前面註冊的Listener向EventBus註冊 -->
    <bean id="myEventBus" class="com.jszhao.demo.bus.MyAsyncEventBus">
        <constructor-arg ref="eventExecutor"/>
        <property name="eventListeners">
            <set>
                <ref bean="myEventListener"/>
            </set>
        </property>
    </bean>

</beans>
  • controller 層
@Slf4j
@RestController("bus")
public class BusController {

	   @Autowired
	   private MyAsyncEventBus eventBus;
	
	   @GetMapping("/add")
	   public void addEvent(String name, Integer age, int threadNum) {
	       for (int i = 0; i < threadNum; i++) {
	           MyEvent event = new MyEvent(name, age);
	           eventBus.post(event);
	       }
	       log.info("main method is end ");
	   }
}
  • 運行結果

入參:name= “jszhao” , age= 18, threadNum = 5

2019-10-10 22:05:40.689  INFO 25039 --- [nio-8080-exec-2] c.jszhao.demo.Controller.BusController   : main method is end 
2019-10-10 22:05:40.787  INFO 25039 --- [eventExecutor-6] com.jszhao.demo.bus.MyEventListener      :  deal event  MyEvent(name=jszhao, age=18)
2019-10-10 22:05:40.787  INFO 25039 --- [eventExecutor-7] com.jszhao.demo.bus.MyEventListener      :  deal event  MyEvent(name=jszhao, age=18)
2019-10-10 22:05:40.788  INFO 25039 --- [eventExecutor-8] com.jszhao.demo.bus.MyEventListener      :  deal event  MyEvent(name=jszhao, age=18)
2019-10-10 22:05:40.793  INFO 25039 --- [eventExecutor-9] com.jszhao.demo.bus.MyEventListener      :  deal event  MyEvent(name=jszhao, age=18)
2019-10-10 22:05:40.793  INFO 25039 --- [ventExecutor-10] com.jszhao.demo.bus.MyEventListener      :  deal event  MyEvent(name=jszhao, age=18)

顯然,從結果來看實現了我們的目的:主鏈路直接返回,下游消費event。

到了這一步,一個簡單的單機異步隊列就已經實現了。
接下來是源碼解析時刻。

核心代碼解析

我們直接看post方法,從這裏進去大致可以分爲這麼幾個步驟

  1. 獲取當前event註冊的Listener列表(在生產者線程中)
  2. 由 dispatcher分發Listener,將Listener添加到當前的隊列中(在生產者線程中)
  3. 開始消費當前隊列,直至隊列爲空。消費的代碼爲將event提交至線程池中執行(在生產者線程中)
  4. 線程池中線程開始執行Listener (線程池中的線程執行)

從上述流程中可以看吹,雖然1、2、3步都在生產者線程中運行,但這三步並沒有任何耗時的操作(I/O磁盤操作、網絡傳輸等可能造成阻塞的行爲),真正可能耗時的操作是執行Listener的時候,這步是在應用統一管控的線程池中執行,是在我們可控範圍內的。所以總體來說效果是不錯的。

    1. 獲取當前event註冊的Listener列表(在生產者線程中)
@Override
public void post(Object event) {
	  try {
	      super.post(event);
	  } catch (Exception e) {
	      e.printStackTrace();
	  }
}
public void post(Object event) {
	  Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
	   if (eventSubscribers.hasNext()) {
	     dispatcher.dispatch(event, eventSubscribers);
	   } else if (!(event instanceof DeadEvent)) {
	     // the event had no subscribers and was not itself a DeadEvent
	     post(new DeadEvent(this, event));
	   }
 }

深入subscribers.getSubscribers(event)

Iterator<Subscriber> getSubscribers(Object event) {
	   ImmutableSet<Class<?>> eventTypes = flattenHierarchy(event.getClass());
	
	   List<Iterator<Subscriber>> subscriberIterators =
	       Lists.newArrayListWithCapacity(eventTypes.size());
	
	   for (Class<?> eventType : eventTypes) {
	     CopyOnWriteArraySet<Subscriber> eventSubscribers = subscribers.get(eventType);
	     if (eventSubscribers != null) {
	       // eager no-copy snapshot
	       subscriberIterators.add(eventSubscribers.iterator());
	     }
	   }
	
	   return Iterators.concat(subscriberIterators.iterator());
  }

這裏subscribers是個Map,其定義爲

private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers =
      Maps.newConcurrentMap();

其中Key爲Event類型,value爲一個線程安全的List。從這裏可以看出:

同一個Event可以有多個下游依賴,AsyncEventBus可根據Listener的入參Event類型來判定,將所有相同Event類型的listener放入一個List中。 具體實現這裏不再贅述,讀者感興趣可以自行查看其實現。

到了這一步,我們就拿到了當前Event對應的Listener列表。

    1. 由 dispatcher分發Listener,將Listener添加到當前隊列中(在生產者線程中)
public void post(Object event) {
	  Iterator<Subscriber> eventSubscribers = subscribers.getSubscribers(event);
	   if (eventSubscribers.hasNext()) {
	     dispatcher.dispatch(event, eventSubscribers);
	   } else if (!(event instanceof DeadEvent)) {
	     // the event had no subscribers and was not itself a DeadEvent
	     post(new DeadEvent(this, event));
	   }
 }

其中dispatcher接口有三個實現,在這裏構造函數默認選用的是LegacyAsyncDispatcher,我們進入到dispatcher.dispatch方法內部來:

void dispatch(Object event, Iterator<Subscriber> subscribers) {
	   checkNotNull(event);
	   while (subscribers.hasNext()) {
	     queue.add(new EventWithSubscriber(event, subscribers.next()));
	   }
	
	   EventWithSubscriber e;
	   while ((e = queue.poll()) != null) {
	     e.subscriber.dispatchEvent(e.event);
	   }
}

在這裏發佈,其將本次Event對應的Listener列表信息全部加入了一個線程安全的隊列queue
而queue的定義如下:

private final ConcurrentLinkedQueue<EventWithSubscriber> queue = Queues.newConcurrentLinkedQueue();

在這一環節,event被提交到線程池中執行

    1. 消費的代碼爲將event提交至線程池中執行(在生產者線程中)
final void dispatchEvent(final Object event) {
    executor.execute(
        new Runnable() {
          @Override
          public void run() {
            try {
              invokeSubscriberMethod(event);
            } catch (InvocationTargetException e) {
              bus.handleSubscriberException(e.getCause(), context(event));
            }
          }
        });
  }
    1. 線程池中線程開始執行Listener (線程池中的線程執行)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章