微服务组件
服务治理
问题

-
item-service这么多实例,cart-service如何知道每一个实例的地址?
-
http请求要写url地址,
cart-service服务到底该调用哪个实例呢? -
如果在运行过程中,某一个
item-service实例宕机,cart-service依然在调用该怎么办? -
如果并发太高,
item-service临时多部署了N台实例,cart-service如何知道新实例的地址?
注册中心原理
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
服务注册
-
引入依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
-
配置Nacos地址
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
-
重启
服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
-
引入依赖
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
-
配置Nacos地址
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
-
发现并调用服务
OpenFeign
用来远程服务调用
引入依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
启用OpenFeign
在服务调用者的启动类上添加@EnableFeignClients注解,启动OpenFeign功能
编写OpenFeign客户端
在服务调用者中定义一个新的接口,编写Feign客户端:
package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
-
@FeignClient("item-service"):声明服务名称 -
@GetMapping:声明请求方式 -
@GetMapping("/items"):声明请求路径 -
@RequestParam("ids") Collection<Long> ids:声明请求参数 -
List<ItemDTO>:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>。
我们只需要直接调用这个方法,即可实现远程调用了。
使用FeignClient

网关
使用原因
- 服务的端口ip都不一样,前端不知道如何发请求
- 每个服务都要验证用户登录信息
现在,前端请求不能直接访问微服务,而是要请求网关:
-
网关可以做安全控制,也就是登录身份校验,校验通过才放行
-
通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去
快速入门
1.创建项目
创建一个新的module,命名为hm-gateway,作为网关微服务
2.引入依赖
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
3.启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4.配置路由
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos注册中心地址
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service
# 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表(转发到哪)
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则(接收哪种请求)
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
网关登录校验
如何在网关转发之前做登录校验
package com.heima.gateway.filters;
import com.heima.gateway.config.AuthProperties;
import com.heima.gateway.util.JwtTool;
import com.hmall.common.exception.UnauthorizedException;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request
ServerHttpRequest request = exchange.getRequest();
//判断当前路径是否需要认证
if(isExclude(request.getPath().toString())){
return chain.filter(exchange);
}
String token=null;
//运行一个请求头包含多个头
List<String> headers = request.getHeaders().get("authorization");
if (headers != null && !headers.isEmpty()){
token = headers.get(0);
}
Long userId = null;
try {
//解析token
userId = jwtTool.parseToken(token);
}catch (UnauthorizedException e){
ServerHttpResponse response = exchange.getResponse();
// 响应状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//请求完结,响应结束
return response.setComplete();
}
String userInfo = userId.toString();
//TODO 传递用户信息
return chain.filter(swe);
}
private boolean isExclude(String path) {
//判断当前路径是否需要认证
//authProperties.getExcludePaths():不需要认证的路径
for(String pathPattern:authProperties.getExcludePaths()){
if (antPathMatcher.match(pathPattern,path)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
网关如何将用户信息传递给微服务

微服务接收用户信息
由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common中,并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能,无需重复编写。
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的用户信息
String userInfo = request.getHeader("user-info");
// 2.判断是否为空
if (StrUtil.isNotBlank(userInfo)) {
// 不为空,保存到ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserContext.removeUser();
}
}
配置登录拦截器
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
如何在微服务之间传递用户信息
下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!
由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。
微服务之间调用是基于OpenFeign来实现的,要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
配置管理:
配置共享
我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
-
在Nacos中添加共享配置
-
微服务拉取配置
1.添加共享配置
在nacos控制台新建配置

2.拉取共享配置
1.引入依赖
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
resources目录新建一个bootstrap.yaml文件
因为共享配置在nacos中,所以要读取nacos要先在bootstrap.yaml中添加nacos的地址
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
配置热更新
当修改配置文件的配置时,微服务无需重启即可使配置生效
分为两步:
1.在Nacos中添加配置
我们在nacos中添加一个配置文件,将购物车的上限数量添加到配置中

文件的dataId格式
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
-
服务名:我们是购物车服务,所以是cart-service -
spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置 -
后缀名:例如yaml
2.在微服务读取配置
新建一个属性读取类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxAmount;
}
使用配置类

微服务保护
雪崩
整个微服务群中与购物车服务、商品服务等有调用关系的服务可能都会出现问题,最终导致整个集群不可用。

微服务保护的方案有很多,比如:
-
请求限流

-
线程隔离

如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。
-
服务熔断

编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
微服务整合
引入sentinel依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置控制台
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
http-method-specify: true # 开启请求方式前缀
请求限流
把查询购物车列表这个簇点资源的流量限制在了每秒6个,也就是最大QPS为6。1s发10次请求,只有6次会被接收,其他4次直接被拒绝
线程隔离

添加线程隔离
分配5个线程给get请求,避免请求的线程就沾满tomcat最大的线程,导致其他功能阻塞

服务熔断

利用线程隔离有两个缺点:
第一,超出的QPS上限的请求就只能抛出异常,从而导致购物车的查询失败。但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。也就是给查询失败设置一个降级处理逻辑。
第二,由于查询商品的延迟较高(模拟的500ms),从而导致查询购物车的响应时间也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断
编写降级逻辑
1.定义降级处理类,实现FallbackFactory
import com.hmall.api.client.ItemClient;
import com.hmall.api.dto.ItemDTO;
import com.hmall.api.dto.OrderDetailDTO;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.CollUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import java.util.Collection;
import java.util.List;
@Slf4j
public class ItemClientFallback implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient() {
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);
// 查询购物车允许失败,查询失败,返回空集合
return CollUtils.emptyList();
}
@Override
public void deductStock(List<OrderDetailDTO> items) {
// 库存扣减业务需要触发事务回滚,查询失败,抛出异常
throw new BizIllegalException(cause);
}
};
}
}
2.在配置类将ItemClientFallback注册为一个Bean
3.使用ItemClientFallbackFactory
实现服务熔断

