前言

大部分框架都是事件订阅功能,即观察者模式,或者叫事件机制。通过订阅某个事件,当触发事件时,回调某个方法。该功能非常的好用,而 SOFA 内部也设计了这个功能,并且内部大量使用了该功能。来看看是如何设计的。

源码分析

核心类有 3 个:

  • EventBus 事件总线
  • Event 事件,即被观察者
  • Subscriber 订阅者,即观察者

Subscriber 是个抽象类, 子类需要自己实现 onEvent 方法,即回调方法。还有一个是否同步执行的参数。

EventBus 类实现了注册功能,反注册功能(删除)。事件发生时通知订阅者功能。

内部使用一个“大型数据结构”保存事件和订阅者的信息。

ConcurrentHashMap<Class<? extends Event>, CopyOnWriteArraySet<Subscriber>> SUBSCRIBER_MAP

所有相关信息都保存在该数据结构中。

看看注册功能。

public static void register(Class<? extends Event> eventClass, Subscriber subscriber) {
CopyOnWriteArraySet<Subscriber> set = SUBSCRIBER_MAP.get(eventClass);
if (set == null) {
set = new CopyOnWriteArraySet<Subscriber>();
CopyOnWriteArraySet<Subscriber> old = SUBSCRIBER_MAP.putIfAbsent(eventClass, set);
if (old != null) {
set = old;
}
}
set.add(subscriber);
}

参数为 一个事件对象,一个订阅对象。

首先从 Map 中根据事件的 Class 获取对应的订阅者集合,注意,这里都是用的并发容器。

下面的判断有点意思,考虑到并发的情况,如果第一次获取 Set 是 null,则尝试创建一个并放进 Map,这里使用的并不是 put 方法,而是 putIfAbsent 方法,该方法作用等同于:

  if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);

所以,这里再一次考虑并发问题,如果这个间隙有其他线程 put 了,就可以获取到那个线程 put 的 Set。很谨慎。而且性能相比较锁要好很多。虽然这个方法并发量不会很高,但也是一种性能优化。

如果发生了并发,就使用已有的 Set,然后将 Set 放置到 Map 中,完成事件和订阅者的映射。

再看看取消注册方法。

public static void unRegister(Class<? extends Event> eventClass, Subscriber subscriber) {
CopyOnWriteArraySet<Subscriber> set = SUBSCRIBER_MAP.get(eventClass);
if (set != null) {
set.remove(subscriber);
}
}

很简单,就是直接删除。

再看看通知功能:

    public static void post(final Event event) {
if (!isEnable()) {
return;
}
CopyOnWriteArraySet<Subscriber> subscribers = SUBSCRIBER_MAP.get(event.getClass());
if (CommonUtils.isNotEmpty(subscribers)) {
for (final Subscriber subscriber : subscribers) {
if (subscriber.isSync()) {
handleEvent(subscriber, event);
} else { // 异步
AsyncRuntime.getAsyncThreadPool().execute(
new Runnable() {
@Override
public void run() {
handleEvent(subscriber, event);
}
});
}
}
}
}

首先看是否开启了总线功能,在性能测试的时候,可能是关闭的。

如果开启了,就根据给定的时间找到订阅者,循环调用 handleEvent 方法(其实就是调用订阅者的 onEvent 方法)。

这里有一个是否异步的判断,如果异步的,则在异步线程池执行。

这个异步线程池 AsyncRuntime 可以看一下:

public static ThreadPoolExecutor getAsyncThreadPool(boolean build) {
if (asyncThreadPool == null && build) {
synchronized (AsyncRuntime.class) {
if (asyncThreadPool == null && build) {
// 一些系统参数,可以从配置或者注册中心获取。
int coresize = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_CORE);
int maxsize = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_MAX);
int queuesize = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_QUEUE);
int keepAliveTime = RpcConfigs.getIntValue(RpcOptions.ASYNC_POOL_TIME); BlockingQueue<Runnable> queue = ThreadPoolUtils.buildQueue(queuesize);
NamedThreadFactory threadFactory = new NamedThreadFactory("SOFA-RPC-CB", true); RejectedExecutionHandler handler = new RejectedExecutionHandler() {
private int i = 1; @Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (i++ % 7 == 0) {
i = 1;
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Task:{} has been reject because of threadPool exhausted!" +
" pool:{}, active:{}, queue:{}, taskcnt: {}", r,
executor.getPoolSize(),
executor.getActiveCount(),
executor.getQueue().size(),
executor.getTaskCount());
}
}
throw new RejectedExecutionException("Callback handler thread pool has bean exhausted");
}
};
asyncThreadPool = ThreadPoolUtils.newCachedThreadPool(
coresize, maxsize, keepAliveTime, queue, threadFactory, handler);
}
}
}
return asyncThreadPool;
}

这里也做了双重检查锁。

默认核心线程大小 10,最大 200, 队列大小 256, 回收时间 60 秒。

因此,获取的队列就是 LinkedBlockingQueue。

这里的拒绝策略很有意思,每失败 6 次,打印详细信息,当前线程数,活动线程数量,队列 size, 任务总数,不知道为什么这么设计(6次??)。

目前框架中 Event 的实现很多,我们在之前的源码分析中也看到很多了。而订阅者目前只有一个 FaultToleranceSubscriber。用于容错处理。是 FaultToleranceModule 模块的功能。该功能也是个扩展点,当系统初始化的时候,会注册 ClientSyncReceiveEvent 事件和 ClientAsyncReceiveEvent。

