Dubbo 应用场景
JavaEE 应用三层架构,在很长一段时间都没有出问题,直到大数据时代的到来,某些数据量巨大的公司开始遇到了新的挑战,某些业务方法频繁调用,需要配置大量资源,某些业务很少调用。只需要配置少量资源。比如:商城应用中
订单服务、积分服务、商品服务使用频率远远大于其他服务,如果仅仅通过布置多台服务器的方式,会造成服务器成本大量提升,资源得不到充分利用,如果遇到某个关系型数据库不好处理的业务场景,技术选型时,也要考虑系统稳定性和冲突问题。所以再次拆分服务,基于服务的分布式应用架构,这种架构就是微服务应用架构,目前比较成熟,社区活跃的微服务架构有 Dubbo,SpringCloud。
生产者:提供业务逻辑实现的角色,对外提供服务
消费者:调用生产者提供的服务。
微服务的优势
降低系统耦合度
服务之间都是相互独立的,互相不干扰,每个服务都能更好的选择符合业务场景的技术
充分利用服务器的硬件资源
降低维护成本和难度
提高应用系统的稳定性
分布式服务和微服务的差别 分布式服务不一定是微服务应用,分布式服务是部署在不同的机器上的。一个服务可能负责几个功能,是一种面向 SOA 架构,服务之间也是通过 RPC 来交互或者 webService 来交互,系统应用部署在超过一台服务器或虚拟机上。且各自分开部署的部分彼此通过各种通讯协议交互信息,就可以算作分布式部署。
生产环境下的微服务肯定是分布式部署的,分布式部署的应用不一定是微服务架构,比如集群部署,它是把相同应用赋值到不同服务器上,逻辑功能还是单体应用。简单说微服务就是很小的服务,小到一个服务只对一个单一的功能,只做一件事。这个服务可以单独部署运行,服务之间通过 RPC 来交互。每个微服务都是由独立的小团队开发,测试,部署,上线,负责它的整个生命周期。
RPC 调用原理
序列化和反序列化 序列化:把 Java 对象转变成二进制数据 反序列化:把二进制数据转变成 Java 对象
注意:在反序列的过程中,需要知道该二进制数据还原成哪几个 Java 对象,因此必须要有序列化 ID 号,在 Java 规范中,必须实现 Serializable 接口开启序列化功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Setter @Getter @ToString @NoArgsConstructor @AllArgsConstructor public class User implements Serializable { private Long id; private String name; } public class App { @Test public void test1 () throws Exception { User u = new User(10L , "逍遥" ); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("f:/user.txt" )); out.writeObject(u); out.close(); } @Test public void test2 () throws Exception { User u = null ; ObjectInputStream in = new ObjectInputStream(new FileInputStream("f:/user.txt" )); u = (User) in.readObject(); in.close(); System.out.println(u); } }
流程图
RPC 远程调用底层的核心技术就是序列化和反序列化
生产者发布服务
消费者按照 RPC 协议要求调用生产者发布的服务
生产者执行方法得到返回的对象
生产者把对象序列化后返回给消费者
消费者接收到数据后对其反序列化,得到 Java 对象
Dubbo
Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,公国使用高性能的 RPC 实现服务的输出和输入功能,可以和 Spring 框架无缝集成。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力
面向接口的远程方法调用
智能容错和负载均衡
服务自动注册和发现
核心组件:
Remoting: 网络通信框架,实现了 sync-over-async 和 reuqest-response 消息机制
RPC: 一个远程过程调用的接口,支持负载均衡、容灾和集群功能
Registry: 服务目录框架用于服务的注册和服务事件的发布和订阅
0 start ,生产者启动 Dubbo 容器,生产服务对象 1 register, 生产者把生成的服务发布到注册中心 2 消费者想注册中心订阅服务,把发布的服务下载到本地缓存 3 订阅服务后,注册中心会通知本地缓存会自动更新生产者发布的服务 4 当消费者需要调用服务时,按照 RPC 协议要求,向生产者发起服务的调用 5 生产者把返回的对象交给 Dubbo 容器进行序列化处理后返回给消费者,消费者接受到返回的数据后对其反序列化,得到 Java 对象,监视器对服务性能做监控统计。
注意:注册中心和监视器都不是必须的,可以缺少。如:缺少注册中心后,消费者就不能自动更新生产者发布的服务信息,当生产者信息发生改变时,消费者很可能调用服务失败。比较出名的注册中心有:zookeeper,eruka
微服务项目结构
上图中可以看出,至少需要创建5个项目
product-api: 定义商品相关的业务方法
member-api: 定义会员相关的业务方法
product-sever: 商品服务的提供者,同时也是会员服务的消费者,底层会去调用会员服务的相关功能
member-sever: 会员服务的提供者,提供会员业务方法的实现
website: 商品服务的消费者,使用商品服务提供的业务功能
Zookeeper
zookeeper 的功能非常多,大数据技术领域的核心组件之一,我们仅仅只是作为注册中心使用,没有注册中心的情况下,dubbo 也可以正常的运行。默认端口:2181
安装
安装好后
1 2 cd /usr/local/etc/zookeeper cat zoo.cfg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # The number of milliseconds of each tick tickTime=2000 # The number of ticks that the initial # synchronization phase can take initLimit=10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit=5 # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir=/usr/local/var/run/zookeeper/data # the port at which the clients will connect clientPort=2181 # the maximum number of client connections. # increase this if you need to handle more clients #maxClientCnxns=60 # # Be sure to read the maintenance section of the # administrator guide before turning on autopurge. # # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance # # The number of snapshots to retain in dataDir #autopurge.snapRetainCount=3 # Purge task interval in hours # Set to "0" to disable auto purge feature #autopurge.purgeInterval=1
启动 zookeeper,同 brew 下载的其他服务
1 2 3 brew services list brew services start zookeeper ==> Successfully started `zookeeper` (label: homebrew.mxcl.zookeeper)
查看zookeeper状态
1 2 3 4 5 zkServer status ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo.cfg Mode: standalone
standalone 表示单机模式。
Dubbo 微服务开发
案例:
商品服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) throws IOException { ApplicationConfig applicationConfig = new ApplicationConfig("product-server" ); ProtocolConfig dubbo = new ProtocolConfig("dubbo" , 20880 ); RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181" ); ServiceConfig<IProductService> config = new ServiceConfig<>(); config.setApplication(applicationConfig); config.setProtocol(dubbo); config.setRegistry(registryConfig); config.setInterface(IProductService.class); config.setRef(new ProductServiceImpl()); config.export(); System.in.read(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { ApplicationConfig applicationConfig = new ApplicationConfig("website" ); RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181" ); ReferenceConfig<IProductService> ref = new ReferenceConfig<>(); ref.setApplication(applicationConfig); ref.setRegistry(registryConfig); ref.setInterface(IProductService.class); IProductService productService = ref.get(); Product product = productService.get(1l , null ); System.out.println(product); }
SpringBoot 集成 Dubbo
website.pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > <version > 2.7.0</version > </dependency > <dependency > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo</artifactId > <version > 2.7.0</version > </dependency > <dependency > <groupId > org.apache.curator</groupId > <artifactId > curator-recipes</artifactId > <version > 2.12.0</version > </dependency > <dependency > <groupId > cn.lizhaoloveit</groupId > <artifactId > product-api</artifactId > <version > 1.0</version > </dependency >
product-server.pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > <version > 2.7.0</version > </dependency > <dependency > <groupId > org.apache.dubbo</groupId > <artifactId > dubbo</artifactId > <version > 2.7.0</version > </dependency > <dependency > <groupId > org.apache.curator</groupId > <artifactId > curator-recipes</artifactId > <version > 2.12.0</version > </dependency > <dependency > <groupId > cn.lizhaoloveit</groupId > <artifactId > product-api</artifactId > <version > 1.0</version > </dependency > <dependency > <groupId > io.netty</groupId > <artifactId > netty-all</artifactId > <version > 4.1.41.Final</version > </dependency >
用一个类来模拟数据库 product-server.productData.java
1 2 3 4 5 6 7 8 9 10 11 12 public abstract class ProductData { private static Map<Long, Product> datas = new HashMap<>(); static { datas.put(1L , new Product(1L , "IPhone XR" )); datas.put(2L , new Product(2L , "华为P30" )); datas.put(3L , new Product(3L , "小米9" )); } public static Product get (Long id) { return datas.get(id); } }
在上面使用 Dubbo 微服务开发的时候,创建的所有配置项,都在 application.properties 中重新配置。
1 2 3 4 5 6 7 8 9 10 dubbo.application.name =product-server dubbo.protocol.name =dubbo dubbo.protocol.port =20880 dubbo.registry.address =zookeeper://127.0.0.1:2181 spring.main.allow-bean-definition-overriding =true
最后一行配置的意思是因为,在 dubbo 内部默认会创建一个初始化的空的 config 的 bean。而我们重新配置发布者信息、协议和端口、地址后,要允许新的配置 bean 会覆盖原有的配置 bean。
@EnableDubbo 在生产者的 springboot 配置类中要加上 @EnableDubbo 注解。
1 2 3 4 5 6 7 @SpringBootApplication @EnableDubbo public class ProductServer { public static void main (String[] args) { SpringApplication.run(ProductServer.class, args); } }
@EnableDubbo 注解是 @EnableDubboConfig 和 @DubboComponenetScan 两者组合的注解。
通过 @EnableDubbo 可以在指定的包名下(scanBasePackages) 或者 (scanBasePackageClasses) 指定的类名下,扫描 Dubbo 服务提供者,以 @Service 注解标注,以及 Dubbo 的服务消费者 以 @Reference 注解标注。
扫描到 Dubbo 的服务提供方和消费者之后,对其做响应的组装并初始化,最终完成服务暴露和引用的工作。
@Service 用 @Service 来配置 Dubbo 的服务提供方
1 2 3 4 5 6 @Service public class AnnotatedGreetingService implements GreetingService { public String sayHello (String name) { return "hello, " + name; } }
通过 @Service 提供的属性可以定制化 Dubbo 服务提供者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package org.apache.dubbo.config.annotation;@Documented @Retention (RetentionPolicy.RUNTIME)@Target ({ElementType.TYPE}) @Inherited public @interface Service { Class<?> interfaceClass() default void .class; String interfaceName () default "" ; String version () default "" ; String group () default "" ; boolean export () default true ; boolean register () default true ; String application () default "" ; String module () default "" ; String provider () default "" ; String[] protocol() default {}; String monitor () default "" ; String[] registry() default {}; }
@Service 贴在服务的实现类上,表示服务的具体实现
interfaceClass 指定服务提供者的 api 类
interfaceName 指定服务提供者的 api 类名
version 服务的版本号
group 指定服务的分组
export 是否暴露服务
registry 是否向注册中心注册服务
application 应用配置
module 模块配置
provider 服务提供者配置
protocol 协议配置
monitor 监控中心配置
registry 注册中心配置
application module provider protocal monitor registry (8->13) 需要提供的是对应的 Spring bean 的名字,这些 bean 的组装要么通过传统的 XML 配置方式完成,要么通过 Java Config 完成。如果用 springboot 集成项目,会在 application.properties 中进行相应配置,并且 springboot 对其已经有了默认的配置项。
@Reference 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.apache.dubbo.config.annotation;@Documented @Retention (RetentionPolicy.RUNTIME)@Target ({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) public @interface Reference { Class<?> interfaceClass() default void .class; String interfaceName () default "" ; String version () default "" ; String group () default "" ; String url () default "" ; String application () default "" ; String module () default "" ; String consumer () default "" ; String protocol () default "" ; String monitor () default "" ; String[] registry() default {}; }
其中比较重要的有
@Reference 可以定义在类中的一个字段上,也可以定义在一个方法上,甚至可以用来修饰另一个 annotation 表示一个服务的引用。通常 @Reference 定义在一个字段上。
interfaceClass:指定服务的 interface 的类
interfaceName:指定服务的 interface 的类名
version:指定服务的版本号
group:指定服务的分组
url:通过指定服务提供方的 URL 地址直接绕过注册中心发起调用
application:应用配置
module:模块配置
consumer:服务消费方配置
protocol:协议配置
monitor:监控中心配置
registry:注册中心配置
另外,需要注意的是,application、module、consumer、protocol、monitor、registry(从 7 到 12)需要提供的是对应的 spring bean 的名字,而这些 bean 的组装要么通过传统的 XML 配置方式完成,要么通过现代的 Java Config 来完成。在本文中,将会展示 Java Config 的使用方式。springboot 中会有默认的配置项,在 application.properties 中完成配置。
ProductServiceImpl.java
1 2 3 4 5 6 7 @Service public class ProductServiceImpl implements IProductService { @Override public Product get (Long productId, Long userId) { return ProductData.get(productId); } }
ProductController.java
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping ("product" )public class ProductController { @Reference private IProductService productService; @RequestMapping ("get" ) public Object get (Long productId, Long userId) { return productService.get(productId, userId); } }
一个服务提供者,调用其他服务提供者的服务 加入会员服务 member-api、member-server
1 2 3 4 5 6 7 8 9 10 11 public abstract class UserData { private static Map<Long, User> datas = new HashMap<>(); static { datas.put(1L , new User(1L , "逍遥" )); datas.put(2L , new User(2L , "bunny" )); } public static User get (Long id) { return datas.get(id); } }
application.properties
1 2 3 4 5 6 7 8 9 10 dubbo.application.name =member-server dubbo.protocol.name =dubbo dubbo.protocol.port =20881 dubbo.registry.address =zookeeper://127.0.0.1:2181 spring.main.allow-bean-definition-overriding =true
UserServiceImpl.java
1 2 3 4 5 6 7 @Service public class UserServiceImpl implements IUserService { @Override public User get (Long id) { return UserData.get(id); } }
在 ProductServiceImpl.java 中远程获取 member-server 服务中的 实现类 bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class ProductServiceImpl implements IProductService { @Reference private IUserService userService; @Override public Product get (Long productId, Long userId) { Product product = ProductData.get(productId); User user = userService.get(userId); product.setUser(user); return product; } }
最后效果
事务问题
A服务开启事务,对B服务发起远程调用,B服务调用也开启了一个事务,然后进行业务操作,业务正常执行,最终提交事务,A服务拿到远程调用的结果继续执行,但是后面出现异常了 这时 A 的事务回滚能影响 B 服务中的事务吗?
事务 A 和 事务 B 是分别在不同机器上开启的事务,相互独立,是不同的两个事务,在传统的事务管理方式是不能应用在分布系统高德,分布式系统有专门的分布式事务处理方式,强一致性、最终一致性。
Dubbo Admin 控制台
dubbo Admin 是个 springboot 项目github源码地址
java -jar dubbo-admin.jar
默认端口号是 7001,用户名和密码都是 root,访问 localhost:7001 访问
服务交叉引用问题
在实际开发中,经常会存在 A 服务引用 B 服务,B 服务也引用 A 服务,那么此时就存在问题,无论哪个服务在启动时都会报错。此时可以设置消费者项目启动时不要去检查服务是否存在,就可以顺利启动项目了,但如果运行时,服务依然不存在,则会报错。
applicaiton.properties
1 2 dubbo.consumer.check =false
服务集群
如果一个服务只有一个服务对象,所有压力都落在这个对象上,逼近极限时,很可能会导致服务挂掉,我们可以通过多发布几个服务对象,通过负载均衡策略来缓解单一服务对象压力过大问题。
生产者多发布几个服务对象,注意修改多个服务发布的端口
1 2 3 dubbo.protocol.port =20880 # 生产者 A dubbo.protocol.port =20883 # 生产者 B
1 2 3 4 RandomLoadBalance: 随机(random),默认策略 RoundRobinLoadBalance: 轮询(roundrobin) ConsistentHashLoadBalance:hash一致(consistenthash) LeastActiveLoadBalance: 最少活跃(leastactive)
application.properties
1 2 3 dubbo.consumer.loadbalance =roundrobin
多版本发布
服务升级时,由于不清楚新版本的服务是否存在 bug,往往采取国度的方式进行切换,此时需要两个版本的服务都要存在。
生产者在生产服务的时候指定该服务的版本号
1 2 3 4 5 @Service (version="1.0" )public class UserServiceImpl implements IUserService {...}@Service (version="2.0" )public class UserServiceImpl implements IUserService {...}
消费者必须明确告知引用哪个版本的服务
1 2 3 4 5 6 7 8 @Reference (version="1.0" )private IUserService userService;@Reference (version="2.0" )private IUserService userService;@Reference (version="*" ) private IUserService userService;
服务超时,重试,容错
服务调用时,可能由于服务生产者的网络环境差,但消费者不知道,依然请求,长时间没有回应,此时可以设置消费者等待的超时时间,调用超过设置的时间时放弃远程的响应。默认超时1秒。超时时,框架并不会马上放弃服务的调用,还会进行重试,重试次数:2次
1 2 3 4 dubbo.consumer.timeout =1500 dubbo.consumer.retries =1
只有幂等性操作才能重试,非幂等性操作不能重试。幂等性的意思是服务执行一次和执行多次结果一样。 此时因超时调用失败,出现的报错页面会直接反馈给消费者,消费者再把报错信息响应出去,用户会直接看到错误页面,这样不友好,应该对错误信息进行统一处理。
服务集群后还能配置集群下的容错机制,
FailoverCluster: 失败自动切换,默认策略,用于幂等性操作,查询 FailfastCluster: 快速失败,只发一次调用,失败立即报错,用于非幂等性操作,插入 FailsafeCluster: 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作 FailbackCluster: 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作 ForkingCluster: 并行调用多个服务器,只要一个成功即返回,通常用于实时性要求较高的读操作,但需要浪费更多服务资源,可以通过 fork=”2” 来设置最大并行数。 BroadcastCluster: 广播调用所有提供者,逐个调用,任意一台报错则报错,通常用于通知所有提供者更新缓存或日志等本地资源信息。
1 2 dubbo.consumer.cluster =failfast
服务降级
为了保证服务 B 的抗压能力,牺牲服务 A,甚至直接把 服务 A 功能关掉,把资源留给服务 B 使用,从 Dubbo Admin 控制台去配置当前服务的降级,消费者访问降级的服务时,不发起远程调用请求,直接返回 null。