一. Nacos(服务注册与发现)
1. 认识与安装nacos
nacos是SpringCloudAlibaba的组件,可以用作服务注册中心和配置中心
在win或linux上安装nacos安装包,用命令启动,最后访问http://127.0.0.1:8848/nacos即可看到nacos管理页面
2. Nacos作为注册中心
2.1 服务如何注册到nacos
(1)引入SpringCloudAlibaba和nacos-discover的依赖
(2)在需要注册到nacos的服务中配置nacos地址
spring: cloud: nacos: server-addr: localhost:8848
(3)在主启动类上添加@EnableDiscoveryClient,启服务后,就能在nacos管理页面看到已注册的服务
服务发现
同Eureka,使用Ribbon即可 @LoadBalanced
2.2. Nacos服务分级存储模型、配置权重、环境隔离
(1)Nacos服务分级存储模型
为了服务容灾,需要把服务部署到多个机房,或者说多个集群,就需要在服务中指定该服务隶属哪一个集群。指定方法如下:
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称
最好再实现同集群优先的负载均衡策略。默认的ZoneAvoidanceRule不能实现,同集群优先需要额外指定为NacosRule。指定方法如下:
userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
(2)配置权重
服务实际部署时,可能出现有的实例所在机器性能好,有的性能差,我们希望给性能好的机器承担更多的请求,就可以配置访问频率的权重
(3)环境隔离
Nacos提供了命名实现环境隔离,不同命名空间中的服务互相不可见。
使用方法为:先创建命名空间,再在服务中配置归属的命名空间
spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID
2.3. Nacos作为注册中心和Eureka的区别
(1)nacos的实例分为两种类型
临时实例:如果实例宕机超过一定时间,Nacos会将其从服务列表中剔除。是默认类型。
非临时实例:如果实例宕机,不会从服务列表中剔除,也叫永久实例
如何配置非临时实例?
spring: cloud: nacos: discovery: ephemeral: false # 设置为非临时实例
(2)Nacos和Eureka的区别
共同点:
都支持服务注册和拉取
都支持服务提供者心跳方式做健康监测
区别:
服务提供者方面主要是心跳机制的不同。Eureka都是服务提供者发送心跳机制;Nacos把服务提供者分为临时实例和非临时实例,临时实例是提供者发送心跳,一旦Nacos监测不到心跳,就会剔除临时实例;非临时实例是Nacos制动询问健康状态,即使服务提供者宕机,也不会剔除,只会标记为非健康状态。
服务消费者方面是服务列表拉取方式的不同。Eureka是拉pull,Nacos是pull+push,服务提供者发生变化,nacos会主动push
3. Nacos作为配置中心
3.1 统一配置管理
(1)在nacos中添加配置文件
(2) 在微服务中拉取配置
1)引入nocos依赖
2)添加bootstrap.yaml文件
这里会先读取nacos地址,再根据userservice-dev.yaml读取nacos的配置文件
3)使用@Value读取配置
3.2 配置热更新
配置热更新指的是 当nacos中配置更新时,微服务无需重启即可让配置生效
(1)方式一:在@Value所在类上添加@RefreshScope注解
(2)方式二:新添加一个类,并使用@ConfigurationProperties
3.3 多环境配置
多环境配置指的是:在多个环境中都要配置相同的变量
如何配置一个变量,让多个环境都可以读取到呢?
在nacos中添加 服务名.yaml 文件即可
配置文件的优先级?
二. Dubbo(服务调用)
1. Dubbo概述
1.1 Dubbo概念
Dubbo是阿里开源的高性能、轻量级的JavaRPC框架,致力于提供高性能的RPC远程服务调用方案。
1.2 Dubbo架构
2. Dubbo快速入门
(1)安装Zookeeper
Dubbo官方推荐使用Zookeeper作为注册中心
(2)快速入门
@Service 标识Dubbo的服务提供方 (@Service是Dubbo中的注解)
@Reference 在服务消费方中远程注入服务提供方(也是Dubbo中的注解)
3. Dubbo高级特性
3.1 dubbo-admin管理平台
dubbo-admin是图形化的管理平台,安装后即可使用
点击详情后
3.2 dubbo常用高级配置
3.2.1 序列化
两个机器之间传输数据,需要将Java对象序列化。Dubbo内部已经封装了序列化和反序列化的过程,我们只需要在定义pojo时实现serializable接口即可。
3.2.3 地址缓存
Dubbo消费者会缓存从注册中心拿到的服务列表地址
注册中心挂了,服务是否可以正常访问?
可以,因为Dubbo服务消费者在第一次调用时,会把从注册中心拿到的服务提供方地址缓存到本地。当服务提供者地址发生变化时,注册中心会通知消费者。
3.2.3 超时机制
Dubbo中引入了超时机制,超过时间阈值还无法完成服务访问,则会自动断开连接。
@Service(timeout=3000) @Reference(timeout=3000)都可以设置超时时间。
服务提供方通常会提供默认的超时时间(5-30秒 根据业务调整),但最终超时时间的决定权通常在消费方手中
如果没有超时机制,当服务消费者调用服务提供者阻塞时,消费者会一直等待,短时间内大量请求到来,会造成线程堆积,势必会造成雪崩
3.3.4 重试
如果出现网络抖动,请求有可能失败,Dubbo提供了重试机制来避免类似问题
@Service(retries=2) @Reference(retries=2) 重试次数默认2次
3.3.5 多版本
灰度发布:当出现新功能,会让一部分用户先使用新功能,用户反馈没问题,再将所有用户迁移到新功能
dubbo中使用version属性来设置和调用同一个接口的不同版本
@Service(version="v1.0") @Reference(version="v1.0")
3.3.6 集群容错
集群容错指的是集群中部分节点依然能保持整个系统能够的稳定性。
集群容错模式:@Reference(cluster="failover")
- Failover Cluster:失败重试,是默认模式,且默认重试两次,一般用于读操作。
- Failfast Cluster:快速失败,只发起一次调用,失败立即报错,通常用于写操作。
- Failback Cluster:失败自动恢复,后台记录失败请求,定时重发
3.3.7 服务降级
服务降级是在系统负载过高时,暂时关闭或降低某些非核心功能,以保证整个系统的可用性
降级的手段:
- 限流或熔断:限制请求量 或 直接中断对该服务的调用
- 返回默认值:直接返回固定值或错误码
- 延迟处理:对于非关键任务,将其延迟处理
三. Ribbon(负载均衡)
四. Sentinel(服务容错)
1. 初识Sentinel
1.1 雪崩问题
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,那么当前服务也就不可用了。
那么,依赖于当前服务的其它服务随着时间的推移,最终也都会变的不可用,形成级联失败,雪崩就发生了
1.2 解决雪崩问题的四种方案
(1)超时处理
设置超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
但是该方法只能缓解,因为如果超时时间内涌入大量请求,依然会拖垮服务链路
(2)舱壁模式(也叫线程隔离)
限制每个业务能使用的线程数,避免耗尽整个Tomcat的资源
(3)断路器
当发现服务D的异常比例过高时,会拦截访问服务D的一切请求,形成熔断
(4)限流
流量控制:限制业务访问的QPS,避免服务因流量突增而故障
【总结】
1. 什么是雪崩问题?
微服务系统中,由于调用链中的一个服务故障,导致整个链路都无法访问
2. 如何解决雪崩问题?
限流对服务的保护,是避免流量激增导致服务故障,属于预防措施。
超时处理、线程隔离、降级熔断是把故障控制在一定的范围,属于补救措施
1.3 微服务保护技术对比
早期比较流行的是Hystrix框架,但目前国内实用最广泛的还是阿里巴巴的Sentinel框架
1.4 Sentinel安装
Sentinel是阿里巴巴开源的一款微服务保护组件
下载Sentinel的Jar包,用java -jar 命令启动后,就能访问http://localhost:8080 控制台。
默认用户名密码都是Sentinel。
登录后发现什么都没有,这是因为我们还没和微服务整合
1.5 微服务整合Sentinel
(1)在OrderService中引入Sentinel依赖
(2)配置控制台,修改application.yaml文件
server: port: 8088 spring: cloud: sentinel: transport: dashboard: localhost:8080
(3)访问order-service的任意端点
打开浏览器,访问http://localhost:8088/order/101,这样才能触发sentinel的监控。
然后再访问sentinel的控制台,查看效果:
![]()
2. 流量控制
2.1 簇点链路
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源。
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
![]()
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
流控:流量控制
降级:降级熔断
热点:热点参数限流,是限流的一种
授权:请求的权限控制
2.2 快速入门
点击资源/order/{orderId}后面的流控按钮,就可以弹出表单。
表单中可以填写限流规则,如下:
其含义是限制 /order/{orderId}这个资源的单机QPS为1,即每秒只允许1次请求,超出的请求会被拦截并报错。
2.3 流控模式
在添加限流规则时,点击高级选项,可以选择三种流控模式:
直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
【直接】
【关联】
语法说明:当/write资源访问量触发阈值时,就会对/read资源限流,避免影响/write资源。
使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
【链路】
例如有两条请求链路:
/test1 --> /common
/test2 --> /common
如果只希望统计从/test2进入到/common的请求,则可以这样配置:
语法说明:只统计 /test2 进入 /common 的QPS,超过阈值则对 /test2 来源的请求限流
实战案例:
需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
步骤:
在OrderService中添加一个queryGoods方法,不用实现业务
在OrderController中,改造/order/query端点,调用OrderService中的queryGoods方法
在OrderController中添加一个/order/save的端点,调用OrderService的queryGoods方法
给queryGoods设置限流规则,从/order/query进入queryGoods的方法限制QPS必须小于2
注意1:
OrderService中的方法是不被Sentinel监控的,需要我们自己通过注解来标记要监控的方法。
给OrderService的queryGoods方法添加@SentinelResource注解:
@SentinelResource("goods") public void queryGoods(){ System.err.println("查询商品"); }
注意2:
链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,会导致链路模式失效。
我们需要关闭这种对SpringMVC的资源聚合,修改order-service服务的application.yml文件:
spring: cloud: sentinel: web-context-unify: false # 关闭context整合
重启服务,访问/order/query和/order/save,可以查看到sentinel的簇点链路规则中,出现了新的资源:
【总结】
- 直接:对当前资源限流
- 关联:高优先级资源触发阈值,对低优先级资源限流。
- 链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
2.4 流控效果
在流控的高级选项中,还有一个流控效果选项:
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。(默认)
warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
流控效果有哪些?
快速失败:QPS超过阈值时,拒绝新的请求
warm up: QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机。
排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝
2.5 热点参数限流
热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
【配置示例】:
刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都限定为5.
而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了:
3. 线程隔离
3.1 隔离和降级实现思路分析
限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。
而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
![]()
不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。
而我们的微服务远程调用都是基于Feign来完成的,因此我们需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。
3.2 FeignClient整合Sentinel
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
(1)修改配置文件
feign: sentinel: enabled: true # 开启feign对sentinel的支持
(2)编写失败降级逻辑
业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。我们使用FallbackFactory,对远程调用的异常做处理
步骤一:在feing-api项目中定义类,实现FallbackFactory:
package cn.itcast.feign.clients.fallback; import cn.itcast.feign.clients.UserClient; import cn.itcast.feign.pojo.User; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserClientFallbackFactory implements FallbackFactory<UserClient> { @Override public UserClient create(Throwable throwable) { return new UserClient() { @Override public User findById(Long id) { log.error("查询用户异常", throwable); return new User(); } }; } }
步骤二:在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean:
@Bean public UserClientFallbackFactory userClientFallbackFactory(){ return new UserClientFallbackFactory(); }
步骤三:在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:
import cn.itcast.feign.clients.fallback.UserClientFallbackFactory; import cn.itcast.feign.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class) public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); }
Feign整合Sentinel的步骤:
在application.yml中配置:feign.sentienl.enable=true
给FeignClient编写FallbackFactory并注册为Bean
将FallbackFactory配置到FeignClient
3.3 线程隔离
线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
信号量隔离 :不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。(Sentinel默认采用)
【配置示例】:
QPS:就是每秒的请求数,在快速入门中已经演示过
线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。
4. 熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
请求成功:则切换到closed状态
请求失败:则切换到open状态
断路器熔断策略有三种:慢调用、异常比例、异常数
4.1 慢调用
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
【配置示例】:
解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
4.2 异常比例、异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
5. 授权规则
授权规则可以对请求方来源做判断和控制。
比如 有人直接用 ip+端口 访问微服务,而不是通过网关。我们想对这类请求做限制,就可以使用授权规则。
5.1. 授权规则
(1)基本规则
点击左侧菜单的授权,可以看到授权规则:
白名单可以访问;黑名单不能访问
(2)如何获取origin
实现RequestOriginParser接口,让不同的请求,返回不同的origin。
package cn.itcast.order.sentinel; import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; @Component public class HeaderOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { // 1.获取请求头 String origin = request.getHeader("origin"); // 2.非空判断 if (StringUtils.isEmpty(origin)) { origin = "blank"; } return origin; } }
(3)如何给网关添加请求头
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。 使用GatewayFilter来实现。
修改gateway服务中的application.yml,添加一个defaultFilter:
spring: cloud: gateway: default-filters: - AddRequestHeader=origin,gateway
(4)配置示例
现在,我们直接跳过网关,访问order-service服务:
5.2 自定义异常结果
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
(1)异常类型
如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
public interface BlockExceptionHandler { /** * 处理请求被限流、降级、授权拦截时抛出的异常:BlockException */ void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception; }
这个方法有三个参数:
HttpServletRequest request:request对象
HttpServletResponse response:response对象
BlockException e:被sentinel拦截时抛出的异常
这里的BlockException包含多个不同的子类:
(2)自定义异常处理
package cn.itcast.order.sentinel; import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class SentinelExceptionHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { String msg = "未知异常"; int status = 429; if (e instanceof FlowException) { msg = "请求被限流了"; } else if (e instanceof ParamFlowException) { msg = "请求被热点参数限流"; } else if (e instanceof DegradeException) { msg = "请求被降级了"; } else if (e instanceof AuthorityException) { msg = "没有权限访问"; status = 401; } response.setContentType("application/json;charset=utf-8"); response.setStatus(status); response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}"); } }
6. 规则持久化
现在,sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在生产环境下,我们必须确保这些规则的持久化,避免丢失。
6.1 规则管理模式
规则是否能持久化,取决于规则管理模式,sentinel支持三种规则管理模式:
原始模式:Sentinel的默认模式,将规则保存在内存,重启服务会丢失。
pull模式
push模式
pull模式
控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
缺陷在于多个客户端不能共享数据
push模式
控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。
6.2 实现push模式
详细步骤可以参考课前资料的《sentinel规则持久化》
五. ScheduleX(分布式s调度)
六. RocketMQ(消息)
先安装RocketMQ
1. RocketMQ架构
- RocketMQ有两大组件,NameServer和Broker
- NameServer相当于RocketMQ的注册中心,Broker向它注册信息,Producer和Consumer也可以通过它获取Broker的信息
- Broker是RocketMQ的核心组件,作用是接收、存储、投递消息。Broker内部包含多个MessageQueue,每个MessageQueue都必须在逻辑上归属一个Topic(用于给消息分类),生产者和消费者针对Topic来发送和接收消息
2. 案例
订单服务
(1)添加RockerMQ依赖
<!--添加rocketmq依赖--> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.4.0</version> </dependency>
(2)添加配置
rocketmq: name-server: 192.168.109.131:9876 #rocketMQ服务的地址 producer: group: shop-order # 生产者组
(3)编写测试代码
@RestController @Slf4j public class OrderController2 { @Autowired private OrderService orderService; @Autowired private ProductService productService; @Autowired private RocketMQTemplate rocketMQTemplate; //准备买1件商品 @GetMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { log.info(">>客户下单,这时候要调用商品微服务查询商品信息"); //通过fegin调用商品微服务 Product product = productService.findByPid(pid); if (product == null){ Order order = new Order(); order.setPname("下单失败"); return order; } log.info(">>商品信息,查询结果:" + JSON.toJSONString(product)); Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(product.getPid()); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderService.save(order); //下单成功之后,将消息放到mq中 rocketMQTemplate.convertAndSend("order-topic", order); return order; } }
用户服务
(1)添加RockerMQ依赖
<!--添加rocketmq依赖--> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.4.0</version> </dependency>
(2)添加配置
rocketmq: name-server: 192.168.109.131:9876 #rocketMQ服务的地址
(3)修改主启动类
@SpringBootApplication @EnableDiscoveryClient public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } }
(4)接收消息
//发送短信的服务 @Slf4j @Service @RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic") public class SmsService implements RocketMQListener<Order> { @Override public void onMessage(Order order) { log.info("收到一个订单信息{},接下来发送短信", JSON.toJSONString(order)); } }
3. 发送不同类型的消息
3.1 发送普通消息
//测试 @RunWith(SpringRunner.class) @SpringBootTest(classes = OrderApplication.class) public class MessageTypeTest { @Autowired private RocketMQTemplate rocketMQTemplate; //同步消息 @Test public void testSyncSend() { //参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法 //参数二: 消息内容 SendResult sendResult = rocketMQTemplate.syncSend("test-topic-1", "这是一条同步消息"); System.out.println(sendResult); } //异步消息 @Test public void testAsyncSend() throws InterruptedException { //参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法 //参数二: 消息内容 //参数三: 回调函数, 处理返回结果 rocketMQTemplate.asyncSend("test-topic-1", "这是一条异步消息", new SendCallback(){ @Override public void onSuccess(SendResult sendResult) { System.out.println(sendResult); } @Override public void onException(Throwable throwable) { System.out.println(throwable); } }); //让线程不要终止 Thread.sleep(30000000); } //单向消息 @Test public void testOneWay() { rocketMQTemplate.sendOneWay("test-topic-1", "这是一条单向消息"); } }
3.2 发送顺序消息
//同步顺序消息[异步顺序 单向顺序写法类似] public void testSyncSendOrderly() { //第三个参数用于队列的选择 rocketMQTemplate.syncSendOrderly("test-topic-1", "这是一条异步顺序消息","xxxx"); }
3.3 发送事务消息
七. Docker(服务部署)
八. Seata(分布式事务)
分布式事务:分布式场景下,多个跨服务的操作同时成功同时失败
1. 理论知识
1.1 CAP定理
Consistency(一致性)
Availability(可用性)
Partition tolerance (分区容错性)
这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
【详解】
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
Partition tolerance (分区容错):
- Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
- Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
【矛盾】
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。
如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那节点之间就会出现数据不一致。
1.2 Base理论
BASE理论是对CAP的一种解决思路,包含三个思想:
Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
1.3 解决分布式事务的思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC):
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
2. 初识Seata
2.1 Seata架构
Seata事务管理中有三个重要的角色:
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。主要管理全局事务,可以认为Seata服务就是TC。
TM (Transaction Manager) - 事务管理器:定义分支事务的范围、开始分支事务、提交或回滚分支事务。主要管理分支事务。
RM (Resource Manager) - 资源管理器:一般是数据库,也可以是其他资源管理器,如消息队列、文件系统等。
Seata基于上述架构提供了四种不同的分布式事务解决方案:
XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入(二阶段提交)
TCC模式:最终一致的分阶段事务模式,有业务侵入
AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
SAGA模式:长事务模式,有业务侵入
2.2 部署TC服务
参考课前资料提供的文档《 seata的部署和集成.md 》部署Seata服务端
2.3 微服务集成Seata
(1)引入Seata依赖
(2)配置TC地址
微服务如何根据这些配置寻找TC的地址呢?
我们知道注册到Nacos中的微服务,确定一个具体实例需要四个信息:
namespace:命名空间
group:分组
application:服务名
cluster:集群名
结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。
3. 动手实践
3.1 XA模式
强一致性
3.1.1两阶段提交
XA模式实现的原理都是基于两阶段提交
一阶段:
事务协调者通知每个事物参与者执行本地事务
本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
二阶段:
事务协调者基于一阶段的报告来判断下一步操作
如果一阶段都成功,则通知所有事务参与者,提交事务
如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
3.1.2 Seata的XA模型
RM一阶段的工作:
① 注册分支事务到TC
② 执行分支业务sql但不提交
③ 报告执行状态到TC
TC二阶段的工作:
TC检测各分支事务执行状态
a.如果都成功,通知所有RM提交事务
b.如果有失败,通知所有RM回滚事务
RM二阶段的工作:
接收TC指令,提交或回滚事务
3.1.3 XA模式优缺点
XA模式的优点是什么?
事务的强一致性,满足ACID原则。
常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
依赖关系型数据库实现事务
3.1.4 实现XA模式
Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:
(1)修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata: data-source-proxy-mode: XA
(2)给发起全局事务的入口方法添加@GlobalTransactional注解:
(3)重启服务并测试
重启order-service,再次测试,发现无论怎样,三个微服务都能成功回滚。
3.2 AT模式(Seata默认)
最终一致性,无业务侵入,Seata默认模式
3.2.1 Seata的AT模型
阶段一RM的工作:
注册分支事务
记录undo-log(数据快照)
执行业务sql并提交
报告事务状态
阶段二提交时RM的工作:
删除undo-log即可
阶段二回滚时RM的工作:
根据undo-log恢复数据到更新前
3.2.2 AT和XA的区别
简述AT模式与XA模式最大的区别是什么?
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
XA模式强一致;AT模式最终一致
3.2.3 脏写问题
在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
AT的全局锁和XA的不释放锁有什么区别?
XA没有释放DB锁,如果其他事务想操作该条数据是操作不了的
AT释放了DB锁,其他非Seata事务想访问该数据是没有问题的,而且全局锁更轻量级
如果Seata事务和非Seata事务同时访问了同一条数据岂不是也会脏写?
Seata记录了修改前的快照和修改后的,如果修改后的快照和数据库不一致,说明期间有人修改过数据,则给编程人员发一个警告消息,让人工干预处理
3.2.4 优缺点
AT模式的优点:
一阶段完成直接提交事务,释放数据库资源,性能比较好
利用全局锁实现读写隔离
没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
两阶段之间属于软状态,属于最终一致
框架的快照功能会影响性能,但比XA模式要好很多
3.2.5 实现AT模式
1)导入数据库表,记录全局锁
资料中lock_table导入到TC服务关联的数据库,undo_log表导入到微服务关联的数据库
2)修改application.yml文件,将事务模式修改为AT模式即可
seata: data-source-proxy-mode: AT # 默认就是AT
3.3 TCC模式
最终一致性,有业务侵入
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Try:资源的检测和预留;
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel:预留资源释放,可以理解为try的反向操作。
3.3.1 流程分析
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
3.3.2 Seata的TCC模型
3.3.3 优缺点
TCC模式的每个阶段是做什么的?
Try:资源检查和预留
Confirm:业务执行和提交
Cancel:预留资源的释放
TCC的优点是什么?
一阶段完成直接提交事务,释放数据库资源,性能好
相比AT模型,无需生成快照,无需使用全局锁,性能最强
不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
软状态,事务是最终一致
需要考虑Confirm和Cancel的失败情况,做好幂等处理
3.3.4 事务悬挂和空回滚
(1)空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。
(2)事务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
3.3.5 实现TCC模式
解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel
(1)思路分析
这里我们定义一张表:
那此时,我们的业务开怎么做呢?
Try业务:
记录冻结金额和事务状态到account_freeze表
扣减account表可用金额
Confirm业务
根据xid删除account_freeze表的冻结记录
Cancel业务
修改account_freeze表,冻结金额为0,state为2
修改account表,恢复可用金额
如何判断是否空回滚?
cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
如何避免业务悬挂?
try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务
接下来,我们改造account-service,利用TCC实现余额扣减功能。
(2)声明TCC接口TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,
我们在account-service项目中的
cn.itcast.account.service
包中新建一个接口,声明TCC三个接口package cn.itcast.account.service; import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountTCCService { @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel") void deduct(@BusinessActionContextParameter(paramName = "userId") String userId, @BusinessActionContextParameter(paramName = "money")int money); boolean confirm(BusinessActionContext ctx); boolean cancel(BusinessActionContext ctx); }
(3)编写实现类:在account-service服务中的cn.itcast.account.service.impl
包下新建一个类,实现TCC业务:package cn.itcast.account.service.impl; import cn.itcast.account.entity.AccountFreeze; import cn.itcast.account.mapper.AccountFreezeMapper; import cn.itcast.account.mapper.AccountMapper; import cn.itcast.account.service.AccountTCCService; import io.seata.core.context.RootContext; import io.seata.rm.tcc.api.BusinessActionContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Slf4j public class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { // 0.获取事务id String xid = RootContext.getXID(); // 1.扣减可用余额 accountMapper.deduct(userId, money); // 2.记录冻结金额,事务状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freeze.setXid(xid); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { // 1.获取事务id String xid = ctx.getXid(); // 2.根据id删除冻结记录 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { // 0.查询冻结记录 String xid = ctx.getXid(); AccountFreeze freeze = freezeMapper.selectById(xid); // 1.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); // 2.将冻结金额清零,状态改为CANCEL freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; } }
3.4 SAGA模式
长事务模式,有业务侵入
3.4.1 原理
在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:
一阶段:直接提交本地事务
二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
3.4.5 优缺点
优点:
事务参与者可以基于事件驱动实现异步调用,吞吐高
一阶段直接提交事务,无锁,性能好
不用编写TCC中的三个阶段,实现简单
缺点:
软状态持续时间不确定,时效性差
没有锁,没有事务隔离,会有脏写
3.5 四种模式对比
4. 高可用
主要是异地多活,根据黑马文档部署即可