【手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理】

手写实现一个简单版的Dubbo,深刻理解RPC框架的底层实现原理

  • RPC框架简介
  • 了解Dubbo的实现原理
    • 服务暴露
    • 服务引入
    • 服务调用
  • 手写实现一个简单版的Dubbo
    • 服务暴露
      • ServiceBean
      • ProxyFactory#getInvoker
      • Protocol#export
      • RegistryProtocol#export
    • 服务引入
      • RegistryProto#refer
      • DubboProtocol#refer
    • 服务调用
      • 服务消费者
      • 服务提供者
    • 与Spring对接
  • 代码

我越来越觉得学习一个开源框架或者中间件的底层原理,最好的方法不是看源码,而是自己尝试去写一个。之前看过许多开源框架和中间件的源码,但是随着时间的推移,多多少少都有点遗忘了,遗忘之后又花时间去把它捡一捡,于是就在这捡起又遗忘,遗忘又捡起这样来来回回的折腾中浪费了许多时间。

Dubbo的源码我很早以前就看过了,而且看了不止一遍,现在对于Dubbo的底层原理只记得个大概,至于源码中的细节,已经忘得差不多了。最近想起之前学习过Dubbo源码,为了加深印象,于是乎自己动手写了一个。这自己动手写跟只用眼睛看,效果还真不一样,因此写成文章分享出来。

RPC框架简介

在以前都是单体应用的年代,是不需要RPC框架的,那时候还不知道RPC是什么,所有的数据都是来自于本地的数据库,然后全都是本地的方法调用。

在这里插入图片描述

但是随着业务的发展,访问量的不断增加,原先的单体应用已经不能满足需求,于是要做应用拆分,原先的单体应用被拆成了多个服务,然后就出现了服务间调用的问题。

在这里插入图片描述

这样复杂度就一下子上来了,如何解决服务间调用的问题呢?我们可以自己写代码,通过http的方式去访问对方,或者通过Socket连上对方的系统进行通信,发送调用的方法名和调用参数等信息。但是这种方式有点笨,因为这种网络通信的代码一般都是重复的,而且也容易出错。

那么有没有什么办法可以让远程调用变成像之前单体应用那样就调一个本地方法,就能获取到返回结果呢?那就是引入一个RPC框架,RPC框架封装了远程调用的底层逻辑,屏蔽掉了网络通信的过程,让API调用工程师们简单调一下方法,就可以获取到远程系统返回的方法调用结果,于是这些API调用工程师们就觉得自己很牛B了。

在这里插入图片描述

我们当然不能当这种API调用工程师,要不然35岁之后就要强势加入骑士军团了。

在这里插入图片描述

其实RPC框架的原理并没有多复杂。

比如现在有一个 UserService接口,在以前还是单体应用的时候,Spring给我们注入的是一个实现类,我们调用这个接口的方法时就可以调用UserServiceImpl实现类。

在这里插入图片描述

而现在这个实现类在别的系统上,我们拿不到,于是RPC框架给我们注入的是一个代理对象,这个代理对象也实现了这个UserService接口,通过这个代理对象可以访问到对方系统的UserServiceImpl。然后RPC框架通过一个stub存根封装了网络通信的逻辑,当我们调用UserService接口的某个方法时,实际上是通过代理对象调用到了底层的存根类,stub存根再把我们调用的方法和参数封装为网络协议包,发送到对端系统。对端系统也有一个stub存根,接收到发送过来的协议包后,解析出要调用的方法和参数,交给UserServiceImpl去处理,然后再把处理结果通过stub存根返回给我们本地的存根,我们本地的存根再把结果返回给我们。

在这里插入图片描述

但是A系统怎么知道B系统的ip地址和端口好呢?如果是在A系统上配死那就太low了,一旦B系统改了ip地址或端口,A系统就要改配置重启。因此,还需要一个注册中心存储B系统发布的ip地址和端口,A系统从注册中心拉取B系统的ip地址和端口。此时B系统就是服务提供者,把ip地址和端口注册到注册中心这个动作就是服务暴露,而A系统就是服务消费者,从注册中心拉取服务提供者的ip地址端口等信息这个动作就是服务引入,A系统调用B系统这个动作就叫服务调用

在这里插入图片描述
这样,一个RPC调用框架就形成了。

了解Dubbo的实现原理

但是,理解到这一层,还远远不够,毕竟别人可以面试前把这些背一背,面试的时候忽悠面试官。于是,我们要阅读源码,对开源框架有更深一层的理解,这样才能让面试官从一众的渣渣中把我们区分出来。

