# mycloud-demo **Repository Path**: chen_jeff/mycloud-demo ## Basic Information - **Project Name**: mycloud-demo - **Description**: Spring Cloud 项目搭建 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-12-06 - **Last Updated**: 2021-12-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [toc] # MyCloud DEMO 项目构建 DEMO ## 1. 版本信息 - cloud: Hoxton.SR1 - boot: 2.2.2.RELEASE - cloud alibaba: 2.1.0.RELEASE - openJDK: 11 - maven: 3.6 - mysql: 8.x ## 2. 工程构建 ### 2.1. 父工程构建 #### 2.1.1. `pom.xml` 引入依赖 ```pom 4.0.0 ink.honp mycloud-demo 1.0-SNAPSHOT 11 11 UTF-8 2.2.2.RELEASE Hoxton.SR1 2.1.0.RELEASE 4.12 1.2.17 1.18.16 8.0.15 1.2.0 2.1.0 org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring.cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring.cloud.alibaba.version} pom import mysql mysql-connector-java ${mysql.version} com.alibaba druid ${druid.version} org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.spring.boot.version} junit junit ${junit.version} log4j log4j ${log4j.version} org.projectlombok lombok ${lombok.version} true org.springframework.boot spring-boot-maven-plugin true true ``` ### 2.2. 服务提供者模块 `cloud-provider-payment` 构建 #### 2.2.1. 初始化库表 SQL 语句 ```sql CREATE DATABASE IF NOT EXISTS cloud_demo DEFAULT CHARACTER SET utf8mb4 ; use cloud_demo; -- payment DROP TABLE IF EXISTS payment; CREATE TABLE payment ( `id` BIGINT (20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `serial` VARCHAR (300) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ; INSERT INTO payment (`id`, `serial`) VALUES(31, 'test001'),(32, 'test002') ; ``` #### 2.2.2. 引入依赖 `pom.xml` ```xml mycloud-demo ink.honp 1.0-SNAPSHOT 4.0.0 cloud-provider-payment payment 服务提供者 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid-spring-boot-starter 1.2.8 mysql mysql-connector-java org.springframework.boot spring-boot-starter-jdbc org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` #### 2.2.3. 添加配置 `application.yml` ```yml server: port: 8081 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://xxxx/cloud_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false username: dev password: xxxx mybatis: mapperLocations: classpath:/mapper/*.xml type-aliases-package: ink.honp.cloud.payment.entities ``` #### 2.2.4. 创建启动类 `ink.honp.cloud.payment.PaymentApplication` ```java package ink.honp.cloud.payment; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author jeff chen * @since 2021-12-06 15:03 */ @SpringBootApplication public class PaymentApplication { public static void main(String[] args) { SpringApplication.run(PaymentApplication.class,args); } } ``` _**启动,测试模块是否构建成功**_ #### 2.2.5. 创建实体和对应的 DAO 类型 1. 创建实体类 `Payment` ```java @Data @NoArgsConstructor @AllArgsConstructor public class Payment implements Serializable { private static final long serialVersionUID = -1305247656330915904L; private Long id; private String serial; } ``` 2. 创建对应的数据库操作接口 `PaymentDao` ```java package ink.honp.cloud.payment.dao; import ink.honp.cloud.payment.entities.Payment; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * @author jeff chen * @since 2021-12-06 15:21 */ @Mapper public interface PaymentDao { int create(Payment payment); Payment getPaymentById(@Param("id") Long id); } ``` 3. 创建对应的 `Mapper.xml` 文件 ```xml insert into payment(serial) values(#{serial}); ``` #### 2.2.6. 创建实服务类 1. 创建服务接口 ```java package ink.honp.cloud.payment.service; import ink.honp.cloud.payment.entities.Payment; /** * @author jeff chen * @since 2021-12-06 15:24 */ public interface PaymentService { int create(Payment payment); Payment getPaymentById(Long id); } ``` 2. 服务接口实现 ```java package ink.honp.cloud.payment.service.impl; import ink.honp.cloud.payment.dao.PaymentDao; import ink.honp.cloud.payment.entities.Payment; import ink.honp.cloud.payment.service.PaymentService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author jeff chen * @since 2021-12-06 15:24 */ @Service @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class PaymentServiceImpl implements PaymentService { private final PaymentDao paymentDao; @Override public int create(Payment payment) { return paymentDao.create(payment); } @Override public Payment getPaymentById(Long id) { return paymentDao.getPaymentById(id); } } ``` #### 2.2.7. 创建 `Controller` 1. 封装统一返回数据结构 ```java package ink.honp.cloud.payment.entities; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @author jeff chen * @since 2021-12-06 15:19 */ @Data @NoArgsConstructor @AllArgsConstructor public class CommonResult implements Serializable { private static final long serialVersionUID = -6645482762631529147L; private Integer code; private String message; private T data; public CommonResult(Integer code,String message){ this(code,message,null); } } ``` 2. 创建 Controller 适配前端调用 ```java package ink.honp.cloud.payment.controller; import ink.honp.cloud.payment.entities.CommonResult; import ink.honp.cloud.payment.entities.Payment; import ink.honp.cloud.payment.service.PaymentService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.PostMapping; import org.springframework.web.bind.annotation.RestController; /** * @author jeff chen * @since 2021-12-06 15:29 */ @Slf4j @RestController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class PaymentController { private final PaymentService paymentService; @PostMapping(value = "/payment/create") public CommonResult create(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}") 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); } } } ``` #### 2.2.8. 测试 #### 2.2.9 项目整体结构 ![cloud-provider-payment-001](./doc/img/cloud_demo_001.png) ### 2.3 热部署 Devtools 设置 1. 添加依赖 ```xml org.springframework.boot spring-boot-devtools runtime true ``` 2. 添加插件 ```xml org.springframework.boot spring-boot-maven-plugin true true ``` 3. IDEA 设置 1. File --> Settings --> Build, Execution, Deployment ![devtool_001](./doc/img/devtool_001.png) 2. 双击 `Shift`, 并选择 `Registry...` ![devtool_002](./doc/img/devtool_002.png) 更新一下的值 ![devtool_003](./doc/img/devtool_003.png) 4. 重启 IDEA ### 2.3 服务消费模块构建 (cloud-consumer-order) #### 2.3.1. 引入依赖 pom.xml ```xml mycloud-demo ink.honp 1.0-SNAPSHOT 4.0.0 cloud-consumer-order 服务消费者 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` #### 2.3.2. 添加配置 application.yml ```yml server: port: 80 spring: application: name: cloud-consumer-order ``` #### 2.3.3. 编写启动类 OrderApplication ```java package ink.honp.cloud.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author jeff chen * @since 2021-12-06 16:46 */ @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } ``` #### 2.3.4. 调用服务提供者的业务 1. 创建 entities 拷贝 `cloud-provider-payment` 实体类 2. 配置 `RestTemplate`, 访问远程的 rest 接口 ```java package ink.honp.cloud.order.config; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** * @author jeff chen * @since 2021-12-06 16:48 */ @SpringBootConfiguration public class ApplicationContextConfig { @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } } ``` 3. 创建 `Controller` 调用服务提供者业务 ```java package ink.honp.cloud.order.controller; import ink.honp.cloud.order.entities.CommonResult; import ink.honp.cloud.order.entities.Payment; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * @author jeff chen * @since 2021-12-06 16:50 */ @Slf4j @RestController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class OrderController { public static final String PAYMENT_URL = "http://localhost:8081"; private final RestTemplate restTemplate; @PostMapping("/consumer/payment/create") public CommonResult create(Payment payment){ return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class); //写操作 } @GetMapping("/consumer/payment/get/{id}") public CommonResult getPayment(@PathVariable("id") Long id){ return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class); } } ``` #### 2.3.5. 测试 ### 2.4. 项目重构 01 项目服务提供者和消费者都使用了相同的 entities, 可以将两个模块共用的实体抽取并建立一个通用的模块 1. 构建通用模块 cloud-api-commons - pom.xml ```xml mycloud-demo ink.honp 1.0-SNAPSHOT 4.0.0 cloud-api-commons API 通用模块 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true ``` - 模块结构 ![cloud-api-commos-001](./doc/img/cloud-api-commons-001.png) 2. 将通用模块的版本管理添加到父项目的 pom.xml 文件 3. cloud-provider-payment 和 cloud-consumer-order 添加 cloud-api-commons 依赖,将原有的entities修改为通用模块的entities ### 2.5. 构建 Eureka 服务注册中心模块 1. 模块名称 cloud-eureka-server 引入依赖 ```xml mycloud-demo ink.honp 1.0-SNAPSHOT 4.0.0 cloud-eureka-server Eureka 注册中心 org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator ink.honp cloud-api-commons org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok org.springframework.boot spring-boot-starter-test test junit junit ``` 2. 配置 ```yml server: port: 7001 eureka: instance: hostname: localhost client: register-with-eureka: false fetchRegistry: false service-url: defaultZone: http://localhost:7001/eureka ``` 3. 创建启动类 ```java package ink.honp.cloud.ecureka.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @author jeff chen * @since 2021-12-06 17:39 */ @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } } ``` 4. 项目结构 ![eureka-server](./doc/img/eureka_server.png) ### 2.6. 服务提供者和服务消费者引入`Eureka`服务注册中心 1. 引入 `Eureka` 客户端依赖 ```java org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` 2. 配置 ```yaml eureka: client: register-with-eureka: true fetchRegistry: true service-url: defaultZone: http://localhost:7001/eureka ``` 3. 激活启动注解 ```java @EnableEurekaClient ``` ### 2.7. OpenFeign 服务接口调用 Feign 集成了 Ribbon, 利用 Ribbon 维护了 Payment 的服务列表,并且通过轮询实现客户端的负载均衡 1. `cloud-consumer-order` 添加依赖 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` 2. 添加业务调用接口 ```java package ink.honp.cloud.order.service; import ink.honp.cloud.commons.entities.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; /** * @author jeff chen * @since 2021-12-06 18:11 */ @Service @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") CommonResult getPaymentById(@PathVariable("id") Long id); } ``` `CLOUD-PAYMENT-SERVICE`: 服务提供者的名称 3. 修改 Controller ```java package ink.honp.cloud.order.controller; import ink.honp.cloud.commons.entities.CommonResult; import ink.honp.cloud.commons.entities.Payment; import ink.honp.cloud.order.service.PaymentFeignService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.RestController; /** * @author jeff chen * @since 2021-12-06 16:50 */ @Slf4j @RestController @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class OrderController { private final PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/get/{id}") public CommonResult getPayment(@PathVariable("id") Long id){ return paymentFeignService.getPaymentById(id); } } ``` 4. 添加启动注解 ```java @EnableFeignClients ``` ### 2.8. OpenFeign 常用配置 #### 2.8.1. 超时配置 默认Feign客户端只等待一秒钟。时候我们需要设置Feign客户端的超时控制,也即Ribbon的超时时间, 因为Feign集成了Ribbon进行负载均衡 ![consumer_read_timeout](./doc/img/consumer_read_timeout.png) 添加 `Ribbon` 超时配置 ```yaml ribbon: ReadTimeout: 3000 ConnectTimeout: 3000 MaxAutoRetries: 1 #同一台实例最大重试次数,不包括首次调用 MaxAutoRetriesNextServer: 1 #重试负载均衡其他的实例最大重试次数,不包括首次调用 OkToRetryOnAllOperations: false #是否所有操作都重试 #hystrix的超时时间 hystrix: command: default: execution: timeout: enabled: true isolation: thread: timeoutInMilliseconds: 9000 ``` 一般情况下 都是 `ribbon的超时时间` < `hystrix的超时时间`(因为涉及到ribbon的重试机制) 重试次数:`MaxAutoRetries` + `MaxAutoRetriesNextServer` + (`MaxAutoRetries` * `MaxAutoRetriesNextServer`) 如果在重试期间,时间超过了hystrix的超时时间,便会立即执行熔断 hystrix超时时间的计算: `(1 + MaxAutoRetries + MaxAutoRetriesNextServer) * ReadTimeout` 即按照以上的配置 hystrix的超时时间应该配置为 (1+1+1)*3=9秒 --- _**当ribbon超时后且hystrix没有超时,便会采取重试机制。当`OkToRetryOnAllOperations`设置为false时, 只会对get请求进行重试。如果设置为true,便会对所有的请求进行重试,如果是put或post等写操作,如果服务器 接口没做幂等性,会产生不好的结果,所以OkToRetryOnAllOperations慎用**_ --- #### 2.8.2. OpenFeign 日志打印 日志级别 - `NONE`:默认的,不显示任何日志 - `BASIC`:仅记录请求方法、RUL、响应状态码及执行时间 - `HEADERS`:除了BASIC中定义的信息之外,还有请求和响应的头信息 - `FULL`:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据 1. 配置`Feign`日志级别 ```java package ink.honp.cloud.order.config; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 配日 Feign 日志级别 * @author jeff chen * @since 2021-12-07 10:28 */ @Configuration public class FeignConfig { @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } ``` 2. 需要开启日志的`Feign`客户端 ```yaml logging: level: ink.honp.cloud.order.service: debug ``` ### 2.9. Hystrix 断路器 功能特性: - 服务降级 - 服务熔断 - 接近实时监控 - ... #### 2.9.1 服务降级 `Fallback` 服务大流量进来的时候,无法正常提供服务,可以将服务降级,也就是返回一个友好提示给客户端 触发降级的场景: - 程序运行异常 - 超时 - 服务熔断 - 线程池等无法提供服务 - 人工降级 #### 2.9.2 服务熔断 `Breaker` 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示 #### 2.9.3 服务限流 `Flowlimit` 实现流程控制,防止高并发造成服务不可用 #### 2.9.4 示例 ##### 2.9.4.1 JMeter 压力测试配置 _Windows apache-jmeter-5.4.1.zip 测试为例_ 1. 安装地址 ```yaml https://jmeter.apache.org/download_jmeter.cgi ``` 2. 修改为中文显示 `~/apache-jmeter-5.4.1/bin/jmeter.properties` ```yaml language=zh_CN ``` 2. 启动 `~/apache-jmeter-5.4.1/bin/jmeter.bat` ![jemeter start](./doc/img/jmeter_start.png) 3. 添加测试线程组 ![jemeter add thread group](./doc/img/jmeter_add_thread_group.png) ![jemeter add thread group](./doc/img/jmeter_thread_group_edit.png) 4. 添加 http 取样器 ![jemeter add thread group](./doc/img/jmeter_http.png) 若需要添加 header 参数,需要添加 `HTTP 信息头管理器` ![jemeter add thread group](./doc/img/jmeter_http_header.png) 5. 添加监听器,监听测试结果 ![jemeter add thread group](./doc/img/jmeter_result.png) ![jemeter add thread group](./doc/img/jmeter_result_edit.png) ##### 2.9.4.2 服务端的降级配置 1. 引入依赖 ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` 2. 业务类添加测试方法 ```java ``` ### 2.10. Spring-cloud-gateway 网关使用 #### 2.10.1. 新建 cloud-gateway 模块 1. 添加依赖 ```xml mycloud-demo ink.honp 1.0-SNAPSHOT 4.0.0 cloud-gateway org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-gateway ink.honp cloud-api-commons org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test ``` 2. 添加配置 ```yaml server: port: 9527 spring: application: name: cloud-gateway cloud: gateway: routes: - id: cloud-payment-service-route #路由的ID,没有固定规则但要求唯一,建议配合服务名 #uri: http://127.0.0.1:8081 #匹配后提供服务的路由地址 uri: lb://cloud-payment-service # lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri predicates: - Path=/payment/get/** #断言,路径相匹配的进行路由 - id: cloud-payment-service-route-2 #uri: http://127.0.0.1:8081 uri: lb://cloud-payment-service predicates: - Path=/payment/lb/** eureka: instance: hostname: cloud-gateway-service client: register-with-eureka: true fetchRegistry: true service-url: defaultZone: http://localhost:7001/eureka ``` 3. 启动类 ```java package ink.honp.cloud.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; /** * @author jeff chen * @since 2021-12-14 10:32 */ @SpringBootApplication @EnableEurekaClient public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } } ```