《谷粒商城》开发记录 11:消息队列和分布式事务

一、消息队列

1 使用场景

消息队列通常有三个使用场景:
● 异步:对于页面请求,只需要将消息发送给消息队列,就可以立即返回。
● 解耦:消息的发送方和接收方可以是两个毫不相干的系统。
● 流量控制(削峰):消息接收方可以自定义接收消息的规则,在大流量下保证最后落到数据库的请求数不超出数据库的处理能力。

2 基本概念

● AMQP:Advanced Message Queuing Protocol,一个提供统一消息服务的应用层高级消息队列协议,基于此协议的客户端与消息中间件之间可以传递消息。RabbitMQ的实现是基于AMQP协议的。
● Message:消息。由消息头和消息体组成。消息头包含了路由键、优先级、传输模式等内容。消息体是不透明的。
● Publisher:生产者。向消息队列推送消息的客户端应用程序。
● Consumer:消费者。从消息队列中获取消息的客户端应用程序。
● Broker:经纪人。消息队列服务器的整体。
    ● Virtual host:虚拟主机。Broker中可以单独完成消息队列功能的单位,包含一批交换器和队列。
        ● Exchange:交换器。负责接收生产者发送的消息,再按照一定的路由规则将这些消息路由给队列。
        ● Queue:队列。从交换器获得消息、保存消息,等消息被消费者处理后,删除消息。
        ● Binding:绑定。基于路由键,将交换器和队列连接起来的路由规则。
● Connection:连接。网络连接。
    ● Channel:信道。多路复用连接中的一条独立的双向数据流通道,是建立在真实的TCP连接内的虚拟连接。不管是发布消息、订阅队列还是接收消息,都通过信道完成。

3 交换器的路由规则

交换器共有四种类型:direct、fanout、topic、headers。
● direct:交换器将消息发送给绑定的、队列名与路由键(routing key)完全一致的单个队列。
● fanout:交换器将消息发送给绑定的每个队列。
● topic:交换器将消息发送给绑定的、队列名与路由键匹配的队列。队列名由若干个单词组成,单词之间使用"."隔开。路由键可以使用两个通配符:"#"和"*","#"匹配0个或多个单词,"*"匹配一个单词。
● headers:在匹配规则上与direct相似,但是性能差很多,目前基本上不用了。

4 RabbitMQ

4.1 整合Spring Boot

1. 引入依赖。
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-amqp
2. 在配置文件中添加配置。
    spring.rabbitmq.host=192.168.56.10
    spring.rabbitmq.port=5672
    spring.rabbitmq.virtual-host=/
3. 在服务启动类上添加@EnableRabbit注解。
4. 在虚拟机上运行RabbitMQ。
    启动服务。
    访问http://192.168.56.10:15672,可以在网页上监控RabbitMQ的情况(默认账号密码都是guest)。
    在网页上可以创建交换器、队列、绑定等,还可以模拟发消息。

4.2 API

4.2.1 声明

依赖注入:
    @Autowired
    AmqpAdmin amqpAdmin;

使用amqpAdmin做声明:
● 声明(Direct)交换器。
    DirectExchange directExchange = new DirectExchange("hello-java-exchange", true, false);
    amqpAdmin.declareExchange(directExchange);
● 声明队列。
    Queue queue = new Queue("hello-java-queue", true, false, false);
    amqpAdmin.declareQueue(queue);
● 声明绑定。
    Binding binding = new Binding("hello-java-queue", Binding.DestinationType.QUEUE, "hello-java-exchange", "hello.java", null);
    amqpAdmin.declareBinding(binding);

可用构造器:
● (Direct)交换器构造器。
    (name: 名称; durable: 是否持久化; autoDelete: 是否自动删除; arguments: 实参)
    public DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments);
    public DirectExchange(String name, boolean durable, boolean autoDelete);
    public DirectExchange(String name);
● 队列构造器。
    (name: 名称; durable: 是否持久化; exclusive: 是否排他; autoDelete: 是否自动删除; arguments: 实参)
    public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments);
    public Queue(String name, boolean durable, boolean exclusive, boolean autoDelet);
    public Queue(String name, boolean durable);
    public Queue(String name);
● 绑定构造器。
    (destination: 目的地; destinationType: 目的地类型; exchange: 交换器; routingKey: 路由键; arguments: 实参)
    public Binding(String destination, DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments);

4.2.2 发布消息

依赖注入:
    @Autowired
    RabbitTemplate rabbitTemplate;

发布消息:
    rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", "hello world");
    三个参数从左到右分别是交换器、路由键、消息。
    消息可以是任意类型的对象,只要该类实现了序列化接口Serializable。

如果想使用JSON格式序列化消息,可以创建一个自定义的config文件:
    @Configuration
    public class MyRabbitMQConfig{
        @Bean
        public MessageConverter messageConverter(){
            return new Jackson2JsonMessageConverter();
        }
    }
    原理是向容器中注入一个Jackson2JsonMessageConverter类型的转换器,当对消息进行序列化时,使用这个转换器,而不是默认的序列化转换器。

4.2.3 监听消息

消费者可以使用@RabbitListener和@RabbitHandler注解来监听消息。
● 单独使用@RabbitListener。
    public class MyConsumer{
        @RabbitListener(queues = {"hello-java-queue"})
        public void receiveMessage(Object content){
            System.out.println(content);
        }
    }
● @RabbitListener和@RabbitHandler搭配使用,监听多种类型的消息分别处理。
    @RabbitListener(queues = {"hello-java-queue"})
    public class MyConsumer{
        @RabbitHandler
        public void receiveMessageA(A content){
            System.out.println(content);
        }
        @RabbitHandler
        public void receiveMessageB(B content){
            System.out.println(content);
        }
    }

4.3 消息可靠投递

消息队列为了保证消息能够被正确处理,引入了多种消息确认机制:
● 保证交换器收到生产者的消息——确认回调。
● 保证消息被队列成功接收——退回回调。
● 保证消息被消费者正确消费——Ack机制。

4.3.1 确认回调

Broker成功收到消息时,执行确认回调方法。
1. 在配置文件中添加配置,开启确认回调机制。
    spring.rabbitmq.publisher-confirms=true
2. 在配置类MyRabbitMQConfig中添加初始化RabbitTemplate的方法。
    @PostConstruct  // MyRabbitMQConfig对象创建完成后,执行该方法
    public void initRabbitTemplate(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause){
                // correlationData: 当前消息的唯一标识; ack: 确认; cause: 异常
                // TODO
            }
        });
    }

4.3.2 退回回调

消息没有成功抵达队列时,执行退回回调方法。
1. 在配置文件中添加配置,开启退回回调机制。
    spring.rabbitmq.publisher-returns=true
    # 消息没有被队列接收时,强制退回
    spring.rabbitmq.template.mandatory=true
2. 在配置类中的初始化RabbitTemplate的方法中,设置退回回调。
    rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
        @Override
        public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey){
            // message: 投递失败的消息; replyCode: 返回的状态码; replyText: 返回的文本信息; exchange: 来源交换器; routingKey: 路由键
            // TODO
        }
    });

4.3.3 Ack机制

消费者处理消息后,可以回复ack给Broker。Broker在收到ack之后才会从队列中删除这条消息。
消费者也可以回复nack给Broker,指定Broker是否丢弃此消息。
1. 默认情况下,只要消息被消费者收到,就会从队列中删除这条消息。这么做的问题是,消费者接收到消息后,消息不一定会被正确处理,所以我们要开启manual模式,根据业务逻辑决定是否删除消息。
    spring.rabbitmq.listener.simple.acknowledge-mode=manual
2. 回复ack或者nack。
    @RabbitHandler
    public void receiveMessage(Message message, T content, Channel channel){
        MessageProperties properties = message.getMessageProperties();
        long deliveryTag = properties.getDeliveryTag();  // 获取当前投递的唯一标识
        try{
            if(true){
                channel.basicAck(deliveryTag, false);  // 批量处理: false
            } else{
                channel.basicNack(deliveryTag, false, true);  // 批量处理: false; 消息重新入队: true
            }
        } catch(Exception e){
            e.printstacktrace();
        }
    }

4.4 消息可靠性问题

4.4.1 消息丢失