总结

这个事件总线功能真是观察者模式的最佳实践,通过系统中发生的事件,能够让外部模块感知到并进行处理,比如上面介绍的容错模块。当发生订阅的事件后,外部模块能够响应,很完美。

SOFA 源码分析— 事件总线的更多相关文章

  1. Spark2.1.0之源码分析——事件总线

    阅读提示:阅读本文前,最好先阅读<Spark2.1.0之源码分析——事件总线>.<Spark2.1.0事件总线分析——ListenerBus的继承体系>及<Spark2. ...

  2. SOFA 源码分析 —— 服务引用过程

    前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ...

  3. 鸿蒙内核源码分析(事件控制篇) | 任务间多对多的同步方案 | 百篇博客分析OpenHarmony源码 | v30.02

    百篇博客系列篇.本篇为: v30.xx 鸿蒙内核源码分析(事件控制篇) | 任务间多对多的同步方案 | 51.c.h .o 进程通讯相关篇为: v26.xx 鸿蒙内核源码分析(自旋锁篇) | 自旋锁当 ...

  4. SOFA 源码分析 — 自动故障剔除

    前言 集群中通常一个服务有多个服务提供者.其中部分服务提供者可能由于网络,配置,长时间 fullgc ,线程池满,硬件故障等导致长连接还存活但是程序已经无法正常响应.单机故障剔除功能会将这部分异常的服 ...

  5. jQuery 2.0.3 源码分析 事件绑定 - bind/live/delegate/on

    事件(Event)是JavaScript应用跳动的心脏,通过使用JavaScript ,你可以监听特定事件的发生,并规定让某些事件发生以对这些事件做出响应 事件的基础就不重复讲解了,本来是定位源码分析 ...

  6. jQuery 2.0.3 源码分析 事件体系结构

    那么jQuery事件处理机制能帮我们处理那些问题? 毋容置疑首先要解决浏览器事件兼容问题 可以在一个事件类型上添加多个事件处理函数,可以一次添加多个事件类型的事件处理函数 提供了常用事件的便捷方法 支 ...

  7. SOFA 源码分析 — 调用方式

    前言 SOFARPC 提供了多种调用方式满足不同的场景. 例如,同步阻塞调用:异步 future 调用,Callback 回调调用,Oneway 调用. 每种调用模式都有对应的场景.类似于单进程中的调 ...

  8. SOFA 源码分析 — 负载均衡和一致性 Hash

    前言 SOFA 内置负载均衡,支持 5 种负载均衡算法,随机(默认算法),本地优先,轮询算法,一致性 hash,按权重负载轮询(不推荐,已被标注废弃). 一起看看他们的实现(重点还是一致性 hash) ...

  9. SOFA 源码分析 — 预热权重

    前言 SOFA-RPC 支持根据权重对服务进行预热功能,具体地址:预热权重. 引用官方文档: 预热权重功能让客户端机器能够根据服务端的相应权重进行流量的分发.该功能也常被用于集群内少数机器的启动场景. ...

随机推荐

  1. Java JDBC封装模式

    模仿DBUtils里面的一些用法,下面是一些简单的实现数据集的操作的方法 下面使用到的两个bean.首先是userbean package bean; public class user { Stri ...

  2. 调用awk的三种方式

    调用awk的三种方式 调用awk有三种方式,一种为Shell命令行方式,另外两种是将awk程序写入脚本文件,然后执行该脚本文件.三种方式的命令格式归纳如下: 一.在Shell命令行输入命令调用awk, ...

  3. UNIX环境高级编程——进程环境

    一.main函数 C程序总是从main函数开始.当内核执行C程序时,在调用main前先调用一个特殊的启动例程.可执行程序文件将此启动例程指定为程序的起始地址--这是由连接编译器设置的,而连接编译器则由 ...

  4. Java进阶(二十五)Java连接mysql数据库(底层实现)

    Java进阶(二十五)Java连接mysql数据库(底层实现) 前言 很长时间没有系统的使用java做项目了.现在需要使用java完成一个实验,其中涉及到java连接数据库.让自己来写,记忆中已无从搜 ...

  5. java,http的post和get

    使用Java发送GET.POST请求 --节选自<疯狂Java讲义>    URL的openConnection()方法将返回一个URLConnection对象,该对象表示应用程序和 UR ...

  6. listview异步加载图片并防止错位

    android listview 异步加载图片并防止错位 网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作. 如果不重用 conver ...

  7. C语言所有作业练习题

    2015.08.11 1.计算十进制 42 转换为二进制.八进制.十六进制分别对应的值 2.计算二进制 11010110 对应的十进制值 3.计算八进制 075 对应的十进制值 4.计算十六进制 0x ...

  8. JavaScript进阶(五)js中取小数整数部分函数

    js中取小数整数部分函数 丢弃小数部分,保留整数部分 js:parseInt(7/2) 向上取整,有小数就整数部分加1 js: Math.ceil(7/2) 四舍五入 js: Math.round(7 ...

  9. RHEL6.4上升级python从2.6.6到2.7.3

    RHEL6.4上升级python从2.6.6到2.7.3 原始安装好的redhat6.4上的python版本是2.6.6,不能满足实际需要.升级的方法很多,从源码升级或者从rpm包升级.其中从rpm包 ...

  10. RecyclerView添加Header和Footer

    使用过RecyclerView的同学就知道它并没有添加header和footer的方法,而ListView和GirdView都有,但是开发过程中难免有需求需要添加一个自定义的header或者foote ...