随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构,还有在Google带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方,还是偏安一隅得过且过?
其实生活不止眼前的苟且,还有诗和远方。所以我们今天就回顾历史,看一看系统架构演变的历程;把握现在,学习现在最火的技术架构;展望未来,争取成为一名优秀的Java工程师。
1.1 集中式架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。
存在的问题:
1.代码耦合,开发维护困难
2.无法针对不同模块进行针对性优化
3.无法水平扩展
4.单点容错率低,并发能力差
1.2 垂直拆分
当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分。
优点:
1.系统拆分实现了流量分担,解决了并发问题
2.可以针对不同模块进行优化
3.方便水平扩展,负载均衡,容错率提高
缺点:
系统间相互独立,会有很多重复开发工作,影响开发效率
1.3 分布式服务
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。
优点:
将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率
缺点:
系统间耦合度变高,调用关系错综复杂,难以维护
1.4 流动计算架构(SOA)
SOA :面向服务的架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
以前出现了什么问题?
服务越来越多,需要管理每个服务的地址
调用关系错综复杂,难以理清依赖关系
服务过多,服务状态难以管理,无法根据服务情况动态管理
服务治理要做什么?
服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
动态监控服务状态监控报告,人为控制服务状态
缺点:
服务间会有依赖关系,一旦某个环节出错会影响较大
服务关系复杂,运维、测试部署困难,不符合DevOps思想
1.5 微服务
就目前而言,业界对微服务并没有一个统一的标准的定义。但通常而言,微服务架构是一种架构模式或者架构风格,它提倡将单一应用程序划分成一组小的服务。每个服务运行在其独立的进程中。服务之间互相协调互相配合,为用户提供最终价值。服务之间采取轻量级的通信机制互相沟通(通常是基于HTTP的Restful API).每个服务都围绕这具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应尽量的避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写这些服务,也可以使用不同的数据存储。
RPC和HTTP
无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?
常见的远程调用方式有以下2种:
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
现在热门的Rest风格,就可以通过http协议来实现。
如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。
相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在我们的项目中,我们会选择SpringCloud套件,因此我们会使用Http方式来实现服务间调用。
微服务是一种架构方式,最终肯定需要技术架构去实施。
微服务的实现方式很多,但是最火的莫过于Spring Cloud了。为什么?
1.后台硬:作为Spring家族的一员,有整个Spring全家桶靠山,背景十分强大。
2.技术强:Spring作为Java领域的前辈,可以说是功力深厚。有强力的技术团队支撑,一般人还真比不了
3.群众基础好:可以说大多数程序员的成长都伴随着Spring框架,试问:现在有几家公司开发不用Spring?SpringCloud与Spring的各个框架无缝整合,对大家来说一切都是熟悉的配方,熟悉的味道。
4.使用方便:相信大家都体会到了SpringBoot给我们开发带来的便利,而SpringCloud完全支持SpringBoot的开发,用很少的配置就能完成微服务框架的搭建
3.1 springcloud简介
SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
1.Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)
2.Zuul:网关组件,提供智能路由,访问过滤功能
3.Ribbon:客户端负载均衡的服务调用组件(客户端负载)
4.Feign:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)
5.Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)
3.2 微服务场景模拟
首先,我们需要模拟一个服务调用的场景
搭建两个工程:oracle-service-provider(服务提供方)和oracle-service-consumer(服务调用方)。
服务提供方:使用mybatis操作数据库,实现对数据的增删改查;并对外提供rest接口服务。
服务消费方:使用restTemplate远程调用服务提供方的rest接口服务,获取数据。
(1)创建服务提供者springclouddemo1_provider
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2.2.1.RELEASE
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.1
mysql
mysql-connector-java
runtime
tk.mybatis
mapper-spring-boot-starter
2.0.4
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.projectlombok
lombok
org.springframework.boot
spring-boot-maven-plugin
server:
port: 8086
spring:
datasource:
username: root
password: 123
url: jdbc:mysql://localhost:3306/ssm?serverTimezone=GMT
driver-class-name: com.mysql.jdbc.Driver
package com.bianyiit.pojo;
import lombok.Data;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
@Table(name = "account") //实体类绑定的表
@Data //lombok插件
public class Account implements Serializable {
private static final long serialVersionUID = 3024382278891947015L;
@Id //标识当前id为主键
@GeneratedValue(strategy= GenerationType.IDENTITY) //主键自增长
private Integer id;
private String name;
private Double money;
}
package com.bianyiit.mapper;
import com.bianyiit.pojo.Account;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;
//mapper插件对于单表的增删改查操作都不用我们自己写 tk.mybatis.mapper.common.mapper
@Repository
public interface AccountMapper extends Mapper {
//集成Mapper(里面的泛型为实体类)
}
package com.bianyiit.service;
import com.bianyiit.mapper.AccountMapper;
import com.bianyiit.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
@Autowired
AccountMapper accountMapper;
//根据id查询Account
public Account findAccountById(Integer id){
return accountMapper.selectByPrimaryKey(id); //通用mapper中提供了很多方法可以让我们去操作单表
}
}
package com.bianyiit.controller;
import com.bianyiit.pojo.Account;
import com.bianyiit.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
AccountService accountService;
@GetMapping("/findAccountById/{id}") //localhost:8080/account/findAccountById/2
public Account getAccountById(@PathVariable("id") Integer id){
return accountService.findAccountById(id);
}
}
package com.bianyiit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.bianyiit.mapper") //设置包扫描mapper,通用mapper提供的注解(tk.mybatis.spring.annotation.MapperSan)
public class ServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceProviderApplication.class);
}
}
(1)创建服务消费者springclouddemo1_consumer
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2.2.1.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.projectlombok
lombok
junit
junit
test
org.springframework.boot
spring-boot-maven-plugin
server:
port: 8084
package com.bianyiit.pojo;
import lombok.Data;
import java.io.Serializable;
//这里不用使用@Table注解修饰,因为不需要去连接数据库查表,只需要进行数据的封装
@Data
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
}
package com.bianyiit.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MyConfig {
//注册RestTemplate,用于服务之间的远程调用
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
package com.bianyiit.controller;
import com.bianyiit.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/consumer")
public class AccountController {
@Autowired
RestTemplate restTemplate; //使用它做一个远程调用
@GetMapping("/findAccountById/{id}")
public Account findAccountById(@PathVariable("id") Integer id){
//http://localhost:8081/account/findAccountById?id=2 @RequstParam
//http://localhost:8081/account/findAccountById/2 @PathVariable
return restTemplate.getForObject("http://localhost:8081/account/findAccountById/"+id,Account.class); //进行远程调用
}
}
3.3 以上案例存在的问题
4.1 认识Eureka
首先我们来解决第一问题,服务的管理。
4.2 Eureka原理图
4.3 搭建Eureka Server
org.springframework.boot
spring-boot-starter-parent
2.2.5.RELEASE
Hoxton.SR1
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
2.2.1.RELEASE
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
spring-milestones
Spring Milestones
https://repo.spring.io/milestone
server:
port: 10087
spring:
application:
name: springclouddemo1_enableEureka #注册的服务名称,会在eureka中显示
eureka:
client:
service-url: #配置eureka的默认访问地址
defaultZone: http://127.0.0.1:10086/eureka
package com.bianyiit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer 声明当前应用是一个Eureka服务中心
public class ServiceEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceEurekaApplication.class, args);
}
}
4.4 将服务注册到eureka
server:
port: 8086
spring:
datasource:
username: root
password: 123
url: jdbc:mysql://localhost:3306/ssm?serverTimezone=GMT
driver-class-name: com.mysql.jdbc.Driver
application:
name: springclouddemo1-provider #注册到eureka的服务名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka #连接eureka的地址
4.5 从Eureka拉取服务
server:
port: 8084
spring:
application:
name: springclouddemo1_consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
4.6 改造服务消费方拉取服务列表硬编码问题
package com.bianyiit.controller;
import com.bianyiit.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/consumer")
public class AccountController {
@Autowired
RestTemplate restTemplate; //使用它做一个远程调用
@Autowired
DiscoveryClient discoveryClient; //服务消费方在eureka注册中心拉取的服务列表都封装到DiscoveryClient里面
@GetMapping("/findAccountById/{id}")
public Account findAccountById(@PathVariable("id") Integer id){
// 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合,需要根据application.yml中注册的服务名称去填写
List instances = discoveryClient.getInstances("springclouddemo1_provider");
// 因为只有一个Service-provider,所以获取第一个实例
ServiceInstance instance = instances.get(0);
String url="http://"+instance.getHost()+":"+instance.getPort()+"/account/findAccountById/"+id;
return restTemplate.getForObject(url,Account.class);
}
}
5.1 Eureka基础架构
Eureka架构中的三个核心角色:
5.2 搭建高可用的Eureka(了解)
动手搭建高可用的EurekaServer:
需求: 我们假设要运行两个EurekaServer的集群,端口分别为:10086和10087。只需要把bianyi-eureka启动两次即可。
复制一个新的Eureka注册中心
将10086端口的Eureka注册到10087端口号的Eureka,然后启动该Eureka注册中心
server:
port: 10086
spring:
application:
name: springclouddemo1_enableEureka #注册的服务名称,会在eureka中显示
eureka:
client:
service-url: #配置eureka的默认访问地址
defaultZone: http://127.0.0.1:10087/eureka
注意:此时启动会报错,很正常。因为10087服务没有启动
由于两个Eureka共用一个application.yml,所以启动完上一个之后,直接更改applicaiton.yml中的端口即可,将10087端口的Eureka注册到端口号是10086端口号的Eureka,然后启动该Eureka注册中心
server:
port: 10087
spring:
application:
name: springclouddemo1_enableEureka #注册的服务名称,会在eureka中显示
eureka:
client:
service-url: #配置eureka的默认访问地址
defaultZone: http://127.0.0.1:10086/eureka
注意:为什么要10086端口的Eureka注册到10087端口的Eureka呢??
因为多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
假如有三个Eureka注册中心,只需要三个Eureka首尾相连形成一个闭环即可
在刚才的案例中,我们启动了一个oracle-service-provider,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。但是实际环境中,我们往往会开启很多个oracle-service-provider的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon:
接下来,我们就来使用Ribbon实现负载均衡
6.1 负载均衡演示
注意:实现负载均衡,服务的名称不能带下划线,否则会消费者报java.lang.IllegalStateException: Request URI does not contain a valid hostname:的异常
在服务消费方的引导类MyConfig上的RestTemplate的配置方法上添加@LoadBalanced注解
package com.bianyiit.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MyConfig {
//注册RestTemplate,用于服务之间的远程调用
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用
访问页面,查看结果:http://localhost:8082/consumer/findAccountById/2
6.2 负载均衡的策略
Ribbon默认的负载均衡策略是简单的轮询,在springcloud中使用RibbonLoadBalanceClient来进行负载均衡的。其中有一个choose方法,找到choose方法的接口方法,是这样介绍的:
现在这个就是负载均衡获取实例的方法。
我们注入这个类的对象,然后对其测试:
package com.bianyiit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ServiceConsumerApplication.class)
public class LoadBalanceTest {
@Autowired
LoadBalancerClient client;
@Test
public void testLoadBalance(){
for(int i = 1;i<=100;i++){
ServiceInstance instance = client.choose("springclouddemo1-provider");
System.out.println(instance.getHost() + ":" +instance.getPort());
}
}
}
符合了我们的预期推测,确实是轮询方式。
我们也可以改变默认的负载均衡策略
SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在springclouddemo1_consumer的application.yml中添加如下配置:
server:
port: 8084
spring:
application:
name: springclouddemo1_consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
springclouddemo1-provider: #注意这里填写的是你的服务提供者注册的名字
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #改变负载均衡策略为随机
再次测试,发现结果变成了随机:
springcloud入门案例参考源码
链接:https://pan.baidu.com/s/1uy8m2DEPEtG8Cu2NupB1ng
提取码:gauq