1 介绍
技术要求
JUC+JWM+git、github+Nginx+RabbitMQ
微服务架构是一种架构模式,它提倡将单一应用程序划分为一组小的服务,服务之间相互协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中,服务和服务间采用轻量级的通信机制互相协作(通常基于HTTP/RESTful API)。每个服务围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应当尽量避免统一地、集中式的服务管理机制,对具体的一个服务而言,应根据业务的要求,选择合适的语言、工具等进行构建。
SpringCloud是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。
1. 牵涉到的内容如下:
- 服务注册与发现。
- 服务调用。
- 服务熔断。
- 负载均衡
- 服务降级。
- 服务消息队列。
- 配置中心管理。
- 服务网关。
- 服务监控。
- 全链路追踪。
- 自动化构建部署。
- 服务定时任务调度操作。
- 京东系统网络架构
- 微服务技术栈
- 主要学习的技术
1.1 Boot和Cloud版本选型
- 通过官网发现,Boot官网强烈推荐我们将其升级到2.X版本。
网址:https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Release-Notes
- 二者的版本依赖
网址:https://spring.io/projects/spring-cloud#overview
更详细的版本对应方法
网址:https://start.spring.io/actuator/info
- 依赖关系确定
同时使用boot和cloud,需要照顾cloud,应该由cloud来确定boot版本。
因此,定稿版为:
-- 主要是和阳哥保持一致
cloud Hoxton.SR1
boot 2.2.2.RELEASE
cloud alibaba 2.1.0.RELEASE
Java 8
Maven 3.5及以上
Mysql 5.7及以上
1.2 SpringCloud升级,部分组件停用
1,服务注册中心:Eureka停用,可以使用zookeeper、Consul和Nacos(推荐)作为服务注册中心。
2,服务调用1:Ribbon准备停更,代替为LoadBalance
3,服务调用2:Feign改为OpenFeign
4,服务降级:Hystrix停更,改为resilence4j或者阿里巴巴的sentienl(推荐)
5.服务网关:Zuul改为gateway
6,服务配置:Config改为Nacos
7,服务总线:Bus改为Nacos
2 新建父工程Project 空间
- 创建项目
- 确定字符编码
- 注解生效激活
- Java编译版本选8
- File Type过滤
- Maven跳过单元测试
- 父工程创建完成后,执行
mvn:install
将父工程发布到仓库中方便子工程继承。
2.1 子模块cloud-provider-payment8001
步骤
创建module
改pom
写yml
主启动
业务类:建表SQL,entities,dao(包括实现的方式mapper),service,
controller。
2.1.1 业务类
项目结构
1、sql
CREATE TABLE `payment` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`serial` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '支付流水号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、实体类
@Data
@NoArgsConstructor
@AllArgsConstructor //主实体
public class Payment implements Serializable {
private Long id; //对应sql中的bignit
private String serial;
}
@Data
@NoArgsConstructor
@AllArgsConstructor //返回前端通用的json实体块
public class CommonResult<T> {
//404 not found
private Integer code; //类似404的一个编码,先判断是否为200
private String message; //传递的消息(success)
private T date; //数据
//有可能data为空。
public CommonResult(Integer code, String message) {
this.code = code;
this.message = message;
}
}
useGeneratedKeys
设置为 true 时,表示如果插入的表id以自增列为主键,则允许 JDBC 支持自动生成主键,并可将自动生成的主键id返回。
3、Dao
@Mapper //推荐使用
public interface PaymentDao {
int create(Payment payment); //写操作,新增
Payment getPaymentById(@Param("id") Long id); //读操作
}
mapper配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaolun.springcloud.dao.PaymentDao">
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values(#{serial});
</insert>
<resultMap id="BaseResultMap" type="com.xiaolun.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"></id>
<id column="serial" property="serial" jdbcType="VARCHAR"></id>
</resultMap>
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
select * from payment where id = #{id};
</select>
</mapper>
4、service
public interface PaymentService {
int create(Payment payment);
Payment getPaymentById(@Param("id") Long id);
}
@Service
public class PaymentServiceImpl implements PaymentService {
@Autowired
private PaymentDao paymentDao;
@Override
public int create(Payment payment) {
return paymentDao.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}
5、controller
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping(value = "/payment/create") //post:写操作
public CommonResult create(@RequestBody Payment payment){
int result = paymentService.create(payment);
log.info("--------插入结果------"+result);
if (result > 0){
return new CommonResult(200,"插入数据库成功",result);
}else {
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping(value = "/payment/get/{id}") //get:读操作
public CommonResult getPaymentById(@PathVariable("id") Long id){
Payment payment = paymentService.getPaymentById(id);
log.info("--------查询结果------"+payment);
if (payment != null){
return new CommonResult(200,"查询成功",payment);
}else {
return new CommonResult(444,"没有对应记录,查询id:"+id,null);
}
}
}
测试
此时,只启动cloud-provider-payment8001
进行测试:
(1)查询测试:
postman
中输入
http://localhost:8001/payment/get/2
返回结果:
{
"code":200,
"message":"查询成功",
"date":{
"id":2,
"serial":"xiaokun01"
}
}
(2)插入测试:
postman
中输入
http://localhost:8001/payment/create?serial=xiaopang
返回结果:
{
"code": 200,
"message": "插入数据库成功",
"date": 1
}
2.2 子模块cloud-consumer-order80
因为这里是消费者类,主要是消费,那么就没有service和dao,需要调用payment模块的方法。并且这里还没有微服务的远程调用,那么如果要调用另外一个模块,则需要使用基本的api
调用。
RestTemplate
提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问RestFul
服务模板类,是Spring
提供的用于访问Rest
服务的客户端模板工具类。
官网地址:
https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
使用
使用RestTemplate
访问Restful接口十分简单:
(url,requestMap,ResponseBean.class)
-- Rest访问地址,请求参数,HTTP响应转换被转换成的对象类型。
项目结构
1、配置类
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
2、controller
@RestController
@Slf4j
public class OrderController {
public static final String PAYMENT_URL = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/create")
public CommonResult<Payment> create(Payment payment){
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
}
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
}
测试
首先启动cloud-provider-payment8001
,再启动cloud-consumer-order80
。
(1)插入数据网址:
http://localhost/consumer/payment/create?serial=4
会遇到后端接收不到数据的情况,需要在接收的实体类中添加@RequestBody
注解。
这是因为:@RequestBody用来接收前端传递给后端的json字符(以对象形式),@RequestPrama 用来接收单个字段,而非对象实体。
(2)查询数据网址:
http://localhost/consumer/payment/get/2
正常显示。
注意:IDEA中现在有两个应用(80/8001端口)正在运行,需要开启下面的模式(RunDashBoard)。
2.2.1 工程重构
即新建一个模块,将重复代码抽取到一个公共模块中。
项目结构
- 创建子模块,将上图的重复代码抽取到一个公共模块中。
- 将其他模块中的公共类给删除。
- 在maven中clear工程重构项目,然后install。
执行mvn clean可将根目录下生成的target文件移除,maven通过install将本地工程打包成jar包,放入到本地仓库中,再通过pom.xml配置依赖引入到当前工程。
- 其他模块中引入该模块,即在其他模块的pom文件中添加下面的代码。
<dependency>
<groupId>com.xiaolun.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
目前工程样图
3 服务注册中心Eureka
3.1 介绍
当服务很多时,单靠代码手动管理是很麻烦的,需要一个公共组件,统一管理多服务,包括服务是否正常运行等。
1、服务治理
SpringCloud封装了Metflix公司开发的Eureka模块来进行服务治理。在传统的rpc远程调用框架中,管理每个服务与服务之间的依赖关系比较复杂,管理比较复杂,所以需要服务治理,用于管理服务之间的依赖关系,可以实现服务调用、负载均衡、容错等,进而实现服务发现和注册。
2、服务注册
Eureka采用CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心,而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server 并维持心跳链接。这样系统的维护人员就可以通过Eureka Server来监控系统中的各个微服务是否正常运行。
3、介绍
在服务注册和发现中,有一个注册中心,当服务启动的时候,会把当前自己服务器的信息(比如服务器地址通讯地址等)以别名的方式注册到注册中心上,另一方(消费者|服务提供者)以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地PC掉用RPC远程调用,框架的核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间关系的依赖(服务治理理念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))。
Eureka包含两个组件:Eureka server 和 Eureka client。
其中,Eureka server提供服务注册服务,各个微服务节点通过配置启动后,会在Eureka server 中进行注册,这样Eureka server 中的服务注册表将会存储所欲可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka client通过注册中心进行访问。这是一个Java客户端,可以简化Eureka server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动之后,将会向Eureka server发送心跳(默认周期为30秒),如果Eureka server在多个心跳周期内没有接到某个节点的心跳,Eureka server将会从服务注册表中将这个服务节点删除(默认90秒)。
4、Eureka集群原理说明
微服务RPC远程服务最核心的是高可用,假如注册中心只有一个(only one),当其出现故障时,会导致整个服务环境不可用。因此,我们要搭建Eureka注册中心集群,实现负载均衡+故障容错。
3.2 Eureka Server服务端安装
步骤
- IDEA生成Eureka Server端服务注册中心(类似物业公司)。
Eureka client
端cloud-provider-payment8001
将注册进Eureka Server
成为服务提供者provider
,类似猿辅导提供授课服务。Eureka client
端cloud-consumer-order80
将注册进Eureka Server
成为服务消费者consumer
,类似猿辅导上课的学生。
1、 生成Eureka Server端服务注册中心cloud-eureka-Server7001-单机版
项目结构
- 配置文件
server:
port: 7001
eureka:
instance:
hostname: locahost #eureka服务端的实例名称
client:
register-with-eureka: false # false表示不向注册中心注册自己
fetch-registry: false #false表示自己是注册中心,职责是维护服务实例,不需要检索服务
service-url: #设置与Eureka Server交互的地址,因为查询服务和注册服务都需要依赖的这个地址。下面的是单机版的,地址:http://localhost:7001/eureka/
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka/
- 主启动类上需要加上
@EnableEurekaServer
,
表明自己是服务注册中心,管理注册,配置等服务。
@SpringBootApplication
@EnableEurekaServer //服务注册中心,管理配置,注册等。
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class,args);
}
}
- 测试
因此此时Eureka对应的是一个单机版的配置,所以此时只启动cloud-eureka-Server7001模块进行如下的测试。
输入下面的网址:
http://localhost:7001/
当弹出下面的界面表示自己配置成功。
2、支付微服务cloud-provider-payment8001入驻Eureka Server
1、添加配置文件
eureka:
client:
register-with-eureka: true # 表示将自己注册进Eureka Server中(默认为true)
#表示从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须为true才能配合ribbon实现负载均衡。
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka #入驻地址
2、主启动类上添加注解@EnableEurekaClient
3、测试
首先要启动Eureka Server(cloud-eureka-Server7001),然后再启动入驻的微服务,在http://localhost:7001/界面上显示:
3、消费微服务cloud-consumer-order80入驻Eureka Server
该模块的注入和支付微服务(cloud-provider-payment8001)入驻Eureka Server的过程一致。
测试
网址输入:
http://localhost/consumer/payment/get/5
能够正常显示查询到的信息。
3.3 Eureka集群
1,就是pay模块启动时,注册自己,并且自身信息也放入eureka
2.order模块,首先也注册自己,放入信息,当要调用pay时,先从eureka拿到pay的调用地址
3.通过HttpClient调用
并且还会缓存一份到本地,每30秒更新一次
微服务RPC远程服务调用的核心是高可用,因此我们需要搭建Eureka注册中心集群,实现负载均衡和故障容错(互相注册,相互守望)。
3.3.1 集群环境搭建
1、项目结构
2、步骤如下
(1)新建子模块cloud-eureka-Server7002
(2)将cloud-eureka-Server7001
中的pom文件粘贴到cloud-eureka-Server7002
模块中。
(3)修改主机的host配置文件。
实现不同的端口映射到同一个地址,其中cloud-eureka-Server7001是一号机,cloud-eureka-Server7001是二号机。通过端口不同,来模拟两台机器。
注意:当上面的域名映射注释之后,会出现一些连接超时的错误。
4、application.yml配置
(1)cloud-eureka-Server7001的配置
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名字
client:
register-with-eureka: false #表示不向注册中心注册自己
fetch-registry: false
service-url:
defaultZone: http://eureka7002.com:7002/eureka/
(2)cloud-eureka-Server7002的配置
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #eureka服务端的实例名字
client:
register-with-eureka: false #表示不向注册中心注册自己
fetch-registry: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
5、测试
输入下面的网址:
http://localhost:7001/
指向7002这个服务注册中心;输入下面的网址:
http://eureka7002.com:7002/
发现指向7001这个服务注册中心。
3.3.2 订单支付两微服务注册到Eureka集群中
1、主要修改cloud-consumer-order80和cloud-provider-payment8001这两个模块中的yml文件,将单机版的入驻地址改成集群版的。
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版
2、测试
- 先启动EurekaServer,cloud-eureka-Server7001和cloud-eureka-Server7002的服务。
- 再启动服务提供者cloud-provider-payment8001模块。
- 最后启动消费者cloud-consumer-order80模块。
输入地址:http://eureka7001.com:7001/,显示:
同时输入地址:http://localhost/consumer/payment/get/5,能够完成正常查询。
3.3.3 cloud-provider-payment8001模块配置为集群模式
1、实现的效果如下图:
2、项目结构
3、创建cloud-provider-payment8002
模块,pom文件,yml文件和主启动类的书写都复制cloud-provider-payment8001
模块的,但是注意端口号要改成8002,服务名称不用改,用一样的。
此时访问cloud-consumer-order80
模块,发现并没有负载均衡到两个payment,模块中,而是只访问8001
模块,我们需要在cloud-consumer-order80
模块中,不要将订单访问地址写死(不然,订单只会访问8001端口。此时,订单每次访问时,都会去Eureka中获取地址,轮询交替获取,这样就达到负载均衡的效果了),而是写成下面的形式:
// public static final String PAYMENT_URL = "http://localhost:8001";
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; //只认微服务名称
4、测试
访问网址:http://localhost/consumer/payment/get/5,将会报出下面的错误:
这是因为cloud-provider-payment8001
和cloud-provider-payment8002
两个微服务向外暴露的是微服务名称,而不是地址和端口,订单服务识别不了。我们需要使用该注解赋予RestTemplate
负载均衡的能力。
@Bean
@LoadBalanced //使用该注解赋予RestTemplate负载均衡的能力
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
此时,Ribbon
和Eureka
整合后,消费者consumer
可以直接调用服务,而不用关心地址和端口号了。
actuator微服务信息完善
1、当前的问题
(1)主机名称:服务名称暴露,需要进行修改
(2)访问信息目前没有ip地址提示,需要有IP信息提示。
eureka:
instance:
instance-id: payment8001 #主机名称的修改
prefer-ip-address: true #访问路径可以显示IP地址
输入下面的网址:
http://eureka7002.com:7002/
(此时仍然是Eureka的集群模式,cloud-eureka-Server7001和cloud-eureka-Server7002在同时启动)显示:
3.3.4 服务发现Discovery
对于注册进Eureka里面的微服务,可以通过服务发现来获得该服务的信息。(自己微服务的信息向外面写好(类似于关于我们的功能))。
1、修改cloud-provider-payment8001
的Controller,在controller中添加注解,并书写方法
//服务发现client端,获得相应的服务信息
//依赖org.springframework.cloud.client.discovery.DiscoveryClient;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/payment/discovery")
public Object discovery(){
//拿到所有注册的信息
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("-----element-----"+element);
}
//拿到指定服务名称的所有服务的注册信息。
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}
2、在启动类中添加注解:
@EnableDiscoveryClient
3、测试
首先,启动EurekaServer,即cloud-eureka-Server7001/7002服务,再启动cloud-provider-payment8001
主启动类,需要稍微等一会,然后输入:http://localhost:8001/payment/discovery,显示:
控制台输出:
3.3.5 Eureka自我保护
1、介绍
保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护,一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也不会注销任何微服务。
如果Eureka Server首页看到这段提时,说明Eureka 进入到了保护模式:
一句话:某时刻某一个微服务不能用了,Eureka不会立即处理,依旧会对该微服务的信息进行保存,输入CAP里面的AP分支。
产生Eureka自我保护模式的原因:为了让EurekaClient可以正常运行,但是与Eureka Server网络不通的情况下,Eureka Server不会立即将EurekaClient服务剔除。
默认情况下,如果EurekaServer在一定的时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒),但是当网络分区故障发生的时候(延时,卡顿,拥挤等),微服务和EurekaServer之间就无法正常通讯了,剔除行为就变得十分危险(微服务本身是健康的,不应该注销这个微服务)。Eureka通过自我保护模式来解决上述问题。
2、禁止自我保护
默认自我保护机制是开启的,一般生产环境中不会禁止自我保护。
1、Eureka服务端cloud-eureka-Server7001
模块关闭自我保护。
server:
enable-self-preservation: false # 关闭自我保护机制,保证不可用服务即使剔除
eviction-interval-timer-in-ms: 2000 #清理间隔
将7001的配置改成单机版,好启动。
2、客户端cloud-provider-payment8001
模块开启健康检查
# Eureka客户端向服务端发送的时间间隔(续约更新时间间隔),单位为秒(默认30S)
lease-renewal-interval-in-seconds: 1
# Eureka服务端在收到最后一次心跳后等待时间上限(续约到期时间),超时剔除服务(默认90秒)。
lease-expiration-duration-in-seconds: 2
3、测试
先启动cloud-eureka-Server7001
,后启动cloud-provider-payment8001
,输入网址:http://eureka7001.com:7001/,然后关闭cloud-provider-payment8001
,可以看到服务立即被剔除了。
3、Eureka停更
网址:https://github.com/Netflix/eureka/wiki