1 Star 0 Fork 0

steven / trans-could

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

trans-could

项目介绍

介绍分布式事务实践解决数据一致性。本项目基于 Spring Cloud 搭建,而Spring Cloud是基于Spring Boot构建的,它们之间的版本有配套的对应关系。在构建 项目时,要注意版本之间的这种对应关系,版本若对应不上则会出现问题。

项目流程

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

达到效果

输入图片说明

项目介绍

分布式事务实践解决数据一致性之spring事务实例

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

输入图片说明

模拟实战

Spring Cloud 全家桶生态中的RPC框架是通过 Feign+Hystrix+Ribbon 组合完成的。具体来说,Feign负责基础的REST调用的序列化和反序列化,Hystrix负责 熔断器、熔断和隔离,Ribbon负责客户端负载均衡

题外话:理论上,如果服务端同一个服务提供者存在多个运行实例,一般的负载均衡方案分为以下两种:

  • 服务端负载均衡。可以通过硬件的方式提供反向代理服务,比如F5专业设备;也可以通过软件的方式提供反向代理服务,比如Nginx反向代理服务器;更多的情况是两种 方式结合,并且有多个层级的反向代理;
  • 客户端负载均衡。客户端自己维护一份从注册中心获取的Provider列表清单,根据自己配置的Provider负载均衡选择算法在客户端进行请求的分发。Ribbon就是 一个客户端的负载均衡开源组件;

一、Eureka

Eureka 是微服务架构中的注册中⼼,专⻔负责服务的注册与发现。假设订单服务想要调⽤库存服务、仓储服务,或者是积分服务,结合如下示意图来仔细 剖析⼀下整个流程:

img.png

如上图所示,库存服务、仓储服务、积分服务中都有⼀个 Eureka Client 组件,这个组件专⻔负责将这个服务的信息注册到 Eureka Server 中。说⽩了, 就是告诉 Eureka Server,⾃⼰在哪台机器上,监听着哪个端⼝。⽽ Eureka Server 是⼀个注册中⼼,⾥⾯有⼀个注册表保存了各服务所在的机器和端⼝号。

订单服务⾥也有⼀个 Eureka Client 组件,这个 Eureka Client 组件会找 Eureka Server 问⼀下:库存服务在哪台机器啊?监听着哪个端⼝啊?仓 储服务呢?积分服务呢?然后就可以把这些相关信息从 Eureka Server 的注册表中拉取到⾃⼰本地缓存起来。各个服务内的 Eureka Client 组件,默认 情况下,每隔 30 秒会发送⼀个请求到 Eureka Server,来拉取最近有变化的服务信息。除此之外Eureka 还有⼀个⼼跳机制,各个 Eureka Client 每 隔 30 秒会发送⼀次⼼跳到 Eureka Server,通知个服务实例还活着。如果某个 Eureka Client 很⻓时间没有发送⼼跳给 Eureka Server,那么就说 明这个服务实例已经挂了。

img_5.png

这时如果订单服务想要调⽤库存服务,就可以在⾃⼰本地的 Eureka Client 找到库存服务在哪台机器,监听哪个端⼝。收到响应后,紧接着就可以发送⼀个 请求过去,调⽤库存服务扣减库存的那个接⼝。同理,如果订单服务要调⽤仓储服务、积分服务,也是如法炮制。

Eureka Server的注册表如何设计的?

假设⼿头有⼀套⼤型的分布式系统,⼀共 100 个服务,每个服务部署在 20 台机器上,机器是 4 核 8G 的标准配置。也就是说相当于你⼀共部署了 100 * 20 = 2000 个服务实例,有 2000 台机器。 每台机器上的服务实例内部都有⼀个 Eureka Client 组件,它会每隔 30 秒请求⼀次 Eureka Server,拉取变化的注册表。此外每个服务实例上的 Eureka Client 都会每隔 30 秒发送⼀次⼼跳请求给 Eureka Server。按照测算⼀个上 百个服务,⼏千台机器的系统,按照这样的频率请求 Eureka Server,⽇请求量在千万级,每秒的访问量在 150 次左右。

所以通过设置⼀个适当拉取注册表以及发送⼼跳的频率,可以保证⼤规模系统⾥对 Eureka Server 的请求压⼒不会太⼤。那么Eureka Server 是如何保 证轻松抗住如此高的请求?⾸先得清楚 Eureka Server 到底是⽤什么来存储注册表的。