场景1:消息发出后,由于网络问题没有抵达服务器。
场景2:消息抵达Broker后,Broker宕机。
场景3:自动ack情况下,消费者收到消息后宕机。
解决方案:
● 开启确认回调机制。
    在数据库中记录发送者发出的每条消息,收到确认ack后,变更消息状态为成功。定期到数据库扫描未成功的消息进行重发。
    create table 'mq_message'(
        'message_id' char(32) not null,
        'content' text,
        'to_exchange' varchar(255) default null,
        'routing_key' varchar(255) default null,
        'class_type' varchar(255) default null,
        'message_status' int(1) default '0' comment '0-新建 1-已发送 2-错误抵达 3-已抵达',
        'create_time' datetime default null,
        'update_time' datetime default null,
        primary key('message_id')
    ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
● 开启手动ack,等消费者消费消息成功后再删除消息。

4.4.2 消息积压

场景1:消费者宕机.
场景2:发送者发送流量太大。
解决方案:
● 上线更多的消费者。
● 先将消息记录到数据库中,之后再离线慢慢处理。
4.4.3 消息重复
场景:消息消费成功,事务已提交,但消费者回复ack失败,Broker再一次发送消息。
解决方案:
● 使用防重表,每一条消息都有业务的唯一标识,消费消息之前先查防重表,如果消息已被处理过,就不再处理。
● 标识消息是否不是第一次投递过来的,比如RabbitMQ每一条消息的的redelivered字段。

5 延时队列

5.1 消息的TTL

TTL,Time To Live,存活时间。
RabbitMQ可以对队列和消息分别设置TTL,超时未被消费的消息称为死信。

5.2 死信路由

死信路由Dead Letter Exchange实际上就是一个普通的交换器,只是接收了来自其他队列或者交换器的死信。

5.3 延时队列的实现

1. 我们可以创建一个没有任何消费者的队列,每条发送到该队列的消息,过一段时间后都会过期,成为死信。
2. 我们可以控制将死信发送给死信路由,死信路由再将死信发送到某个指定的交换器,然后死信被发送到某个队列,再被消费。
这样就实现了一个延时队列。

二、分布式事务

1 数据库事务

1.1 事务的ACID特性

● 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间的某个环节。当事务在执行过程中发生错误时,数据库会被恢复(回滚)到事务开始前的状态,就像这个事务从来没有执行过一样。
● 一致性(Consistency):在事务开始之前和事务结束以后,数据库中的数据符合业务逻辑。
● 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据不一致。
● 持久性(Durability):事务处理结束后,对数据的修改是永久的,即便系统故障也不会丢失。

1.2 事务的隔离级别

● 读未提交(READ UNCOMMITTED):在一个事务的执行过程中,能读取到其他事务修改过,但还没有提交的数据。
    脏读:一个事务读到其他事务修改过的数据后,其他事务又撤销了修改。
● 读已提交(READ COMMITTED):在一个事务的执行过程中,能读取到其他事务在该事务开始后提交的数据。
    不可重复读:一个事务读到某条数据后,其他事务修改了这条数据并提交了修改,该事务再去读这条数据,两次读的结果不一致。
● 可重复读(REPEATABLE READ):在一个事务的执行过程中,读取的所有数据不允许被其他事务修改。
    幻读:一个事务按照一定的查询条件做查询得到了一个结果集,其他事务新增了几条符合查询条件的数据,该事务再次按照之前的查询条件做查询,得到的结果集多了几条数据,好像出现了幻觉。
● 序列化(SERIALIZABLE):所有事务以串行的方式逐个执行,能避免一切数据不一致的情况。

1.3 事务的传播行为

● PROPAGATION_REQUIRED:必需。如果当前存在事务,就加入,如果当前没有事务,创建新事务。
● PROPAGATION_SUPPORTS:支持。如果当前存在事务,就加入,如果当前没有事务,以非事务方式执行。
● PROPAGATION_MANDATORY:强制。如果当前存在事务,就加入,如果当前没有事务,抛出异常。
● PROPAGATION_REQUIRES_NEW:必需-新建。无论当前是否存在事务,都创建新事务。
● PROPAGATION_NOT_SUPPORTED:不支持。以非事务方式执行,如果当前存在事务,把当前事务挂起。
● PROPAGATION_NEVER:从不。以非事务方式执行,如果当前存在事务,抛出异常。
● PROPAGATION_NESTED:嵌套。如果当前存在事务,就嵌套在事务内执行(如果执行失败,不影响外部事务),如果当前没有事务,创建新事务。

1.4 本地事务失效问题

同一个类中的两个带事务的方法相互调用时,被调用方法的事务设置会失效。原因是:事务是由对象控制的,出现上述情况时,调用两个方法的对象是同一个,两个方法的事务也只会是同一个。
为解决这个问题,可以使用代理对象来控制事务:
1. 引入依赖。
    groupId: org.springframework.boot
    artifactId: spring-boot-starter-aop
2. 在服务启动类上添加@EnableAspectJAutoProxy(exposeProxy=true)注解,开启aspectj动态代理功能。
3. 在同一个类(TestServiceImpl)中,一个带事务的方法调用另一个带事务的方法(anotherMethod)时,创建一个代理对象来调用。
    TestServiceImpl testService = (TestServiceImpl) AopContext.currentProxy();
    testService.anotherMethod();

2 分布式

2.1 CAP定理

在一个分布式系统中,一致性、可用性、分区容错性三个要素最多只能同时实现两个,不可能三者兼顾。
● 一致性(Consistency):在分布式系统中一条数据的所有备份,在同一时刻的值都是一样的。
● 可用性(Availability):集群可以响应客户端的读写请求,即使是在集群的一部分节点发生故障后。
● 分区容错性(Partition tolerance):大多数分布式系统分布在多个子网络,每个子网络叫作一个区。分区容错意味着:即使区间通信失败也不影响集群的使用。

2.2 BASE理论

一般来说,区间通信失败是无法避免的,因此必须保证分区容错性。因此我们必须在一致性或可用性上有所让步。
BASE理论是对CAP定理的延伸,思想是:如果无法做到高可用,做到基本可用也是可以的,如果无法做到强一致,做到最终一致也是可以的。
BASE指的是:
● 基本可用(Basically Available):当分布式系统出现故障的时候,允许损失部分可用性。比如响应时间增加、功能降级等。
● 软状态(Soft State):允许系统存在中间状态,该中间状态不会影响系统整体的可用性。
● 最终一致性(Eventual Consistency):系统中的所有数据副本经过一定的时间后,最终能够达到一致的状态。

2.3 领导选举机制

https://blog.csdn.net/qq_42082161/article/details/113555895

3 分布式事务

3.1 本地事务的局限性

一个事务的所有操作,都是在同一个连接中执行的。
本地事务对外部服务没有控制权。因此在远程调用外部服务后,即使业务执行失败,本地事务也不能要求外部服务回滚。

3.2 分布式事务过程

分布式事务包含3个核心组件:
● Transaction Coordinator(TC):事务协调器。维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
● Transaction Manager(TM):事务管理器。控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
● Resource Manager(RM):资源管理器。控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

一个典型的(分布式)事务过程包括:
1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
2. XID在微服务调用链路的上下文中传播。
3. RM向TC注册分支事务,将其纳入XID对应的全局事务的管辖。
4. TM向TC发起针对XID的全局提交或回滚决议。
5. TC调度XID下管辖的全部分支事务完成提交或回滚请求。

3.3 分布式事务模式

https://zhuanlan.zhihu.com/p/78599954
● AT模式。
    1. 执行阶段
        代理JDBC数据源,解析业务SQL,生成更新前后的镜像数据,形成UNDO LOG。
        向TC注册分支。
        把业务数据的更新和UNDO LOG放在同一个本地事务中提交。
    2. 完成阶段
        全局提交:收到TC的分支提交请求,异步删除相应分支的UNDO LOG。
        全局回滚:收到TC的分支回滚请求,查询分支对应的UNDO LOG记录,生成补偿回滚的SQL语句,执行分支回滚并返回结果给TC。
● TCC模式。
    1. 执行阶段
        向TC注册分支。
        执行业务定义的Try方法。
        向TC上报Try方法执行情况:成功或失败。
    2. 完成阶段
        全局提交:收到TC的分支提交请求,执行业务定义的Confirm方法。
        全局回滚:收到TC的分支回滚请求,执行业务定义的Cancel方法。
● Saga模式。
    1. 执行阶段
        向TC注册分支。
        执行业务方法。
        向TC上报业务方法执行情况:成功或失败。
    2. 完成阶段
        全局提交:RM不需要处理。
        全局回滚:收到TC的分支回滚请求,执行业务定义的补偿回滚方法。
● XA模式。
    1. 执行阶段
        向TC注册分支。
        XA Start,执行业务SQL,XA End。
        XA prepare,并向TC上报XA分支的执行情况:成功或失败。
    2. 完成阶段
        收到TC的分支提交请求,XA Commit。
        收到TC的分支回滚请求,XA Rollback。

4 Seata

Seata是一款开源的分布式事务解决方案。

4.1 安装事务协调器TC

1. 安装事务协调器TC。
    下载链接:https://github.com/seata/seata/releases/
2. 配置registry.conf。
    配置registry的type为"nacos"。
3. 将file.conf和registry.conf两个文件复制到使用到分布式事务的服务的resources目录下。
4. 配置使用到分布式事务的服务中的file.conf。
    将 vgroup_mapping.my_test_tx_group = "default"
    修改为 vgroup_mapping.【服务名】-fescar-service-group = "default"。
5. 启动Nacos后,启动seata-server。
    运行seata-server.bat。

4.2 整合Spring Boot

1. 引入依赖。
    groupId: com.alibaba.cloud
    artifactId: spring-cloud-starter-alibaba-seata
2. 使用Seata DataSourceProxy代理数据源。
    @Configuration
    public class MySeataConfig{
        @Autowired
        DataSourceProperties dataSourceProperties;
        @Bean
        public DataSource dataSource(DataSourceProperties dataSourceProperties){
            HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
            if(StringUtils.hasText(dataSourceProperties.getName())){
                dataSource.setPoolName(dataSourceProperties.getName());
            }
            return new DataSourceProxy(dataSource);
        }
    }
3. 在使用到分布式事务的业务方法入口添加@GlobalTransactional注解。
    这个注解与原本的@Transactional注解不冲突。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值