Dubbo是众多的RPC框架中用的比较多的一个,我们来参考参考Dubbo的原理,也是分为服务暴露、服务引入、服务调用三块。

服务暴露

首先我们写的类是这样子的:
在这里插入图片描述

UserServiceImpl上声明了@Service注解(这个是Dubbo自己定义的@Service注解,不是Spring的那个),就代表我们要把这个UserServiceImpl暴露出来供服务消费者调用。

Dubbo要扫描它,然后把它封装成ServiceBean,ServiceBean是Dubbo封装的一个用来做服务暴露的实现类,它实现了Spring的ApplicationListener<ContextRefreshedEvent>接口,会监听ContextRefreshedEvent容器刷新完成事件触发服务暴露,而ServiceBean的export方法就是服务暴露的入口。

在这里插入图片描述

Dubbo在服务暴露中,一共做了三件事:

  1. 把我们的UserServiceImpl对象包装成Invoker,再把Invoker包装成Exporter,再把Exporter放入一个map中,key是根据实现类计算出来的一个唯一的serviceKey。这一步相当于是本地注册,方便后面接收到远程调用请求时,根据请求信息计算出serviceKey,找到目标实现类。
  2. 开启Netty服务端,用于接收远程调用请求。
  3. ip地址、端口、协议等服务信息注册到注册中心,这些信息注册到注册中心后,消费方就可以从注册中心中获取到服务提供者的信息了。

在这里插入图片描述

服务引入

至于服务引入,我们写的消费者类一般是这样:
在这里插入图片描述

属性上声明了这个@Reference注解,表示这个属性需要通过Dubbo的服务引入来进行属性注入。

那么,Dubbo就是扫描所有被@Reference注解修饰的属性,进行属性注入。Dubbo这里处理属性注入的逻辑与Spring处理@Autowired的逻辑大体是相同的,这里先不用管(后面会讲解)。扫描到的属性,通过ReferenceBean的get()方法触发服务引入,返回一个代理对象,注入到该属性中。

在这里插入图片描述

而服务引入里面做了些啥呢?其实就是从注册中心中获取服务提供者的ip端口等信息,开启NettyClient连接上服务提供者,然后封装到一个Invoker中,这个Invoker就等同于上面说的存根类。然后通过动态代理返回一个代理对象,这个代理对象会调用Invoker进行远程方法调用。

在这里插入图片描述

服务调用

服务调用的逻辑自然是被隐藏在了Invoker里面。服务间通过Netty进行网络通信,服务消费者把调用的方法名,接口的类全限定名,方法参数等封装成一个Invocation对象,然后序列化成二进制,通过Netty发送给服务提供者,服务提供者的Netty接收到服务消费者发来的数据时,会反序列化成Invocation对象,根据服务消费者提供的信息,找到具体的实现类进行处理,处理结果再由Netty返回给服务消费者。

在这里插入图片描述

最终整体流程就是这样:
在这里插入图片描述

没有看过源码的,也可以记一下这张图,然后面试的时候喷给面试官,也能拿个60分。

按照这个思路,我们就可以动手写代码了。

手写实现一个简单版的Dubbo

我们写代码的顺序还是按照服务暴露、服务引入、服务调用的顺序来,但是我们实现的迷你版Dubbo是要与Spring对接的,所以最后还要实现与Spring对接的逻辑。

这里要注意,下面还是以一边讲解一边画图的方式进行,不会贴代码。因为如果贴代码的话,这文章没个几万字是结束不了的。想看代码的可以到代码仓去下载,文末会附上代码仓的地址。

服务暴露

ServiceBean

服务暴露的逻辑从ServiceBean开始,我们就先从ServiceBean开始实现我们的迷你版Dubbo。

我们的ServiceBean也是实现ApplicationListener<ContextRefreshedEvent>接口,在onApplicationEvent(ContextRefreshedEvent event)方法中调用export()方法触发服务暴露。

在这里插入图片描述

export()方法首先要读取注册中心的配置,Dubbo是通过RegistryConfig类型封装注册中心的配置的,我们也定义一个RegistryConfig类封装注册中心的配置,这里通过RegistryConfig获取到注册中心的ip地址和端口等信息。

Dubbo是通过URL对象去封装服务暴露的信息,比如协议、ip地址、端口、访问路径(调哪个类的哪个方法)、方法参数等信息,然后注册到注册中心的也是这个URL对象转成的url格式的字符串。我们这里也定义一个URL对象去存这些信息,我们组装我们的URL对象。

URL中包含的协议、端口等信息,可以通过ProtocolConfig进行配置,然后从ProtocolConfig中取出放入到URL对象中。

封装好URL对象后,接下来就是通过ProxyFactory(代理工厂)生成服务提供方的Invoker,这个Invoker保存了真正的服务实现类。

最后就是通过Protocol对象的export方法进行服务暴露,这里也是参考Dubbo的,Dubbo真正的服务暴露逻辑是封装在Protocol的export方法里面的,本地注册、开启Netty、注册相关信息到注册中心等动作都是在这里面进行。我们也定义一个Protocol接口。

这样看来,ServiceBean需要RegistryConfig和ProtocolConfig这两个对象,我们可以通过ApplicationContext去获取,因此ServiceBean需要实现ApplicationContextAware这个接口,通过这个接口获取到ApplicationContext,然后ServiceBean还要实现InitializingBean接口,在InitializingBean接口的afterPropertiesSet()方法里面通过ApplicationContext获取RegistryConfig和ProtocolConfig这两个对象。

这里的ApplicationContextAware、InitializingBean、ApplicationListener等接口都是Spring的接口,不熟悉的可以去补一下Spring的知识。

在这里插入图片描述

ProxyFactory#getInvoker

ProxyFactory.getInvoker(ref, this.interfaceClass, url) 这一步是封装具体实现类ref为Invoker的。这个Invoker的invoke方法会从Invocation中获取方法名、方法参数类型和方法参数,然后通过反射调用具体实现类ref。

在这里插入图片描述

Protocol#export

protocol.export(registryURL, invoker);这一步是真正进行服务暴露的,但是这里的protocol其实是一个代理类,Dubbo通过他自己实现的SPI机制,会调用到具体的Protocol实现类。我们这里也是一个代理类,使用的时JDK的动态代理,但是我们就不实现自己的SPI机制了,我们就使用Java的SPI机制。在InvocationHandler的invoke方法中通过Java的SPI机制加载所有的实现类,循环遍历进行匹配,匹配逻辑是看URL中的protocol属性,也就是url中的协议,然后反射调用匹配到的具体实现类。

在这里插入图片描述

现在的整体流程就走到这里:
在这里插入图片描述

RegistryProtocol#export

这里的URL的protocol属性是registry,因此这里匹配到的是RegistryProtocol类型的Protocol,于是会调用到RegistryProtocol的export方法。

RegistryProtocol的export方法又会再次调用protocol的export方法,这里的protocol依然是代理对象,但是这次的URL的protocol属性是dubbo,因此会调用到DubboProtocol的export方法。

DubboProtocol的export方法会把Invoker包装成Exporter,用接口类全限定名作为key,Exporter作为value,放入到一个map,这样当接收到远程调用请求时,就可以通过接口名找到对应的Invoker,进而调用里面的实现类。

DubboProtocol的export方法接下来会开启Netty服务端,用于接收远程调用请求。

DubboProtocol的export方法结束后返回到RegistryProtocol的export方法,接下来会调用注册中心的客户端把服务暴露信息注册到注册中心,比如注册中心是Zookeeper,则通过Zookeeper的客户端把服务暴露信息发布到Zookeeper注册中心。这里注册中心的具体类型还是通过动态代理加上Java的SPI机制来动态进行匹配的,而注册到注册中心的信息也是url格式的。

在这里插入图片描述

到这里,服务暴露的流程就结束了:
在这里插入图片描述

服务引入

服务引入的入口是在ReferenceBean的get()方法,我们ReferenceBean的get()的会返回一个代理对象,注入到被@Reference注解修饰的属性上。这里先不用管ReferenceBean的get()方法如何被调用到,我们先完成ReferenceBean的get()方法的服务引入逻辑。

ReferenceBean的get()方法第一步也是通过RegistryConfig获取到注册中心的ip和端口等信息。

然后调用Protocol的refer方法进行服务引入,这个方法会返回一个Invoker对象,这个Invoker对象的invoke方法会通过Netty向远端发起远程调用。

最后通过ProxyFactory的getProxy方法把Invoker封装到一个代理对象中,这里我们还是使用JDK的动态代理,InvocationHandler的invoke方法会调用Invoker的invoke方法。

在这里插入图片描述

RegistryProto#refer

