欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

分析几个有名中间件源码的核心类的设计 中间件 

程序员文章站 2022-03-08 15:41:23
...
    分析过不少中间件产品后,自己也设计过产品后,也需要在更高的高度上总结一下。本文先总结了一下核心类的定义与主要的设计特点,再进一步用druid,rocketmq,spring-jms等源码的核心类进行说明(包括启动/停止),之后还介绍了一下dubbo的核心类在哪?(它更像一个产业链结构,核心类控制采购与销售,中间可能层层外包出去)

    在讲核心类之前,其实在软件系统有之前,我们的社会组织,或者企业组织,都是这么个结构,核心类就是*,就是总经理。当然也有与dobbo对应的组织形态,控制头尾,中间层层分包出去。

    理解了核心类,也就很容易理解整个产品的的结构设计了,另外对于应用系统,对于分布式架构,对于微服务架构设计与组织,也有思想上的启发。类比一下,核心类类似DDD(领域驱动设计)中的聚合根,类似于组合多个resposity的service,类似于微服务中的聚合服务。



## 一、什么是核心类

我们说,擒贼先擒王,研究一个东西要抓核心,那一个软件产品的核心在哪?研究过一些产品后,发现产品都有核心类,自己也应用这个思想把控组件设计。

核心类是产品功能的最主要的实现类,也是类中的老大哥,它下面的兄弟众多,它可能只负责协调调用,也可能功能需要兄弟们协助,也可能根据不同的情况选择不同的兄弟,也可能安排助手去监督兄弟们的工作...

核心类不一定是最花时间的,但一定是稳定系统结构的,显示出整体的思想与高度。有助于在万事开头难的情况下跨出第一步。现在常说API接口优先,那是实现角度,复杂系统的功能是如何进行组织的,可能是更优先的设计。



核心类的基本结构与考量如下:

  • - 自身可能是工厂产生,或者是单例对象,一般复杂的都是工厂产生的。
  • - 有自身状态,有生命周期。构造,inti,优雅的启动与关闭,在构造中组建好自己的团队成员
  • - 它持有其它功能类成员,这些功能类一般不相互引用,但它把this传给功能类,可相互引用
  • - 特殊的成员比如,通讯成员,负责对外接收发送数据,也可能对内对外协议不同有不同的通讯员。如果外对支持多协议,还要插入一个通用API层。
  • - 成员基于接口引用,所以核心类持有的成员兄弟很多可替换,如果是策略模式,加载根据运行情况,可通过spi,或者aware spring容器去取,或者配置参数用工厂来产生,或者如dubbo统一建一个extension中心。
  • - 交给一些成员重要功能时,可能会给它设置一个引用自己的回调类,让核心类感知变化,协调资源
  • - 可能包含多个线程或者线程池,可能分工作线程与自身守护线程定时运行,可能需要锁进行同步,Synchronized优化偏向锁,自旋锁,锁再升级,性能可以的
  • - 包含共享变量,或者共享数据容器,注意volitile/concurrent/atom使用
  • - 核心类自身产生多个对象各自工作,一般用享元模式存放,即static map,当然也可能有个上层进行简单管理,比如多个jms消费容器,上头有注册器统一管理。




## 二、核心类例子

### 1. rocketmq中的BrokerController

rocketmq是阿里的消息中间件,它有一个重要模块是broker,功能是接收消息并存储,索引,分队列的,也是推送消息的客户端的。这个模块的核心类是org.apache.rocketmq.broker.BrokerController,它的特点有:

- 成员众多且不直接引用
  •   1. 有几个config成员,管不同组件的配置
  •   2. 有很多Manager成员,比如管消费偏移量,管客户端
  •   3. 有Service,比如扫描客户端通道状态与变化
  •   4. 有些Listener,比如监听消息到来
  •   5. 有负责内部通讯的romotingServer
  •   6. 有处理消息的Processor,有存储负责DefaultMessageStore等,各司其职。DefaultMessageStore也是存储方面的子核心类,结构关系很相似。
  •   7. 有很多BlockingQueue与ExecutorService,它们组合起来,再与Processor交给romotingServer用于处理接收不同类型的消息。

- 构造函数与initialize初始化方法
  •   1. 构造函数中有很多类似this.topicConfigManager = new TopicConfigManager(this);的语句,即产生了成员对象,也实现了核心类与功能类对象的相互引用。如果设计不好,走“捷径”,成员之间相互引用,就乱成毛线团了。而成员由于引用了核心类对象,找谁也找的到,也就无所不能了。
  •   2. initialize中,先由一些成员load加载数据,满足条件后,再进一步产生一些成员,比如存储管理,通讯管理等成员。

