集团网站网页模板,专门做ppt的网站名称,谷歌浏览器 官网下载,提供服务的网站微服务学习
soa和微服务
业务系统实施服务化改造之后#xff0c;原本共享的业务被拆分形成可复用的服务#xff0c;可以在最大程度上避免共享业务的重复建设、资源连接瓶颈等问题。那么被拆分出来的服务是否也需要以业务功能为维度来进行拆分和独立部署#xff0c;以降低业…微服务学习
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.listOfServershttp://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用的是RibbonClientFeign用的是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: - Cookieityouknow, kee.e
使用 curl 测试命令行输入:
curl http://localhost:8080 --cookie ityouknowkee.e//8080是网关的地址则会返回页面代码如果去掉–cookie “ityouknowkee.e”后台汇报 404 错误。
Header Route Predicate 和 Cookie Route Predicate 一样也是接收 2 个参数一个 header 中属性名称和一个正则表达式这个属性值和正则表达式匹配则执行。
spring: cloud: gateway: routes: - id: header_route uri: http://ityouknow.com predicates: - HeaderX-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: - Querysmile
curl localhost:8080?smilexid2
使用实例
如何使用Gateway功能
两种等价方式通过YAML配置文件
spring:cloud:gateway:enabled: trueroutes:- id: path_routeorder: 500predicates:- Path/demo/listfilters:- name: Retryargs:retries: 1uri: 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: 9001spring:application:name: gatewaycloud:gateway:routes:- id: gateway-serviceuri: https://blog.csdn.netpredicates:- Path/huanzi833则请求http://localhost:9001/huanzi833时会跳转到csdn网站
StripPrefix参数
StripPrefix网关过滤器工厂采用一个参数StripPrefix。 StripPrefix参数表示在将请求发送到下游之前从请求中剥离的路径个数。
spring:cloud:gateway:routes:- id: crmuri: http://crmpredicates:- Path/crm/**filters:- StripPrefix2如果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 resulttrue:
return true
else:
return false
def unlock:
execsql:delete from lockedorder where order_idorder_idSpringCloud有什么优势
使用 Spring Boot 开发分布式微服务时我们面临以下问题 1与分布式系统相关的复杂性-这种开销包括网络问题延迟开销带宽问题安全问题。 2服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录在该目录中注册服务然后能够查找并连接到该目录中的服务。 3冗余-分布式系统中的冗余问题。 4负载平衡 --负载平衡改善跨多个计算资源的工作负荷诸如计算机计算机集群网络链路中央处理单元或磁盘驱动器的分布。 5性能-问题 由于各种运营开销导致的性能问题。 6部署复杂性-Devops 技能的要求
微服务架构面临的挑战
微服务架构不是银弹它并不能解决所有的架构问题。虽然它本身具备非常多的优势但是也给我们的开发工作带来了非常大的挑战。在拥抱微服务架构的过程中我们经常会遇到数据库的拆分、API交互、大量的微服务开发和维护、运维等问题。即便成功实现了微服务的主体也还是会面临下面这样一些挑战。
· 故障排查一次请求可能会经历多个不同的微服务的多次交互交互的链路可能会比较长每个微服务会产生自己的日志在这种情况下如果出现一个故障开发人员定位问题的根源会比较困难。
· 服务监控在一个单体架构中很容易实现服务的监控因为所有的功能都在一个服务中。在微服务架构中服务监控开销会非常大可以想象一下在几百个微服务组成的架构中我们不仅要对整个链路进行监控还需要对每一个微服务都实现一套类似单体架构的监控。
· 分布式架构的复杂性微服务本身构建的是一个分布式系统分布式系统涉及服务之间的远程通信而网络通信中网络的延迟和网络故障是无法避免的从而增加了应用程序的复杂度。
· 服务依赖微服务数量增加之后各个服务之间会存在更多的依赖关系使得系统整体更为复杂。假设你在完成一个案例需要修改服务A、B、C而A依赖BB依赖C。在单体式应用中你只需要改变相关模块整合变化再部署就好了。对比之下微服务架构模式就需要考虑相关改变对不同服务的影响。比如你需要更新服务C然后是B最后才是A幸运的是许多改变一般只影响一个服务需要协调多服务的改变很少。
· 运维成本在微服务中需要保证几百个微服务的正常运行对于运维的挑战是巨大的。比如单个服务流量激增时如何快速扩容、服务拆分之后导致故障点增多如何处理、如何快速部署和统一管理众多的服务等。微服务架构主要的目的是实现业务服务的解耦。随着公司业务的高速发展微服务组件会越来越多导致服务与服务之间的调用关系越来越复杂。同时服务与服务之间的远程通信也会因为网络通信问题的存在变得更加复杂比如需要考虑重试、容错、降级等情况。那么这个时候就需要进行服务治理将服务之间的依赖转化为服务对服务中心的依赖。除此之外还需要考虑
· 分布式配置中心。
· 服务路由。
· 负载均衡。
· 熔断限流。
· 链路监控。
这些都需要对应的技术来实现我们是自己研发还是选择市场上比较成熟的技术拿来就用呢如果市场上有多种相同的解决方案应该如何做好技术选型以及每个技术解决方案中的底层实现原理是什么
服务治理
服务治理可以说是微服务架构中最为核心和基础的模块它主要用来实现各个微服务实例的自动化注册与发现。为什么我们在微服务架构中那么需要服务治理模块呢微服务系统没有它会有什么不好的地方吗 在最初开始构建微服务系统的时候可能服务并不多我们可以通过做一些静态配置来完成服务的调用。比如有两个服务A和B其中服务A需要调用服务B来完成一个业务操作时为了实现服务B的高可用不论采用服务端负载均衡还是客户端负载均衡都需要手工维护服务B的具体实例清单。但是随着业务的发展系统功能越来越复杂相应的微服务应用也不断增加我们的静态配置就会变得越来越难以维护。并且面对不断发展的业务我们的集群规模、服务的位置、服务的命名等都有可能发生变化如果还是通过手工维护的方式那么极易发生错误或是命名冲突等问题。同时对于这类静态内容的维护也必将消耗大量的人力。 为了解决微服务架构中的服务实例维护问题产生了大量的服务治理框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。
服务注册在服务治理框架中通常都会构建一个注册中心每个服务单元向注册中心登记自己提供的服务将主机与端口号、版本号、通信协议等一些附加信息告知注册中心注册中心按服务名分类组织服务清单。
比如我们有两个提供服务A的进程分别运行于192.168.0.1008000和192.168.0.1018000位置上另外还有三个提供服务B的进程分别运行于192.168.0.1009000、192.168.0.1019000、192.168.0.1029000位置上。当这些进程均启动并向注册中心注册自己的服务之后注册中心就会维护类似下面的一个服务清单。
另外服务注册中心还需要以心跳的方式去监测清单中的服务是否可用若不可用需要从服务清单中剔除达到排除故障服务的效果。服务发现由于在服务治理框架下运作服务间的调用不再通过指定具体的实例地址来实现而是通过向服务名发起请求调用实现。所以服务调用方在调用服务提供方接口的时候并不知道具体的服务实例位置。因此调用方需要向服务注册中心咨询服务并获取所有服务的实例清单以实现对具体服务实例的访问。比如现有服务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理论
一个分布式系统最多只能同时满足一致性CConsistency、可用性AAvailability和分区容错性PPartition 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 ConsistencyCAP 的一致性就是强一致性但应用可以采用适合的方式达到最终一致性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
2PCTwo-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;}//如果所有参与者都okif 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_COMMITwrite GLOBAL_COMMIT to local log;else if DECISION GLOBAL_ABORTwrite 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_COMMITsend GLOBAL_COMMIT to requesting participant;else if STATE INIT or STATE GLOBAL_ABORT;send GLOBAL_ABORT to requesting participant;elseskip; /* 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次如果失败就会启动熔断机制。熔断机制的注解是HystrixCommandHystrix会找有这个注解的方法并将这类方法关联到和熔断器连在一起的代理上。当前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.port1111 eureka.instance.hostnamelocalhost
eureka.client.register-with-eurekafalse
eureka.client.fetch-registryfalse
eureka.client.serviceUrl.defaultZonehttp://${eureka.instance.hostname}:${server.port}/eureka/由于后续内容也都会在本地运行为了与后续要进行注册的服务区分这里将服务注册中心的端口通过server.port属性设置为1111。 eureka.client.register-with-eureka由于该应用为注册中心所以设置为false代表不向注册中心注册自己。 eureka.client.fetch-registry由于注册中心的职责就是维护服务实例它并不需要去检索服务所以也设置为false。 在完成了上面的配置后启动应用并访问http://localhost1111/。可以看到如下图所示的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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-eureka/artifactId /dependency
/dependencies
dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId versionBrixton.SR5/version typepom/type scopeimport/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.namehello-service eureka.client.serviceUrl.defaultZonehttp://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-eurekafalse eureka.client.fetch-registryfalse
**Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己这样就可以形成一组互相注册的服务注册中心**以实现服务清单的互相同步达到高可用的效果。
下面我们就来尝试搭建高可用服务注册中心的集群。可以在上面实现的服务注册中心的基础之上进行扩展构建一个双节点的服务注册中心集群。 创建application-peer1.properties作为peer1服务中心的配置并将serviceUrl指向peer2 spring.application.nameeureka-server
server.port1111 eureka.instance.hostnamepeer1
eureka.client.serviceUrl.defaultZonehttp://peer2:1112/eureka/ 创建application-peer2.properties作为peer2服务中心的配置并将serviceUrl指向peer1
spring.application.nameeureka-server
server.port1112 eureka.instance.hostnamepeer2
eureka.client.serviceUrl.defaultZonehttp://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和peer2java -jar eureka-server-1.0.0.jar --spring.profiles.activepeer1 java -jar eureka-server-1.0.0.jar --spring.profiles.activepeer2
此时访问peer1的注册中心http://localhost1111/如下图所示我们可以看到registered-replicas中已经有peer2节点的eureka-server了。同样的我们访问peer2的注册中心http://localhost1112/也能看到registered-replicas中已经有peer1节点并且这些节点在可用分片available-replicase之中。我们也可以尝试关闭peer1刷新http://localhost1112/可以看到peer1的节点变为了不可用分片unavailable-replicas。
在设置了多节点的服务注册中心之后服务提供方还需要做一些简单的配置才能将服务注册到Eureka Server集群中。我们以hello-service为例修改application.properties配置文件如下所示
spring.application.namehello-service eureka.client.serviceUrl.defaultZonehttp://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.port8081 java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port8082
在成功启动两个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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version1.3.7.RELEASE/version relativePath/ !-- lookup parent from repository -- /parent
dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-eureka/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-ribbon/artifactId /dependency
/dependencies
dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId versionBrixton.SR5/version typepom/type scopeimport/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.nameribbon-consumer server.port9000 eureka.client.serviceUrl.defaultZonehttp://localhost:1111/eureka/启动ribbon-consumer应用后我们可以在Eureka信息面板中看到当前除了HELLO-SERVICE之外还多了我们实现的RIBBON-CONSUMER服务。
通过向http://localhost9000/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和KafkaBinderBinder是应用与消息中间件之间的封装目前实行了Kafka和RabbitMQ的Binder通过Binder可以很方便的连接中间件可以动态的改变消息类型(对应于Kafka的topicRabbitMQ的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.RibbonEurekaAutoConfigurationConsul中的org.springframework.cloud.consul.discovery.RibbonConsulAuto-Configuration。在实际使用的时候我们可以通过查看这两个类的实现以找到它们的配置详情来帮助我们更好地使用它。
分布式幂等性如何设计
在高并发场景的架构里幂等性是必须得保证的。比如说支付功能用户发起支付如果后台没有做幂等校验刚好用户手抖多点了几下于是后台就可能多次受到同一个订单请求不做幂等很容易就让用户重复支付了这样用户是肯定不能忍的。
解决方案 1查询和删除不在幂等讨论范围查询肯定没有幂等的说删除第一次删除成功后后面来删除直接返回0也是返回成功。 2建唯一索引唯一索引或唯一组合索引来防止新增数据存在脏数据 当表存在唯一索引并发时新增异常时再查询一次就可以了数据应该已经存在了返回结果即可。 3token机制由于重复点击或者网络重发或者nginx重发等情况会导致数据被重复提交。前端在数据提交前要向后端服务的申请tokentoken放到Redis 或JVM 内存token有效时间。提交后后台校验token同时删除token生成新的token返回。redis要用删除操作来判断token删除成功代表token校验通过如果用selectdelete来校验token存在并发问题不建议使用。 4悲观锁
select id ,name from table_# where id‘##’ for update;
悲观锁使用时一般伴随事务一起使用数据锁定时间可能会很长根据实际情况选用另外还要考虑id是否为主键如果id不是主键或者不是InnoDB 存储引擎那么就会出现锁全表。 5乐观锁给数据库表增加一个version字段可以通过这个字段来判断是否已经被修改了
update table_xxx set name#name#,versionversion1 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)补偿事务(TCCTry-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表示模块。
大型网站架构
使用缓存后数据访问压力得到有效缓解但是单一应用服务器能够处理的请求连接有限在网站访问高峰期应用服务器成为整个网站的瓶颈。
使用应用服务器集群改善网站的并发处理能力
使用集群是网站解决高并发、海量数据问题的常用手段。当一台服务器的处理能力、存储空间不足时不要企图去更换更强大的服务器对大型网站而言不管多么强大的服务器都满足不了网站持续增长的业务需求。这种情况下更恰当的做法是增加一台服务器分担原有服务器的访问及存储压力。 对网站架构而言只要能通过增加一台服务器的方式改善负载压力就可以以同样的方式持续增加服务器不断改善系统性能从而实现系统的可伸缩性。应用服务器实现集群是网站可伸缩架构设计中较为简单成熟的一种通过负载均衡调度服务器可以将来自用户浏览器的访问请求分发到应用服务器集群中的任何一台服务器上如果有更多用户就在集群中加入更多的应用服务器使应用服务器的压力不再成为整个网站的瓶颈。