Protocol的refer方法是真正进行服务引入的方法,这里还是通过动态代理,首先走到RegistryProto的refer方法

RegistryProto的refer方法返回的Invoker对象是比较复杂的,有个几层的嵌套。

考虑到服务提供者有可能是以集群的形式部署的,因此我们这里定义了一个ClusterInvoker类型的Invoker与之对应。ClusterInvoker包装了一个RegistryDirectory对象,它是一个服务目录,里面保存了一个List<Invoker>,这里的一个Invoker对应一个服务提供者。这样我们的服务就具备一定的容错能力,可以通过负载均衡选出一个Invoker,一个调不通,可以换下一个。

在这里插入图片描述

那List<Invoker>里面的Invoker是怎么来的呢?

首先,我们会监听注册中心,一旦服务提供者上下线导致注册中心中的信息有变动,我们会收到通知,收到通知后我们重新从注册中心中查询服务提供者的url。同时在创建RegistryDirectory对象时,RegistryDirectory的构造方法也会主动去注册中心查一次。

然后,我们定义了一个NotifyListener接口,我们的RegistryDirectory实现了NotifyListener接口,NotifyListener的notify方法接收从注册中心查询回来的url,循环调用protocol.refer(url, serviceType)方法。protocol.refer(url, serviceType)会调用到DubboProtocol的refer方法,DubboProtocol的refer方法就会返回与服务提供者对应的一个Invoker。

在这里插入图片描述

DubboProtocol#refer

DubboProtocol的refer方法会从参数url中解析出服务提供者的ip地址和端口号,开启Netty客户端,连接到服务提供者,然后包装成一个Invoker返回。

在这里插入图片描述

那么服务引入的整体逻辑就是这样:

在这里插入图片描述

服务调用

服务消费者

服务消费者的代理对象会通过InvocationHandler的invoke方法,调用到ClusterInvoker的invoke方法。

ClusterInvoker的invoke方法调用RegistryDirectory的list方法获取List<Invoker>,然后通过负载均衡算法选出一个进行调用,调用失败则切换下一个。

负载均衡选出一个Invoker后,调用Invoker的invoke方法就会进入到DubboInvoker的invoke方法。DubboInvoker的invoke方法把接口类全限定名、方法名、方法参数类型、方法参数等信息包装成Invocation对象,调用Netty客户端发送请求。

Netty客户端的编解码器会把Invocation对象序列化成二进制,然后发送到网络。

因为Netty是异步的,因此这里要创建一个Future对象然后绑定一个id,发送请求时把这id带上,服务提供者处理完后返回处理结果时同时返回这个id,服务消费者就能通过id拿到Future,设置返回结果到这个Future上,那么当时发送请求的线程就能通过这个Future拿到返回结果。

在这里插入图片描述

服务提供者

服务提供者的Netty接收到请求后,反序列成Invocation对象,根据Invocation中的接口类全限定名,从map中取出Exporter。然后从Exporter中取出Invoker,调用Invoker的invoke方法,Invoker的invoke方法从Invocation取出方法名,方法参数类型、方法参数,反射调用具体实现类,具体实现类处理完成后返回的结果,带上服务消费者传过来的id,一起返回给服务提供者。

在这里插入图片描述

与Spring对接

这些都写好以后,一个RPC框架大体就形成了,但是我们还差最后一步,就是与Spring对接。

我们参考Dubbo,定义了一个@EnableDubbo注解,@EnableDubbo注解通过@Import注解Spring容器导入一个DubboComponentScanRegistrar类,DubboComponentScanRegistrar实现了Spring的ImportBeanDefinitionRegistrar接口,重写了ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法,DubboComponentScanRegistrar的registerBeanDefinitions方法会被Spring回调到。DubboComponentScanRegistrar的registerBeanDefinitions方法往Spring容器中注入两个bean,一个是ServiceAnnotationBeanPostProcessor类型,实现了Spring的BeanDefinitionRegistryPostProcessor接口,用于扫描@Service注解然后注册BeanDefinition到Spring容器中;另一个是ReferenceAnnotationBeanPostProcessor类型,是一个Bean后置处理器,用于扫描@Reference注解进行服务引入的。

在这里插入图片描述

ServiceAnnotationBeanPostProcessor通过Spring的ClassPathBeanDefinitionScanner类扫描出@Service注解修饰的类的BeanDefinition,然后创建一个ServiceBean类型的BeanDefinition,配置相应的interfaceClass和ref等属性,然后注册到容器中。