-
closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
-
open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
-
half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
-
请求成功:则切换到closed状态
-
请求失败:则切换到open状态
-

分布式事务
Seata

-
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
-
TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
-
RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
微服务集成Seata
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
Seata的XA模型

RM一阶段的工作:
-
注册分支事务到
TC -
执行分支业务sql但不提交
-
报告执行状态到
TC
TC二阶段的工作:
-
TC检测各分支事务执行状态-
如果都成功,通知所有RM提交事务
-
如果有失败,通知所有RM回滚事务
-
RM二阶段的工作:
-
接收
TC指令,提交或回滚事务
优缺点
XA模式的优点是什么?
-
事务的强一致性,满足ACID原则
-
常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
-
因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
-
依赖关系型数据库实现事务
实现步骤
1.Nacos中的共享shared-seata.yaml配置文件中设置
seata:
data-source-proxy-mode: XA
2.利用@GlobalTransactional标记分布式事务的入口方法,给事务的参与者添加@Transactional
AT模式

阶段一RM的工作:
-
注册分支事务
-
记录undo-log(数据快照)
-
执行业务sql并提交
-
报告事务状态
阶段二提交时RM的工作:
-
删除undo-log即可
阶段二回滚时RM的工作:
-
根据undo-log恢复数据到更新前
AT与XA的区别
-
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。 -
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。 -
XA模式强一致;AT模式最终一致
可见,AT模式使用起来更加简单,无业务侵入,性能更好。因此企业90%的分布式事务都可以用AT模式来解决。
RabbitMQ
入门
原项目:
第一,拓展性差
第二,性能下降
由于我们采用了同步调用,调用者需要等待服务提供者执行完返回结果后,才能继续向下执行,也就是说每次远程调用,调用者都是阻塞等待状态。最终整个业务的响应时长就是每次远程调用的执行时长之和:
第三,级联失败
由于我们是基于OpenFeign调用交易服务、通知服务。当交易服务、通知服务出现故障时,整个事务都会回滚,交易失败。
基本介绍
类似于darabase,隔离不同的项目

快速入门
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置mq地址
spring:
rabbitmq:
host: 192.168.150.101 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
消息发送
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
消息接收
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
// 利用RabbitListener来声明要监听的队列信息
// 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。
// 可以看到方法体中接收的就是消息体的内容
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
WorkQueues模型
Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。

50个消息,一个消费者消费快,一个消费慢,但还是每个25
默认情况下消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。导致1个消费者空闲,另一个消费者忙的不可开交。
能者多劳
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
处理快的拿的多,慢的拿的少
Fanout交换机

交换机把消息发送给绑定过的所有队列
与直接向队列发消息不同的是,改为向交换机发消息
@Test
public void testFanoutExchange() {
// 交换机名称
String exchangeName = "hmall.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, null, message);
}
Direct交换机

