一文读懂微内核架构

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 微内核是一种典型的架构模式 ,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。

什么是微内核架构?


微内核是一种典型的架构模式 ,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。微内核架构在开源框架中的应用非常广泛,比如常见的 ShardingSphere 还有Dubbo都实现了自己的微内核架构。那么,在介绍什么是微内核架构之前,我们有必要先阐述这些开源框架会使用微内核架构的原因。


为什么要使用微内核架构?


微内核架构本质上是为了提高系统的扩展性 。所谓扩展性,是指系统在经历不可避免的变更时所具有的灵活性,以及针对提供这样的灵活性所需要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不需要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统具有较好的可扩展性。


就架构设计而言,扩展性是软件设计的永恒话题。而要实现系统扩展性,一种思路是提供可插拔式的机制来应对所发生的变化。当系统中现有的某个组件不满足要求时,我们可以实现一个新的组件来替换它,而整个过程对于系统的运行而言应该是无感知的,我们也可以根据需要随时完成这种新旧组件的替换。


1.png


比如在 ShardingSphere 中提供的分布式主键功能,分布式主键的实现可能有很多种,而扩展性在这个点上的体现就是, 我们可以使用任意一种新的分布式主键实现来替换原有的实现,而不需要依赖分布式主键的业务代码做任何的改变 。


微内核架构模式为这种实现扩展性的思路提供了架构设计上的支持,ShardingSphere 基于微内核架构实现了高度的扩展性。在介绍如何实现微内核架构之前,我们先对微内核架构的具体组成结构和基本原理做简要的阐述。


什么是微内核架构?


从组成结构上讲, 微内核架构包含两部分组件:内核系统和插件 。这里的内核系统通常提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各种业务代码,用来向内核系统增强或扩展额外的业务能力。在 ShardingSphere 中,前面提到的分布式主键就是插件,而 ShardingSphere 的运行时环境构成了内核系统。


2.png


那么这里的插件具体指的是什么呢?这就需要我们明确两个概念,一个概念就是经常在说的 API ,这是系统对外暴露的接口。而另一个概念就是 SPI(Service Provider Interface,服务提供接口),这是插件自身所具备的扩展点。就两者的关系而言,API 面向业务开发人员,而 SPI 面向框架开发人员,两者共同构成了 ShardingSphere 本身。


3.png


可插拔式的实现机制说起来简单,做起来却不容易,我们需要考虑两方面内容。一方面,我们需要梳理系统的变化并把它们抽象成多个 SPI 扩展点。另一方面, 当我们实现了这些 SPI 扩展点之后,就需要构建一个能够支持这种可插拔机制的具体实现,从而提供一种 SPI 运行时环境 。


如何实现微内核架构?


事实上,JDK 已经为我们提供了一种微内核架构的实现方式,就是JDK SPI。这种实现方式针对如何设计和实现 SPI 提出了一些开发和配置上的规范,ShardingSphere、Dubbo 使用的就是这种规范,只不过在这基础上进行了增强和优化。所以要理解如何实现微内核架构,我们不妨先看看JDK SPI 的工作原理。


JDK SPI


SPI(Service Provider Interface)主要是被框架开发人员使用的一种技术。例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。


下面我们通过一个简单的示例演示一下JDK SPI的使用方式:


  • 首先我们定义一个生成id键的接口,用来模拟id生成


publicinterfaceIdGenerator {
/*** 生成id* @return*/StringgenerateId();
}


  • 然后创建两个接口实现类,分别用来模拟uuid和序列id的生成


publicclassUuidGeneratorimplementsIdGenerator {
@OverridepublicStringgenerateId() {
returnUUID.randomUUID().toString();
    }
}
publicclassSequenceIdGeneratorimplementsIdGenerator {
privatefinalAtomicLongatomicId=newAtomicLong(100L);
@OverridepublicStringgenerateId() {
longleastId=this.atomicId.incrementAndGet();
returnString.valueOf(leastId);
    }
}


  • 在项目的resources/META-INF/services 目录下添加一个名为com.github.jianzh5.spi.IdGenerator的文件,这是 JDK SPI 需要读取的配置文件,内容如下:


com.github.jianzh5.spi.impl.UuidGeneratorcom.github.jianzh5.spi.impl.SequenceIdGenerator


  • 创建main方法,让其加载上述的配置文件,创建全部IdGenerator 接口实现的实例,并执行生成id的方法。


publicclassGeneratorMain {
publicstaticvoidmain(String[] args) {
ServiceLoader<IdGenerator>serviceLoader=ServiceLoader.load(IdGenerator.class);
Iterator<IdGenerator>iterator=serviceLoader.iterator();
while(iterator.hasNext()){
IdGeneratorgenerator=iterator.next();
Stringid=generator.generateId();
System.out.println(generator.getClass().getName() +"  >>id:"+id);
        }
    }
}


  • 执行结果如下:


4.png


JDK SPI 源码分析


