目录
1、传统MVC和SOA
2、Dubbo分布式服务化示例
2.1 需求
2.2 代码实现
2.3 服务化建议
3、Dubbo服务化策略
3.1 异步操作
3.2 事件通知
3.3 本地存根
3.4 本地伪装
3.5 延迟暴露
3.6 配置的覆盖策略
3.7 连接应用
3.8 高效序列化
传统的MVC:分层的思想
业务控制controller层
业务逻辑处理service层
数据库操作dao层
SOA(面向服务)服务化:分割的思想
用户中心
支付中心
订单中心
即把接口、服务提供者、服务消费者三者分别部署在不同的服务器上,减轻了各个服务器的压力
1)、新建三个工程,分别代表用户中心,支付中心,订单中心
2)、新建一个接口工程,用于各服务依赖
3)、创建订单时,订单服务远程调用用户服务查询用户余额,用户服务远程调用支付服务完成金额扣减,从而完成整个订单的执行流程
项目结构如下:
2.2.1 创建父工程配置依赖
4.0.0
com.ydt
dubbo-distributed-module
pom
1.0-SNAPSHOT
dubbo-order
dubbo-pay
dubbo-user
dubbo-interface
org.springframework
spring-context
5.2.9.RELEASE
org.springframework
spring-web
5.2.9.RELEASE
org.springframework
spring-webmvc
5.2.9.RELEASE
org.apache.dubbo
dubbo
2.7.7
io.netty
netty-all
io.netty
netty-all
4.1.50.Final
org.apache.zookeeper
zookeeper
3.4.14
org.slf4j
slf4j-api
org.slf4j
slf4j-log4j12
io.netty
netty
log4j
log4j
org.apache.curator
curator-recipes
4.2.0
2.2.2 创建dubbo-interface
/*----------------------------------------------用户接口 -----------------------------------------------*/
package com.ydt.dubbo.service;
public interface UserService {
public String findUser();
}
/*----------------------------------------------支付接口 -----------------------------------------------*/
package com.ydt.dubbo.service;
public interface PayService {
public String payMoney();
}
/*---------------------------------------------dubbo公共配置 -----------------------------------------------*/
package com.ydt.dubbo.config;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.context.annotation.Configuration;
/**
* dubbo扫描配置,你还可以使用引入dubbo.properties的方式
*/
@Configuration
@EnableDubbo(scanBasePackages = "com.ydt.dubbo.service")
/*@PropertySource("classpath:/dubbo-provider.properties")*/
public class DubboConfiguration {
}
2.2.3 创建dubbo-order
package com.ydt.dubbo.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.ydt.dubbo.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单Controller接口
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Reference
private UserService userService;
@RequestMapping(value = "/create",produces = "application/json; charset=utf-8")
public String create(){
return "创建订单,用户信息:" + userService.findUser();
}
}
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
utf-8
forceEncoding
true
CharacterEncodingFilter
/*
springmvc
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring-mvc.xml
springmvc
*.do
2.2.4 创建dubbo-user
package com.ydt.dubbo.service;
import com.alibaba.dubbo.config.annotation.DubboReference;
import com.alibaba.dubbo.config.annotation.DubboService;
/**
* 用户接口service实现,服务提供者,同时也是服务消费者,消费支付服务
*/
@DubboService
public class UserServiceImpl implements UserService{
@DubboReference
private PayService payService;
public String findUser() {
return "老胡,余额人民币999999999999元整," + payService.payMoney();
}
}
Archetype Created Web Application
contextConfigLocation
classpath:applicationContext.xml
org.springframework.web.context.ContextLoaderListener
2.2.5 创建dubbo-pay
package com.ydt.dubbo.service;
import com.alibaba.dubbo.config.annotation.DubboService;
/**
* 支付服务接口实现,作为用户服务的服务提供者
*/
@DubboService
public class PayServiceImpl implements PayService{
public String payMoney() {
return "已支付订单人民币999块,剩余人民币999999999000元整";
}
}
Archetype Created Web Application
contextConfigLocation
classpath:applicationContext.xml
org.springframework.web.context.ContextLoaderListener
2.2.6 测试结果
dubbo-order,dubbo-user,dubbo-pay三个工程分别在pom文件添加tomcat插件,并且依赖dubbo-interface工程,以不同端口启动
dubbo-distributed-module
com.ydt
1.0-SNAPSHOT
4.0.0
dubbo-pay
war
org.apache.tomcat.maven
tomcat7-maven-plugin
9999
/
com.ydt
dubbo-interface
1.0-SNAPSHOT
compile
1)、建议将服务接口,服务模型(实体),服务异常等都放置在公共包中
2)、服务接口尽可能大粒度,每个服务方法应该代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo暂未提供分布式事务支持,需要额外处理,其实实际上不可能完全避免.....
3)、服务接口建议以业务场景为单位划分,并对相近业务做抽象或者封装,防止接口数量爆炸,毕竟你需要在zookeeper上创建znode
4)、如果版本升级比较频繁,应该给每个接口或者服务定义版本号,为后续不兼容升级提供可能
5)、建议使用版本号大于两位,bug修改为小版本,功能迭代为大版本
6)、当不兼容升级时,新旧版本同时发布,等新版本稳定后再将用户使用版本都升级为新版本并且使用半数升级机制,平滑过渡
7)、注意服务接口增加方法或者服务模型增加字段,可向后兼容,删除方法或者删除字段,将不兼容
从v2.7.0开始,Dubbo的所有异步编程接口开始以CompletableFuture为基础
Consumer端异步调用
基于 NIO 的异步非阻塞实现并行调用,客户端不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
//开启异步调用
@Reference(timeout = 10000,async = true)
private AsyncService asyncService;
@RequestMapping(value = "/sayHello",produces = "application/json; charset=utf-8")
public String sayHello() throws ExecutionException, InterruptedException {
asyncService.sayHello("laohu");
ResponseFuture responseFuture= ((FutureAdapter)RpcContext.getContext().getFuture()).getFuture();
responseFuture.setCallback(new ResponseCallback() {
public void done(Object response) {
System.out.println("回调消息:" + response);
}
public void caught(Throwable e) {
e.printStackTrace();
}
});
return "哈哈,我先出来哒.....";
}
//服务提供者人为阻塞,让结果慢一点返回
public String sayHello(String name) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello " + name;
}
过了五秒后台才打印:
Provider端异步执行
Provider端异步执行将阻塞的业务从Dubbo内部线程池切换到业务自定义线程,避免Dubbo线程池的过度占用,有助于避免不同服务间的互相影响。但是需要注意provider端异步执行对节省资源和提升RPC响应性能是没有效果的,这时是因为如果服务处理比较耗时,虽然不是使用Dubbo框架内部线程处理,但是还是需要业务自己的线程来处理,另外副作用还有会新增一次线程上下文切换
@RequestMapping(value = "/sayByeBye",produces = "application/json; charset=utf-8")
public String sayByeBye() {
return asyncService.sayByeBye("laohu");
}
@Override
public String sayByeBye(String name) {
final AsyncContext asyncContext = RpcContext.startAsync();
new Thread(() -> {
// 如果要使用上下文,则必须要放在第一句执行
asyncContext.signalContextSwitch();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 写回响应
asyncContext.write("ByeBye " + name );
}).start();
return "我出不来,为什么?";
}
在调用之前、调用之后、出现异常时,会触发 oninvoke
、onreturn
、onthrow
三个事件,可以配置当事件发生时,通知哪个类的哪个方法
package com.ydt.dubbo.service;
import org.apache.dubbo.config.annotation.DubboService;
/**
* 时间通知方法,写在消费者端
* 注意:
* oninvoke方法:
* 必须具有与真实的被调用方法sayHello相同的入参列表:例如,oninvoke(String name)
* onreturn方法:
* 至少要有一个入参且第一个入参必须与sayHello的返回类型相同,接收返回结果:例如,onreturnWithoutParam(String result)
* 可以有多个参数,多个参数的情况下,第一个后边的所有参数都是用来接收sayHello入参的:例如, onreturn(String result, String name)
* onthrow方法:
* 至少要有一个入参且第一个入参类型为Throwable或其子类,接收返回结果;例如,onthrow(Throwable ex)
* 可以有多个参数,多个参数的情况下,第一个后边的所有参数都是用来接收sayHello入参的:例如,onthrow(Throwable ex, String name)
* 如果是consumer在调用provider的过程中,出现异常时不会走onthrow方法的,onthrow方法只会在provider返回的
* RpcResult中含有Exception对象时,才会执行。
* (dubbo中下层服务的Exception会被放在响应RpcResult的exception对象中传递给上层服务)
*/
@DubboService(interfaceName = "notifyService")
public class NotifyServiceImpl implements NotifyService{
@Override
public void onReturn() {
System.out.println("调用后消息返回啦!");
}
@Override
public void onInvoke() {
System.out.println("开始调用啦!");
}
@Override
public void onThrow(Throwable ex) {
System.out.println("调用抛异常啦!" + ex.getMessage());
}
}
/*消费者事件通知配置,你想配置几个通知都可以*/
@DubboReference(timeout = 10000,
methods = @Method(name = "findUser",oninvoke = "notifyService.onInvoke"
,onreturn = "notifyService.onReturn",onthrow = "notifyService.onThrow"))
private UserService userService;
使用分布式服务化后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub ,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
消费者实现服务提供者同样的接口方法:
package com.ydt.dubbo.service;
public class BarServiceStub implements BarService {
private final BarService barService;
// 构造函数传入真正的远程代理对象,必须要有,否则预执行中远程对象为空
public BarServiceStub(BarService barService){
this.barService = barService;
}
public String sayHello(String name) {
// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等
try {
return barService.sayHello("长沙-" + name);
} catch (Exception e) {
// 你可以容错,可以做任何AOP拦截事项
return "容错数据";
}
}
}
提供者实现同样的接口方法:
package com.ydt.dubbo.service;
import org.apache.dubbo.config.annotation.DubboService;
@DubboService
public class BarServiceImpl implements BarService{
public String sayHello(String name) {
return "hello stub,"+name;
}
}
/*开启本地存根*/
@DubboReference(stub = "true")
private BarService barService;
本地伪装通常用于服务降级,比如验权服务,当服务提供方全部挂掉后,客户端不抛出异常,而是通过 Mock 数据返回授权失败。
Mock 是 Stub 的一个子集,便于服务提供方在客户端执行容错逻辑,因经常需要在出现 RpcException (比如网络失败,超时等)时进行容错,而在出现业务异常(比如登录用户名密码错误)时不需要容错,如果用 Stub,可能就需要捕获并依赖 RpcException 类,而用 Mock 就可以不依赖 RpcException,因为它的约定就是只有出现 RpcException 时才执行
服务消费者:
package com.ydt.dubbo.service;
/**
* 容错伪装mock类,实现同样的方法,可用于服务降级
*/
public class BarServiceMock implements BarService {
//你可以伪造容错数据,此方法只在出现RpcException时被执行
public String sayHello(String name) {
return "容错数据";
}
}
服务提供者:
package com.ydt.dubbo.service;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.dubbo.rpc.RpcException;
@DubboService
public class BarServiceImpl implements BarService{
public String sayHello(String name) {
try {
int i = 1/0;
}catch (Exception e){
throw new RpcException("RPC异常");
}
return "hello stub,"+name;
}
}
如果你的服务需要预热时间,比如初始化缓存,等待相关资源就位等,可以使用 delay 进行延迟暴露。我们在 Dubbo 2.6.5 版本中对服务延迟暴露逻辑进行了细微的调整,将需要延迟暴露(delay > 0)服务的倒计时动作推迟到了 Spring 初始化完成后进行。你在使用 Dubbo 的过程中,并不会感知到此变化,因此请放心使用。
这个功能可以解决一个非常重要的bug:Spring 初始化死锁问题:
在 Spring 解析到
时,就已经向外暴露了服务,而 Spring 还在接着初始化其它 Bean。如果这时有请求进来,并且调用服务的实现类里有调用 applicationContext.getBean()
的用法。
请求线程的 applicationContext.getBean() 调用,先同步 singletonObjects 判断 Bean 是否存在,不存在就同步 beanDefinitionMap 进行初始化,并再次同步 singletonObjects 写入 Bean 实例缓存。
而 Spring 初始化线程,因不需要判断 Bean 的存在,直接同步 beanDefinitionMap 进行初始化,并同步 singletonObjects 写入 Bean 实例缓存。
这样就导致 getBean 线程,先锁 singletonObjects,再锁 beanDefinitionMap,再次锁 singletonObjects。 而 Spring 初始化线程,先锁 beanDefinitionMap,再锁 singletonObjects。反向锁导致线程死锁,不能提供服务,启动不了。
规避办法
强烈建议不要在服务的实现类中有 applicationContext.getBean() 的调用,全部采用 IOC 注入的方式使用 Spring的Bean。
如果实在要调 getBean(),可以将 Dubbo 的配置放在 Spring 的最后加载。
如果不想依赖配置顺序,可以使用
,使 Dubbo 在 Spring 容器初始化完后,再暴露服务。
如果大量使用 getBean(),相当于已经把 Spring 退化为工厂模式在用(而不是IOC的模板方法模式),可以将 Dubbo 的服务隔离单独的 Spring 容器。
以 timeout 为例,下图显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似:
方法级优先,接口级次之,全局配置再次之。
如果级别一样,则消费方优先,提供方次之。
其中,服务提供方配置,通过 URL 经由注册中心传递给消费方
建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置)。
测试方法:
1、服务提供方接口休眠5s
2、提供方设置全局超时时间5s------》超时报错
3、消费方设置全局超时时间6s-----》成功调用
4、提供方设置Service实例配置超时时间5s----》超时报错
@DubboService(timeout = 5000)
5、消费方设置Reference实例注入超时时间6s----》成功调用
@DubboReference(timeout = 6000)
6、提供方设置方法调用超时时间5s----》超时报错
@DubboService(timeout = 5000,methods = @Method(name = "sayHello",timeout = 5000))
7、消费方设置方法调用超时时间6s----》成功调用
@DubboReference(timeout = 6000,methods = @Method(name = "sayHello",timeout = 6000))
延迟连接用于减少长连接数。当有调用发起时,再创建长连接。
注意:该配置只对使用长连接的 dubbo 协议生效。
粘滞连接用于有状态服务,尽可能让客户端总是向同一提供者发起调用,除非该提供者挂了,再连另一台。
粘滞连接将自动开启延迟连接,以减少长连接数。
@DubboReference(sticky = true)
private LoadBalanceService loadBalanceService;
Dubbo 支持方法级别的粘滞连接,如果你想进行更细粒度的控制,还可以这样配置。
@DubboReference(sticky = true,methods = @Method(name = "sayHello",sticky = true))
private LoadBalanceService loadBalanceService;
所以粘滞连接跟nginx的ip_hash模式非常类似,在某些场景下(IP固定)的情况下可以作为分布式session共享的技术
3.8.1 启用Kryo和FST
Dubbo RPC 主要用于两个 Dubbo 系统之间的远程调用,特别适合高并发、小数据的互联网场景。而序列化对于远程调用的响应速度、吞吐量、网络带宽消耗等同样也起着至关重要的作用,Dubbo RPC 默认采用 hessian2 序列化,但 hessian 是一个比较老的序列化实现了,而且它是跨语言的,所以不是单独针对 Java 进行优化的。而 dubbo RPC 实际上完全是一种 Java to Java 的远程调用,其实没有必要采用跨语言的序列化方式
Kryo 是一种非常成熟的序列化实现,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。而 FST 是一种较新的序列化实现,目前还缺乏足够多的成熟使用案例,但是这两种针对Java序列化的方式无论是哪种,都会比hessian性能更高
使用Kryo和FST非常简单,只需要在dubbo RPC的XML配置中添加一个属性即可:
3.8.2 注册被序列化类
要让Kryo和FST完全发挥出高性能,最好将那些需要被序列化的类注册到dubbo系统中,例如,我们可以实现如下回调接口:
public class SerializationOptimizerImpl implements SerializationOptimizer {
public Collection> getSerializableClasses() {
List> classes = new LinkedList>();
classes.add(User.class);
return classes;
}
}
然后在XML配置中添加:
在注册这些类后,序列化的性能可能被大大提升,特别针对小数量的嵌套对象的时候。
当然,在对一个类做序列化的时候,可能还级联引用到很多类,比如Java集合类。针对这种情况,我们已经自动将JDK中的常用类进行了注册,所以你不需要重复注册它们(当然你重复注册了也没有任何影响),包括:
GregorianCalendar
InvocationHandler
BigDecimal
BigInteger
Pattern
BitSet
URI
UUID
HashMap
ArrayList
LinkedList
HashSet
TreeSet
Hashtable
Date
Calendar
ConcurrentHashMap
SimpleDateFormat
Vector
BitSet
StringBuffer
StringBuilder
Object
Object[]
String[]
byte[]
char[]
int[]
float[]
double[]
由于注册被序列化的类仅仅是出于性能优化的目的,所以即使你忘记注册某些类也没有关系。事实上,即使不注册任何类,Kryo和FST的性能依然普遍优于hessian和dubbo序列化。
实践测试:
1、创建两个POJO实体类,嵌套
package com.ydt.dubbo.pojo;
import java.io.Serializable;
public class User implements Serializable {
private String name;
private Role role;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}
package com.ydt.dubbo.pojo;
import java.io.Serializable;
public class Role implements Serializable {
private String roleName;
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
2、自定义定义一个序列化类,实现SerializationOptimizer接口,添加User实体类序列化注册
package com.ydt.dubbo.serialize;
import com.ydt.dubbo.pojo.User;
import org.apache.dubbo.common.serialize.support.SerializationOptimizer;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
public class SerializationOptimizerImpl implements SerializationOptimizer {
public Collection> getSerializableClasses() {
List> classes = new LinkedList>();
classes.add(Role.class);
classes.add(User.class);
return classes;
}
}
3、配置生产者spring文件,增加序列化配置
4、消费者测试代码:
@Test
public void test4(){
ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext(new String[] {"applicationContext.xml"});
DemoService demoService = (DemoService) context.getBean("demoService");
context.start();
System.out.println("容器加载完成....");
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
demoService.getUser();
}
System.out.println("消耗时间:" + (System.currentTimeMillis()-start));
}
没增加序列化配置时耗时:
序列化后耗时:
未来,当Kryo或者FST在dubbo中当应用足够成熟之后,我们很可能会将dubbo RPC的默认序列化从hessian2改为它们中间的某一个。