一、前言
最近正好面试,发现各大公司对于spring cloud的越来越重视,已经是微服务时代的基础技能了,于是特别针对spring cloud的常用组件做阐述,并且完整搭建一个spring cloud架构的项目,供需要的人参考
二、组件介绍
(针对以下概念了解的可直接跳过看后面的搭建过程)
要理解spring cloud架构的组件,首先要理解微服务架构,什么是微服务?粗略的讲就是随着单机架构包含的service层越来越多,越来越大,大家开始把同功能属性的service层单独抽离出来做成了一个个的单机架构,每个单独的单机架构就是一个微服务,每个服务的职责更加细致化、专业化。于是微服务架构就是多个单机架构的集合。
那么多个单机架构之间要正常运行起来要如何交流、如何管理、如何监控、如何统一对外服务呢?针对这些问题的解决就产生了各大组件。
(以下针对各大组件只做简单、核心的介绍,更多的可以自行了解)
1、Eureka - 注册中心
注册中心的核心概念是所有的服务提供者都向这个中心注册登记自己的信息,这样在这个注册中心就有了一份服务列表,调用者就可以根据这个列表知道自己要调用的服务的实际访问地址了。eureka就是充当这样的作用的,eureka分为了server端和client端,client端是要在每个服务提供者那里引入的,相当于每个服务提供者都是一个 client端,每个client端都会从server端定期拉取一份服务列表到自己的本地,这样调取的时候直接从本地的服务列表中找到自己想要的访问地址即可。
因此Eureka是基于客户端的注册中心,也就是说Eureka的服务列表是在客户端也要维护的;而zookeeper是基于服务端的注册中心,只在服务端维护这个服务列表
2、Ribbon - 负载均衡(LB: LoadBalance)
所谓负载均衡(LB)就是指把请求按照制定的策略分发到同一服务的不同实例上。比如后端有3个user-server服务,如果采取的负载均衡策略是轮询策略,ribbon就会把针对user-server的请求轮流分发到这3个服务实例上。
想想如果没有负载均衡服务器会怎么样?对于单机架构没有影响,因为本身就一个实例,何谈负载与均衡呢。对于多个实例的,就要请求方手动控制自己具体访问到哪个实例上。这该是件多么麻烦的事情啊
ribbon要做负载均衡的话,其服务列表从什么地方来?
1、可以在配置文件中自定义
ribbon.listOfServers=localhost:8080,localhost:8081
2、从eureka中拉取,因此这就要求ribbon服务本身也要是个eureka client,这样才能从eureka server上拉取服务列表
3、Zuul - 网关
网关简单来讲它提供了一个统一的入口,这样用户的请求统一都是打到网关上的,由网关来根据预定好的逻辑将不同路径的请求分发到不同的后台服务中。这里注意到前面说到LB服务的作用也是分发请求
那么网关和负载均衡之间的差别在哪儿呢?网关更多的强调的是统一入口,分发不同类型的请求到不同的服务上,而LB负责的是把对同种服务的请求按照预设的策略分发到不同的实例上,因此他们两个是紧密关联的,网关在前LB在后。
实际上LB也能脱离网关使用,自己来实现一个类似网关的功能,请求统一打到LB上,自己实现controller,在controller层针对不同的路径分发不同的请求到不同的服务上。(这一点我们下面也会实际搭建一下,通过这点来体会一下有zuul和没有zuul的区别)
Zuul是集合了ribbon,hystrix的依赖的,因此项目中引入了zuul就不用再引入ribbon,hystrix
4、Hystrix - 服务降级、熔断、限流
Hystrix是专门用于实现服务降级、熔断、限流的一个组件,这些操作的时间一般都是在网关处实现的,因为统一入口在这儿,后续有多少服务在入口处是可以把控的,所以Hystrix也被整合到Zuul中了,同时也可以配合着ribbon来实现,具体后续会实际操作
服务降级和熔断的区别是什么?降级是主动的,因为针对某些服务的大量访问,或者预估到针对某些服务可能会有大量访问,主动关闭一些非必要的服务,来腾出资源给核心的服务使用。比如双11的时候,查物流、退货等服务是不能用的,腾出来给下单、付款、查看商品等服务使用。熔断是被动的,某服务因为网络或者本身崩溃了,多次请求都无法访问后就直接将该服务的后续请求拦截,直接返回“无法访问”或者其他友好性的提示,不至于又让请求达到已经无法访问的服务上,浪费时间和资源。
5、Feign、RestTemplate
Feign和RestTemplate都是微服务间会话的,简单讲就是微服务间接口相互调用的,但是一般不会直接去调用想要调用的微服务地址,而是通过把请求打到网关上,由网关统一分配。他们两个的区别具体在下面操作中体现
这里考虑一下为什么不直接去访问想要访问的微服务而是先把请求打到网关?(服务间调用不涉及权限)
!---------------------------------
1、我们的架构是微服务,那么就无法限制服务后续是否会开启多个实例,如果只是把请求打给同一个实例,那么也失去了开启多个实例的意义(开启多实例就是为了平台请求压力)。
2、而且想象一下如果我们每个请求都要单独发给某个实例,如果每个调用都是一条线的话,那么微服务之间的调用一多,整个网络就会错综复杂,难以管理。引入网关就可以统一管理,所有的请求都放到网关上,每个请求只和网关关联,后面的转发无需关心。
6、拓展组件: sleuth,zipkin,admin,mail
待更新
7、分布式事务组件 seata
当服务A调用了服务B的接口,如果服务A中出现了错误,那么服务A本身的事务要进行回滚,但是服务B的事务却不能进行回滚,因为两个不在一个事务中,这时单机事务就不能满足分布式的需求了,就需要用到分布式事务,对于分布式事务的原理这里不做过多介绍了,直接说明可以在生产中解决分布式事务的组件seata,seata是springcloud alibaba全家桶中的组件之一。详细的安装使用见下述
三、spring cloud 项目搭建
1、项目概括
api-order:订单服务,服务提供方
api-user:用户模块,服务提供方
cloud-eureka: 注册中心
cloud-ribbon: 负载均衡服务,restTemplate实现服务间会话
cloud-ribbon-feign: 负载均衡服务,feign实现服务间会话
cloud-zuul: 网关,整合了ribbon,hystrix,所以实际开发中只需要搭建一个zuul服务,是不需要cloud-ribbon,cloud-ribbon-feign模块的,这里只是为了演示各个组件的作用将其独立出来
common: 公共模块,盛装通用类,通用接口
项目地址
https://gitee.com/wuhanxue/cloud-test
修改hosts
项目创建开始之前可以先把hosts配置了,通过这个服务名映射到具体的ip地址上,把服务名注册到eureka上,方便进行负载均衡,所以实际的生产环境,这个hosts的配置应该是在eureka服务器上进行的,同时其映射ip也不应该是本地
linux/Mac系统命令行输入指令,window可自行查找hosts文件位置
添加
127.0.0.1 eureka-7900
127.0.0.1 user-server
127.0.0.1 order-server
2、创建母项目
1、创建一个maven项目,无需引入其他组件
2、将项目下的src目录删除
3、创建公共模块common
微服务架构不可避免的会有多个服务调用到相同的类,相同的接口,为了开发快捷、节约资源,可以把这些公共类、接口单独放到一个服务中,其他服务需要用到的引入该服务即可
甚至如果项目整体较大的,可以再将接口单独放在一个模块中,或者再把公共资源细化
1、右键父项目目录->new->Module
2、新建一个spring boot项目,项目名common,引入lombok插件
ps: 如果新建时网络不佳,可以用阿里镜像地址:https://start.aliyun.com/
3、将配置文件修改为yml后缀,个人更推荐使用yml格式,结构更加清晰,能少写几个字
# 应用名称
spring:
application:
name: common
4、创建实体类Order,User
@Data
public class Order {
private Integer id;
private String orderNo;
private Date createTime;
public Order(Integer id, String orderNo, Date createTime) {
this.id = id;
this.orderNo = orderNo;
this.createTime = createTime;
}
}
package com.wu.common.entity;
import lombok.Data;
/**
* @author whx
* @date 2021/7/9
*/
@Data
public class User {
private Integer id;
private String name;
private String userName;
private String password;
public User(Integer id, String name, String userName, String password) {
this.id = id;
this.name = name;
this.userName = userName;
this.password = password;
}
}
5、创建接口返回值包装类ResultData及返回状态枚举类ResultDataEnum
package com.wu.common.entity;
import lombok.Getter;
/**
* @author whx
* @date 2021/7/9
*/
public enum ResultDataEnum {
SUCCESS(1,"请求成功"),
FAIL(0,"请求失败");
@Getter
private final int state;
@Getter
private final String message;
ResultDataEnum(int state, String message) {
this.state = state;
this.message = message;
}
}
package com.wu.common.entity;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author whx
* @date 2021/7/9
*/
@Data
@Accessors(chain = true)
public class ResultData<T> {
private int state;
private String message;
private T data;
public ResultData SUCCESS(T data){
return new ResultData().setData(data)
.setState(ResultDataEnum.SUCCESS.getState())
.setMessage(ResultDataEnum.SUCCESS.getMessage());
}
public ResultData FAIL(String message){
if(message != null && !"".equals(message)){
return new ResultData().setState(ResultDataEnum.FAIL.getState())
.setMessage(message);
}else{
return new ResultData().setState(ResultDataEnum.FAIL.getState())
.setMessage(ResultDataEnum.FAIL.getMessage());
}
}
}
4、创建注册中心cloud-eureka
1、在父目录下创建新module,spring boot项目,项目名cloud-eureka,引入依赖spring web, eureka server
2、启动类添加注解@EnableEurekaServer
3、修改配置文件
spring:
application:
name: eureka
server:
port: 7900
eureka:
client:
service-url:
# eureka本身也是一个client,要向注册中心注册服务,这里表示注册中心服务的地址
defaultZone: http://localhost:7900/eureka/
instance:
# 这里的hostname要在hosts文件中配置映射: 127.0.0.1 eureka-7900
hostname: eureka-7900
注:这里只设定了最简单的配置,实际生产中eureka的很多配置参数是要根据具体业务要求去设置的;这里只搭建了eureka server的单机架构,本身其是运行在web 容器上的(如tomcat),所能承担的并发线程数有限(tomcat限制500),所以实际生产环境可以需要搭建eureka server集群来分担压力,集群具体搭建过程这里不再扩展,后续会单开博客说明
【待办】eureka集群搭建以及生产环境优化
4、配置完,启动服务,查看localhost:7900,正常显示eureka页面即搭建成功
新增项目启动类没有显示到Run Dashboard上怎么解决
edit configration -> 选择已有的一个启动类配置复制一份,或者自己新建一个spring boot配置,如下图
修改名称,修改Main class
5、服务提供方模块api-user、api-order
1、在父目录下创建新module,spring boot项目,项目名api-user,引入依赖spring web, eureka client
2、修改配置文件
spring:
application:
name: user-server
# 应用名称
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
instance:
hostname: user-server
server:
port: 9092
添加一个配置文件application-9091.yml,用于后续开启多个实例,测试负载均衡
spring:
application:
name: user-server
# 应用名称
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
instance:
hostname: user-server
server:
port: 9091
3、pom添加common的依赖
<dependency>
<groupId>com.wu</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
4、创建UserController类,这里方便测试使用,直接在controller层实现数据返回,实际生产中应该放到server层去实现,同时返回数据中包含端口号,后续方便确认实际访问的服务实例
package com.wu.apiuser.controller;
import com.wu.common.entity.ResultData;
import com.wu.common.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author whx
* @date 2021/7/9
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${server.port}")
String port;
@GetMapping("/list")
public ResultData list(){
List<User> list = new ArrayList<>(2);
User user1 = new User(1,"name1_"+port,"userName1","123");
User user2 = new User(2,"name2_"+port,"userName2","123");
list.add(user1);
list.add(user2);
return new ResultData().SUCCESS(list);
}
@GetMapping("/hi")
public String hi(){
return "hi";
}
@PostMapping("/getById")
public ResultData getById(@RequestParam("id") Integer id){
User user = new User(id,"name"+id,"userName"+id,port);
return new ResultData().SUCCESS(user);
}
}
5、重复上述步骤创建api-order模块,只是下述文件不同
配置文件
# 应用名称
spring:
application:
name: order-server
# 应用服务 WEB 访问端口
server:
port: 8082
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
instance:
hostname: order-server
添加一个配置文件application-8081.yml
# 应用名称
spring:
application:
name: order-server
# 应用服务 WEB 访问端口
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
instance:
hostname: order-server
OrderController:
package com.wu.apiorder.controller;
import com.wu.common.entity.Order;
import com.wu.common.entity.ResultData;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author whx
* @date 2021/7/9
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Value("${server.port}")
String port;
@GetMapping("/list")
public ResultData list(){
List<Order> list = new ArrayList<>();
list.add(new Order(1,"1111_"+port,new Date()));
list.add(new Order(2,"2222_"+port,new Date()));
list.add(new Order(3,"3333_"+port,new Date()));
return new ResultData().SUCCESS(list);
}
@PostMapping("/getById")
public ResultData getById(Integer id){
return new ResultData().SUCCESS(new Order(id,id+"_"+port,new Date()));
}
}
6、配置多个启动实例
点击edit configurations
common启动配置可以删除,它无需启动。修改统一的名称,更加清爽:api-user,api-order,cloud-eureka
如图复制api-user的configuration->修改name为api-user1,profiles为9091(与application-9091.yml对应)
复制api-order的configuration->修改name为api-order1,profiles为8081(与application-8081.yml对应)
7、启动4个实例,分别访问接口,以及查看eureka中服务注册列表中服务是否正常
6、创建负载均衡 cloud-ribbon
创建过程
1、父目录下新建module,springboot项目,项目名cloud-ribbon,引入依赖ribbon, eureka client
2、修改配置文件
# 应用名称
spring:
application:
name: cloud-ribbon
# 应用服务 WEB 访问端口
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
这里是ribbon配合了eureka来获取注册服务,如果不想使用eureka,可以手动在ribbon中配置服务列表
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 注册列表手动配置,不从eureka拉取
listOfServsers: localhost:9091,localhost:9092
ribbon:
eureka:
# 是否从eureka拉取注册列表,默认true
enable: false
3、引入common模块依赖,spring web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.wu</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
ribbon + restTemplate
1、这里使用RestTemplate来实现服务间接口调用,创建配置类RestTemplateConfig,实现RestTemplate的Bean,bean方法上引入注解@LoadBalanced,表示这个RestTemplate会自动调用ribbon的负载均衡策略
package com.wu.cloudribbon.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;
/**
* @author whx
* @date 2021/7/10
*/
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
2、在配置文件中设置负载均衡策略,这里做演示只设置了user-server的负载均衡策略,如果有多个服务应该要单独设置其负载均衡策略,user-server就是注册到eureka上的服务名,比如订单服务的就是order-server。默认的负载均衡策略是轮询策略,下面会单独说明ribbon支持的所有负载均衡策略
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
2、创建控制类TemplateController
通过loadBalancerClient.choose(“user-server”);声明使用的是哪个服务的负载均衡策略,与上述配置文件中的对应。discoveryClient.getServices();可以获取到支持的服务列表,这里只做打印。可以通过discoveryClient获取到服务列表后自己自定义负载均衡策略,详细的实现可额外自行学习。
如果不清楚RestTemplate的各个方法的详细使用的可参考后面的RestTemplate章节
package com.wu.cloudribbon.controller;
import com.wu.common.entity.ResultData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @author whx
* @date 2021/7/10
*/
@RestController
public class TemplateController {
@Autowired
RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
DiscoveryClient discoveryClient;
@GetMapping("/user/list")
public ResultData userList(){
List<String> list = discoveryClient.getServices();
System.out.println(list.size());
loadBalancerClient.choose("user-server");
return restTemplate.getForObject("http://user-server/user/list",ResultData.class);
}
@PostMapping("/user/getById")
public ResultData userGetById(Integer id){
// loadBalancerClient.choose("user-server");
MultiValueMap<String,Integer> param = new LinkedMultiValueMap<>(1);
param.add("id",id);
return restTemplate.postForObject("http://user-server/user/getById",param,ResultData.class);
}
}
启动服务api-user,cloud-eureka,cloud-ribbon,测试一下:访问localhost:8080/user/list
访问的是ribbon的8080端口,请求转发到实际的9092端口的api-user服务上了。再多刷新几次,会发现端口会随机在9091,9092中出现,且不是轮询出现,因为之前设置的负载均衡策略是随机访问策略
ribbon + restTemplate + hystrix
1、引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2、启动类添加注解@EnableCircuitBreaker
3、在要添加fallback的请求方法上添加上fallback方法,所谓fallback即在规定时间内接口连接失败的回调方法,那么针对失败后的处理都可以在这个方法中进行。
@GetMapping("/user/list")
@HystrixCommand(defaultFallback="back")
public ResultData userList(){
List<String> list = discoveryClient.getServices();
System.out.println(list.size());
loadBalancerClient.choose("user-server");
return restTemplate.getForObject("http://user-server/user/list",ResultData.class);
}
public ResultData back(){
return new ResultData().FAIL("服务熔断");
}
添加连接超时时间
@HystrixCommand(defaultFallback = "back", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
4、测试:把api-user服务停掉,重启cloud-ribbon服务,再访问http://localhost:8080/user/list,返回fallback信息
这里只是做简单的测试,开发环境下的做法,一种是可以添加一个AtomicInteger类,用于计算请求失败的次数,达到阈值之后就直接访问这个方法或者直接返回兜底数据
如果要做降级处理的话,是主动将非核心服务关闭,然后在fallback中书写回调方法,可以在回调方法中返回友好性提示或者兜底数据
ribbon的负载均衡策略
1、ZoneAvoidanceRule(区域权衡策略):复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。
2、BestAvailableRule(最低并发策略):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。
3、RoundRobinRule(轮询策略):以简单轮询选择一个服务器。按顺序循环选择一个server。
4、RandomRule(随机策略):随机选择一个服务器。
5、AvailabilityFilteringRule(可用过滤策略):会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。
6、WeightedResponseTimeRule(响应时间加权策略):据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。
7、RetryRule(重试策略):先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时
ribbon的重试机制
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false
#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000
RestTemplate使用详解
GET请求
1、不带参数的get请求,参数:url;返回值类型,url接口返回的是什么这里类型就是什么
restTemplate.getForObject("http://user-server/user/list",ResultData.class);
2、带参数的get请求
ribbon:
@GetMapping("/user/page")
public ResultData userPageMap(@RequestParam("id") int id,@RequestParam("name") String name){
loadBalancerClient.choose("user-server");
Map<String,Object> params = new HashMap<>(2);
params.put("id",id);
params.put("name",name);
return restTemplate.getForObject("http://user-server/user/page?id={id}&name={name}",ResultData.class,params);
}
api-user: 添加接口
@GetMapping("/page")
public ResultData page(@RequestParam("id")Integer id,@RequestParam("name")String name){
return new ResultData().SUCCESS(new User(id,name,"userName",port));
}
测试:访问:http://localhost:8080/user/page?id=1&name=123
同时还可以使用这样的形式:
@GetMapping("/user/page2")
public ResultData userPage(@RequestParam("id") int id,@RequestParam("name") String name){
loadBalancerClient.choose("user-server");
return restTemplate.getForObject("http://user-server/user/page?id={1}&name={2}",ResultData.class,id,name);
}
测试:访问:http://localhost:8080/user/page2?id=1&name=123
更多的用法可以自己参考RestTemplate.class
POST请求
1、不带参数的请求
ribbon:
@PostMapping("/getOne")
public ResultData getOne(){
User user = new User(1,"name1","userName1",port);
return new ResultData().SUCCESS(user);
}
api-user:
@PostMapping("/user/getOne")
public ResultData userGetOne(){
loadBalancerClient.choose("user-server");
return restTemplate.postForObject("http://user-server/user/getOne",null,ResultData.class);
}
测试
2、带基础类型参数的请求
ribbon:
@PostMapping("/getById")
public ResultData getById(@RequestParam("id") Integer id){
User user = new User(id,"name"+id,"userName"+id,port);
return new ResultData().SUCCESS(user);
}
测试:
3、待自定义对象的请求
ribbon
@PostMapping("/user/getUser")
public ResultData userGetUser(@RequestBody User user){
loadBalancerClient.choose("user-server");
return restTemplate.postForObject("http://user-server/user/getUser",user,ResultData.class);
}
api-user
@PostMapping("/getUser")
public ResultData getUser(@RequestBody User user){
user.setPassword(port);
return new ResultData().SUCCESS(user);
}
User: 这里还要给User添加一个无参构造方法,否则会报错
测试
7、实现服务调用 cloud-ribbon-feign
创建过程
1、在父目录下创建module,springboot项目,项目名cloud-ribbon-feign,引入依赖:spring web, eureka client , openFeign
2、引入common模块的依赖
3、修改配置文件
# 应用名称
spring:
application:
name: cloud-ribbon-feign
# 应用服务 WEB 访问端口
server:
port: 9090
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
4、启动类添加注解@EnableFeignClients
5、创建OrderApi接口,用来转发api-order的请求,fallbackFactory表示接口访问失败时的回调方法类,还可以用fallback,但是工厂类更好的是它可以获取到报错信息,这样就能根据不同的报错类型做不同的处理
package com.wu.cloudribbonfeign.api;
import com.wu.cloudribbonfeign.fallback.OrderServerBackFactory;
import com.wu.common.entity.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author whx
* @date 2021/7/10
*/
@FeignClient(name = "order-server",fallbackFactory = OrderServerBackFactory.class)
public interface OrderApi {
@GetMapping("/order/list")
ResultData orderList();
@PostMapping("/order/getById")
ResultData orderGetById(@RequestParam("id") Integer id);
}
6、创建控制器FeignController
package com.wu.cloudribbonfeign.controller;
import com.wu.cloudribbonfeign.api.OrderApi;
import com.wu.common.entity.ResultData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author whx
* @date 2021/7/10
*/
@RestController
@RequestMapping("/order")
public class FeignController {
@Autowired
OrderApi orderApi;
@GetMapping("/list")
public ResultData orderList(){
return orderApi.orderList();
}
@PostMapping("/getById")
public ResultData orderGetById(Integer id){
return orderApi.orderGetById(id);
}
}
7、创建OrderServerBackFactory类,可以看到它的核心实现就是申明FallbackFactory接口,然后实现create方法,这个方法要返回的就是调用它的api接口类
package com.wu.cloudribbonfeign.fallback;
import com.wu.cloudribbonfeign.api.OrderApi;
import com.wu.common.entity.ResultData;
import feign.FeignException;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author whx
* @date 2021/7/10
*/
@Component
public class OrderServerBackFactory implements FallbackFactory<OrderApi> {
@Override
public OrderApi create(Throwable throwable) {
return new OrderApi() {
@Override
public ResultData orderList() {
if(throwable instanceof FeignException.InternalServerError){
return new ResultData().FAIL("远程服务器访问500");
}
throwable.printStackTrace();
return new ResultData().FAIL(throwable.getMessage());
}
@Override
public ResultData orderGetById(@RequestParam("id") Integer id) {
return null;
}
};
}
}
8、默认情况下feign是不用hystrix做熔断降级的,所以还需要在配置文件中开启
feign:
hystrix:
enabled: true
测试:访问localhost:9090/order/list
feign + ribbon 实现负载均衡
结合上章节,自己尝试一下将order-server的负载均衡策略修改为随机策略
!---------------------
直接修改配置文件即可,会发现比RestTemplate少了一步在转发方法中申明服务名的操作,因为F eign已经在api接口上申明过了
order-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
注意
1、feign默认所以带参数的请求都是POST,如果一个GET请求带了参数会被解析成POST,导致报错。要想使用GET带参数的请求,需要引入以下依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
并声明提交方式
@RequestMapping(value = "/alived", method = RequestMethod.POST)
@GetMapping("/findById")
2、api接口中参数要添加注解
GET :
基础类型:@RequestParam
Map: @RequestParam
自定义实体类:@RequestBody
POST :
基础类型:@RequestParam
Map: @RequestBody
自定义实体类:@RequestBody
比如
@FeignClient(name="user-provider")
public interface UserApi {
@GetMapping("/findById")
public Map findById(@RequestParam("id") Integer id);
@GetMapping("/getMap")
public Map getMap(@RequestParam Map<String,Object> map);
@PostMapping("/register")
public Map<String, String> reg(@RequestBody User user);
@PostMapping("/register2")
public Map<String, String> reg2(@RequestBody Map<String,Object> map);
}
feign不结合eureka
直接在注解中添加url属性,这样就无需通过eureka获取服务列表
@FeignClient(name="server_name",url="http://localhost:81")
public interface UserApi {
@GetMapping("/alive")
public String alive;
@GetMapping("/add")
public String add();
}
8、创建网关 cloud-zuul
创建过程
1、父目录下创建module,springboot项目,项目名cloud-zuul,引入依赖:zuul,eureka client
引入zuul会默认引入其他组件
下图中的spring web无需引入,系我手快点重了
2、引入common模块依赖
3、修改配置文件
可以看到zuul.routes中的配置,这就是路由配置,path表示来源请求路径,server-id表示要把该请求转发到哪个服务上,这里就看到了zuul的便利性,不用再额外配置feign或者RestTemplate就能实现请求转发
# 应用名称
spring:
application:
name: cloud-zuul
# 应用服务 WEB 访问端口
server:
port: 90
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
zuul:
routes:
user-server:
path: /user-server/**
# 配合eureka,服务名
server-id: user-server
# 不需要eureka,直接根据地址转接请求
# url: http://localhost:8081/
order-server:
path: /order-server/**
server-id: order-server
4、启动类添加注解@EnableZuulProxy
5、测试: 访问localhost:90/order-server/order/list
整合ribbon实现负载均衡
前面说过了zuul集合了ribbon,所以对于负载均衡的实现,直接在配置文件中配置即可
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
整合hystrix实现服务降级、熔断、限流
服务降级、熔断
zuul 的fallback是通过声明FallbakcProvider类,实现getRoute,fallbackResponse方法实现的
package com.wu.cloudzuul.fallback;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
/**
* @author whx
* @date 2021/7/10
*/
@Component
public class OrderFallback implements FallbackProvider {
/**
* 需要降级处理的服务名
* @return
*/
@Override
public String getRoute() {
return "order-server";
}
/**
* 服务不可用时返回的托底信息
* 具体的降级处理就在这里完成
* @param route
* @param cause
* @return
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
/**
* ClientHttpResponse 的 fallback 的状态码 返回HttpStatus
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
/**
* ClientHttpResponse 的 fallback 的状态码 返回 int
*/
@Override
public int getRawStatusCode() throws IOException {
return getStatusCode().value();
}
/**
* ClientHttpResponse 的 fallback 的状态信息
*/
@Override
public String getStatusText() throws IOException {
return getStatusCode().getReasonPhrase();
}
@Override
public void close() {
}
/**
* 设置响应体
*/
@Override
public InputStream getBody() throws IOException {
System.out.println("order 服务降级");
String msg = "当前服务不可用";
return new ByteArrayInputStream(msg.getBytes());
}
/**
* 设置响应的头信息
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders= new HttpHeaders();
MediaType mediaType = new MediaType("application","json", Charset.forName("utf-8"));
httpHeaders.setContentType(mediaType);
return httpHeaders;
}
};
}
}
zuul的过滤器/请求拦截
package com.wu.cloudzuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
/**
* @author whx
* @date 2021/7/11
*/
@Component
public class SimpleFilter extends ZuulFilter {
/**
* 返回过滤器类型
* zuul内置四种过滤器类型:
* pre 请求处理前,route 处理时,post 处理后,error 报错时
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器执行顺序,默认0
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 是否开启过滤器,默认false
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器主体方法
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
System.out.println("过滤器执行");
return null;
}
}
生产环境限流功能实现
package com.wu.cloudzuul.filter;
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.wu.common.entity.ResultData;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* @author whx
* @date 2021/7/11
*/
@Component
public class RateLimiterFilter extends ZuulFilter {
@Value("${zuul.routes.order-server.path}")
String orderServerPath;
//每秒产生100个令牌
private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return -1;
}
@Override
public boolean shouldFilter() {
RequestContext context = new RequestContext().getCurrentContext();
HttpServletRequest request = context.getRequest();
// 要限流的路径
List<String> interfaces = new ArrayList<>();
interfaces.add(orderServerPath);
AntPathMatcher matcher = new AntPathMatcher();
// 判断访问的路径是否在限流路径中
for (String s : interfaces) {
if(!StringUtils.isEmpty(s) && matcher.match(s,request.getRequestURI())){
return true;
}
}
return false;
}
@Override
public Object run() throws ZuulException {
if(!RATE_LIMITER.tryAcquire()){
System.out.println("当前访问人数太多");
context.setSendZuulResponse(false);
// 状态码503
context.setResponseStatusCode(HttpStatus.SC_SERVICE_UNAVAILABLE);
context.setResponseBody("目前访问量过大,限流了...");
//解决中文乱码
context.getResponse().setCharacterEncoding("UTF-8");
context.getResponse().setContentType("text/html;charset=UTF-8");
}
return null;
}
}
测试:
先将之前设置的OrderFallback,SimpleFilter关闭:取消 @Component注解即可
然后使用jmeter开200个线程测试,访问order/list接口,观察控制台输出结果
还可以用postman同步访问
9、[补充] eureka注册中心的替代方案
eureka2.0版本后官方停止维护,不再开源,本身使用eureka1.0版本的没有什么问题,但是从长远考虑要开始寻找注册中心的替代方案,一般公司针对注册中心的替代组件有:zookeeper,consul,nacos
9.1 Zuul + zookeeper
如果在上述的zuul项目下直接搭建的需要去除eureka的依赖及相关配置,否则会报错有两个同名bean
1、父目录下创建新module,springboot项目,项目名:cloud-zuul-zookeeper,引入依赖zuul,zookeeper discovery
2、启动类添加@EnableDiscoveryClient,@EnableZuulProxy注解
3、修改配置文件
# 应用名称
spring:
application:
name: cloud-zuul
# 配置zookeeper
cloud:
zookeeper:
discovery:
enabled: true
connect-string: localhost:2181
# 应用服务 WEB 访问端口
server:
port: 90
#eureka:
# client:
# service-url:
# defaultZone: http://localhost:7900/eureka/
management:
endpoints:
web:
exposure:
# actuator暴露的端点,默认只暴露/health,/info,要暴露全部端点设置为*
include: hystrix.stream,health,info,routes
zuul:
routes:
user-server:
path: /user-server/**
# 配合eureka,服务名
server-id: user-server
# 不需要eureka,直接根据地址转接请求
# url: http://localhost:8081/
order-server:
path: /order-server/**
server-id: order-server
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 20000
4、父目录下创建新module,springboot 项目api-user-zk,引入依赖 spring web,common模块,zookeeper discovery
项目内容与api-user项目完全一致,只是使用zookeeper注册服务,这里为了方便区分直接新建一个项目,自己操作的可以在原api-user上操作,引入zookeeper-discovery依赖,去掉eureka相关依赖和配置文件即可
5、api-user-zk修改配置文件
spring:
application:
name: user-server
# 配置zookeeper
cloud:
zookeeper:
discovery:
enabled: true
connect-string: localhost:2181
server:
port: 9092
与api-user一样再创建一个application-9091.yml
spring:
application:
name: user-server
# 配置zookeeper
cloud:
zookeeper:
discovery:
enabled: true
connect-string: localhost:2181
server:
port: 9091
6、api-user-zk启动类添加注解@EnableDiscoveryClient
7、参考之前的操作,添加启动配置,然后启动api-user-zk和cloud-zuul-zookeeper服务
访问:http://localhost:90/user-server/user/list
9.2 zuul + nacos
安装nacos
这里我采用docker安装nacos,可以参考以下这篇博客,这里就不详述了
https://www.jianshu.com/p/54f6da71ac48
# 拉取镜像
docker pull nacos/nacos-server
# 安装容器
docker run --env MODE=standalone --name nacos-local -d -p 8848:8848 -p 9848:9848 -p 9849:9849 nacos/nacos-server
创建注册中心 cloud-zuul-nacos
1、创建springboot项目,项目名cloud-zuul-nacos
2、引入依赖,nacos service discovery, zuul
这里会自动创建NacosDiscoveryConfiguration类,添加了@EnableDiscoveryClient注解,如果没有自动创建需要在启动类中加上这个注解
3、修改配置文件
# 应用名称
spring:
application:
name: cloud-zuul-nacos
cloud:
# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
nacos:
discovery:
# Nacos认证信息
username: nacos
password: nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
server-addr: localhost:28848
# 注册到 nacos 的指定 namespace,默认为 public
namespace: public
server:
port: 90
zuul:
routes:
user-server:
path: /user-server/**
server-id: user-server
# 不需要eureka
# url: http://localhost:8081/
order-server:
path: /order-server/**
server-id: order-server
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
order-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
4、给启动类添加上@EnableZuulProxy注解
5、创建springboot项目,项目名api-user-nacos,引入依赖nacos service discovery,spring web,common ,并且修改配置文档
这里会自动创建NacosDiscoveryConfiguration类,添加了@EnableDiscoveryClient注解,如果没有自动创建需要在启动类中加上这个注解
# 应用名称
spring:
application:
name: user-server
# 应用服务 WEB 访问端口
cloud:
# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
nacos:
discovery:
# Nacos认证信息
username: nacos
password: nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
server-addr: localhost:28848
# 注册到 nacos 的指定 namespace,默认为 public
namespace: public
server:
port: 9091
6、再和之前一样,复制一份配置文件,叫application-9092.yml,修改端口为9092
7、和之前一样,分别配置两个user启动项
8、在api-user-nacos中创建UserController
package com.wu.apiusernacos.controller;
import com.wu.common.entity.ResultData;
import com.wu.common.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author whx
* @date 2021/9/28
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${server.port}")
String port;
@GetMapping("/list")
public ResultData list(){
List<User> list = new ArrayList<>();
User user1 = new User(1,"name1_"+port,"userName1","123");
User user2 = new User(1,"name1_"+port,"userName1","123");
list.add(user1);
list.add(user2);
// int i = 1/0;
return new ResultData().SUCCESS(list);
}
@PostMapping("/getById")
public ResultData getById(@RequestParam("id")Integer id){
User user = new User(id,"name"+id,"userName"+id,port);
return new ResultData().SUCCESS(user);
}
}
9、启动api-user-nacos-9091,api-user-nacos-9092,cloud-zuul-nacos三个项目
访问localhost:90/user-server/user/list
注:这里如果报错循环依赖问题
The dependencies of some of the beans in the application context form a cycle:
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration (field private org.springframework.cloud.client.serviceregistry.Registration org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration.registration)
┌─────┐
| nacosRegistration defined in class path resource [com/alibaba/cloud/nacos/registry/NacosServiceRegistryAutoConfiguration.class]
↑ ↓
| nacosProperties (field private java.util.Optional com.alibaba.cloud.nacos.NacosDiscoveryProperties.nacosAutoServiceRegistrationOptional)
↑ ↓
| nacosAutoServiceRegistration defined in class path resource [com/alibaba/cloud/nacos/registry/NacosServiceRegistryAutoConfiguration.class]
└─────┘
这里由于springcloud版本问题,在nacos开源项目中也有人提出这个问题
https://github.com/alibaba/nacos/issues/3932
通过描述知道,这是spring cloud 2.2.2.RELEASE版本的问题,将版本改为2.2.5.RELEASE
重新启动,访问接口,成功!
10、分布式事务组件搭建 seata
以下搭建步骤参考官方步骤,尽量以官方为准。seata配置eureka作为配置中心搭建,以feign实现微服务间接口调用,以ribbon作为负载均衡
建议直接下载官方样例,运行起来,结合着看效果更佳
seata server端下载安装
为了方便安装、管理,这里仅介绍docker安装seate-server,其余安装方式可参考官方文档 1、创建seata数据库
create table branch_table
(
branch_id bigint not null
primary key,
xid varchar(128) not null,
transaction_id bigint null,
resource_group_id varchar(32) null,
resource_id varchar(256) null,
branch_type varchar(8) null,
status tinyint null,
client_id varchar(64) null,
application_data varchar(2000) null,
gmt_create datetime(6) null,
gmt_modified datetime(6) null
)
charset = utf8;
create index idx_xid
on branch_table (xid);
-- auto-generated definition
create table global_table
(
xid varchar(128) not null
primary key,
transaction_id bigint null,
status tinyint not null,
application_id varchar(32) null,
transaction_service_group varchar(32) null,
transaction_name varchar(128) null,
timeout int null,
begin_time bigint null,
application_data varchar(2000) null,
gmt_create datetime null,
gmt_modified datetime null
)
charset = utf8;
create index idx_gmt_modified_status
on global_table (gmt_modified, status);
create index idx_transaction_id
on global_table (transaction_id);
-- auto-generated definition
create table lock_table
(
row_key varchar(128) not null
primary key,
xid varchar(96) null,
transaction_id bigint null,
branch_id bigint not null,
resource_id varchar(256) null,
table_name varchar(32) null,
pk varchar(36) null,
gmt_create datetime null,
gmt_modified datetime null
)
charset = utf8;
create index idx_branch_id
on lock_table (branch_id);
2、准备seata-server配置文件
在本地路径/Library/software/dockerdata/seata-config下创建file.conf,registry.conf文件
(这个路径是我docker配置文件的统一路径,可根据自己的实际情况更改,用来存放配置文件,映射到docker容器内)
file.conf
service {
#vgroup->rgroup
#修改事务组名称为:my_test_tx_group,和客户端自定义的名称对应
# server-seata 服务名,注册到eureka上显示的服务名,默认是default
vgroup_mapping.my_test_tx_group = "server-seata"
#only support single node
# server-seata与上述保持一致,默认是default
server-seata.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
#我本地用的mysql8.0所以驱动用的cj,mysql5.0用以下驱动
# driverClassName = "com.mysql.jdbc.Driver"
driverClassName = "com.mysql.cj.jdbc.Driver"
# 上述配置的数据库
url = "jdbc:mysql://192.168.101.109:3306/seata?allowPublicKeyRetrieval=true"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
registry.conf
这里为了让大家参考其余方式的配置,保留了其他的配置,但实际如果用的是eureka,只需保留eureka项即可
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
nacos {
application = "default"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
eureka {
# eureka地址
serviceUrl = "http://192.168.101.109:7900/eureka"
application = "server-seata"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
dataId = "seataServer.properties"
}
consul {
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
apollo {
appId = "seata-server"
## apolloConfigService will cover apolloMeta
apolloMeta = "http://192.168.1.204:8801"
apolloConfigService = "http://192.168.1.204:8080"
namespace = "application"
apolloAccesskeySecret = ""
cluster = "seata"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
nodePath = "/seata/seata.properties"
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file:/root/seata-config/file.conf"
}
}
mysql8.0+seata配置 (mysql5.0跳过)
本机测试时使用默认的mysql驱动器会报错,原因是本机mysql版本为8.0,因此要用8.0的驱动器,但seata中默认是5.0的驱动器,所以要在file.conf中修改配置
store.db.driverClassName = "com.mysql.cj.jdbc.Driver"
但是seata中好像是没有这个驱动器jar包的,因此要把这个jar包拷贝到seata安装路径的libs下
这里以docker的seata容器为例进行操作:
1、拷贝mysql-connector-java-8.0.22.jar包到上述设置的本地的容器映射路径/Library/software/dockerdata/seata-config下,这个jar包可以在本地maven仓库中拷贝
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
2、进入seata容器内执行拷贝指令
cp /root/seata-config/registry/mysql-connector-java-8.0.22.jar /seata-server/libs/
3、输入以下指令
# 查询seata镜像
docker search seata
# 下载镜像
docker pull seataio/seata-server
# 安装镜像并设置容器映射路径
# SEATA_IP为宿主机IP
# -v 宿主机路径:容器内路径
docker run -d --name seata-server \
-p 8091:8091 \
-e SEATA_IP=192.168.101.109 \
-e STORE_MODE=db \
-e SEATA_CONFIG_NAME=file:/root/seata-config/registry \
-v /Library/software/dockerdata/seata-config:/root/seata-config \
seataio/seata-server
seata client端配置
所谓client端就是我们需要用到分布式事务的微服务端
首先概述一下要怎么模拟分布式事务
1、模拟业务服务A调用业务服务B,B报错,A、B数据库回滚
2、模拟负载均衡服务调用服务A,B,B报错,或者负载均衡服务接口报错,A,B数据库回滚
common项目
common项目添加依赖
这样不用分别添加到其他微服务中了(其他服务都引用了common)
<!-- 用于数据库时间类型数据映射到实体类 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.1.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>
api-order项目配置
为模拟分布式事务,在api-user,api-order服务中创建对应接口
1、引入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
2、启动类中移除DataSourceAutoConfiguration类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
3、配置文件修改
# 应用名称
spring:
application:
name: order-server
# mysql
datasource:
url: jdbc:mysql://localhost:3306/order_test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# seata
cloud:
alibaba:
seata:
# 与file.conf中server.vgroupMapping.xxx的xxx保持一致
tx-service-group: my_test_tx_group
# 应用服务 WEB 访问端口
server:
port: 8082
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
instance:
hostname: order-server
4、resources文件夹下添加eureka配置文件file.conf,registry.conf
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.my_test_tx_group = "server-seata"
#only support when registry.type=file, please don't set multiple addresses
server-seata.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:7900/eureka"
application = "server-seata"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
password = ""
cluster = "default"
timeout = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
5、创建数据源配置类DataSourceConfiguration
package com.wu.apiorder;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
/**
* 数据源代理
*/
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
6、创建测试接口
OrderController
@PostMapping("/update")
public ResultData update(int id){
orderMapper.update(id);
// int i = 1/0;
return new ResultData().SUCCESS("success");
}
OrderMapper
@Mapper
public interface OrderMapper {
@Update("update `order` set order_no='111' where id = #{0}")
void update(int id);
}
7、创建数据库order_test
创建order表
create table `order`
(
id int auto_increment
primary key,
order_no varchar(100) default '' not null,
create_time timestamp null
);
INSERT INTO order_test.`order` (id, order_no, create_time) VALUES (1, 'no_1', '2021-07-18 15:37:08');
INSERT INTO order_test.`order` (id, order_no, create_time) VALUES (2, 'no_2', '2021-07-18 15:37:09');
INSERT INTO order_test.`order` (id, order_no, create_time) VALUES (3, 'no_3', '2021-07-18 15:37:10');
INSERT INTO order_test.`order` (id, order_no, create_time) VALUES (4, 'no_4', '2021-07-18 15:37:09');
8、在业务表中添加undo_log表
-- auto-generated definition
create table undo_log
(
id bigint auto_increment
comment 'increment id'
primary key,
branch_id bigint not null
comment 'branch transaction id',
xid varchar(100) not null
comment 'global transaction id',
context varchar(128) not null
comment 'undo_log context,such as serialization',
rollback_info longblob not null
comment 'rollback info',
log_status int not null
comment '0:normal status,1:defense status',
log_created datetime not null
comment 'create datetime',
log_modified datetime not null
comment 'modify datetime',
constraint ux_undo_log
unique (xid, branch_id)
)
comment 'AT transaction mode undo table'
charset = utf8;
api-user项目配置
1、引入依赖
<!--数据库依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、启动类中移除DataSourceAutoConfiguration类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
因为api-user中还引用了feign,所以要在启动类中添加如下注解
@EnableFeignClients
@EnableDiscoveryClient
3、配置文件修改
spring:
application:
name: user-server
# mysql
datasource:
url: jdbc:mysql://localhost:3306/user_test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# seata
cloud:
alibaba:
seata:
# 与file.conf中server.vgroupMapping.xxx的xxx保持一致
tx-service-group: my_test_tx_group
## 应用名称
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
instance:
hostname: user-server
server:
port: 9092
4、resources文件夹下添加eureka配置文件file.conf,registry.conf(与api-order中一致)
5、创建数据源配置类DataSourceConfiguration(与api-order中一致)
6、创建测试接口
UserController
@PostMapping("/update")
@GlobalTransactional(name = "update-user",rollbackFor = Exception.class)
public ResultData update(int id){
// orderApi.orderUpdate(id);
userMapper.updateNameById(id);
return new ResultData().SUCCESS("success");
}
UserMapper
package com.wu.apiuser.mapper;
import com.wu.common.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* @author whx
* @date 2021/7/18
*/
@Mapper
public interface UserMapper {
@Select("select * from user")
List<User> findAll();
@Update("update user set name='111' where id=#{0}")
void updateNameById(int id);
}
7、创建数据库user_test
-- auto-generated definition
create table user
(
id int auto_increment
primary key,
name varchar(10) default '' null,
user_name varchar(50) default '' null,
password varchar(50) default '' null,
constraint user_id_uindex
unique (id)
);
INSERT INTO user_test.user (id, name, user_name, password) VALUES (1, 'user_1', '1', '1');
INSERT INTO user_test.user (id, name, user_name, password) VALUES (2, 'user_2', '2', '2');
INSERT INTO user_test.user (id, name, user_name, password) VALUES (3, 'user_3', '3', '3');
8、在业务数据库user_test中创建undo_log表(与api-order中一致)
cloud-ribbon-feign项目配置
1、api类添加接口
OrderApi
package com.wu.cloudribbonfeign.api;
import com.wu.cloudribbonfeign.fallback.OrderServerBackFactory;
import com.wu.common.entity.ResultData;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author whx
* @date 2021/7/10
*/
@FeignClient(name = "order-server",fallbackFactory = OrderServerBackFactory.class)
public interface OrderApi {
@GetMapping("/order/list")
ResultData orderList();
@PostMapping("/order/getById")
ResultData orderGetById(@RequestParam("id") Integer id);
@PostMapping("order/update")
ResultData orderUpdate(@RequestParam("id") int id);
}
UserApi
package com.wu.cloudribbonfeign.api;
import com.wu.common.entity.ResultData;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author whx
* @date 2021/7/19
*/
@FeignClient(name = "user-server")
public interface UserApi {
@GetMapping("user/findAll")
ResultData userFindAll();
@PostMapping("user/update")
ResultData userUpdate(@RequestParam("id") int id);
}
2、FeignController添加测试接口
@PostMapping("/updateBoth")
@GlobalTransactional(name = "update-both",rollbackFor = Exception.class)
public ResultData updateBoth(int id){
ResultData res2 = orderApi.orderUpdate(id);
ResultData res1 = userApi.userUpdate(id);
int i = 1/0;
return res1;
}
3、修改配置文件
# 应用名称
spring:
application:
name: cloud-ribbon-feign
# seata
cloud:
alibaba:
seata:
# 与file.conf中server.vgroupMapping.xxx的xxx保持一致
tx-service-group: my_test_tx_group
# 应用服务 WEB 访问端口
server:
port: 9090
eureka:
client:
service-url:
defaultZone: http://localhost:7900/eureka/
order-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
feign:
hystrix:
enabled: true
ribbon:
eureka:
enabled: true
4、resources目录下添加eureka配置文件 file.conf,registry.conf(与api-order一致)
因为本模块中只负责转发请求,并没有连接数据库,所以不涉及数据源配置类的创建
测试
启动服务api-user,api-order,cloud-ribbon-feign
模拟负载均衡服务调用服务A,B,B报错,或者负载均衡服务接口报错,A,B数据库回滚
1、调用接口http://localhost:9090/order/updateBoth
因为updateBoth接口中调用了orderApi.orderUpdate与userApi.userUpdate,两个执行完后又执行了1/0,会报错,理论上order库与user库的更新应该会回退。2、在int I = 1/0; 上打个断点,观察数据库
如下图所示,user表和order表中数据都已经更新了,并且undo_log表中还有一条记录
报错回滚后,会发现数据回滚了,undo_log的记录也消失了
以上分布式事务部署成功
模拟业务服务A调用业务服务B,B报错,A、B数据库回滚
1、将api-order服务中的1/0打开,模拟报错
@PostMapping("/update")
public ResultData update(int id){
orderMapper.update(id);
int i = 1/0;
return new ResultData().SUCCESS("success");
}
2、将api-user对于api-order的调用打开
@PostMapping("/update")
@GlobalTransactional(name = "update-user",rollbackFor = Exception.class)
public ResultData update(int id){
userMapper.updateNameById(id);
orderApi.orderUpdate(id);
return new ResultData().SUCCESS("success");
}
3、调用user/update接口测试
4、在orderApi.orderUpdate(id);上打断点观察
user库中数据已经更新了
报错后数据回滚
测试成功
总结
通过上述测试得知,@GlobalTransactional注解是添加在分布式事务的调用方的, 在被调用方无需添加。
比如上述的user/update调用了order/update,则在user/update上添加@GlobalTransactional即可;
updateBoth中调用了user/update和order/update,所以在updateBoth上添加@GlobalTransactional即可
【具体项目可在上述项目地址中下载】
11、 nacos实现配置中心
nacos + zuul 实现动态网关
12、gateway实现网关
13、fastDFS