img_6.png

图中的这个名字叫做 registry 的 ConcurrentHashMap,就是注册表的核⼼结构。所以Eureka Server 的注册表直接基于纯内存,即在内存⾥维护了⼀ 个数据结构,各个服务的注册、服务下线、服务故障,全部会在内存⾥维护和更新这个注册表。这个 ConcurrentHashMap 的 key 就是服务名称,⽐如 “inventory-service”,就是⼀个服务名称,value 则代表了⼀个服务的多个服务实例。在Map<String, Lease>, key 就是服务实 例的 id, InstanceInfo 就代表了服务实例的具体信息,⽐如机器的 ip 地址、hostname 以及端⼝号。⽽这个 Lease,⾥⾯则会维护每个服务最近⼀ 次发送⼼跳的时间。

基于内存来承载各个服务的请求,为什么能可以处理这么多请求呢?这就要依靠多级缓存机制了(具体介绍可见 https://gitee.com/stevenchin/product-basic-service 的 feature/v_20.0_multithreading 分支)。

在拉取注册表的时候:

  • ⾸先从 ReadOnlyCacheMap ⾥查缓存的注册表;
  • 若没有,就找 ReadWriteCacheMap ⾥缓存的注册表;
  • 如果还没有,就从内存中获取实际的注册表数据;

在注册表发⽣变更的时候:

  • 会在内存中更新变更的注册表数据,同时过期掉 ReadWriteCacheMap。此过程不会影响 ReadOnlyCacheMap 提供⼈家查询注册表;
  • ⼀段时间内(默认 30 秒),各服务拉取注册表会直接读 ReadOnlyCacheMap。30 秒过后,Eureka Server 的后台线程发现 ReadWriteCacheMap 已经清空了,也会清空 ReadOnlyCacheMap 中的缓存;
  • 下次有服务拉取注册表,⼜会从内存中获取最新的数据了,同时填充各个缓存;

img_7.png

通过多级缓存机制确保了不会针对内存数据结构发⽣频繁读写并发冲突操作,进⼀步提升性能。

快速创建微服务: 输入图片说明

启动类(不加入HystrixDashboard):

@SpringBootApplication
@EnableEurekaServer		//配置为eureka服务中心
public class RegistryApplication {

	public static void main(String[] args) {
		SpringApplication.run(RegistryApplication.class, args);
	}
}

启动类(加入HystrixDashboard):

在微服务架构⾥,⼀个系统会有很多的服务。假设其中的订单服务在⼀个业务流程⾥需要调⽤三个服务,现在订单服务⾃⼰最多只有 100 个线程可以处理请求, 然后积分服务不幸的挂了,每次订单服务调⽤积分服务的时候都会卡住⼏秒钟,然后抛出—个超时异常。这样会导致什么问题?

  • 如果系统处于⾼并发的场景下,⼤量请求涌过来的时候,订单服务的 100 个线程都会卡在请求积分服务这块。导致订单服务没有⼀个线程可以处理请求;
  • 然后就会导致别⼈请求订单服务的时候,发现订单服务也挂了,不响应任何请求了;

就是微服务架构中恐怖的服务雪崩问题。这么多服务互相调⽤,要是不做任何保护的话,某⼀个服务挂了就会引起连锁反应,导致别的服务也挂。⽐如积分服 务挂了会导致订单服务的线程全部卡在请求积分服务这⾥,没有⼀个线程可以⼯作。瞬间导致订单服务也挂了,别⼈请求订单服务全部会卡住,⽆法响应。

所以需要切断这种连锁反应。假设积分服务挂了,⼤不了等恢复之后,慢慢⼈⾁⼿⼯恢复数据。不要因为⼀个积分服务挂了就直接导致订单服务也挂了。结合 业务来看:⽀付订单的时候,只要把库存扣减了,然后通知仓库发货就 OK 了。

这时就轮到 Hystrix 闪亮登场了。Hystrix 是隔离、熔断以及降级的⼀个框架。Hystrix 会搞很多个⼩⼩的线程池,⽐如订单服务请求库存服务是⼀个 线程池,请求仓储服务是⼀个线程池,请求积分服务是⼀个线程池。每个线程池⾥的线程就仅仅⽤于请求那个服务。

所以如果积分服务挂了,当然会导致订单服务⾥的那个⽤来调⽤积分服务的线程都卡死不能⼯作了,只不过调⽤积分服务的时候,每次都会报错。但是由于 订单服务调⽤库存服务、仓储服务的这两个线程池都是正常⼯作的,所以这两个服务不会受到任何影响。

但是如果积分服务都挂了,每次调⽤都要去卡住⼏秒钟⼲啥呢?没有意义。所以直接对积分服务熔断不就得了,⽐如在 5 分钟内请求积分服务直接就返回了, 不要去⾛⽹络请求卡住⼏秒钟,这个过程,就是所谓的熔断。

积分服务挂了后就熔断,还要降级处理:例如每次调⽤积分服务,就在数据库⾥记录⼀条消息,说给某某⽤户增加了多少积分,因为积分服务挂了导致没增加 成功。这样等积分服务恢复了,就可以根据这些记录⼿⼯加⼀下积分,这个过程就是所谓的降级。

Hystrix 隔离、熔断和降级的全流程示意图如下:

img_3.png

常⻅的服务的降级逻辑有哪些呢(具体⽤什么降级策略要根据业务来定,不是⼀成不变的)?

  • 如果查询数据的服务挂了可以查本地的缓存;
  • 如果写⼊数据的服务挂了可以先把这个写⼊操作记录⽇志到⽐如 mysql ⾥,或者写⼊ MQ ⾥,后⾯再慢慢恢复;
  • 如果 redis 挂了可以查 mysql;
  • 如果 mysql 挂了可以把操作⽇志记录到 es ⾥去,后⾯再慢慢恢复数据;
@SpringBootApplication
@EnableEurekaServer			//配置为eureka服务注册中心
@EnableHystrixDashboard		//在eureka服务注册中心实现HystrixDashboard(也可以单独新建一个微服务实现HystrixDashboard)
public class RegistryApplication {

	public static void main(String[] args) {
		SpringApplication.run(RegistryApplication.class, args);
	}
}

当把HystrixDashboard也加入eureka注册中心时,需要加入相关依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>

当其他注册上来的微服务需要受Hystrix监控时同样需要加入相关依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

同时其启动类需要加上Hystrix的客户端注解,例如下:

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients
public class UserApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserApplication.class, args);
	}
}

