近期从不同渠道了解到了一些中间件相关的新的知识,记录一下收获。涉及到的中间件包括RPC调用、动态配置中心、MQ、缓存、数据库、限流等,通过对比加深理解,方便实际应用时候更明确如何进行设计和技术选型。
目前主流的RPC中间件包括Dubbo、HSF、Thrift、GRPC、Spring Cloud等。结合自己的具体场景选择合适的框架。从性能、通信方式、序列化、语言、易用性、生态等方面对比分析如下:
名称 | 语言支持 | 底层通信方式 | 序列化协议 | 注册中心 | 性能 |
---|---|---|---|---|---|
Dubbo | java开发 | TCP长连接 | hessian,json,java默认的序列化等 | ZK等 | **(还可以) |
HSF | java为主,支持C++ | Netty框架,本质TCP长连接 | 默认Hessian2 | ConfigServer等 | **(还可以) |
Thrift | 通过IDL构建支持跨语言 | 自定义的协议,在Tcp上层包装 | Thrift | Consul等 | ****(比grpc快2-5倍) |
GRPC | 跨语言 | Http2.0 | Protobuf | etcd(go)等 | ***(平均比Dubbo略好一点) |
Spring Cloud | java开发 | Http | Jackson、FastJson等 | Nacos等 | *(也够用) |
SPI(Service Provider Interface),本质原理----策略模式+配置+反射:将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。我们需要搞清楚java的SPI和Dubbo的SPI区别:
基本原理:Java的SPI|用来设计给服务提供商做插件使用的。基于策略模式来实现动态加载的机制。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现。
实现过程:需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service2)在该目录下创建一个 文本文件,该文件需要满足以下几个条件文件名必须是扩展的接口的全路径名称文件内部描述的是该扩展接口的所有实现类文件的编码格式是 UTF-83)通过 java.util.ServiceLoader 的加载机制来加载服务。
工作流程:1)当调用 ServiceLoader.load(Class clz) 方法时,会到jar中中的目录 “META-INF/services/“ + clz.getName 进行文件读取,2)当在调用ServiceLoader.forEach()方法时,实际走的是LazyIterator,当在调用LazyIterator.hasNext() 时,在文件中读取到实际的服务实现类并把它们通过调用 Class.forName(String name, boolean initialize,ClassLoader loader)。
应用:javaSPI我们最熟悉的应用就是数据库驱动了,mysql和oracle驱动针对JDBC分别有自己的实现,这就有赖于java的SPI机制。
基本原理:在dubbo中也有SPI机制,虽然都需要将接口全限定名配置在文件中,但是dubbo并没有使用java的spi机制,而是重新实现了一套功能更强的 SPI 机制, 支持了AOP与依赖注入,并且 利用缓存提高加载实现类的性能,同时 支持实现类的灵活获取。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。例如dubbo当中的protocol,LoadBalance等都是通过SPI机制扩展。
实现过程:1)需要在 classpath 下创建一个目录,该目录命名可以是:META-INF/service/、META-INF/dubbo/、META-INF/dubbo/internal/2)在该目录下创建一个 文本文件,该文件需要满足以下几个条件文件名必须是扩展的接口的全路径名称文件内部描述的是该扩展接口的所有实现类,将服务实现类写成KV键值对的形式,Key是拓展类的name,Value是扩展的全限定名实现类。3)通过 org.apache.dubbo.common.extension.ExtensionLoader 的加载机制来加载服务
工作流程:两次SPI过程 1)我们首先通过 ExtensionLoader的 getExtensionLoader 方法获取一个接口的 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象 2)通过 ExtensionLoader.getExtensionLoader取到接口的加载器Loader之后,再通过 getExtension方法获取需要拓展类对象。
补充几个核心机制:服务发现机制 SPI、自适应机制 Adaptive、包装机制 Wrapper 与激活机制 Activate;参考https://juejin.cn/post/7112764945120362526?searchId=202401142324596D6049137EA11C7B3412
应用:例如dubbo的多协议的实现等。
区别点:
(1)Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源。
(2)dubboSPI有选择性地加载所需要的SPI接口。javaSPI配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。而dubboSPI配置文件中以键值对的形式有别名,易于区分。
(3)SPI扩展如果依赖其他的扩展,javaspi做不到自动注入和装配,dubbo可以实现自动注入。
(4)javaSPI不提供类似于Spring的IOC和AOP功能,dubboSPI是支持的
以上参考链接:https://www.zhihu.com/question/389551161/answer/2615909871
发起调用后线程阻塞住同步等待调用结果,适用耗时短的场景。Dubbo有容错机制,包括以下:FailoverClusterInvoker
(dubbo默认的容错机制)失败重试机制。失败自动切换,当出现失败,重试其它服务器。支持重试的,查询接口,支持幂等的写接口
FailsafeClusterInvoker
如果调用失败的化,不用抛出错误,直接打印一个异常log日志就可以了。一般来说,你要是写一些类似与远程日志数据,审计数据,或者是一些可有可无的,可以丢失的一些数据
ForkingClusterInvoker
就是会并行的调用几个服务,如果谁能先返回结果,就用谁的。cpu负载过高。
FailfastClusterInvoker
一旦调用的时候遇到了异常,直接抛出异常,不在进行重试了。
FailbackClusterInvoker
如果调用失败了,会把这个请求记录存储起来。后续根据时间轮的策略,再去隔一段时间去重试。默认是3次调用以后就会进行存储到失败列表中
BroadcastClusterInvoker
广播的形式的。所有的invoker服务实例都会接收到请求
异步RPC的实现:目前成熟的RPC框架都会支持异步调用、异步监听、callback调用
https://blog.csdn.net/qq_34760272/article/details/123830970
客户端发起请求之后,不必等待获取结果,第一次执行返回Null,随后通过Future.get()阻塞获取执行结果。
有时候我们发起一个调用请求之后,并不想通过Future的get获取结果,因为get的时候是阻塞的,而是希望调用请求之后可以去做其他事情,通过一个监听去监测,当有结果返回的时候直接获取结果,然后进行逻辑处理。
callback回调支持同步/异步方式,观察者模式可以伴随异步监听或者回调。
RPC是以TCP全双工的协议进行通信的,基于长连接,服务端便具备了可以“调用”客户端callback函数的能力。如果在服务端接口里面完成一个业务逻辑功能有3个过程,那么这3个过程中可以分别调用callback方法,形成“一次调用,多次通知”的机制,这一点在异步监听中是没有办法实现的,异步回调更像是“一次性买卖”。在高并发场景下建议使用异步监听的方式,因为callback方式客户端会对Callback实例的个数有限制。
参考文章地址:https://blog.csdn.net/qq_34760272/article/details/123830970
补充一个异步注解@Async
https://blog.csdn.net/weixin_47872288/article/details/125512173?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-5-125512173-blog-51165251.235v40pc_relevant_anti_vip&spm=1001.2101.3001.4242.4&utm_relevant_index=8
以上内容参考:
https://blog.csdn.net/asdcls/article/details/121661651
https://zhuanlan.zhihu.com/p/458254270
https://www.zhihu.com/question/389551161?utm_id=0
HTTP协议是基于请求/响应模式的,因此只要服务端给了响应,本次HTTP连接就结束了,或者更准确的说,是本次HTTP请求就结束了,根本没有长连接这一说,那么自然也就没有短连接这一说了;网上所说的长连接、短连接,本质其实说的是TCP连接。TCP连接是一个双向通道,它是可以保持一段时间不关闭,因此TCP才有正真的长连接、短连接 。Http1.1以前是短连接,单次请求后关闭连接,下次重新TCP三次握手建立连接。长连接回保持TCP连接,使得多个HTTP请求可以复用同一个TCP连接。
短连接的操作步骤是:
建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
长连接的操作步骤是:
建立连接——数据传输…(保持连接)…数据传输——关闭连接
优缺点:长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。在长连接的应用场景下,client 端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候。
应用场景:一般用于实时通信,比如Dubbo和HSF框架的注册中心和服务的发布与订阅者之间的通信。以及RocketMQ(MetaQ支持推push与拉poll模型)的NameServer和Broker与生产者、消费者之间的通信。
参考:https://www.jianshu.com/p/f36f83684f93
https://www.cnblogs.com/zhaozl/p/11168185.html
短轮询由客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接;即在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器(可以理解为TCP连接不复用)。
长轮询:请求进来,有数据就返回,没有就hang住(先不把请求响应给前端),直到有数据或者超时再返回(然后立即再发起一个请求过来)。客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
轮询:客户端每隔一段时间ajax
长轮询:客户端请求1——服务端hold——服务端返回——客户端请求2
优缺点:短轮询实现容易,当请求中有大半是无用,难于维护,浪费带宽和服务器资源;响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。长轮询在无消息的情况下不会频繁的请求,耗费资源小,HTTP连接都不会复用TCP连接。
应用场景:动态配置中心,大量客户端请求,但是更新不频繁的请求与推送场景。比如Diamond持久化配置中心的动态更新通知,支持基于HTTP短轮询的“拉模型”和基于HTTP长轮询的“推模型”。
这里做简要解释,长连接是基于传输层的TCP连接通道,具备实时的双向传输能力,建立连接的过程中,就算server不回应,client也可以源源不断的向server发消息,连接不能“挂起”,需要一直保持连接状态,TCP连接多了,会对server造成很大连接负担。
长轮询是基于HTTP的连接,必须是请求—响应模式的,server可以不做出响应,将HTTP连接“挂起”,去处理其他的HTTP连接,对服务器的负担较小,因为server可以处理其他的HTTP请求,不用一直关注被“挂起”的HTTP连接。
实质上Diamond即支持由server向client动态的推送最新的配置,也支持client如果有需要可以主动从server拉最新的配置,非常灵活,可以适应各种业务场景。
参考:https://blog.csdn.net/dddgg__/article/details/133251289
参考https://www.cnblogs.com/Joeris/articles/10999373.html
https://blog.csdn.net/H200102/article/details/107354799
从兼容性角度考虑,短轮询 > 长轮询 > 长连接SSE > WebSocket;
从性能方面考虑,WebSocket > 长连接SSE > 长轮询 > 短轮询。
https://blog.csdn.net/sugelachao/article/details/124490112
HTTP与TCP通信区别:
Http是无转态的连接,TCP是有状态的长连接。
传输内容上:HTTP超文本传输,包的大小比TCP大。tcp比http快,是因为发送同样的有效数据,http比tcp多封装一次,也就是硬件实际要发送的数据量更大,那么自然耗时更多。
RPC为什么比HTTP快:
假设
这里的RPC服务一般是指基于TCP/IP协议上开发的二进制协议服务
Http服务一般是指基于Http1.0 / Http1.1协议上开发的REST服务
对比
序列化方式:
RPC服务序列化是针对二进制协议(0/1)来做序列化和反序列化,所以性能高。
而Http服务是基于文本的序列化和反序列化,需要读一行一行的文本(比如json格式的),进行序列化和反序列化,所以性能低。
报文长度:
RPC服务是自定义的传输协议,传输的报文都是干货。
而Http服务里面包括很多没用的报文内容,比如Http Header里面的accept,referer等等
连接的复用:
RPC服务是基于TCP/IP协议的,是长连接。
而Http服务大都是短连接,虽然Http1.1支持长连接,但是这个也是要取决于服务端是否支持长连接,不太可控。
结论
在追求性能的场合,用RPC服务(基于TCP/IP传输协议)
在追求兼容性多语言的场合,用REST服务
企业内部一般微服务互相调用用基于TCP的高性能RPC,对外网关支持HTTP请求。
https://blog.csdn.net/u013531487/article/details/130337143
https://blog.csdn.net/hudmhacker/article/details/108375757
数据库分库分表:对比ShardingJDBC和MyCat的设计。
缓存的热点数据处理:基于集群分片多级缓存打散读请求,合并写请求。
分库分表的主键ID选择:为什么不推荐UUID 全局自增 雪花算法
https://blog.csdn.net/qq_43665821/article/details/123883614?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-1-123883614-blog-112151582.235%5Ev40%5Epc_relevant_anti_vip&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-1-123883614-blog-112151582.235%5Ev40%5Epc_relevant_anti_vip&utm_relevant_index=2
https://zhuanlan.zhihu.com/p/459357903
常见的几种实现包括:基于Redis的SetNX、LUA脚本、Redission红锁框架、ZK的临时顺序节点创建&监听等。
主要区别:基于CAP和BASE理论,基于Redis的分布式锁是AP系统(集群数据同步机制是Gossip协议,会有短期的数据不一致问题),ZK的是CP系统(集群数据同步采用ZAB协议,会牺牲掉一部分的可用性)
关于分布式系统的一致性协议划分:
强一致性,保证系统改变提交以后立即改变集群的状态,有以下几个模型:Paxos、Raft、Zab
弱一致性,也叫最终一致性,不用保证提交立刻在集群内全部生效,但需要随着时间的推移生效,有以下几个模型:DNS系统、Gossip协议
协议介绍参考:https://zhuanlan.zhihu.com/p/617831925
关于是否支持可重入锁、支持公平/非公平锁:
其实Redisson和ZK都能实现,具体实现方式参考如下:
https://www.jianshu.com/p/cee3c8092aa5
https://blog.51cto.com/u_15162069/2806447
本文总结了一些中间件框架的进阶知识,涉及到的中间件包括RPC调用、动态配置中心、MQ、缓存、数据库、限流等。主要通过对比的方式增强记忆和理解,方便实际应用过程中进行技术方案的设计和选择。