- init中安排成员工作
  •   1. 比如安排通讯成员工作。在初始化中,通讯成员启动后把Processor与ExecutorService交给它,通讯成员就开始工作了,监听tcp端口,处理请求了。那请求时需要核心类帮忙怎么办?通过SendMessageProcessor sendProcessor = new SendMessageProcessor(this);,处理器可以找到核心类,那工作都好做了。
  •   2. 比如安排存储成员工作。先产生了DLedgerRoleChangeHandler传给存储成员,存储发生变化时会通知这个handler,hander由于持有核心类对象,所以也无所不能。

- 生命周期管理
  •   1. 比如start()方法,让很多成员也start(),也让自己的定时守线线程启动起来。
  •   2. shutdown()让很多成员也shutdown(),线程池也shutdown了。

- 如何启动/停止核心类
  •   1. BrokerStartup用main方法开始,调用构造函数new 一个BrokerController。
  •   2. 并在Runtime停止时,用hook关闭核心类对象。
  •   3. 另外提一下,客户端client的核心类是懒加载的,有producer了才启动。没人用,不空转。


**通过上面的总结,一个简单的消息处理过程是这样的**:通讯成员接收客户端消息,它在线程池中用处理器处理时,处理器可以找到核心类,核心类会让存储存好数据,再返回结果,通讯成员就可以回复客户端消息接收成功了。

### 2. druid中的DruidDataSource

druid是阿里的数据库连接池产品,代理了几个常用的连接,并在所有操作中切入filter得到监控数据。这的最外层核心类是:com.alibaba.druid.pool.DruidDataSource,包括它父类。它的特点有:

- 成员众多
  •   1. 它不是上面那样很多功能类,而是很多基本数据,如:long弄的Count或者AtomicLong。正好符合统计监控的需要
  •   2. 重要的数据,即一堆真实的数据库连接。DruidConnectionHolder[]
  •   3. 有两个重要线程Thread,分别用于增加与减少真实的连接数。还有logStatsThread统计线程。
  •   4. 有个CountDownLatch,用于保证init操作在两个额外线程完成后完成。
  •   5. List<Filter>用于存放将插入监控的过滤器,Init中也反持有核心类。
  •   6. 有ReentrantLock用于控制共享数据的操作,其它略。

- 构造函数与初始化
  •   1. 由于它实现了jdbc的DataSource接口,可以就当一个DB数据源使用。在getConnection获取数据库连接时,进行Init()操作。
  •   2. Init中初始化一些值,包括连接的容器外,还启动了控制pool中连接数量的两个线程。是否可以用一个?
  •   3. 过滤器filter是插入sql操作中,真正进行监控的工具,核心类用List保存它们,而在核心类的Init中有:filter.init(this);说明每个过滤器都要持有核心类来工作,至少会把数据写在核心类的统计属性中。

- 关闭数据库连接
  •   1. close()中,interrupt这些线程,connHolder中的真正连接都close(),清空容器,清空filter的list。


**通过上面的总结,一个简单的连接过程是这样的**:调用DruidDataSource获取连接方法,如果DruidDataSource没初始化就进行初始化,从DruidDataSource中再拿到filter来处理统计,再用filter反持有的核心类DruidDataSource真正做事。实际中用了过滤链模式。这个druid原理很简单,但所有操作,所有对象要包装起来,琐碎工作量还是很大的。


### 3. spring-jms中的消费容器DefaultMessageListenerContainer

与前面的核心类相比,这个org.springframework.jms.listener.DefaultMessageListenerContainer并不是很突出,不过获取消息进行消费都是在这个容器中进行的。而且每一个注解@jmsListen都产生一个,它有多层父类,其特点有:

- 它有也不少属性成员
  •   1. 包括很多参数,重要成员有:Executor,transactionManager,messageListener,messageConverter等
  •   2. 还有放置runnable对象的容器Set<AsyncMessageListenerInvoker>,可放Executor中执行,线程之间有Monitor对象,用synchronized进行同步处理
  •   3. 它持有的线程启动后,很聪明的进行自我管理,可减少,可增加,可休息