进入Hystrix Dashboard:

http://localhost: 8761/hystrix

输入图片说明

输入图片说明

配置文件:

server:
  port: 8761
    
# 设置服务名字,以便注册中心查找(服务注册中心自身时可以不用设置的,但是注册的服务需要)  
spring:
  application:
    name: registry
    
# 限制非法的服务注册 (在注册的服务中配置defaultZone时加上,例如:defaultZone: http://springcloud:springcloudstudy@localhost:8761/eureka/)
security:
  basic:
    enabled: true		 		# 开启基于HTTP basic的认证
  user:
    name: studycloud		    # 配置登录的账号
    password: study123456	    # 配置登录的密码
    
# 服务注册中心配置(单机不需要注册,但是多机时需要配置,避免单点故障)  
eureka:
  instance:
    hostname: eureka-registry
  client:
    # 单机不需要注册
    register-with-eureka: false		
    # 单机不需要查找其他服务中心
    fetch-registry: false
    serviceUrl:
      # 服务注册中心地址
      defaultZone: http://${security.user.name}:${security.user.password}@localhost:${server.port}/eureka/
      

进入eureka控制面板

http://localhost:8761/

查看环境情况

http://localhost:8761/env

二、网关之 Zuul

注意事项:在 Zuul 中使用 Hystrix 监控时,可以不加入 Hystrix 相关依赖和配置,因为 Zuul 默认就带有。

Zuul,也就是微服务⽹关。这个组件是负责⽹络路由的。如果没有⽹关的⽇常⼯作会怎样?

  • 客户端需要进行负载均衡,从多个Provider中挑选最合适的微服务提供者;
  • 存在跨域请求时,服务端需要进行额外处理;
  • 每个服务需要进行独立的用户认证;

⼀般微服务架构中都必然会设计⼀个⽹关在⾥⾯,像 android、ios、pc 前端、微信⼩程序、H5 等等,不⽤去关⼼后端有多少个服务,就知道有⼀个 ⽹关,所有请求都往⽹关⾛,⽹关会根据请求中的⼀些特征,将请求转发给后端的各个服务。⽽且有⽹关之后还有很多好处,⽐如可以做统⼀的降级、限流、 认证授权、安全、路由、负载均衡等。zuul位置示意图如下:

img_4.png

微服务网关的实现框架有多种,Spring Cloud全家桶中比较常用的有Zuul和Spring Cloud Gateway两大框架。在高并发的使用场景中则推荐使用 SpringCloud Gateway 框架作为网关。

输入图片说明

启动类:

@SpringBootApplication
@EnableEurekaClient			//设置为注册服务
@EnableZuulProxy			//设置为网关服务
public class ProxyApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProxyApplication.class, args);
	}
}

配置文件:

server:
  port: 8088

# 设置服务名字,以便注册中心查找(服务注册中心自身时可以不用设置的,但是注册的服务需要)   
spring:
  application:
    name: proxy

eureka:
  client:
    serviceUrl:
      # 需要指定security在服务注册中心的用户名和密码,防止恶意注册,同时注意端口
      defaultZone: http://studycloud:study123456@localhost:8761/eureka/

三、Sleuth服务

Spring Cloud Sleuth 集成了 Zipkin 组件。它主要用于 聚集 来自各个 异构系统 的 实时监控数据,用来追踪 微服务架构 下的 系统延时问题。可参考:

四、Order服务

输入图片说明

直接访问order微服务:http://localhost:8082/api/order;

通过网关访问order微服务:http://localhost: 8088 / order /api/order。

可以发现,通过网关访问的url 加粗部分,端口是网关服务的端口,url路径多了/order,这就是order微服务的application.name。

启动类(不加入Hystrix监控):

@SpringBootApplication
@EnableEurekaClient
public class OrderApplication {

	public static void main(String[] args) {
		SpringApplication.run(OrderApplication.class, args);
	}
}

启动类(加入Hystrix监控):

@SpringBootApplication
@EnableEurekaClient
@EnableHystrix
public class OrderApplication {

	public static void main(String[] args) {
		SpringApplication.run(OrderApplication.class, args);
	}
}

需要加入的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

需要监控的url方法上需要加上@HystrixCommand注解,如下:

@GetMapping("")
@HystrixCommand		// 此url受 Hystrix 监控
public List<Order> getAll() {
    return orderRepository.findAll();
}

输入图片说明

输入图片说明

注意:如果想统一查看监控,可以使用turbine.stream。

配置文件:

server:
  port: 8082
  
spring:
  application:
    name: order

eureka:
  client:
    serviceUrl:
      defaultZone: http://studycloud:study123456@localhost:8761/eureka/ 

五、User服务

输入图片说明

启动类(不使用Feign调用rest接口):

@SpringBootApplication
@EnableEurekaClient			//设置为注册服务
public class UserApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserApplication.class, args);
	}
}

配置文件:

server:
  port: 8081
  
spring:
  application:
    name: user

eureka:
  client:
    serviceUrl:
      defaultZone: http://studycloud:study123456@localhost:8761/eureka/
      
  • 使用Feign方式调用服务(以user服务调用order服务为例)

假设有个订单服务确实库存服务、积分服务、仓库服务在哪⾥了,同时也监听着哪些端⼝号了。但是新问题⼜来了:难道订单服务要⾃⼰写⼀⼤堆代码,跟其他服 务建⽴⽹络连接,然后构造⼀个复杂的请求,接着发送请求过去,最后对返回的响应结果再写⼀⼤堆代码来处理吗?

Feign 已为我们提供好了优雅解决⽅案。Feign是在RestTemplate基础上封装的,使用注解的方式来声明一组与服务提供者Rest接口所对应的本地Java API接口方法。 Feign Client 会在底层根据注解,跟指定的服务建⽴连接、构造请求、发起请求、获取响应、解析响应等。 Feign 是如何做到这么神奇的呢?很简单 Feign 的⼀个 关键机制就是使⽤了动态代理。示意图如下:

img_1.png

  • ⾸先,如果对某个接⼝定义了 @FeignClient 注解,Feign 就会针对这个接⼝创建⼀个动态代理;
  • 接着要是调⽤那个接⼝,本质就是会调⽤ Feign 创建的动态代理;
  • Feign 的动态代理会根据在接⼝上的 @RequestMapping 等注解,来动态构造出要请求的服务的地址;

