1.1、支持的协议
1.1.1dubbo协议
Dubbo缺省协议采用单一长连接和NIO异步通讯,使用基于于netty+hessian(序列化方式)交互。适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。Dubbo缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。
1.1.2 rmi协议
走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用,在大数据量下的传输性,建议使用rmi协议
1.1.3 hessian协议
走hessian序列化协议,多个短连接,适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用
1.1.4 http协议
走json序列化
1.1.5 webservice
走SOAP文本序列化
1.2 dubbo的抓包分析(比较复杂)
head固定自由16Bit,body默认采用Hessian二进制序列化
调用控制协议 | 对象序列化协议 | ||||||||||
header(16B) 头部信息 |
body(默认hessian序列化) | ||||||||||
magic(2B) 魔数(2) |
flag(1B) 标识(1) |
status(1B) 响应状态(1) |
messageId(8B) 消息ID(8) |
bodyLength(4B) 内容长度(4) |
dubbo版本 | 服务接口名 | 服务版本号 | 服务方法名 | 参数描述符 | 参数值序列化 | 隐式参数 |
da bb | c5(请求报文) | 16进制 | |||||||||
0a------换行符号------"\n" | 请求报文 | ||||||||||
da bb c5 00 00 00 00 00 00 00 00 00 00 00 00 cd 22 32 2e 35 2e 33 22 0d 0a 22 63 6f 6d 2e 74 6f 6e 79 2e 74 65 73 74 2e 64 75 62 62 6f 2e 44 65 6d 6f 53 65 72 76 69 63 65 22 0d 0a 22 30 2e 30 2e 30 22 0d 0a 22 73 61 79 48 65 6c 6c 6f 22 0d 0a 22 4c 6a 61 76 61 5c 2f 6c 61 6e 67 5c 2f 53 74 72 69 6e 67 3b 22 0d 0a 22 74 6f 6e 79 22 0d 0a 7b 22 70 61 74 68 22 3a 22 63 6f 6d 2e 74 6f 6e 79 2e 74 65 73 74 2e 64 75 62 62 6f 2e 44 65 6d 6f 53 65 72 76 69 63 65 22 2c 22 69 6e 74 65 72 66 61 63 65 22 3a 22 63 6f 6d 2e 74 6f 6e 79 2e 74 65 73 74 2e 64 75 62 62 6f 2e 44 65 6d 6f 53 65 72 76 69 63 65 22 2c 22 76 65 72 73 69 6f 6e 22 3a 22 30 2e 30 2e 30 22 7d 0d 0a |
魔数 | 0xda, 0xbb | |||||||||
0d------回车符号------"\r" | 标识 | 0xc5 | |||||||||
响应状态 | 0x00 | ||||||||||
Q:16进制32为什么对应字符 2? | 消息ID | 00 00 00 00 00 00 00 00 00 | |||||||||
A:16进制32对应 10进制50 50在ASCII码表中, |
数据长度 | 00 00 00 cd 205 | |||||||||
"2.5.3" "com.tony.test.dubbo.DemoService" "0.0.0" "sayHello" "Ljava\/lang\/String;" "tony" {"path":"com.tony.test.dubbo.DemoService","interface":"com.tony.test.dubbo.DemoService","version":"0.0.0"} |
|||||||||||
响应报文 | |||||||||||
da bb 05 14 00 00 00 00 00 00 00 00 00 00 00 10 31 0d 0a 22 6e 61 6d 65 3a 74 6f 6e 79 22 0d 0a |
魔数 | da bb | |||||||||
标识 | 05 | ||||||||||
响应状态 | 0x14 | ||||||||||
消息ID | 00 00 00 00 00 00 00 00 00 | ||||||||||
数据长度 | 0x10 16 | ||||||||||
0x31 | 49 | ||||||||||
1 "name:tony" |
|||||||||||
dubbo协议为什么比http协议高效?
1.单元一长连接减少三次握手四次挥手的次数据
2.报文比http更少
1.3 dubbo工作原理和流程
1.3.1、dubbo的项目结构
第一层:service层,接口层,给服务提供者和消费者来实现的
第二层:config层,配置层,主要是对dubbo进行各种配置的
第三层:proxy层,服务代理层,透明生成客户端的stub和服务单的skeleton
第四层:registry层,服务注册层,负责服务的注册与发现
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
第七层:protocol层,远程调用层,封装rpc调用
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
第九层:transport层,网络传输层,抽象mina和netty为统一接口
第十层:serialize层,数据序列化层
1.3.2、dubbo的工作流程:
1)第一步,provider向注册中心去注册
2)第二步,consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
3)第三步,consumer调用provider
4)第四步,consumer和provider都异步的通知监控中心
1.3.3、注册中心挂了可以继续通信吗?
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信
1)random loadbalance 随机
默认情况下,dubbo是random load balance随机调用实现负载均衡,可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高,一般就用这个默认的就可以了。
2)roundrobin loadbalance 轮询
还有roundrobin loadbalance,这个的话默认就是均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高。所以此时需要调整权重,让性能差的机器承载权重小一些,流量少一些。
3)leastactive loadbalance 最少活跃数
相同活跃数的随机,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
4)consistanthash loadbalance 一致性hash
一致性Hash算法,相同参数的请求一定分发到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。
更加详细的解释可以看官网:
http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html
1)failover cluster模式
失败自动切换,自动重试其他机器,默认就是这个,常见于读操作
可通过retries="2"来设置重试次数(不含第一次,默认就是2)
2)failfast cluster模式
快速失败,一次调用失败就立即失败,常见于写操作
3)failsafe cluster模式
失败安全,出现异常时忽略掉,常用于不重要的接口调用,比如记录日志
4)failback cluster模式
失败自动恢复,失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种
5)forking cluster
并行调用多个provider,只要一个成功就立即返回
6)broadcacst cluster
逐个调用所有的provider
4.1、什么是spi
spi,简单来说,就是service provider interface,说白了是什么意思呢,比如你有个接口,现在这个接口有3个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要spi了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。
接口A -> 实现A1,实现A2,实现A3
配置一下,接口A = 实现A2
在系统实际运行的时候,会加载你的配置,用实现A2实例化一个对象来提供服务
比如说你要通过jar包的方式给某个接口提供实现,然后你就在自己jar包的META-INF/services/目录下放一个跟接口同名的文件,里面指定接口的实现里是自己这个jar包里的某个类。ok了,别人用了一个接口,然后用了你的jar包,就会在运行的时候通过你的jar包的那个文件找到这个接口该用哪个实现类。
这是jdk提供的一个功能。
但是dubbo也用了spi思想,不过没有用jdk的spi机制,是自己实现的一套spi机制。
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
这行代码就是dubbo里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现好了,没问题。
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
Exporter export(Invoker invoker) throws RpcException;
@Adaptive
Invoker refer(Class type, URL url) throws RpcException;
void destroy();
}
在dubbo自己的jar里,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol文件中:
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
4.2、如何扩展dubbo中的组件
自己写个工程,要是那种可以打成jar包的,里面的src/main/resources目录下,搞一个META-INF/services,里面放个文件叫:com.alibaba.dubbo.rpc.Protocol,文件里搞一个my=com.zhss.MyProtocol。自己把jar弄到nexus私服里去。
然后自己搞一个dubbo provider工程,在这个工程里面依赖你自己搞的那个jar,然后在spring配置文件里给个配置:
这个时候provider启动的时候,就会加载到我们jar包里的my=com.zhss.MyProtocol这行配置里,接着会根据你的配置使用你定义好的MyProtocol了,这个就是简单说明一下,你通过上述方式,可以替换掉大量的dubbo内部的组件,就是扔个你自己的jar包,然后配置一下即可。
dubbo里面提供了大量的类似上面的扩展点,就是说,你如果要扩展一个东西,只要自己写个jar,让你的consumer或者是provider工程,依赖你的那个jar,在你的jar里指定目录下配置好接口名称对应的文件,里面通过key=实现类。
然后对对应的组件,用类似
下面的demo展示了如何应用SPI
https://gitee.com/lzhcode/maven-parent/blob/master/lzh-springboot/lzh-springboot-spi/src/main/java/com/lzhsite/spring/Application.java
4.3、SPI 在实际项目中的应用
4.3.1、在mysql-connector-java-xxx.jar中发现了META-INF\services\java.sql.Driver文件,里面只有两行记录:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
我们可以分析出,java.sql.Driver是一个规范接口,com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver则是mysql-connector-java-xxx.jar对这个规范的实现接口。
4.3.2、在jcl-over-slf4j-xxxx.jar中发现了META-INF\services\org.apache.commons.logging.LogFactory文件,里面只有一行记录:
org.apache.commons.logging.impl.SLF4JLogFactory
相信不用我赘述,大家都能理解这是什么含义了
在Dubbo中,没有使用CGLib进行代理,而是使用JDK和Javassist来进行动态代理!我们知道,动态代理是无法用反射做的,只能靠动态生成字节码,这就需要使用字节码工具包,比如asm和Javassist等,在Spring3.2.2之前版本的源码中,我们可以看到是有单独spring-asm的模块的,但在Spring3.2.2版本开始,就没有spring-asm模块了,不是不使用了,而是spring-asm已经整合到spring-core中了,可见asm在Spring中的地位(CGLib使用的就是asm),至于Dubbo为什么不使用CGLib?
虽然ASM稍快,但并没有快一个数量级,
而JAVAASSIST的字节码生成方式比ASM方便,
JAVAASSIST只需用字符串拼接出Java源码,便可生成相应字节码,
而ASM需要手工写字节码。
所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款,不能多插入一条数据,不能将统计值多加了1。这就是幂等性。
其实保证幂等性主要是三点:
(1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单id,一个订单id最多支付一次,对吧
(2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见的方案是在mysql中记录个状态啥的,比如支付之前记录一条这个订单的支付流水,而且支付流水采
(3)每次接收请求需要进行判断之前是否处理过的逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
首先,一般来说,我个人给你的建议是,你们从业务逻辑上最好设计的这个系统不需要这种顺序性的保证,因为一旦引入顺序性保障,会导致系统复杂度上升,而且会带来效率低下,热点数据压力过大,等问题。
下面我给个我们用过的方案吧,简单来说,首先你得用dubbo的一致性hash负载均衡策略,将比如某一个订单id对应的请求都给分发到某个机器上去,接着就是在那个机器上因为可能还是多线程并发执行的,你可能得立即将某个订单id对应的请求扔一个内存队列里去,强制排队,这样来确保他们的顺序性(dubbo的一致性hash负载均衡策略能保证99.99%有序,除非服务端识别到的客户端发起请求和客户端真正发起请求的顺序是不同的,如果需要的话可以考虑引入分布式锁,但是会影响性能)
但是这样引发的后续问题就很多,比如说要是某个订单对应的请求特别多,造成某台机器成热点怎么办?解决这些问题又要开启后续一连串的复杂技术方案。。。曾经这类问题弄的我们头疼不已,所以,还是建议什么呢?
最好是比如说刚才那种,一个订单的插入和删除操作,能不能合并成一个操作,就是一个删除,或者是什么,避免这种问题的产生。
在dubbo的provider和consumer的配置文件中,如果都配置了timeout的超时时间,dubbo默认以consumer中配置的时间为准
在Provider上尽量多配置Consumer端属性
原因如下:作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等
在Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作为Consumer的缺省值。否则,Consumer会使用Consumer端的全局设置,这对于Provider不可控的,并且往往是不合理的
PS: 配置的覆盖规则:
1) 方法级配置别优于接口级别,即小Scope优先
2) Consumer端配置 优于 Provider配置 优于 全局配置,最后是Dubbo Hard Code的配置值(见配置文档)