- 它受spring生命周期管理,有明确的initialize()/start()/stop()方法。
- 它受上级管理
  •   1. 虽然它做核心功能,但由于它有很多相同的对象一起工作,所以有上层的管理者JmsListenerEndpointRegistry。
  •   2. JmsListenerEndpointRegistry用工厂JmsListenerContainerFactory构造出它后,用Map<String, MessageListenerContainer> listenerContainers保存它。算享元模式。
  •   3. 上级也试图对它们进行start()/stop()控制。


由于这个工具是在spring中,启动过程就是扫描注解,按@jmsListen产生这样的真正工作的容器,并接受spring的控制。

### 4.dubbo中的核心类

与前面几个产品中的核心类不同,回忆dubbo还真不如上面的这么明显存在一个核心类。

先整理下服务端接收调用过程例子:一个包含invocation的TCP请求过来,netty从早就建好的了channel中decode出来特定应用协议msg,看消息头,如果是业务处理msg,就找相应的handler处理消息,handler会从一个map表中找到invocation对应的invoker,invoker是由service统一包装出来的,再反射调用得到结果,再封装成msg,再由netty从channel中发回去。

服务端本来只有一个个service,通过dubbo产生上面的所有相关的东西。dubbo操作的过程是:

  • - 先把一个个service变成invoker
  • - 一个个invoker以invocation为key,存放在map中
  • - 启动一个netty通讯服务端,这个过程中要产生Netty用的decoder/encoder/msgHandler等类
  • - 要给msgHandler设置一个专门处理业务的内部handler
  • - 内部handler要能从msg中拿到invocation,再从map中拿到invoker
  • - 最后调用到service,得到结果,后面不提了。


这里,被spring加工的对象就是service,结果是让外部的各种请求可以调的到它。那么是否有设计一个核心类来做上面这一系列工作呢?没有。

或者说,由于dubbo的微核心化,大多数功能的实现类都是不确定的,在运行时由参数来确定。实现类统一放在extension中存放,甚至extension本身也有两种实现。在spring中用的话,那么上面的功能实际上由两个主要的类来实现。

核心类丙个:一个是ServiceBean,一个是协议如DubboProtocol。spring启动前者开始工作,前者再根据配置启动DubboProtocol工作,当然最主要的工作还是后者来做了。而且两个核心类的角度不同,ServiceBean对应一个service,但可以用多个Protocol暴露,而一个Protocol持有多个service的invoker,就是多对多的关系。

DubboProtocol持有的成员,与工作有:

  • - 持有一个New构造的真正处理RPC请求的requestHandler
  • - 持有exporterMap,让requestHandler可以找到对应的Invoker
  • - 提供参数启动具体的传输层,并把requestHandler交给它用,由于requestHandler是内部类,不用象上面用this引用核心类,就可以使用核心类了。
  • - 传输层具体怎么做就不管了,反正参数与最终的处理对象都给了它,算是分包出去了。
  • - 传输层会根据参数产生具体的netty,产生具体的encode/decode,再产生具体的serilize对象来处理tcp包中的dubbo协议包。


虽然dubbo的核心类不清晰,但顺着业务用例的流动,可以梳理出一个核心类调用另一个核心类工作的结构。如果不是要设计的这么灵活,特定的协议下,也可以写出一个清晰的核心类来做上面的工作。

### 5. 图

看着也蛮简单的,主要还是很多细节工作要做,而且细节要考虑的很仔细。比如rocketmq主要是存储,索引,队列和高可用这样的细节工作。

分析几个有名中间件源码的核心类的设计
            
    
    
        中间件 

## 三、总结

其实还有些小例子就不一一举例了,我在设计组件时,也基本上采取这样的设计,感觉比较清楚,再复杂都可以可以把握全局,如同现实生活中,做好业务就有一个好的公司架构,也有强大的核心。

自己设计中,就可以把相似的功能复制过来用,比如rocketmq的内部通讯可以拿来用;或者把局部好的设计抄过来用,比如线程的自我控制,通讯处理器与带有阻塞队列线程池的组合,细粒度控制;设置监听了解成员的工作;另外成员其实也是局部功能中心,它也可以是层次结构;成员可以在运行中替换,比如dubbo中动态生成适配器成员,按参数持有真正成员;成员可以统一存放,用多种方式加载,比如工厂,比如SPI...

话说现在最喜欢的源码还是rocketmq,1主多从小集合比kafka全对等好,这与看到的某支3城5中心中的10个组,以及某信抢红包的一个set,都是相同的理念;另外对文件的处理值得参考;还有通讯设计可以直接用...

了解了结构也可以更好的使用,给使用者提供简单明了的操作入口。
相关标签: 中间件