客户端RPC(这里的Feign Client)实现类位于远程调用Java接口和Provider微服务实例之间,承担了以下职责:

  • 拼装REST请求:根据Java接口的参数,拼装目标REST接口的URL;
  • 发送请求和获取结果:通过Java HTTP组件(如HttpClient、OkHttp 等)调用Provider微服务实例的REST接口,并且获取REST响应;
  • 结果解码:解析REST接口的响应结果,封装成目标POJO对象(Java接口的返回类型)并且返回;

img.png

使用Feign进行RPC远程调用时,对于每一个Java远程调用接口,Feign都会生成一个RPC远程调用客户端实现类,只是对于开发者来说这个实现类是透明的,感觉 不到这个实现类的存在。关于代理模式的更多介绍可见 https://gitee.com/stevenchin/product-basic-service/tree/feature%2Fv_8.0_java/src

现在新的问题⼜来了,如果库存服务部署在了 5 台机器上,同一个服务名下有五个实例, Feign 怎么知道该请求哪台机器呢?

Spring Cloud Ribbon 就派上⽤场了。Feign组件自身不具备负载均衡能力,Ribbon 就是专⻔解决这个问题的。它会帮你在每次请求时选择⼀台机器,均匀的把请 求分发到各个机器上。Ribbon 的负载均衡默认使⽤的最经典的 Round Robin 轮询算法(还有随机、权重、最少连接、重试、可用过滤、区域过滤等多种方式实现负载 均衡)。简单来说就是如果订单服务对库存服务发起 10 次请求,那就先请求第 1 台机器、然后是第 2 台机 器、第 3 台机器、第 4 台机器、第 5 台机器,接着再 来—个循环,第 1 台机器、第 2 台机器。。。以此类推。

那么 Ribbon 是如何与 Feign、Eureka 紧密协作的呢?

  • ⾸先 Ribbon 会从 Eureka Client ⾥获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端⼝号;
  • 然后 Ribbon 就可以使⽤算法,从中选择⼀台机器;
  • 最后 Feign 就会针对这台机器,构造并发起请求;

示意图如下:

img_2.png

Spring Cloud Ribbon是Spring Cloud集成Ribbon开源组件的一个模块,它不像服务注册中心Eureka Server、配置中心Spring Cloud Config那样独立部 署,而是作为基础设施模块,几乎存在于每个Spring Cloud微服务提供者中。微服务间的RPC调用以及API网关的代理请求的RPC转发调用,实际上都需要通过Ribbon 来实现负载均衡。虽然Spring Cloud集成了Ribbon组件,但是要在Provider微服务中开启Ribbon负载均衡组件,还需要在Maven的pom文件中增加以下 Spring Cloud Ribbon集成模块的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

在Spring Cloud的Provider中使用Ribbon,只需要导入Spring Cloud Ribbon依赖,Ribbon在RPC调用时就会生效。

Ribbon实现的负载均衡策略还可以实现自定义的策略类。可以通过Provider配置文件的ribbon.NFLoadBalancerRuleClassName配置项更改实际的负载均衡策略。

创建user服务启动类(使用Feign调用rest接口):

@SpringBootApplication
@EnableEurekaClient			//设置为注册服务
@EnableFeignClients			//开启声明式rest调用接口
public class UserApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserApplication.class, args);
	}
}

使用Feign调用rest接口user服务的pom文件需要添加Feign的相关依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

新建一个 maven project 的公用模块用于提供 Feign 调用需要的 interface 和 dto 等。

输入图片说明

输入图片说明

输入图片说明

输入图片说明

在user服务中加入service公共模块依赖,同样在order服务中也加入service公共模块的依赖:

<dependency>
    <groupId>com.cloud</groupId>
    <artifactId>service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

在order服务中需要受Feign调用的controller继承service公共模块的interface:

@RestController
@RequestMapping("/api/order")
public class OrderResourceController implements IOrderService {
	
	/**
	 * @PostConstruct 注解在服务器加载Servle的时候运行,并且只会被服务器执行一次。
	 */
	@PostConstruct
    public void init() {
        Order order = new Order();
        order.setAmount(100);
        order.setTitle("MyOrder");
        order.setDetail("Bought a imooc course");
        //使用H2作为内嵌的内存数据库。写入数据库完成测试,内存数据库不提供数据的持久化存储;当应用启动时你需要填充你的数据库,当应用结束时数据将会丢弃。
        orderRepository.save(order);
    }
	