ClassPathBeanDefinitionScanner是Spring提供的一个用来扫描指定包路径获取BeanDefinition的工具类。

在这里插入图片描述

ReferenceAnnotationBeanPostProcessor参考Spring处理@Autowired的逻辑,ReferenceAnnotationBeanPostProcessor继承了InstantiationAwareBeanPostProcessorAdapter,并重写了InstantiationAwareBeanPostProcessorAdapter的postProcessPropertyValues方法,ReferenceAnnotationBeanPostProcessor的postProcessPropertyValues方法会被Spring回调到,给当前bean进行属性注入,然后postProcessPropertyValues方法中收集当前bean中被@Reference注解修饰的属性,创建ReferenceBean,对ReferenceBean进行相应配置,调用ReferenceBean的get()方法返回一个代理对象,注入到该属性中。

InstantiationAwareBeanPostProcessorAdapter是Spring提供的一个Bean后置处理器,可以通过继承InstantiationAwareBeanPostProcessorAdapter并重写postProcessPropertyValues方法定制我们的属性注入逻辑。

在这里插入图片描述

代码

到这里,我们的RPC框架就大功告成了。基本上是参考Dubbo来写的,保留了Dubbo的核心逻辑,又去掉了一些细枝末节,比起原来的Dubbo代码简化了许多,但是确实能够帮助我们比较深刻的理解Dubbo的原理和逻辑。

本文章涉及的所有代码,都上传到git代码仓库了,由于篇幅有限,这些代码就不贴到本篇文章中了,有兴趣的可以到代码仓库下载下来看一下。

git代码仓库地址:https://gitcode.net/weixin_43889578/mini-dubbo

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/188258.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开源称重系统-有源代码

最近发现网上有个不错的开源称重软件&#xff0c;界面做的非常漂亮&#xff0c;收藏一下&#xff1b;还有硬件对接&#xff1a;耀华A9仪表、海康威视监控&#xff0c;IC卡读卡器&#xff0c;控制器对接都有&#xff0c;是个不错的软件&#xff0c;非常感谢作者分享&#xff1b;…

Jenkins 插件下载速度慢安装失败?这篇文章可能解决你头等难题!

Jenkins部署完毕&#xff0c;如果不安装插件的话&#xff0c;那它就是一个光杆司令&#xff0c;啥事也做不了&#xff01; 所以首先要登陆管理员账号然后点击系统管理再点击右边的插件管理安装CI/CD必要插件。 但是问题来了&#xff0c;jenkins下载插件速度非常慢&#xff0c…

使用bard分析视频内容

11月21日的bard update 更新了分析视频的功能&#xff0c;使用方法如下&#xff1a; 1、打开bard网站。https://bard.google.com/ 2、点击插件。 3、点击YouTube中的 research a topic 选项。 4、输入需要分析的内容&#xff1a; Please analyze how many technologies are in…

Compose入门

​ 本篇文章主要是为了对Compose有一个初步了解。知道Compose是做什么的&#xff0c;用Compose能干什么&#xff0c;在目前的各种UI框架下面有些优势&#xff0c;参考Google官网的解释加上一些自己的理解生成的一篇文章。本人也是Compose初学者&#xff0c;通过每一步学习遇到哪…

4.前端--HTML标签3【2023.11.25】

1.表格 1.1表格的作用 表格的作用&#xff1a;表格主要用于显示、展示数据 1.2表格的基本格式 <table><tr><td>单元格内的文字</td><td>单元格内的文字</td>...</tr>... </table><table> </table> 是用于定义表…

[蓝桥杯训练]———高精度乘法、除法

高精度乘法、除法 一、高精度乘法⭐1.1 初步理解1.1.1 高精度的定义1.1.2 为什么会有高精度1.1.3 高精度乘法的复杂度 1.2 思想讲解1.3 代码实现1.3.1 声明1.3.2 实现高精度乘法1.3.3 整体实现1.3.4 代码测试 二、高精度除法⭐2.1 初步理解2.2 思想讲解2.3 代码实现2.3.1 声明2…

【Vulnhub靶机】Jarbas--Jenkins

文章目录 信息收集主机发现端口扫描目录爆破 漏洞探测whatwebhash-identifierwhatweb 文档说明&#xff1a;https://www.vulnhub.com/entry/jarbas-1,232/ 靶机下载&#xff1a;Download (Mirror): 信息收集 主机发现 扫描C段 sudo nmap -sn 10.9.75.0/24端口扫描 sudo nma…