@Test
public void testFanoutExchange() {
// 交换机名称
String exchangeName = "hmall.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, blue, message);
}
Topic交换机

声明队列和交换机

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
//声明交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("hmall.fanout");
}
//第1个队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
//绑定队列和交换机
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
// 第2个队列
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
//绑定队列和交换机
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
基于注解声明
基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),//消息队列的名称
exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),//交换机的名称
key = {"red", "blue"}//绑定关系
))
public void listenDirectQueue1(String msg){
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
//运行后会自动生成消息队列和交换机
消息转换器
原始模式下,发送一个自定义对象消息格式非常不友好。

改造:
引入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
配置消息转换器,在publisher和consumer两个服务的启动类中添加一个Bean即可:
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
高级
发送者的可靠性
生产者重试机制
为了解决生产与MQ的连接中断,SpringAMQP提供的消息发送时的重试机制。即:当RabbitTemplate与MQ连接超时后,多次重试。
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
生产者确认机制

-
当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功
-
临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
-
持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功
-
其它情况都会返回NACK,告知投递失败
开启生产者确认
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
这里publisher-confirm-type有三种模式可选:
-
none:关闭confirm机制 -
simple:同步阻塞等待MQ的回执 -
correlated:MQ异步回调返回回执
ConfirmCallback
-
临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
-
持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功
ReturnCallback
当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功
MQ的可靠性
数据持久化
消息到达MQ以后,MQ会把数据保存到内存中,如果MQ宕机,会导致消息丢失
内存有限,如果消费者故障,会导致消息挤压,导致mq堵塞
为了保证数据的可靠性,必须配置数据持久化,包括:
-
交换机持久化(交换机持久化是指将交换机(包括名称、类型、路由规则、绑定关系等配置信息)持久化到磁盘存储中,确保即使服务重启,交换机的配置也能被恢复,无需重新创建和配置。)
-
队列持久化(队列中的数据通常存储在内存,队列持久化通过将队列中的消息从内存写入持久化存储)
-
消息持久化(如果是持久化的消息,消息会自动存入磁盘,如果是非持久化的消息,信息队列满了才会存入磁盘)
交换机持久化
LazyQueue
-
接收到消息后直接存入磁盘而非内存
-
消费者要消费消息时才会从磁盘中读取并加载到内存(也就是懒加载)
消费者的可靠性
消费者确认机制

通过下面的配置可以修改SpringAMQP的ACK处理方式:
由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:
-
none:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用 -
manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,但更灵活 -
auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack. 当业务出现异常时,根据异常判断返回不同结果:-
如果是业务异常,会自动返回
nack; -
如果是消息处理或校验异常,自动返回
reject;
-
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none # 不做处理
失败重试机制
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。
消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
失败处理策略
在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,显然不太合适了。
因此Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery接口来定义的,它有3个不同实现:
-
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式 -
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队 -
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
业务幂等性
在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的。例如:
-
根据id删除数据
-
查询数据
但数据的更新往往不是幂等的,如果重复执行可能造成不一样的后果。比如:
-
取消订单,恢复库存的业务。如果多次恢复就会出现库存重复增加的情况
-
退款业务。重复退款对商家而言会有经济损失。
因此,我们必须想办法保证消息处理的幂等性。这里给出两种方案:
-
唯一消息ID
-
每一条消息都生成一个唯一的id,与消息一起投递给消费者。
-
消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
-
如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
我们该如何给消息添加唯一ID呢?
其实很简单,SpringAMQP的MessageConverter自带了MessageID的功能,我们只要开启这个功能即可。
以Jackson的消息转换器为例:
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}
缺点:要查数据库,业务侵入性
业务状态判断
类似于乐观锁
兜底方案
既然MQ通知不一定发送到交易服务,那么交易服务就必须自己主动去查询支付状态。这样即便支付服务的MQ通知失败,我们依然能通过主动查询来保证订单状态的一致。
延迟消息

死信交换机

DelayExchange插件(延迟消息插件)
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}", msg);
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class DelayExchangeConfig {
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct") // 指定交换机类型和名称
.delayed() // 设置delay的属性为true
.durable(true) // 持久化
.build();
}
@Bean
public Queue delayedQueue(){
return new Queue("delay.queue");
}
@Bean
public Binding delayQueueBinding(){
return BindingBuilder.bind(delayedQueue()).to(delayExchange()).with("delay");
}
}
更多推荐




所有评论(0)