	@Autowired
    private OrderRepository orderRepository;
	
	@PostMapping("")
    public OrderDTO create(@RequestBody OrderDTO dto) {
        Order order = new Order();
        order.setAmount(dto.getAmount());
        order.setTitle(dto.getTitle());
        order.setDetail(dto.getDetail());
        order = orderRepository.save(order);
        dto.setId(order.getId());
        return dto;
    }

    @GetMapping("/{id}")
    public OrderDTO getMyOrder(@PathVariable Long id) {
        Order order = orderRepository.findOne(id);
        OrderDTO dto = new OrderDTO();
        dto.setId(order.getId());
        dto.setAmount(order.getAmount());
        dto.setTitle(order.getTitle());
        dto.setDetail(order.getDetail());
        return dto;
    }

    @GetMapping("")
    @HystrixCommand		// 此url受 Hystrix 监控
    public List<Order> getAll() {
        return orderRepository.findAll();
    }
}

在user服务中实现FeignClient,指明需要调用服务的application.name和对应的controller:

//value = "order" order项目的application.name;path = "/api/order" controller的RequestMapping
@FeignClient(value = "order", path = "/api/order")	
public interface OrderClient extends IOrderService {
	
	/**
	 * 使用Feign在使用@PathVariable注解时,必须指明字段对应的name
	 */
	@GetMapping("/{id}")
	OrderDTO getMyOrder(@PathVariable(name = "id") Long id);

	@PostMapping("")
	OrderDTO create(@RequestBody OrderDTO dto);
}

在user服务的controller层实现order服务的调用整合:

@RestController
@RequestMapping("/api/user")
public class CustomerResourceController {

    @PostConstruct
    public void init() {
        Customer customer = new Customer();
        customer.setUsername("imooc");
        customer.setPassword("111111");
        customer.setRole("User");
        customerRepository.save(customer);
    }

    @Autowired
    private CustomerRepository customerRepository;
	
    @Autowired
    private OrderClient orderClient;
    
    @GetMapping("/my")
    public Map<String, Object> getMyInfo() {
        Customer customer = customerRepository.findOneByUsername("imooc");
        OrderDTO order = orderClient.getMyOrder(1l);
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("customer", customer);
        result.put("order", order);
        return result;
    }
}

Spring cloud 参数调优示例

新系统上线,⼀开始⽤户规模很⼩,注册⽤户量⼩⼏⼗万,⽇活⼏千⽤户,平时正常 QA 测试没发现什么⼤⽑病,感觉性能还不错,⼀切都很完美。

每天都有新的数据进⼊数据库的表中,就这么⽇积⽉累的没想到数据规模居然慢慢吞吞增⻓到了单表⼏百万。这个时候呢,看起来也没太⼤的⽑病,就是有⽤ 户反映系统有些操作会感觉卡顿⼏秒钟,会刷不出来⻚⾯。这是为啥呢?

核⼼原因是单表数据量⼤了⼀些,达到了⼏百万。有个别服务存在慢SQL,可能因为⼀⼤堆的多表关联。并且还没有设计好索引或者是设计了索引,但⽆奈 写了上百⾏的⼤SQL导致数据存在笛卡尔积和索引失效。

对微服务框架有点了解的话应该知道,Feign + Ribbon 组成的服务调⽤框架,是有接⼝调⽤超时这⼀说的,有⼀些参数可以设置接⼝调⽤的超时时间。如果 调⽤⼀个接⼝超过设置时间导致异常返回,⽤户就刷不出来⻚⾯了或返回异常提示信息。

⼀般碰到这种事情,⼀⼤坨屎 SQL 摆在那⼉,写 SQL 的⼈过⼀个⽉⾃⼰都看不懂,80% 的⼯程师看着都不愿意去花时间重写和优化。如果是乱改⼀通重构, 把系统核⼼业务流程搞挂了怎么办?

所以第⼀反应是增加超时时间!接⼝慢点可以,但是别超时不响应。如下示例:

img_8.png

大家知道 Spring Cloud ⾥⼀般会⽤ hystrix 的线程池来执⾏接⼝调⽤的请求。所以设置超时⼀般设置两个地⽅,feign 和 ribbon 那块的超时,还有 hystrix 那块的超时。其中后者那块的超时⼀般必须⼤于前者