【教学类-06-11】20231125(55格版)X-Y之间“除法÷题”(以1-9乘法口诀表倒推)(随机抽取和正序抽取)

图片展示 &#xff08;随机打乱排序&#xff09; 正序&#xff08;每张都一样&#xff09; 背景需求&#xff1a; 前面三篇写到了随机加法、随机减法、随机乘法&#xff0c;既然做了三套&#xff0c;怎么能不试试最后一款“除法”呢 模仿乘法版本&#xff0c;制作打乱版和正…

安卓用SQLite数据库存储数据

什么是SQLite&#xff1f; SQLite是安卓中的轻量级内置数据库&#xff0c;不需要设置用户名和密码就可以使用。资源占用较少&#xff0c;运算速度也比较快。 SQLite支持&#xff1a;null&#xff08;空&#xff09;、integer&#xff08;整形&#xff09;、real&#xff08;小…

前端入门(三)Vue生命周期、组件技术、脚手架、存储、事件总线、

文章目录 Vue生命周期Vue 组件化编程 - .vue文件非单文件组件组件的注意点组件嵌套Vue实例对象和VueComponent实例对象Js对象原型与原型链Vue与VueComponent的重要内置关系 应用单文件组件构建 Vue脚手架 - vue.cli项目文件结构组件相关高级属性引用名 - ref数据接入 - props混…

安卓系统修图软件(一)

平时我们会不时在朋友圈发自己的自拍照&#xff0c;或者是风景图等&#xff0c;许多小伙伴们此时会对照片进行一定的修理&#xff0c;比如添加滤镜等操作。在电脑上用ps修图比较繁琐&#xff0c;日常中大可不必用这把宰牛刀&#xff1b;而手机自带的编辑器&#xff0c;或者是QQ…

【Java基础系列】文件上传功能

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

反思一次效能提升

前天与一个大佬交流。想起自己在6年多前在团队里做的一次小小的效能提升。 改进前 在同一个产品团队&#xff0c;同时有前端工程师和后端工程师。他们经常需要共同协作完成features。 前端是一个传统的多页应用。前端渲染是由后端的velocity模板引擎实现的。 打包后&#xff0c…

【电路笔记】-分流器

分流器 文章目录 分流器1、概述2、通用/网络配置3、无功分流器3.1 电阻电容分流器3.2 电阻-电感分流器 4、总结 我们在之前关于分压器的文中已经看到&#xff0c;分压过程是通过在串联配置中关联相同的组件来实现的。 在本文中&#xff0c;我们将重点关注电流分频器执行的电流分…

“不得了·放飞杯” 2023年四川省健身健美锦标赛启动在成都隆重召开

“不得了放飞杯” 2023年四川省健身健美锦标赛启动在成都隆重召开 为了更好地推动四川省健身健美运动的普及和发展&#xff0c;结合《四川全民健身实施计划》的现状&#xff0c;适应新时代健身私教服务产业的发展需求&#xff0c;由中国健美协会指导&#xff0c;四川省健美健美…

M2BLS

U are randomly generated&#xff0c;g is an activation function 辅助信息 作者未提供代码

如何通过ShardingJDBC进行读写分离

背景信息&#xff1a; 面对日益增加的系统访问量&#xff0c;数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说&#xff0c;将数据库拆分为主库和从库。其中主库负责处理事务性的增删改操作&#xff0c;从库负责处理查询操作&#…

【Qt绘制仪表盘】

目的 使用Qt的绘制事件绘制一个仪表盘 思路 需要创建一个带绘制事件的控件重写绘制事件显示 实现 以下是实现代码&#xff0c;可复制到程序到&#xff0c;直接运行。 .h // GaugeWidget.h #ifndef GAUGEWIDGET_H #define GAUGEWIDGET_H#include <QWidget>class Ga…

Docker Swarm总结+基础、集群搭建维护、安全以及集群容灾(1/3)

博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码下载地址&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb;…

【WSA】无法打开 适用于 Android™ 的 Windows 子系统,因为它处于脱机状态。可能缺少存储设备,或者存储设备已断开连接。

问题描述 之前可以正常使用适用于 Android™ 的 Windows 子系统&#xff08;WSA&#xff09;&#xff0c;但突然间无法启动了。 当尝试启动WSA中的软件时&#xff0c;都会出现以下错误提示&#xff1a; 无法打开 适用于 Android™ 的 Windows 子系统&#xff0c;因为它处于脱…