微服务学习
soa和微服务
业务系统实施服务化改造之后,原本共享的业务被拆分形成可复用的服务,可以在最大程度上避免共享业务的重复建设、资源连接瓶颈等问题。那么被拆分出来的服务是否也需要以业务功能为维度来进行拆分和独立部署,以降低业务的耦合及提升容错性呢?
微服务就是这样一种解决方案,从名字上来看,面向服务(SOA)和微服务本质上都是服务化思想的一种体现。如果SOA是面向服务开发思想的雏形,那么微服务就是针对可重用业务服务的更进一步优化,我们可以把SOA看成微服务的超集,也就是多个微服务可以组成一个SOA服务。伴随着服务粒度的细化,会导致原本10个服务可能拆分成了100个微服务,一旦服务规模扩大就意味着服务的构建、发布、运维的复杂度也会成倍增加,所以实施微服务的前提是软件交付链路及基础设施的成熟化。
因此微服务在我看来并不是一个新的概念,它本质上是服务化思想的最佳实践方向。 由于SOA和微服务两者的关注点不一样,造成了这两者有非常大的区别:
微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。
SOA关注的是服务的重用性及解决信息孤岛问题。
· 微服务关注的是解耦,虽然解耦和可重用性从特定的角度来看是一样的,但本质上是有区别的,解耦是降低业务之间的耦合度,而重用性关注的是服务的复用。
· 微服务会更多地关注在DevOps的持续交付上,因为服务粒度细化之后使得开发运维变得更加重要,因此微服务与容器化技术的结合更加紧密。
如图1-4所示,将每个具体的业务服务构成可独立运行的微服务,每个微服务只关注某个特定的功能,服务之间采用轻量级通信机制REST API进行通信。细心的读者会发现SOA中的服务和微服务架构中的服务粒度是一样的,**不是说SOA是微服务的超集吗?其实我们可以把用户服务拆分得更细,比如用户注册服务、用户鉴权服务等。**实际上,微服务到底要拆分到多大的粒度没有统一的标准,更多的时候是需要在粒度和团队之间找平衡的,微服务的粒度越小,服务独立性带来的好处就越多,但是管理大量的微服务也会越复杂。
微服务架构有很多好处,下面简单罗列了几个比较突出的点。
· 复杂度可控:通过对共享业务服务更细粒度的拆分,一个服务只需要关注一个特定的业务领域,并通过定义良好的接口清晰表述服务边界。由于体积小、复杂度低,开发、维护会更加简单。
· 技术选型更灵活:每个微服务都由不同的团队来维护,所以可以结合业务特性自由选择技术栈。
· 可扩展性更强:可以根据每个微服务的性能要求和业务特点来对服务进行灵活扩展,比如通过增加单个服务的集群规模,提升部署了该服务的节点的硬件配置。
· 独立部署:由于每个微服务都是一个独立运行的进程,所以可以实现独立部署。当某个微服务发生变更时不需要重新编译部署整个应用,并且单个微服务的代码量比较小,使得发布更加高效。
· 容错性:在微服务架构中,如果某一个服务发生故障,我们可以使故障隔离在单个服务中。其他服务可以通过重试、降级等机制来实现应用层面的容错。
SpringBoot和SpringCloud的区别?
SpringBoot专注于快速方便的开发单个个体微服务。
SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系.
SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
Springcloud的核心组件
服务注册与发现–eureka nacos zookeeper
客户端负载均衡–ribbon loadbalancer
服务熔断—hystrix sentinel
网关–zuul gateway
接口调用–feign resttemplate
链路追踪–sleuth pinpoint
配置中心–spring cloud config、apollo、nacos
Ribbon客户端负载均衡原理
Ribbon是一个基于 HTTP 和 TCP 客户端 的负载均衡的工具。它可以 在客户端 配置RibbonServerList(服务端列表),使用 HttpClient 或 RestTemplate 模拟http请求
对于一个客户端负载均衡实现方案,主要核心三个步骤:
服务发现:能够自动发现所依赖服务的列表服务监听:能够监测到失败的服务,并高效地将失败服务从服务列表中移除负载均衡策略:能够决定如何在多个服务实例中选择一个有效的服务实例,并进行相应的服务请求处理。
Ribbon在具体实现上,有以下组件:
服务器列表(ServerList)
服务器列表就是客户端负载均衡所使用的各服务的服务实例列表。Ribbon在实现上支持3中服务列表方式
静态服务器列表:通过Ribbon的BaseLoadBalancer所提供的setServerList()方法直接进行设置。基于配置的服务器列表:需要在项目配置文件中通过<服务名称>.ribbon.listOfServers进行设置。(如user-service.ribbon.listOfServers=http://127.0.0.1:8000,http://127.0.0.1:8001)基于服务发现的服务器列表:同时使用Ribbon和Eureka时,默认使用该方式,在应用启动时Ribbon就会从Eureka服务器中获取所有注册服务的列表数据,并保持同步。服务器列表过滤(ServerListFilter)
该组件会对原始服务列表使用一定策略进行过滤,并返回有效的服务器列表给客户端负载均衡器使用。
ZoneAffinityServerListFilter:基于区域感知的方式,实现对服务实例的过滤,仅返回与本身所处区域一直的服务提供者实例列表。ServerListSubsetFilter:该过滤器继承自ZoneAffinityServerListFilter,在进行区域感知过滤后,仅返回一个固定大小的服务列表。默认将返回20个服务实例,可以通过ribbon.ServerListSubsetFilter.size进行设置。ZonePreferenceServerListFilter:使用Eureka和Ribbon时默认的过滤器。实现通过配置或者Eureka所属区域来过滤出同区域的服务实例列表。
服务实例存活探测(IPing)
用来检测一个微服务实例是否有相应。Ribbon通过该组件来判断所持有的服务实例列表中各服务可用情况,如果检测到某服务实例不存在,则会从列表中及时移除。
PingUrl:通过定期访问指定的URL判断PingConstant:不做任何处理,只返回一个固定值,用来表示该服务是否可用,默认值为true。NoOpPing:不做任何处理,直接返回true,表示该服务器可用,默认策略。DummyPing:直接返回true,但实现了initWithNiwsConfig方法。NIWSDiscoverPing:根据DiscoveryEnabledServer中InstanceInfo的InstanceStatus属性判断,如果该属性的值为InstanceStatus.UP,则表示服务器可用。
负载均衡策略(IRule)
负责选择一个最终服务实例地址作为负载均衡处理结果。Ribbon提供的选择策略有轮询、根据相应时间加权、断路器(当Hystrix可用时)等。
负载均衡器(ILoadBalancer)
Ribbon负载均衡主要是通过LoadBalancerClient类实现的,而LoadBalancerClient又将具体处理委托给ILoadBalancer处理。
ILoadBalancer通过配置IRule、IPing等信息,并通过ServerList获取服务器注册列表的信息,默认以每10s的频率想服务列表中每个服务实例发送ping请求,检测服务实例是否存活,最后使用负责均衡策略对ServerListFilter过滤得到最终可用的服务实例列表进行处理,并获取到最终要调用的服务实例,然后交给服务调用器进行调用。
ILoadBalance也是一个接口,提供了3个具体实现,分别是DynamicServerListLoadBalancer、ZoneAwareLoadBalancer和NoOpLoadBalancer。
DynamicServerListLoadBalancer继承自ILoadBalancer基础实现BaseLoadBalancer,在基础的负载均衡功能上增加了运行期间对服务实例动态更新和过滤的功能。
ZoneAwareLoadBalancer则是继承DynamicServerListLoadBalancer,在此基础上增加防止跨区域访问的问题。
服务调用器(RestClient)
就是负载均衡后,Ribbon向服务提供者发起REST请求的工具。
@LoadBalance注解
当给RestTemplate增加了@LoadBalance注解后,LoadBalancerAutoConfiguration就会对该RestTemplate进行处理,在RestTemplate的拦截器列表中添加一个LoadBalancerInterceptor拦截器,当通过RestTemplate进行请求请求时,LoadBalancerInterceptor中的拦截方法就会启动,通过LoadBalancerClient使请求具有负载均衡功能。
2、Ribbon负载均衡策略及配置
RoundRobinRule:轮询策略,默认策略
RandomRule:随机策略
BestAvailableRule:最大可用策略,即先过滤出故障服务实例后,选择一个当前并发请求数最小的。
WeightedResponseTimeRule:带有加权的轮询策略,对各个服务实例响应时间进行加权处理,然后再采用轮询的方式获取相应的服务实例。
AvailabilityFilteringRule:可用过滤策略,先过滤出有故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选择一个。
ZoneAvoidanceRule:区域感知策略,先使用主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例,依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最小过滤数和最小过滤百分比,最后对满足条件的服务实例使用轮询方式。配置文件配置方式
代码使用方式
3、直接使用Ribbon API
注意:在使用lbcRestTemplate的时候,不能使用之前@LoadBanlanced的RestTemplate,否则在进行请求时restTempate会把服务实例的逻辑名称当做服务名称来使用。
Feign的理解
Ribbon和Feign都是用于调用其他服务的,不过方式不同。
1.启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的是@EnableFeignClients。
2.服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
3.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。
Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,
不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。
1、Feign的参数绑定
Spring Cloud对Feign进行了增强,使得Feign支持Spring MVC注解。
Spring MVC常用注解:
@RequestParanm:绑定单个请求参数值@PathVariable:绑定URI模板变量值@RequestHeader:绑定请求头数据@RequestBody:绑定请求的内容区数据并能进行自动类型转换。2、Feign中的继承
虽然Fegin中是可以与正常的Service继承的。但在使用微服务架构开发时有一个原则,就是保持各微服务的自治性。如果进行继承点的话,就会造成服务提供方与服务消费方之间的代码紧耦合。因此不建议Fegin调用接口与业务Service继承相同的Service。
3、Feign与Swagger的冲突
如果项目中使用了Swagger,需要把Swgger升级到2.6.1版本以上。否则会发生冲突,项目无法启动。
网关gateway
API 网关的定义
网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。
API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSxGNzE3-1678367193921)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220216002812079.png)]
Spring Cloud Gateway 可以看做是一个 Zuul 1.x 的升级版和代替品,比 Zuul 2 更早的使用 Netty 实现异步 IO,从而实现了一个简单、比 Zuul 1.x 更高效的、与 Spring Cloud 紧密配合的 API 网关。
Spring Cloud Gateway 里明确的区分了 Router 和 Filter,并且一个很大的特点是内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用。
比如内置了 10 种 Router,使得我们可以直接配置一下就可以随心所欲的根据 Header、或者 Path、或者 Host、或者 Query 来做路由。
断言(Predicates)
在上边的配置文件中,routes中的predicates就是一系列的断言,意思就是说,只有满足这样的条件就可以怎么怎么地。上边用到了Path这一个属性,除此之外,还有
比如区分了一般的 Filter 和全局 Filter,内置了 20 种 Filter 和 9 种全局 Filter,也都可以直接用。当然自定义 Filter 也非常方便。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bo4UDLR-1678367193921)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220216002917604.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6TX0to9s-1678367193922)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220216002954333.png)]
通过 Cookie 匹配
Cookie Route Predicate 可以接收两个参数,一个是 Cookie name , 一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
配置文件中的id:随便取,url:要转发的地址
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://ityouknow.com
predicates:
- Cookie=ityouknow, kee.e
使用 curl 测试,命令行输入:
curl http://localhost:8080 --cookie "ityouknow=kee.e"//8080是网关的地址
则会返回页面代码,如果去掉–cookie “ityouknow=kee.e”,后台汇报 404 错误。
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://ityouknow.com
predicates:
- Header=X-Request-Id, \d+
使用 curl 测试,命令行输入:
curl http://localhost:8080 -H "X-Request-Id:666666"
通过 Host 匹配
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://ityouknow.com
predicates:
- Host=**.ityouknow.com
通过请求参数匹配
Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://ityouknow.com
predicates:
- Query=smile
curl localhost:8080?smile=x&id=2
使用实例:
如何使用Gateway功能
两种等价方式:通过YAML配置文件
spring:
cloud:
gateway:
enabled: true
routes:
- id: path_route
order: 500
predicates:
- Path=/demo/list
filters:
- name: Retry
args:
retries: 1
uri: http://127.0.0.1:8080/demo/list
通过RouteLocatorBuilder注册
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder
.routes()
.route("path_route", r -> r.order(500).path("/demo/list").filters(f -> f.retry(1)).uri("http://127.0.0.1:8080/demo/list"))
.build();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qvXY6vWS-1678367193923)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220216004045612.png)]
例子2:
server:
port: 9001
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://blog.csdn.net
predicates:
- Path=/huanzi833
则请求http://localhost:9001/huanzi833时会跳转到csdn网站
StripPrefix参数:
StripPrefix网关过滤器工厂采用一个参数StripPrefix。 StripPrefix参数表示在将请求发送到下游之前从请求中剥离的路径个数。
spring:
cloud:
gateway:
routes:
- id: crm
uri: http://crm
predicates:
- Path=/crm/**
filters:
- StripPrefix=2
如果url以lb开头:
# 采用 LoadBalanceClient 方式请求,以 lb:// 开头,后面的是注册在 Nacos 上的服务名
uri: lb://mogu-admin
当通过网关向/name/bar/foo发出请求时,对nameservice的请求将类似于http://crm/foo。
集群、分布式、SOA、微服务的概念及区别
🕳集群:不同服务器部署同一套应用服务对外提供访问,实现服务的负载均衡或者互备(热备,主从等),指同一种组件的多个实例,形成的逻辑上的整体。单个节点可以提供完整服务。集群是物理形态
🕳分布式:服务的不同模块部署在不同的服务器上,单个节点不能提供完整服务,需要多节点协调提供服务(也可以是相同组件部署在不同节点、但节点间通过交换信息协作提供服务),分布式强调的是工作方式
🕳SOA:面向服务的架构,一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务通常以独立的形式存在于操作系统进程中。各个服务之间通过网络调用。
🕳🕳中心化实现:ESB(企业服务总线),各服务通过ESB进行交互,解决异构系统之间的连通性,通过协议转换、消息解析、消息路由把服务提供者的数据传送到服务消费者。很重,有一定的逻辑,可以解决一些公用逻辑的问题。
🕳🕳去中心化实现:微服务
🕳微服务:在 SOA 上做的升华,微服务架构强调的一个重点是业务需要彻底的组件化和服务化,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成
🕳服务单一职责
轻量级通信:去掉ESB总线,采用restapi通信
🕳简述CAP理论
数据一致性(consistency):如果系统对一个写操作返回成功,那么之后的读请求都必须读到这个新数据;如果返回失败,那么所有读操作都不能读到这个数据,对调用者而言数据具有强一致性(strong consistency)
服务可用性(availability):所有读写请求在一定时间内得到响应,可终止、不会一直等待
分区容错性(partition-tolerance):在网络分区的情况下,被分隔的节点仍能正常对外服务
如果选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
反证:
如果 CAP 三者可同时满足,由于允许 P 的存在,则一定存在节点之间的丢包,如此则不能保证 C
因为允许分区容错,写操作可能在节点 1 上成功,在节点 2 上失败,这时候对于 Client 1 (读取节点1)和 Client 2(读取节点2),就会读取到不一致的值,出现不一致的情况。如果要保持一致性,写操作必须同时失败, 也就是降低系统的可用性。
🕳简述Base理论
cap理论的一种妥协,由于cap只能二取其一,base理论降低了发生分区容错时对可用性和一致性的要求
1、基本可用:允许可用性降低(可能响应延长、可能服务降级),
2、软状态:指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性。
2、最终一致性:节点数据同步可以存在时延),但在一定的期限后必须达成数据的一致,状态变为最终状态
数据一致性模型有哪些
强一致性:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值,这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。
弱一致性:系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。用户读到某一操作对系统数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。
最终一致性:最终一致性是弱一致性的特例,强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,**而不需要实时保证系统数据的强一致性。**到达最终一致性的时间 ,就是不一致窗口时间,在没有故障发生的前提下,**不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。**最终一致性模型根据其提供的不同保证可以划分为更多的模型,包括因果一致性和会话一致性等。
因果一致性:要求有因果关系的操作顺序得到保证,非因果关系的操作顺序则无所谓。进程 A 在更新完某个数据项后通知了进程 B,那么进程 B 之后对该数据项的访问都应该能够获取到进程A 更新后的最新值,并且如果进程 B 要对该数据项进行更新操作的话,务必基于进程 A 更新后的最新值。
分布式系统的设计目标
可扩展性:通过对服务、存储的扩展,来提高系统的处理能力,通过对多台服务器协同工作,来完成单台服务器无法处理的任务,尤其是高并发或者大数据量的任务。
高可用:单点不影响整体,单点故障指系统中某个组件一旦失效,会让整个系统无法工作
无状态:无状态的服务才能满足部分机器宕机不影响全部,可以随时进行扩展的需求。
可管理:便于运维,出问题能不能及时发现定位
高可靠:同样的请求返回同样的数据;更新能够持久化;数据不会丢失
数据模型
注册中心的核心数据是服务的名字和它对应的网络地址,当服务注册了多个实例时,我们需要对不健康的实例进行过滤或者针对实例的一些特征进行流量的分配,那么就需要在实例上存储一些例如健康状态、权重等属性。随着服务规模的扩大,渐渐的又需要在整个服务级别设定一些权限规则、以及对所有实例都生效的一些开关,于是在服务级别又会设立一些属性。再往后,我们又发现单个服务的实例又会有划分为多个子集的需求,例如一个服务是多机房部署的,那么可能需要对每个机房的实例做不同的配置,这样又需要在服务和实例之间再设定一个数据级别。
Zookeeper没有针对服务发现设计数据模型,它的数据是以一种更加抽象的树形K-V组织的,因此理论上可以存储任何语义的数据。而Eureka或者Consul都是做到了实例级别的数据扩展,这可以满足大部分的场景,不过无法满足大规模和多环境的服务数据存储。Nacos在经过内部多年生产经验后提炼出的数据模型,则是一种服务-集群-实例的三层模型。如上文所说,这样基本可以满足服务在所有场景下的数据存储和管理。
Nacos的数据模型虽然相对复杂,但是它并不强制你使用它里面的所有数据,在大多数场景下,你可以选择忽略这些数据属性,此时可以降维成和Eureka和Consul一样的数据模型。
另外一个需要考虑的是数据的隔离模型,作为一个共享服务型的组件,需要能够在多个用户或者业务方使用的情况下,保证数据的隔离和安全,这在稍微大一点的业务场景中非常常见。另一方面服务注册中心往往会支持云上部署,此时就要求服务注册中心的数据模型能够适配云上的通用模型。Zookeeper、Consul和Eureka在开源层面都没有很明确的针对服务隔离的模型,Nacos则在一开始就考虑到如何让用户能够以多种维度进行数据隔离,同时能够平滑的迁移到阿里云上对应的商业化产品。
Nacos提供了四层的数据逻辑隔离模型,用户账号对应的可能是一个企业或者独立的个体,这个数据一般情况下不会透传到服务注册中心。一个用户账号可以新建多个命名空间,每个命名空间对应一个客户端实例,这个命名空间对应的注册中心物理集群是可以根据规则进行路由的,这样可以让注册中心内部的升级和迁移对用户是无感知的,同时可以根据用户的级别,为用户提供不同服务级别的物理集群。再往下是服务分组和服务名组成的二维服务标识,可以满足接口级别的服务隔离。
Nacos 1.0.0介绍的另外一个新特性是:临时实例和持久化实例。在定义上区分临时实例和持久化实例的关键是健康检查的方式。临时实例使用客户端上报模式,而持久化实例使用服务端反向探测模式。临时实例需要能够自动摘除不健康实例,而且无需持久化存储实例,那么这种实例就适用于类Gossip的协议。右边的持久化实例使用服务端探测的健康检查方式,因为客户端不会上报心跳,那么自然就不能去自动摘除下线的实例。
Zookeeper使用场景
数据发布与订阅
数据发布与订阅模型,即所谓的全局配置中心,就是发布者将需要全局统一管理的数据发布到 ZooKeeper 节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。
例如全局的配置信息,分布式服务[框架]的服务地址列表等就非常适合使用。
统一命名服务
命名服务也是分布式系统中比较常见的一类场景。
在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源服务的地址,提供者等信息。
被命名的实体通常可以是集群中的机器,提供的服务地址,进程对象等,这些都可以统称为名字( Name )。
其中较为常见的就是一些分布式服务框架中的服务地址列表。
通过调用 ZooKeeper 提供的创建节点的 API ,能够很容易创建一个全局唯一的 path ,这个 path 就可以作为一个名称。
接下来,介绍统一命名服务的主要应用场景
阿里开源的分布式服务框架 Dubbo 中使用 ZooKeeper 来作为其命名服务,维护全局的服务地址列表。
在 Dubbo 实现中:
服务提供者在启动的时候,向 ZooKeeper 上的指定节点/dubbo/${serviceName}/providers 目录下写入自己的 URL 地址,这个操作就完成了服务的发布。
服务消费者启动的时候,订阅 /dubbo/ ${serviceName}/providers 目录下的提供者 URL 地址,并向/dubbo/ ${serviceName}/consumers 目录下写入自己的 URL 地址。
小提示:
(1) 所有向 ZooKeeper 上注册的地址都是临时节点,这样能够保证服务提供者和消费者能够自动感应资源的变化。
(2) Dubbo 还有针对服务粒度的监控,方法是訂阅/dubbo/${serviceName} 目录下所有提供者和消费者的信息。
分布式锁
分布式锁,主要得益于 ZooKeeper 保证了数据的强一致性。
锁服务可以分为两类,一个是保持独占,另一个是控制时序。
所谓保持独占,就是将所有试图来获取这个锁的客户端,最终只有一个客户端可以成功获得这把锁,从而执行相应操作(通常的做法是把 ZooKeeper 上的一个 Znode 看作是一把锁,通过创建临时节点的方式来实现);
ZooKeeper分布式锁
一) ZooKeeper的每一个节点,都是一个天然的顺序发号器。
在每一个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面,会加上一个次序编号,而这个生成的次序编号,是上一个生成的次序编号加一。
例如,有一个用于发号的节点“/test/lock”为父亲节点,可以在这个父节点下面创建相同前缀的临时顺序子节点,假定相同的前缀为“/test/lock/seq-”。第一个创建的子节点基本上应该为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,如果10-5所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aWOLklFM-1678367193924)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010002840.png)]
(二) ZooKeeper节点的递增有序性,可以确保锁的公平
一个ZooKeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程,都在这个节点下创建个临时顺序节点。由于ZK节点,是按照创建的次序,依次递增的。
为了确保公平,可以简单的规定:编号最小的那个节点,表示获得了锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。
(三)ZooKeeper的节点监听机制,可以保障占有锁的传递有序而且高效
每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode
的通知就可以了。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个,击鼓传花似的依次向后。
ZooKeeper的节点监听机制,能够非常完美地实现这种击鼓传花似的信息传递。具体的方法是,每一个等通知的Znode节点,只需要监听(linsten)或者监视(watch)排号在自己前面那个,而且紧挨在自己前面的那个节点,就能收到其删除事件了。
只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,自己就获得锁。
另外,ZooKeeper的内部优越的机制,能保证由于网络异常或者其他原因,集群中占用锁的客户端失联时,锁能够被有效释放。一旦占用Znode锁的客户端与ZooKeeper集群服务器失去联系,这个临时Znode也将自动删除。排在它后面的那个节点,也能收到删除事件,从而获得锁。正是由于这个原因,在创建取号节点的时候,尽量创建临时znode节点,
(四)ZooKeeper的节点监听机制,能避免羊群效应
ZooKeeper这种首尾相接,后面监听前面的方式,可以避免羊群效应。所谓羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大压力,所以有了临时顺序节点,当一个节点挂掉,只有它后面的那一个节点才做出反应。
图解:分布式锁的抢占过程
接下来我们一起来看看,多客户端获取及释放zk分布式锁的整个流程及背后的原理。
首先大家看看下面的图,如果现在有两个客户端一起要争抢zk上的一把分布式锁,会是个什么场景?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pSuiULZ6-1678367193925)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010254958.png)]
如果大家对zk还不太了解的话,建议先自行百度一下,简单了解点基本概念,比如zk有哪些节点类型等等。
参见上图。zk里有一把锁,这个锁就是zk上的一个节点。然后呢,两个客户端都要来获取这个锁,具体是怎么来获取呢?
咱们就假设客户端A抢先一步,对zk发起了加分布式锁的请求,这个加锁请求是用到了zk中的一个特殊的概念,叫做“临时顺序节点”。
简单来说,就是直接在"my_lock"这个锁节点下,创建一个顺序节点,这个顺序节点有zk内部自行维护的一个节点序号
比如说,第一个客户端来搞一个顺序节点,zk内部会给起个名字叫做:xxx-000001。然后第二个客户端来搞一个顺序节点,zk可能会起个名字叫做:xxx-000002。大家注意一下,最后一个数字都是依次递增的,从1开始逐次递增。zk会维护这个顺序。
所以这个时候,假如说客户端A先发起请求,就会搞出来一个顺序节点,大家看下面的图,Curator框架大概会弄成如下的样子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Na6Jf6Sg-1678367193925)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010416294.png)]
大家看,客户端A发起一个加锁请求,先会在你要加锁的node下搞一个临时顺序节点,这一大坨长长的名字都是Curator框架自己生成出来的。
然后,那个最后一个数字是"1"。大家注意一下,因为客户端A是第一个发起请求的,所以给他搞出来的顺序节点的序号是"1"。
接着客户端A创建完一个顺序节点。还没完,他会查一下"my_lock"这个锁节点下的所有子节点,并且这些子节点是按照序号排序的,这个时候他大概会拿到这么一个集合:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-027Fv1aN-1678367193926)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010517908.png)]
接着客户端A会走一个关键性的判断,就是说:唉!兄弟,这个集合里,我创建的那个顺序节点,是不是排在第一个啊?
如果是的话,那我就可以加锁了啊!因为明明我就是第一个来创建顺序节点的人,所以我就是第一个尝试加分布式锁的人啊!
bingo!加锁成功!大家看下面的图,再来直观的感受一下整个过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jA1NrtML-1678367193926)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010530814.png)]
客户端B过来排队
接着假如说,客户端A都加完锁了,客户端B过来想要加锁了,这个时候他会干一样的事儿:先是在"my_lock"这个锁节点下创建一个临时顺序节点,此时名字会变成类似于:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dF27EbbZ-1678367193927)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010606611.png)]
客户端B因为是第二个来创建顺序节点的,所以zk内部会维护序号为"2"。
接着客户端B会走加锁判断逻辑,查询"my_lock"锁节点下的所有子节点,按序号顺序排列,此时他看到的类似于:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SiMYh30j-1678367193927)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010637167.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OrN4eCjA-1678367193928)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010706285.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Frveq7wM-1678367193928)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010725138.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D39dNbdX-1678367193929)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223010745177.png)]
zookeeper分布式锁优缺点
总结一下ZooKeeper分布式锁:
(1)优点:ZooKeeper分布式锁(如InterProcessMutex),能有效的解决分布式问题,不可重入问题,使用起来也较为简单。
(2)缺点:ZooKeeper实现的分布式锁,性能并不太高。为啥呢?
因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。大家知道,ZK中创建和删除节点只能通过Leader服务器来执行,然后Leader服务器还需要将数据同不到所有的Follower机器上,这样频繁的网络通信,性能的短板是非常突出的。
总之,在高性能,高并发的场景下,不建议使用ZooKeeper的分布式锁。而由于ZooKeeper的高可用特性,所以在并发量不是太高的场景,推荐使用ZooKeeper的分布式锁。
在目前分布式锁实现方案中,比较成熟、主流的方案有两种:
(1)基于Redis的分布式锁
(2)基于ZooKeeper的分布式锁
两种锁,分别适用的场景为:
(1)基于ZooKeeper的分布式锁,适用于高可靠(高可用)而并发量不是太大的场景;
(2)基于Redis的分布式锁,适用于并发量很大、性能要求很高的、而可靠性问题可以通过其他方案去弥补的场景。
总之,这里没有谁好谁坏的问题,而是谁更合适的问题。
mysql分布式锁:
MySQL如何做分布式锁?
在Mysql中创建一张表,设置一个主键或者UNIQUEKEY这个KEY就是要锁的KEY(商品ID),所以同一个KEY在mysq表里只能插入一次了,这样对锁的竟争就交给了数据库,处理同一个KEY数据库保证了只有一个节点能
插入成功,其他节点都会插入失败。
DB分布式锁的实现:通过主键id或者唯一索性的唯一性进行加锁,说白了就是加锁的形式是向一张表中插入一条数据,该条数据的id就是一把分布式锁,例如当一次请求插入了一条id为1的数据,其他想要进行插入数据的并发请求必须等第一次请求执行完成后删除这条id为1的数据才能继续插入,实现了分布式锁的功能。
这样lock和unlock的思路就很简单了,伪代码:
def lock:
execsq1:insert into locked-table(xxx)values(xx)
if result==true:
return true
else:
return false
def unlock:
execsql:delete from lockedorder where order_id='order_id'
SpringCloud有什么优势
使用 Spring Boot 开发分布式微服务时,我们面临以下问题
(1)与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
(2)服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
(3)冗余-分布式系统中的冗余问题。
(4)负载平衡 --负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
(5)性能-问题 由于各种运营开销导致的性能问题。
(6)部署复杂性-Devops 技能的要求
微服务架构面临的挑战
微服务架构不是银弹,它并不能解决所有的架构问题。虽然它本身具备非常多的优势,但是也给我们的开发工作带来了非常大的挑战。在拥抱微服务架构的过程中,我们经常会遇到数据库的拆分、API交互、大量的微服务开发和维护、运维等问题。即便成功实现了微服务的主体,也还是会面临下面这样一些挑战。
· 故障排查:一次请求可能会经历多个不同的微服务的多次交互,交互的链路可能会比较长,每个微服务会产生自己的日志,在这种情况下如果出现一个故障,开发人员定位问题的根源会比较困难。
· 服务监控:在一个单体架构中很容易实现服务的监控,因为所有的功能都在一个服务中。在微服务架构中,服务监控开销会非常大,可以想象一下,在几百个微服务组成的架构中,我们不仅要对整个链路进行监控,还需要对每一个微服务都实现一套类似单体架构的监控。
· 分布式架构的复杂性:微服务本身构建的是一个分布式系统,分布式系统涉及服务之间的远程通信,而网络通信中网络的延迟和网络故障是无法避免的,从而增加了应用程序的复杂度。
· 服务依赖:微服务数量增加之后,各个服务之间会存在更多的依赖关系,使得系统整体更为复杂。假设你在完成一个案例,需要修改服务A、B、C,而A依赖B,B依赖C。在单体式应用中,你只需要改变相关模块,整合变化,再部署就好了。对比之下,微服务架构模式就需要考虑相关改变对不同服务的影响。比如,你需要更新服务C,然后是B,最后才是A,幸运的是,许多改变一般只影响一个服务,需要协调多服务的改变很少。
· 运维成本:在微服务中,需要保证几百个微服务的正常运行,对于运维的挑战是巨大的。比如单个服务流量激增时如何快速扩容、服务拆分之后导致故障点增多如何处理、如何快速部署和统一管理众多的服务等。微服务架构主要的目的是实现业务服务的解耦。随着公司业务的高速发展,微服务组件会越来越多,导致服务与服务之间的调用关系越来越复杂。同时,服务与服务之间的远程通信也会因为网络通信问题的存在变得更加复杂,比如需要考虑重试、容错、降级等情况。那么这个时候就需要进行服务治理,将服务之间的依赖转化为服务对服务中心的依赖。除此之外,还需要考虑:
· 分布式配置中心。
· 服务路由。
· 负载均衡。
· 熔断限流。
· 链路监控。
这些都需要对应的技术来实现,我们是自己研发还是选择市场上比较成熟的技术拿来就用呢?如果市场上有多种相同的解决方案,应该如何做好技术选型?以及每个技术解决方案中的底层实现原理是什么?
服务治理
服务治理可以说是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册与发现。为什么我们在微服务架构中那么需要服务治理模块呢?微服务系统没有它会有什么不好的地方吗? 在最初开始构建微服务系统的时候可能服务并不多,我们可以通过做一些静态配置来完成服务的调用。比如,有两个服务A和B,其中服务A需要调用服务B来完成一个业务操作时,为了实现服务B的高可用,不论采用服务端负载均衡还是客户端负载均衡,都需要手工维护服务B的具体实例清单。但是随着业务的发展,系统功能越来越复杂,相应的微服务应用也不断增加,我们的静态配置就会变得越来越难以维护。并且面对不断发展的业务,我们的集群规模、服务的位置、服务的命名等都有可能发生变化,如果还是通过手工维护的方式,那么极易发生错误或是命名冲突等问题。同时,对于这类静态内容的维护也必将消耗大量的人力。 为了解决微服务架构中的服务实例维护问题,产生了大量的服务治理框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。
服务注册:在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。
比如,我们有两个提供服务A的进程分别运行于192.168.0.100:8000和192.168.0.101:8000位置上,另外还有三个提供服务B的进程分别运行于192.168.0.100:9000、192.168.0.101:9000、192.168.0.102:9000位置上。当这些进程均启动,并向注册中心注册自己的服务之后,注册中心就会维护类似下面的一个服务清单。
另外,服务注册中心还需要以心跳的方式去监测清单中的服务是否可用,若不可用需要从服务清单中剔除,达到排除故障服务的效果。服务发现:由于在服务治理框架下运作,服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。所以,服务调用方在调用服务提供方接口的时候,并不知道具体的服务实例位置。因此,调用方需要向服务注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问。比如,现有服务C希望调用服务A,服务C就需要向注册中心发起咨询服务请求,服务注册中心就会将服务A的位置清单返回给服务C,如按上例服务A的情况,C便获得了服务A的两个可用位置192.168.0.100:8000和192.168.0.101:8000。当服务C要发起调用的时候,便从该清单中以某种轮询策略取出一个位置来进行服务调用,这就是后续我们将会介绍的客户端负载均衡。这里我们只是列举了一种简单的服务治理逻辑,以方便理解服务治理框架的基本运行思路。实际的框架为了性能等因素,不会采用每次都向服务注册中心获取服务的方式,并且不同的应用场景在缓存和服务剔除等机制上也会有一些不同的实现策略。
服务消费者
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u3BLk1Vv-1678367193929)(C:\Users\14172\AppData\Roaming\Typora\typora-user-images\image-20211010142010530.png)]
DevOps
DevOps 是一个完整的面向IT运维的工作流,以 IT 自动化以及持续集成(CI)、持续部署(CD)为基础,来优化程式开发、测试、系统运维等所有环节。
DevOps的概念
DevOps一词的来自于Development和Operations的组合,突出重视软件开发人员和运维人员的沟通合作,通过自动化流程来使得软件构建、测试、发布更加快捷、频繁和可靠
DevOps是为了填补开发端和运维端之间的信息鸿沟,改善团队之间的协作关系。不过需要澄清的一点是,从开发到运维,中间还有测试环节。DevOps其实包含了三个部分:开发、测试和运维。
换句话说,DevOps希望做到的是软件产品交付过程中IT工具链的打通,使得各个团队减少时间损耗,更加高效地协同工作。专家们总结出了下面这个DevOps能力图,良好的闭环可以大大增加整体的产出。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MlfJua1a-1678367193930)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211225123155804.png)]
由上所述,相信大家对DevOps有了一定的了解。但是除了触及工具链之外,作为文化和技术的方法论,DevOps还需要公司在组织文化上的变革。回顾软件行业的研发模式,可以发现大致有三个阶段:瀑布式开发、敏捷开发、DevOps。
DevOps早在九年前就有人提出来,但是,为什么这两年才开始受到越来越多的企业重视和实践呢?因为DevOps的发展是独木不成林的,现在有越来越多的技术支撑。微服务架构理念、容器技术使得DevOps的实施变得更加容易,计算能力提升和云环境的发展使得快速开发的产品可以立刻获得更广泛的使用。
好处是什么?
**DevOps的一个巨大好处就是可以高效交付,这也正好是它的初衷。**Puppet和DevOps Research and Assessment (DORA) 主办了2016年DevOps调查报告,根据全球4600位各IT公司的技术工作者的提交数据统计,得出高效公司平均每年可以完成1460次部署。
与低效组织相比,高效组织的部署频繁200倍,产品投入使用速度快2555倍,服务恢复速度快24倍。在工作内容的时间分配上,低效者要多花22%的时间用在为规划好或者重复工作上,而高效者却可以多花29%的时间用在新的工作上。所以这里的高效不仅仅指公司产出的效率提高,还指员工的工作质量得到提升。
**DevOps另外一个好处就是会改善公司组织文化、提高员工的参与感。**员工们变得更高效,也更有满足和成就感;调查显示高效员工的雇员净推荐值(eNPS:employee Net Promoter Score)更高,即对公司更加认同。
快速部署同时提高IT稳定性。这难道不矛盾吗?
快速的部署其实可以帮助更快地发现问题,产品被更快地交付到用户手中,团队可以更快地得到用户的反馈,从而进行更快地响应。而且,DevOps小步快跑的形式带来的变化是比较小的,出现问题的偏差每次都不会太大,修复起来也会相对容易一些。
分布式系统
什么是分布式系统
随着软件环境和需求的变化 ,软件的架构由单体结构演变为分布式架构,具有分布式架构的系统叫分布式系统,分布式系统的运行通常依赖网络,它将单体结构的系统分为若干服务,服务之间通过网络交互来完成用户的业务处理,当前流行的微服务架构就是分布式系统架构.
CAP理论
一个分布式系统最多只能同时满足一致性C(Consistency)、可用性A(Availability)和分区容错性P(Partition tolerance)这三项中的两项,
一致性指“All nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。对于一致性,可以分为从客户端和服务端两个不同的视角来看。 从客户端来看,一致性主要指多并发访问时更新过的数据如何获取的问题。 从服务端来看,则是如何将更新复制分布到整个系统,以保证数据的最终一致性问题。 一致性是因为有并发读写才有的问题,因此在理解一致性的问题时,一定要注意结合考虑并发读写的场景。 从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。 对于关系型数据库,要求更新过的数据、后续的访问都能看到,这是强一致性。 如果能容忍后续的部分或者全部访问不到,则是弱一致性。
如下图,是商品信息管理的执行流程:
整体执行流程如下
- 商品服务请求主数据库写入商品信息(添加商品、修改商品、删除商品)
- 主数据库向商品服务响应写入成功
- 商品服务请求从数据库读取商品信息
一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个节点上,从任意结点读取到的数据都是最新的状态。
上图中,商品信息的读写要满足一致性就是要实现如下目标:
- 商品服务写入主数据库成功,则向从数据库查询新数据也成功。
- 商品服务写入主数据库失败,则向从数据库查询新数据也失败。
如何实现一致性?
- 写入主数据库后要将数据同步到从数据库。
- 写入主数据库后,在向从数据库同步期间要将从数据库锁定,待同步完成后再释放锁,以免在新数据写入成功后,向从数据库查询到旧的数据。
分布式系统一致性的特点:
- 由于存在数据同步的过程,写操作的响应会有一定的延迟。
- 为了保证数据一致性会对资源暂时锁定,待数据同步完成释放锁定资源。
- 如果请求数据同步失败的结点则会返回错误信息,一定不会返回旧数据。
注意:分布式架构的时候必须做出取舍。 一致性和可用性之间取一个平衡。多余大多数web应用,其实并不需要强一致性。
因此牺牲C换取P,这是目前分布式数据库产品的方向
A - Availability
可用性是指任何事务操作都可以得到响应结果,且不会出现响应超时或响应错误。
上图中,商品信息读取满足可用性就是要实现如下目标:
1. 从数据库接收到数据查询的请求则立即能够响应数据查询结果。
2. 从数据库不允许出现响应超时或响应错误。
如何实现可用性
1. 写入主数据库后要将数据同步到从数据库。
2. 由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定。
3. 即时数据还没有同步过来,从数据库也要返回要查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或响应超时。
分布式系统可用性的特点:所有请求都有响应,且不会出现响应超时或响应错误
比如:
服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结 点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ipLVBmMk-1678367193931)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220222121316977.png)]
P - Partition tolerance
通常分布式系统的各各结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间通信失败,此时仍可对外提供服务,这叫分区容忍性。
上图中,商品信息读写满足分区容忍性就是要实现如下目标:
1. 主数据库向从数据库同步数据失败不影响读写操作。
2. 其一个结点挂掉不影响另一个结点对外提供服务。
如何实现分区容忍性?
1. 尽量使用异步取代同步操作,例如使用异步方式将数据从主数据库同步到从数据,这样结点之间能有效的实现松耦合。
2. 添加从数据库结点,其中一个从结点挂掉其它从结点提供服务。
分布式分区容忍性的特点:分区容忍性分是布式系统具备的基本能力
在所有分布式事务场景中不会同时具备 CAP 三个特性,因为在具备了P的前提下C和A是不能共存的
- AP
放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
例如:上边的商品管理,完全可以实现 AP,前提是只要用户可以接受所查询到的数据在一定时间内不是最新的即可。
通常实现 AP 都会保证最终一致性,后面将的 BASE 理论就是根据 AP 来扩展的,一些业务场景比如:订单退款,今日退款成功,明日账户到账,只要用户可以接受在一定的时间内到账即可。 - CP
放弃可用性,追求一致性和分区容错性,zookeeper 其实就是追求的强一致,又如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。 - CA
放弃分区容忍性,即不进行分区,不考虑由于网络不通或结点挂掉的问题,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,最常用的关系型数据库就满足了 CA。
base理论
eBay 的架构师 Dan Pritchett 源于对大规模分布式系统的实践总结,在 ACM 上发表文章提出 BASE 理论,BASE 理论是对 CAP 理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。
基本可用(Basically Available): 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
软状态(Soft State): 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。MySQL Replication 的异步复制也是一种体现。
最终一致性(Eventual Consistency): 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
ACID 和 BASE 的区别与联系:
ACID 是传统数据库常用的设计理念,追求强一致性模型。BASE 支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。
ACID 和 BASE 代表了两种截然相反的设计哲学,在分布式系统设计的场景中,系统组件对一致性要求是不同的,因此 ACID 和 BASE 又会结合使用。
什么是微服务
就目前来看,微服务本身并没有一个严格的定义,每个人对微服务的理解都不同。Martin Fowler 在他的博客中是这样描述微服务的:(中文)
微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中 ,服务间通信采用轻量级通信机制(通常用 HTTP 资源 API ) 这些服务围绕业务能力构建并且可通过全自动部署机制独立部署这些服务共用一个最小型的集中式的管理,服务可用不同的语言开发,使用不同的数据存储技术。
微服务架构的优点 微服务架构有如下优点。
●易于开发和维护:一个微服务只会关注一个特定的业务功能,所以它业务清晰、代码量较少。开发和维护单个微服务相对简单。而整个应用是由若干个微服务构建而成的,所以整个应用也会被维持在一个可控状态。
●单个微服务启动较快:单个微服务代码量较少,所以启动会比较快。
●局部修改容易部署:单体应用只要有修改,就得重新部署整个应用,微服务解决了这样的问题。一般来说,对某个微服务进行修改,只需要重新部署这个服务即可。
●技术栈不受限:在微服务架构中,可以结合项目业务及团队的特点,合理地选择技术栈。例如某些服务可使用关系型数据库MySQL;某些微服务有图形计算的需求,可以使用Neo4j;甚至可根据需要,部分微服务使用Java开发,部分微服务使用Node.js开发。
●按需伸缩:可根据需求,实现细粒度的扩展。例如,系统中的某个微服务遇到了瓶颈,可以结合这个微服务的业务特点,增加内存、升级CPU或者是增加节点。
微服务架构的主要特征如下。
1)原子服务,专注于做一件事:与面向对象原则中的“单一职责原则”类似,功能越单一,对其他功能的依赖就越少,内聚性就越强,“高内聚、松耦合”。
2)高密度部署:重要的服务可以独立进程部署,非核心服务可以独立打包,合设到同一个进程中,服务被高密度部署。物理机部署,可以在一台服务器上部署多个服务实例进程;如果是云端部署,则可以利用LXC(例如Docker)实现容器级部署,以降低部署成本,提升资源利用率。
3)敏捷交付:服务由小研发团队负责设计、开发、测试、部署、线上治理、灰度发布和下线,运维整个生命周期支撑,实现真正的DevOps。
4)微自治:服务足够小,功能单一,可以独立打包、部署、升级、回滚和弹性伸缩,不依赖其他服务,实现局部自治。
使用微服务架构面临的挑战
**。
●运维要求较高:更多的服务意味着更多的运维投入。在单体架构中,只需要保证一个应用的正常运行。而在微服务中,需要保证几十甚至几百个服务的正常运行与协作,这给运维带来了很大的挑战。
●分布式固有的复杂性:使用微服务构建的是分布式系统。对于一个分布式系统,系统容错、网络延迟、分布式事务等都会带来巨大的挑战。
●接口调整成本高:微服务之间通过接口进行通信。如果修改某一个微服务的API,可能所有使用了该接口的微服务都需要做调整。
gradle init --type pom可以将maven转化为pom项目
●开发框架的选择:可使用Spring Cloud作为微服务开发框架。首先,Spring Cloud具备开箱即用的生产特性,可大大提升开发效率;再者,Spring Cloud的文档丰富、社区活跃,遇到问题比较容易获得支持;更为可贵的是,SpringCloud为微服务架构提供了完整的解决方案。 当然,也可使用其他的开发框架或者解决方案来实现微服务,例如Dubbo、Dropwizard、Armada等。
●运行平台:微服务并不绑定运行平台,将微服务部署在PCServer,或者阿里云、AWS等云计算平台都是可以的。
解决硬编码–服务发现组件
把提供者的网络地址(IP和端口等)硬编码在代码中或者将其提取到配置文件中去。例如:
user:
userServiceUrl: http://localhost:8000
代码:
@Value(“${user. userServiceUrl}”)
private String userServiceUrl;
@GetMapping(“/user/{id}”)
public User findById(@PathVariable long id){
return this.xxx;}
这种方式有很多问题,如下所示。
●适用场景有局限:如果服务提供者的网络地址(IP和端口)发生了变化,会影响服务消费者。例如,用户微服务的网络地址发生变化,就需要修改电影微服务的配置,并重新发布。这显然是不可取的。
●无法动态伸缩:在生产环境中,每个微服务一般都会部署多个实例,从而实现容灾和负载均衡。在微服务架构的系统中,还需要系统具备自动伸缩的能力,例如动态增减节点等。硬编码无法适应这种需求。
要想解决这些问题,服务消费者需要一个强大的服务发现机制,服务消费者使用这种机制获取服务提供者的网络信息。不仅如此,即使服务提供者的信息发生变化,服务消费者也无须修改配置文件。
服务发现组件提供这种能力。在微服务架构中,服务发现组件是一个非常关键的组件。
服务发现组件应具备以下功能。
●服务注册表:是服务发现组件的核心,它用来记录各个微服务的信息,例如微服务的名称、IP、端口等。服务注册表提供查询API和管理API,查询API用于查询可用的微服务实例,管理API用于服务的注册和注销。
●服务注册与服务发现:服务注册是指微服务在启动时,将自己的信息注册到服务发现组件上的过程。服务发现是指查询可用微服务列表及其网络地址的机制。
●服务检查:服务发现组件使用一定机制定时检测已注册的服务,如发现某实例长时间无法访问,就会从服务注册表中移除该实例。
Spring Cloud提供了多种服务发现组件的支持,例如Eureka、Consul和ZooKeeper等。
配置中心是如何保证安全性的
1.保证容器文件访问的安全性,即保证所有的网络资源请求都需要登录
2.将配置中心所有配置文件中的密码进行加密,保证其密文性
3.开发环境禁止拉取生产环境的配置文件
分布式事务六种解决方案
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务.
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
但是在分布式环境下,会变成下边这样:
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;
可以设想,当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了张三减少金额的操作,此时张三和李四的数据就不一致了。
因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用,由于网络问题就会导致分布式事务问题。
分布式事务场景
1、典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务。
2、单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。 简言之:跨数据库实例产生分布式事务。
3、多服务访问同一个数据库实例 比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因就是跨JVM进程,两个微服务持有了不同的数据库链接进行数据库操作,此时产生分布式事务。
对于分布式事务而言几乎满足不了 ACID,其实对于单机事务而言大部分情况下也没有满足 ACID,不然怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应用上的分布式事务了。
我们先来看下 2PC。
2PC
2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
2PC将分布式事务分成了两个阶段,两个阶段分别为提交请求(投票)和提交(执行)。协调者根据参与者的响应来决定是否需要真正地执行事务,具体流程如下。
注意这只是协议或者说是理论指导,只阐述了大方向,具体落地还是有会有差异的。
让我们来看下两个阶段的具体流程。
准备阶段协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了。
同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务)。
假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。
提交请求(投票)阶段
- 协调者向所有参与者发送prepare请求与事务内容,询问是否可以准备事务提交,并等待参与者的响应。
- 参与者执行事务中包含的操作,并记录undo日志(用于回滚)和redo日志(用于重放),但不真正提交。
- 参与者向协调者返回事务操作的执行结果,执行成功返回yes,否则返回no。
提交(执行)阶段
分为成功与失败两种情况。
若所有参与者都返回yes,说明事务可以提交:
- 协调者向所有参与者发送commit请求。
- 参与者收到commit请求后,将事务真正地提交上去,并释放占用的事务资源,并向协调者返回ack。
- 协调者收到所有参与者的ack消息,事务成功完成。
若有参与者返回no或者超时未返回,说明事务中断,需要回滚:
- 协调者向所有参与者发送rollback请求。
- 参与者收到rollback请求后,根据undo日志回滚到事务执行前的状态,释放占用的事务资源,并向协调者返回ack。
- 协调者收到所有参与者的ack消息,事务回滚完成。
让我们来看一下流程图。
假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败。
那可能就有人问了,那第二阶段提交失败的话呢?
这里有两种情况。
第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。
第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功,到最后真的不行只能人工介入处理。
大体上二阶段提交的流程就是这样,我们再来看看细节。
首先 2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。
在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!
协调者故障分析
协调者是一个单点,存在单点故障问题。
假设协调者在发送准备命令之前挂了,还行等于事务还没开始。
假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其它操作。
假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着。
假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。
假设协调者在发送提交事务命令之前挂了,这个不行,傻了!这下是所有资源都阻塞着。
假设协调者在发送提交事务命令之后挂了,这个还行,也是至少命令发出去了,很大概率都会提交成功,然后释放资源,但是如果出现网络分区问题某些参与者将因为收不到命令而阻塞着。
协调者故障,通过选举得到新协调者
因为协调者单点问题,因此我们可以通过选举等操作选出一个新协调者来顶替。
如果处于第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。
如果处于第二阶段,假设参与者都没挂,此时新协调者可以向所有参与者确认它们自身情况来推断下一步的操作。
假设有个别参与者挂了!这就有点僵硬了,比如协调者发送了回滚命令,此时第一个参与者收到了并执行,然后协调者和第一个参与者都挂了。
此时其他参与者都没收到请求,然后新协调者来了,它询问其他参与者都说OK,但它不知道挂了的那个参与者到底O不OK,所以它傻了。
问题其实就出在每个参与者自身的状态只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。
虽然协议上没说,不过在实现的时候我们可以灵活的让协调者将自己发过的请求在哪个地方记一下,也就是日志记录,这样新协调者来的时候不就知道此时该不该发了?
但是就算协调者知道自己该发提交请求,那么在参与者也一起挂了的情况下没用,因为你不知道参与者在挂之前有没有提交事务。
如果参与者在挂之前事务提交成功,新协调者确定存活着的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。
如果参与者在挂之前事务还未提交成功,参与者恢复了之后数据是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保持事务的一致。
所以说极端情况下还是无法避免数据不一致问题。
talk is cheap 让我们再来看下代码,可能更加的清晰。以下代码取自 <<Distributed System: Principles and Paradigms>>
。
这个代码就是实现了 2PC,但是相比于2PC增加了写日志的动作、参与者之间还会互相通知、参与者也实现了超时。这里要注意,一般所说的2PC,不含上述功能,这都是实现的时候添加的。
协调者:
write START_2PC to local log; //开始事务
multicast VOTE_REQUEST to all participants; //广播通知参与者投票
while not all votes have been collected {
wait for any incoming vote;
if timeout { //协调者超时
write GLOBAL_ABORT to local log; //写日志
multicast GLOBAL_ABORT to all participants; //通知事务中断
exit;
}
record vote;
}
//如果所有参与者都ok
if all participants sent VOTE_COMMIT and coordinator votes COMMIT {
write GLOBAL_COMMIT to local log;
multicast GLOBAL_COMMIT to all participants;
} else {
write GLOBAL_ABORT to local log;
multicast GLOBAL_ABORT to all participants;
}
参与者:
write INIT to local log; //写日志
wait for VOTE_REQUEST from coordinator;
if timeout { //等待超时
write VOTE_ABORT to local log;
exit;
}
if participant votes COMMIT {
write VOTE_COMMIT to local log; //记录自己的决策
send VOTE_COMMIT to coordinator;
wait for DECISION from coordinator;
if timeout {
multicast DECISION_REQUEST to other participants; //超时通知
wait until DECISION is received; /* remain blocked*/
write DECISION to local log;
}
if DECISION == GLOBAL_COMMIT
write GLOBAL_COMMIT to local log;
else if DECISION == GLOBAL_ABORT
write GLOBAL_ABORT to local log;
} else {
write VOTE_ABORT to local log;
send VOTE_ABORT to coordinator;
}
每个参与者维护一个线程处理其它参与者的DECISION_REQUEST请求:
while true {
wait until any incoming DECISION_REQUEST is received;
read most recently recorded STATE from the local log;
if STATE == GLOBAL_COMMIT
send GLOBAL_COMMIT to requesting participant;
else if STATE == INIT or STATE == GLOBAL_ABORT;
send GLOBAL_ABORT to requesting participant;
else
skip; /* participant remains blocked */
}
至此我们已经详细地分析的 2PC 的各种细节,我们来总结一下!
2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
当然具体的实现可以变形,而且 2PC 也有变种,例如 Tree 2PC、Dynamic 2PC。
还有一点不知道你们看出来没,2PC 适用于数据库层面的分布式事务场景,而我们业务需求有时候不仅仅关乎数据库,也有可能是上传一张图片或者发送一条短信。
而且像 Java 中的 JTA 只能解决一个应用下多数据库的分布式事务问题,跨服务了就不能用了。
简单说下 Java 中 JTA,它是基于XA规范实现的事务接口,这里的 XA 你可以简单理解为基于数据库的 XA 规范来实现的 2PC。(至于XA规范到底是啥,篇幅有限,下次有机会再说)
2pc的缺点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1efegs4l-1678367193933)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220304204456663.png)]
接下来我们再来看看 3PC。
3PC
3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
让我们来详细看一下。
3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit
。
看起来是把 2PC 的提交阶段变成了预提交阶段和提交阶段,但是 3PC 的准备阶段协调者只是询问参与者的自身状况,比如你现在还好吗?负载重不重?这类的。
而预提交阶段就是和 2PC 的准备阶段一样,除了事务的提交该做的都做了。
看一下图。
不管哪一个阶段有参与者返回失败都会宣布事务失败,这和 2PC 是一样的(当然到最后的提交阶段和 2PC 一样只要是提交请求就只能不断重试)。
我们先来看一下 3PC 的阶段变更有什么影响。
首先准备阶段的变更成不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。
而预提交阶段的引入起到了一个统一状态的作用,它像一道栅栏,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。
假如你是一位参与者,你知道自己进入了预提交状态那你就可以推断出来其他参与者也都进入了预提交状态。
但是多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。
我们再来看下参与者超时能带来什么样的影响。
我们知道 2PC 是同步阻塞的,上面我们已经分析了协调者挂在了提交请求还未发出去的时候是最伤的,所有参与者都已经锁定资源并且阻塞等待着。
那么引入了超时机制,参与者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。
然而超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。
当然 3PC 协调者超时还是在的,具体不分析了和 2PC 是一样的。
从维基百科上看,3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。
新协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令。
所以说 3PC 就是通过引入预提交阶段来使得参与者之间的状态得到统一,也就是留了一个阶段让大家同步一下。
但是这也只能让协调者知道该如果做,但不能保证这样做一定对,这其实和上面 2PC 分析一致,因为挂了的参与者到底有没有执行事务无法断定。
所以说 3PC 通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一致,除非挂了的那个参与者恢复。
让我们总结一下, 3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。
所以 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。
我再说下 3PC 我没有找到具体的实现,所以我认为 3PC 只是纯的理论上的东西,而且可以看到相比于 2PC 它是做了一些努力但是效果甚微,所以只做了解即可。
TCC
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!
TCC 指的是Try - Confirm - Cancel
。
- Try 指的是预留,即资源的预留和锁定,注意是预留。
- Confirm 指的是确认操作,这一步其实就是真正的执行了。
- Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
用户在实现TCC服务时,有以下注意事项
1、业务操作分两阶段完成:
如下图所示,接入TCC前,业务操作只需要一步就能完成,但是在接入TCC之后,需要考虑如何将其分成2阶段完成,把资源的检查和预留放在一阶段的Try操作中进行,把真正的业务操作的执行放在二阶段的Confirm操作中进行;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DRLOdZrN-1678367193933)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220307173026556.png)]
TCC服务要保证第一阶段Try操作成功之后,二阶段Confirm操作一定能成功;
2、允许空回滚;
如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;
TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YSqcsr07-1678367193934)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220307173127422.png)]
3、防悬挂控制;
如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;
用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9V1g7one-1678367193934)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20220307173245707.png)]
4、幂等控制:
无论是网络数据包重传,还是异常事务的补偿执行,都会导致TCC服务的Try、Confirm或者Cancel操作被重复执行;用户在实现TCC服务时,需要考虑幂等控制,即Try、Confirm、Cancel 执行次和执行多次的业务结果是一样的;什么是幂等性,推荐看这篇文章《服务高可用:幂等性设计》
5、业务数据可见性控制;
TCC服务的一阶段Try操作会做资源的预留,在二阶段操作执行之前,如果其他事务需要读取被预留的资源数据,那么处于中间状态的业务数据该如何向用户展示,需要业务在实现时考虑清楚;通常的设计原则是“宁可不展示、少展示,也不多展示、错展示”;
6、业务数据并发访问控制;
TCC服务的一阶段Try操作预留资源之后,在二阶段操作执行之前,预留的资源都不会被释放;如果此时其他分布式事务修改这些业务资源,会出现分布式事务的并发问题;
用户在实现TCC服务时,需要考虑业务数据的并发控制,尽量将逻辑锁粒度降到最低,以最大限度的提高分布式事务的并发性;
其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。
比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。
我们来看下流程,TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。
可以看到流程还是很简单的,难点在于业务上的定义,对于每一个操作你都需要定义三个动作分别对应Try - Confirm - Cancel
。
因此 TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。
还有一点要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
相对于 2PC、3PC ,TCC 适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。
本地消息表
本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
它是将远程分布式事务拆分成一系列的本地事务。如果不考虑性能及设计优雅,借助关系型数据库中的表即可实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t0tYIhQ9-1678367197091)(null)]
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过 MQ 发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
消息事务
RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。
第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
可以看到消息事务实现的也是最终一致性。
最大努力通知
其实我觉得本地消息表也可以算最大努力,事务消息也可以算最大努力。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。
总结
可以看出 2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。
而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。
本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。
Hystrix
Hystrix是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。
通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。
2.重试加大流量(用户重试,代码逻辑重试)
3.服务调用者不可用(同步等待造成的资源耗尽)最终的结果就是一个服务不可用,导致一系列服务的不可用,而往往这种后果是无法预料的。
1.2如何解决灾难性雪崩效应
我们可以通过以下5种方式来解决雪崩效应
1.降级
超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。实现一个fallback方法,当请求后端服务出现异常的时候,可以使用fallback方法返回的值
2.缓存
Hystrix为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的URL没有变化,那么Hystrix不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。
3.请求合并
在微服务架构中,我们将一个项目拆分成很多个独立的模块,这些独立的模块通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解Hystrix的请求合并。
4.熔断
当失败率(如因网络故障/超时造成的失败率高)达到阀值自动触发降级,熔断器触发的快速失败会进行快速恢复。
5.隔离(线程池隔离和信号量隔离)
限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
什么是服务熔断?什么是服务降级?
在介绍熔断机制之前,我们需要了解微服务的雪崩效应。在微服务架构中,微服务是完成一个单一的业务功能,这样做的好处是可以做到解耦,每个微服务可以独立演进。但是,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。
熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand,Hystrix会找有这个注解的方法,并将这类方法关联到和熔断器连在一起的代理上。当前,@HystrixCommand仅当类的注解为@Service或@Component时才会发挥作用。
参考:http://www.cnblogs.com/lvgg/p/7843809.html
服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。
Hystrix相关注解
@EnableHystrix:开启熔断
@HystrixCommand(fallbackMethod=”XXX”):声明一个失败回滚处理函数XXX,当被注解的方法执行
超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gvO0VJJZ-1678367193935)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223021443296.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiPerOIt-1678367193936)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223021515035.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2Y2PvO2-1678367193936)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211223021542194.png)]
Eureka服务治理实例
eureka和zookeeper
由于Spring Cloud Eureka实现的服务治理机制强调了CAP原理中的AP,即可用性与可靠性,它与ZooKeeper这类强调CP(一致性、可靠性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下它宁愿接受故障实例也不要丢掉“健康”实例,比如,当服务注册中心的网络发生故障断开时,由于所有的服务实例无法维持续约心跳,在强调AP的服务治理中将会把所有服务实例都剔除掉,而Eureka则会因为超过85%的实例丢失心跳而会触发保护机制,注册中心将会保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即使其中有部分故障节点,但这样做可以继续保障大多数的服务正常消费。 由于Spring Cloud Eureka在可用性与一致性上的取舍,不论是由于触发了保护机制还是服务剔除的延迟,引起服务调用到故障实例的时候,我们还是希望能够增强对这类问题的容错。所以,我们在实现服务调用的时候通常会加入一些重试机制。在目前我们使用的Brixton版本中,对于重试机制的实现需要我们自己来扩展完成。而从Camden SR2版本开始,Spring Cloud整合了Spring Retry来增强RestTemplate的重试能力,对于开发者来说只需通过简单的配置,原来那些通过RestTemplate实现的服务访问就会自动根据配置来实现重试策略。
启动一个服务注册中心
通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用进行对话。这一步非常简单,只需在一个普通的Spring Boot应用中添加这个注解就能开启此功能,比如下面的例子:
@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为,只需在application.properties中增加如下配置:
server.port=1111
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
由于后续内容也都会在本地运行,为了与后续要进行注册的服务区分,这里将服务注册中心的端口通过server.port属性设置为1111。 eureka.client.register-with-eureka:由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己。 eureka.client.fetch-registry:由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置为false。 在完成了上面的配置后,启动应用并访问http://localhost:1111/。可以看到如下图所示的Eureka信息面板,其中Instances currently registered with Eureka栏是空的,说明该注册中心还没有注册任何服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZwhZV7Ds-1678367193936)(C:\Users\14172\AppData\Roaming\Typora\typora-user-images\image-20211011145004275.png)]
注册服务提供者
在完成了服务注册中心的搭建之后,接下来我们尝试将一个既有的Spring Boot应用加入Eureka的服务治理体系中去。 可以使用上一章中实现的快速入门工程来进行改造,将其作为一个微服务应用向服务注册中心发布自己。首先,修改pom.xml,增加Spring Cloud Eureka模块的依赖,具体代码如下所示:
<dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope>
</dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.SR5</version> <type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
改造/hello请求处理接口,通过注入DiscoveryClient对象,在日志中打印出服务的相关内容。
@RestController
public class HelloController {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String index() {
ServiceInstance instance = client.getLocalServiceInstance();
logger.info("/hello, host:" + instance.getHost() + ", service_id:" + instance.getServiceId());
return "Hello World";
}
}
//然后,在主类中通过加上@EnableDiscoveryClient注解,激活Eureka中的DiscoveryClient实现(自动化配置,创建DiscoveryClient接口针对Eureka客户端的EurekaDiscoveryClient实例),才能实现上述Controller中对服务信息的输出。
@EnableDiscoveryClient
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
最后application.properties配置文件中,通过spring.application.name属性来为服务命名,比如命名为hello-service。再通过eureka.client.serviceUrl.defaultZone属性来指定服务注册中心的地址,这里我们指定为之前构建的服务注册中心地址,完整配置如下所示:
spring.application.name=hello-service
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
下面我们分别启动服务注册中心以及这里改造后的hello-service服务。在hello-service服务控制台中,Tomcat启动之后,com.netflix.discovery.DiscoveryClient对象打印了该服务的注册信息,表示服务注册成功。
我们也可以通过访问Eureka的信息面板,在Instances currently registered with Eureka一栏中看到服务的注册信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TEwjymK3-1678367193936)(C:\Users\14172\AppData\Roaming\Typora\typora-user-images\image-20211011150120102.png)]
高可用注册中心
在微服务架构这样的分布式环境中,我们需要充分考虑发生故障的情况,所以在生产环境中必须对各个组件进行高可用部署,对于微服务如此,对于服务注册中心也一样。但是到本节为止,我们一直都在使用单节点的服务注册中心,这在生产环境中显然并不合适,我们需要构建高可用的服务注册中心以增强系统的可用性。 Eureka Server的设计一开始就考虑了高可用问题,在Eureka的服务治理设计中,所有节点即是服务提供方,也是服务消费方,服务注册中心也不例外。是否还记得在单节点的配置中,我们设置过下面这两个参数,让服务注册中心不注册自己: eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
**Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,**以实现服务清单的互相同步,达到高可用的效果。
下面我们就来尝试搭建高可用服务注册中心的集群。可以在上面实现的服务注册中心的基础之上进行扩展,构建一个双节点的服务注册中心集群。 创建application-peer1.properties,作为peer1服务中心的配置,并将serviceUrl指向peer2:
spring.application.name=eureka-server
server.port=1111
eureka.instance.hostname=peer1
eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/
创建application-peer2.properties,作为peer2服务中心的配置,并将serviceUrl指向peer1:
spring.application.name=eureka-server
server.port=1112
eureka.instance.hostname=peer2
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/
在/etc/hosts文件中添加对peer1和peer2的转换,让上面配置的host形式的serviceUrl能在本地正确访问到;
Windows系统路径为C:\Windows\System32\drivers\etc\hosts。 127.0.0.1 peer1
127.0.0.1 peer2 通过spring.profiles.active属性来分别启动peer1和peer2:java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer1
java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer2
此时访问peer1的注册中心http://localhost:1111/,如下图所示,我们可以看到,registered-replicas中已经有peer2节点的eureka-server了。同样的,我们访问peer2的注册中心http://localhost:1112/,也能看到registered-replicas中已经有peer1节点,并且这些节点在可用分片(available-replicase)之中。我们也可以尝试关闭peer1,刷新http://localhost:1112/,可以看到peer1的节点变为了不可用分片(unavailable-replicas)。
在设置了多节点的服务注册中心之后,服务提供方还需要做一些简单的配置才能将服务注册到Eureka Server集群中。我们以hello-service为例,修改application.properties配置文件,如下所示:
spring.application.name=hello-service
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
上面的配置主要对eureka.client.serviceUrl.defaultZone属性做了改动,将注册中心指向了之前我们搭建的peer1与peer2。
服务的发现与消费
通过上面的内容介绍与实践,我们已经搭建起微服务架构中的核心组件—服务注册中心(包括单节点模式和高可用模式)。同时,还对上一章中实现的Spring Boot入门程序做了改造。通过简单的配置,使该程序注册到Eureka注册中心上,成为该服务治理体系下的一个服务,命名为hello-service。现在我们已经有了服务注册中心和服务提供者,下面就来尝试构建一个服务消费者,它主要完成两个目标,发现服务以及消费服务。其中,服务发现的任务由Eureka的客户端完成,而服务消费的任务由Ribbon完成。Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到均衡负载的作用。当Ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心中获取服务端列表。同时它也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。在本章中,我们对Ribbon不做详细的介绍,读者只需要理解它在Eureka服务发现的基础上,实现了一套对服务实例的选择策略,从而实现对服务的消费。下一章我们会对Ribbon做详细的介绍和分析。 下面我们通过构建一个简单的示例,看看在Eureka的服务治理体系下如何实现服务的发现与消费。 首先,我们做一些准备工作。启动之前实现的服务注册中心eureka-server以及hello-service服务,为了实验Ribbon的客户端负载均衡功能,我们通过java-jar命令行的方式来启动两个不同端口的hello-service,具体如下:
java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8081
java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8082
在成功启动两个hello-service服务之后,如下图所示,从Eureka信息面板中可以看到名为HELLO-SERVICE的服务中出现了两个实例单元,分别是通过命令行启动的8081端口和8082端口的服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JV1mKdBE-1678367193937)(C:\Users\14172\AppData\Roaming\Typora\typora-user-images\image-20211011151027463.png)]
创建一个Spring Boot的基础工程来实现服务消费者,取名为ribbon-consumer,并在pom.xml中引入如下的依赖内容。较之前的hello-service,我们新增了Ribbon模块的依赖spring-cloud-starter-ribbon。
<parent>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>
1.3.7.RELEASE
</version>
<relativePath/> <!-- lookup parent from repository --> </parent>
<dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
创建应用主类ConsumerApplication,通过@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获得服务发现的能力。
同时,在该主类中创建RestTemplate的Spring Bean实例,并通过@LoadBalanced注解开启客户端负载均衡。
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args)
{ SpringApplication.run(ConsumerApplication.class, args);
}
}
创建ConsumerController类并实现/ribbon-consumer接口。在该接口中,通过在上面创建的RestTemplate来实现对HELLO-SERVICE服务提供的/hello接口进行调用。**可以看到这里访问的地址是服务名HELLO-SERVICE,而不是一个具体的地址,**在服务治理框架中,这是一个非常重要的特性,也符合在本章一开始对服务治理的解释。
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer() {
return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class).getBody();
}
}
在application.properties中配置Eureka服务注册中心的位置,需要与之前的HELLO-SERVICE一样,不然是发现不了该服务的,同时设置该消费者的端口为9000,不能与之前启动的应用端口冲突。
spring.application.name=ribbon-consumer
server.port=9000
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
启动ribbon-consumer应用后,我们可以在Eureka信息面板中看到,当前除了HELLO-SERVICE之外,还多了我们实现的RIBBON-CONSUMER服务。
通过向http://localhost:9000/ribbon-consumer发起GET请求,成功返回了“Hello World”。
此时,我们可以在ribbon-consumer应用的控制台中看到如下信息,Ribbon输出了当前客户端维护的HELLO-SERVICE的服务列表情况。其中包含了各个实例的位置,Ribbon就是按照此信息进行轮询访问,以实现基于客户端的负载均衡。另外还输出了一些其他非常有用的信息,如对各个实例的请求总数量、第一次连接信息、上一次连接信息、总的请求失败数量等。
再尝试发送几次请求,并观察启动的两个HELLO-SERVICE的控制台,可以看到两个控制台会交替打印下面的日志,这是我们之前在HelloController中实现的对服务信息的输出,可以用来判断当前ribbon-consumer对HELLO-SERVICE的调用是否是负载均衡的。
com.didispace.web.HelloController : /hello, host:PC-201602152056, service_id:hello-service
nacos
SpringBoot使用Nacos作为配置中心服务和服务注册中心_牧竹子-CSDN博客_nacos使用
Seata解决方案整体介绍
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata 中有三大模块,分别是 TM、RM 和 TC。其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。
角色划分:
TM:事务管理器,开启、 提交、回滚分布式事务
RM: 资源管理器,注册、 汇报、执⾏资源
TC : 事务管理器服务功能,存储事务日志、补偿异常事务等、集中管理事务全局锁(全局行锁),seata服务端
事务执行整体流程:
- TM 开启分布式事务(TM 向 TC 注册全局事务记录);
- 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
- TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
- TC 汇总事务信息,决定分布式事务是提交还是回滚;
- TC 通知所有 RM 提交/回滚 资源,事务二阶段结束;
Spring cloud stream【入门介绍】
案例代码:https://github.com/q279583842q/springcloud-e-book
在实际开发过程中,服务与服务之间通信经常会使用到消息中间件,而以往使用了哪个中间件比如RabbitMQ,那么该中间件和系统的耦合性就会非常高,如果我们要替换为Kafka那么变动会比较大,这时我们可以使用SpringCloudStream来整合我们的消息中间件,来降低系统和中间件的耦合性。
一、什么是SpringCloudStream
官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。
应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream 中binder 交互,通过我们配置来 binding ,而 Spring Cloud Stream 的 binder 负责与消息中间件交互。所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。目前仅支持RabbitMQ、Kafka。
二、Stream 解决了什么问题?
Stream解决了开发人员无感知的使用消息中间件的问题,因为Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qu6zH04i-1678367193937)(C:\Users\heziyi6\AppData\Roaming\Typora\typora-user-images\image-20211123124126235.png)]
组成 | 说明 |
---|---|
Middleware | 中间件,目前只支持RabbitMQ和Kafka |
Binder | Binder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道,通过该输入通道接收到的消息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 指信道channel和exchange绑定在一起 |
Spring Cloud Stream
SCS与各模块之间的关系是:
SCS 在 Spring Integration 的基础上进行了封装,提出了 Binder, Binding, @EnableBinding, @StreamListener 等概念;
SCS 与 Spring Boot Actuator 整合,提供了 /bindings, /channels endpoint;
SCS 与 Spring Boot Externalized Configuration 整合,提供了 BindingProperties, BinderProperties 等外部化配置类;
SCS 增强了消息发送失败的和消费失败情况下的处理逻辑等功能。
SCS 是 Spring Integration 的加强,同时与 Spring Boot 体系进行了融合,也是 Spring Cloud Bus 的基础。它屏蔽了底层消息中间件的实现细节,希望以统一的一套 API 来进行消息的发送/消费,底层消息中间件的实现细节由各消息中间件的 Binder 完成。
负载均衡
负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如F5等;而软件负载均衡则是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作,比如Nginx等。不论采用硬件负载均衡还是软件负载均衡,只要是服务端负载均衡都能以类似的架构方式构建起来:
硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。
当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发。 而客户端负载均衡和服务端负载均衡最大的不同点在于上面所提到的服务清单所存储的位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务端清单,而这些服务端的清单来自于服务注册中心,比如上一章我们介绍的Eureka服务端。同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳去维护服务端清单的健康性,只是这个步骤需要与服务注册中心配合完成。在Spring Cloud实现的服务治理框架中,默认会创建针对各个服务治理框架的Ribbon自动化整合配置,比如Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAuto-Configuration。在实际使用的时候,我们可以通过查看这两个类的实现,以找到它们的配置详情来帮助我们更好地使用它。
分布式幂等性如何设计?
在高并发场景的架构里,幂等性是必须得保证的。比如说支付功能,用户发起支付,如果后台没有做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次受到同一个订单请求,不做幂等很容易就让用户重复支付了,这样用户是肯定不能忍的。
解决方案
1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除成功后,后面来删除直接返回0,也是返回成功。
2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
3,token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端在数据提交前要向后端服务的申请token,token放到Redis 或JVM 内存,token有效时间。提交后后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
4,悲观锁
select id ,name from table_# where id=‘##’ for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考虑id是否为主键,如果id不是主键或者不是InnoDB 存储引擎,那么就会出现锁全表)。
5,乐观锁,给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了
update table_xxx set name=#name#,version=version+1 where version=#version#
6,分布式锁,比如Redis 、Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。
说说 RPC 的实现原理
首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。
说说你对分布式事务的了解
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
首先要搞清楚:ACID、CAP、BASE理论。
ACID:
指数据库事务正确执行的四个基本要素:
(1)原子性(Atomicity)一致性(Consistency)隔离性(Isolation))持久性(Durability
CAP
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
BASE理论
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
你知道哪些分布式事务解决方案?
我目前知道的有五种:
- 两阶段提交(2PC)
- 三阶段提交(3PC)
- 补偿事务(TCC=Try-Confirm-Cancel)
- 本地消息队列表(MQ)
- Sagas事务模型(最终一致性
说完上面五种,面试官一般都会继续问下面这几个问题(可能就问一两个,也可能全部问)。
什么是二阶段提交?
两阶段提交2PC是分布式事务中最强大的事务类型之一,两段提交就是分两个阶段提交:
第一阶段询问各个事务数据源是否准备好。
第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
处理流程如下
阶段一
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。两种情况处理如下:
情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
问题
- 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
- 可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
- 数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
DDD的分层结构
DDD将系统分为用户接口层、应用层、领域层和基础设施层,
应用层是很薄的一层,负责接收用户接口层传来的参数和路由到对应的领域层,系统的业务逻辑主要集中在领域层中,所以领域层在系统架构中占据了很大的面积。上下层之间应该通过接口进行通信,这样接口定义的位置就决定了上下层之间的依赖关系。
DDD的基本元素
DDD的基本元素有Entity、Value Object、Service、Aggregate、Repository、Factory、Domain Event和Moudle等。
◎ Entity:可以表示一个实体。
◎ Value Object:表示一个没有状态的对象。
◎ Service:可以包含对象的行为。
◎ Aggregate:一组相关对象的集合。
◎ Repository:一个存储仓库。
◎ Factory:一个生成聚合对象的工厂。
◎ Domain Event:表示领域事件。
◎ Moudle:表示模块。
大型网站架构
使用缓存后,数据访问压力得到有效缓解,但是单一应用服务器能够处理的请求连接有限,在网站访问高峰期,应用服务器成为整个网站的瓶颈。
使用应用服务器集群改善网站的并发处理能力
使用集群是网站解决高并发、海量数据问题的常用手段。当一台服务器的处理能力、存储空间不足时,不要企图去更换更强大的服务器,对大型网站而言,不管多么强大的服务器,都满足不了网站持续增长的业务需求。这种情况下,更恰当的做法是增加一台服务器分担原有服务器的访问及存储压力。 对网站架构而言,只要能通过增加一台服务器的方式改善负载压力,就可以以同样的方式持续增加服务器不断改善系统性能,从而实现系统的可伸缩性。应用服务器实现集群是网站可伸缩架构设计中较为简单成熟的一种,通过负载均衡调度服务器,可以将来自用户浏览器的访问请求分发到应用服务器集群中的任何一台服务器上,如果有更多用户,就在集群中加入更多的应用服务器,使应用服务器的压力不再成为整个网站的瓶颈。