通过上述示例,我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法,在这个方法中首先会尝试获取当前使用的 ClassLoader,然后调用 reload() 方法,调用关系如下图所示:


5.png


在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。


publicvoidreload() {
providers.clear();
lookupIterator=newLazyIterator(service, loader);
}


在前面的示例中,main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法:hasNext() 方法和 next() 方法。这里的 LazyIterator 中的 next() 方法最终调用的是其 nextService() 方法,hasNext() 方法最终调用的是 hasNextService() 方法,我们来看看 hasNextService()方法的具体实现:


privatestaticfinalStringPREFIX="META-INF/services/"; 
Enumeration<URL>configs=null; 
Iterator<String>pending=null; 
StringnextName=null; 
privatebooleanhasNextService() {
if (nextName!=null) {
returntrue;
 }
if (configs==null) {
try {
//META-INF/services/com.github.jianzh5.spi.IdGeneratorStringfullName=PREFIX+service.getName();
if (loader==null)
configs=ClassLoader.getSystemResources(fullName);
elseconfigs=loader.getResources(fullName);
  } catch (IOExceptionx) {
fail(service, "Error locating configuration files", x);
  }
 }
// 按行SPI遍历配置文件的内容 while ((pending==null) ||!pending.hasNext()) {
if (!configs.hasMoreElements()) {
returnfalse;
  }
// 解析配置文件 pending=parse(service, configs.nextElement());
 }
// 更新 nextName字段 nextName=pending.next();
returntrue;
}


在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法「负责实例化 hasNextService() 方法读取到的实现类」,其中会将实例化的对象放到 providers 集合中缓存起来,核心实现如下所示:


privateSnextService() { 
Stringcn=nextName; 
nextName=null; 
// 加载 nextName字段指定的类 Class<?>c=Class.forName(cn, false, loader); 
if (!service.isAssignableFrom(c)) { // 检测类型 fail(service, "Provider "+cn+" not a subtype"); 
    } 
Sp=service.cast(c.newInstance()); // 创建实现类的对象 providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存 returnp; 
} 


以上就是在 main() 方法中使用的迭代器的底层实现。最后,我们再来看一下 main() 方法中使用 ServiceLoader.iterator()方法拿到的迭代器是如何实现的,这个迭代器是依赖 LazyIterator 实现的一个匿名内部类,核心实现如下:


publicIterator<S>iterator() { 
returnnewIterator<S>() { 
// knownProviders用来迭代providers缓存 Iterator<Map.Entry<String,S>>knownProviders=providers.entrySet().iterator(); 
publicbooleanhasNext() { 
// 先走查询缓存,缓存查询失败,再通过LazyIterator加载 if (knownProviders.hasNext())  
returntrue; 
returnlookupIterator.hasNext(); 
        } 
publicSnext() { 
// 先走查询缓存,缓存查询失败,再通过 LazyIterator加载 if (knownProviders.hasNext()) 
returnknownProviders.next().getValue(); 
returnlookupIterator.next(); 
        } 
// 省略remove()方法     }; 
} 


JDK SPI 在 JDBC 中的应用


了解了 JDK SPI 实现的原理之后,我们再来看实践中 JDBC 是如何使用 JDK SPI 机制加载不同数据库厂商的实现类。


JDK 中只定义了一个 java.sql.Driver 接口,具体的实现是由不同数据库厂商来提供的。这里我们就以 MySQL 提供的 JDBC 实现包为例进行分析。


在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,如下所示:


com.mysql.cj.jdbc.Driver


在使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时候,我们会用到如下语句创建数据库连接:


Stringurl="jdbc:xxx://xxx:xxx/xxx"; 
Connectionconn=DriverManager.getConnection(url, username, pwd); 


「DriverManager 是 JDK 提供的数据库驱动管理器」,其中的代码片段,如下所示:


static { 
loadInitialDrivers();
println("JDBC DriverManager initialized"); 
} 


在调用 getConnection() 方法的时候,DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行;在 loadInitialDrivers()方法中通过 JDK SPI 扫描 Classpath 下 java.sql.Driver 接口实现类并实例化,核心实现如下所示:


privatestaticvoidloadInitialDrivers() { 
Stringdrivers=System.getProperty("jdbc.drivers") 
// 使用 JDK SPI机制加载所有 java.sql.Driver实现类 ServiceLoader<Driver>loadedDrivers=ServiceLoader.load(Driver.class); 
Iterator<Driver>driversIterator=loadedDrivers.iterator(); 
while(driversIterator.hasNext()) { 
driversIterator.next(); 
    } 
String[] driversList=drivers.split(":"); 
for (StringaDriver : driversList) { // 初始化Driver实现类 Class.forName(aDriver, true, 
ClassLoader.getSystemClassLoader()); 
    } 
} 


在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,同样有一段 static 静态代码块,这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中(CopyOnWriteArrayList 类型),如下所示:


static { 
java.sql.DriverManager.registerDriver(newDriver()); 
}