优化了参数后,看上去效果不错,⽤户虽然觉得有的⻚⾯慢是慢点,但是起码过⼏秒能刷出来。⼤家看看下⾯这张图,感受⼀下现场氛围:

img_9.png

随着时间的推移,公司业务继续⾼速发展,研发⼈员在后台系统发现,⾃⼰的⽤户量蹭蹭蹭的直线增⻓。⾼峰期每秒的并发请求居然达到了近万的程度。在这 个过程中,先是紧张的各种扩容服务,⼀台变两台,两台变四台。然后数据库主从架构挂上去,读写分离是必须的,否则单个数据库服务器哪能承载那么⼤的 请求。多搞⼏个从库扛⼀下⼤量的读请求,这样基本就扛住了。

突然有一天的⾼峰期,系统的某个功能⻚⾯突然就整个死了,就是没法再响应任何请求,所有⽤户刷新这个⻚⾯全部都是⽆法响应。原因很简单,⼀个服务 A 的实例⾥专⻔调⽤服务 B 的那个线程池⾥的线程总共可能就⼏⼗个。每个线程调⽤服务 B 都会卡住 5 秒钟。 那如果每秒钟过来⼏百个请求这个服务实例 ⼀下⼦那个线程池⾥的线程就全部堵死了,没法再响应任何请求。

img_10.png

遇到⻚⾯刷不出来只能重启机器,相当于短暂初始化了⼀下机器内的资源。然后接着运⾏⼀段时间⼜卡死,再次重启......⽤户的体验是极差。

没办法了,必须要追根溯源,治标治本了,关键点,优化图中核⼼服务 B 的性能。

  • 互联⽹公司核⼼业务逻辑,⾯向 C 端⽤户⾼并发的请求,不要⽤上百⾏的⼤SQL多表关联,那样单表⼏百万⾏数据量的话,会导致⼀下执⾏过慢。最佳⽅ 式就是对数据库就执⾏简单的单表查询和更新,然后复杂的业务逻辑全部放在 java 系统中来执⾏,⽐如⼀些关联或者是计算之类的⼯作。那个核⼼服务 B 的响应速度就已经优化成⼏⼗毫秒;

  • 超时时间设置。不要因为系统接⼝的性能过差⽽懒惰,搞成⼏秒甚⾄⼏⼗秒的超时,⼀般超时 定义在 1 秒以内,是⽐较通⽤以及合理的。因为⼀个接⼝ 理论的最佳响应速度应该在 200ms 以内,或者慢点的接⼝就⼏百毫秒。如果⼀个接⼝响应时间达到 1 秒 +,建议考虑⽤缓存、索引、NoSQL 等各种能 想到的技术⼿段,优化⼀下性能;

  • 可是要是超时时间设置成了 1 秒,如果就是因为偶然发⽣的⽹络抖动,导致接⼝某次调⽤就是在 1.5 秒呢?这个是经常发⽣的,因为⽹络的问题接⼝调 ⽤偶然超时。所以此时配合着超时时间,⼀般都会设置⼀个合理的重试,如下所示:

img_11.png

  • 设置这段重试之后,Spring Cloud 中的 Feign + Ribbon 的组合,在进⾏服务调⽤的时候,如果发现某台机器超时请求失败,会⾃动重试这台机器, 如果还是不⾏会换另外⼀台机器重试

  • 涉及到了重试那么必须上接⼝的幂等性保障机制。要是对⼀个接⼝重试了好⼏次,结果⼈家重复插⼊了多条数据,该怎么办呢?根据业务来常⻅的⽅案如下:

    • 在数据库⾥建⼀个唯⼀索引,插⼊数据的时候如果唯⼀索引冲突了就不会插⼊重复数据;
    • 或者通过 redis ⾥放⼀个唯⼀ id 值,然后每次要插⼊数据,都通过 redis 判断⼀下,那个值如果已经存在了,那么就不要插⼊重复数据了;
    • 类似这样的⽅案还有⼀些。总之要保证⼀个接⼝被多次调⽤的时候,不能插⼊重复的数据;

img_12.png

空文件

简介

分布式事务实践解决数据一致性 展开 收起
Java
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/stevenchin/trans-could.git
git@gitee.com:stevenchin/trans-could.git
stevenchin
trans-could
trans-could
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891