getConnection() 方法中,DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection,核心实现如下所示:


privatestaticConnectiongetConnection(Stringurl, java.util.Propertiesinfo, Class<?>caller) throwsSQLException { 
// 省略 try/catch代码块以及权限处理逻辑 for(DriverInfoaDriver : registeredDrivers) { 
Connectioncon=aDriver.driver.connect(url, info); 
returncon; 
    } 
} 


小结


本文我们详细讲述了微内核架构的一些基本概念并通过一个示例入手,介绍了 JDK 提供的 SPI 机制的基本使用,然后深入分析了 JDK SPI 的核心原理和底层实现,对其源码进行了深入剖析,最后我们以 MySQL 提供的 JDBC 实现为例,分析了 JDK SPI 在实践中的使用方式。


掌握了JDK的SPI机制就等于掌握了微内核架构的核心,以上,希望对你有所帮助!

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
Java API 数据库
Java一分钟之-JPA注解:@Entity, @Table, @Id等
【6月更文挑战第14天】Java Persistence API (JPA) 是Java开发中的ORM框架,通过注解简化数据访问层。本文介绍了三个核心注解:`@Entity`标识实体类,`@Table`自定义表名,`@Id`定义主键。易错点包括忘记添加`@Entity`、未正确设置主键。建议使用`@GeneratedValue`和`@Column`细化主键策略和字段映射。正确理解和应用这些注解能提高开发效率和代码质量。
967 3
|
机器学习/深度学习 人工智能 自然语言处理
AI初探:人工智能的定义、历史与未来展望
【7月更文第15天】在科技飞速发展的今天,人工智能(Artificial Intelligence, AI)已经成为推动社会进步的关键力量,渗透到我们生活的方方面面,从智能家居到自动驾驶汽车,从精准医疗到智能金融,无不展现出其深远的影响。本文旨在为读者揭开人工智能的神秘面纱,从基本概念出发,回顾其发展历程,并探索未来的无限可能。
1735 2
|
11月前
|
前端开发 JavaScript API
2025年前端框架是该选vue还是react?有了大模型-例如通义灵码辅助编码,就不用纠结了!vue用的多选react,react用的多选vue
本文比较了Vue和React两大前端框架,从状态管理、数据流、依赖注入、组件管理等方面进行了详细对比。当前版本和下载量数据显示React更为流行,但Vue在国内用户量增长迅速。Vue 3通过组合式API提供了更灵活的状态管理和组件逻辑复用,适合中小型项目;React则更适合大型项目和复杂交互逻辑。文章还给出了选型建议,强调了多框架学习的重要性,认为技术问题已不再是选型的关键,熟悉各框架的最佳实践更为重要。
6481 1
|
存储 SQL 关系型数据库
校园二手商品交易系统的设计与实现(论文+源码)_kaic
校园二手商品交易系统的设计与实现(论文+源码)_kaic
|
11月前
|
移动开发 前端开发 JavaScript
xss模糊测试
xss模糊测试
|
12月前
|
传感器 安全 物联网
蓝牙5.0:革新无线通信的新时代
蓝牙5.0:革新无线通信的新时代
536 12
|
人工智能 云计算
官宣!阿里云正式亮相巴黎奥运会
官宣!阿里云正式亮相巴黎奥运会
381 3
|
前端开发 安全 API
前端怎么样限制用户截图?
其实限制用户截图这个方案本身就不合理,除非整个设备都是定制的,在软件上阉割截图功能。为了这个需求添加更复杂的功能对于一些安全性没那么高的需求来说,有点本末倒置了。 下面聊聊正经方案: 1.对于后台系统敏感数据或者图片,主要是担心泄漏出去,可以采用斜45度七彩水印,想要完全去掉几乎不可能,就是观感比较差。 2.对于图片版权,可以使用现在主流的盲水印,之前看过腾讯云提供的服务,当然成本比较高,如果版权需求较大,使用起来效果比较好。 3.视频方案,tiktok下载下来的时候会有一个水印跑来跑去,当然这个是经过处理过的视频,非原画,画质损耗也比较高。Netflix等视频网站采用的是服务端权限控制,走的
|
存储 网络协议 Linux
高效调试与分析:利用ftrace进行Linux内核追踪(下)
高效调试与分析:利用ftrace进行Linux内核追踪
|
机器学习/深度学习 人工智能 算法
认识AI,探索AI如何奇思妙想
AI的快速演进正在加速AGI的到来,不止步于工具的AI让我们意识到它也绝不仅仅意味着算法和代码。当我们真的把人工智能当作智能体的时候总要去思考“AI是什么”这一个问题。关于意识的理论模型各自提供了意识产生机制于AI的不同解释,目前尚无定论,但它们都在学术界激发了广泛的讨论与研究。也欢迎你在评论区聊聊你会怎么向别人介绍AI?你认为AI是如何奇思妙想的,它具有意识吗?
426 0