谷粒商城二十一订单服务分布式事务

库存锁定

// 提交订单
@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();

        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        confirmVoThreadLocal.set(vo);

        // 1. 验证令牌【令牌的对比和删除必须保证原子性】
        // 如果redis调用get方法获取key的值,get的值等于ARGV(传过来的值),就会删除这个值,否则返回0
        // 脚本最终返回值 0 令牌失败(没有该key,不等于,删除失败都是失败) 1 删除成功(整个流程都是成功的)
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();

        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);

        if (result == 0L){
            // 令牌验证失败
            response.setCode(1);
            return response;
        }else {
            // 创建订单,验令牌,验价格,锁库存。。。
            // 令牌验证成功

            // 1. 创建订单、订单项等信息
            OrderCreateTo order = createOrder();
            // 2. 验价(其实不必要验价我感觉,如果后台更新价格,购物车页面和订单页面同时刷新一下就可以了)
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();
            if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
                // 金额对比

                // 3. 保存订单
                saveOrder(order);

                // 4. 库存锁定。只要有异常回滚订单数据。
                // 订单号,所有订单项(skuId,skuName,num)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());

                List<OrderItemVo> locks = order.getItems().stream().map(item -> {
                    OrderItemVo itemVo = new OrderItemVo();
                    itemVo.setSkuId(item.getSkuId());
                    itemVo.setCount(item.getSkuQuantity());
                    itemVo.setTitle(item.getSkuName());
                    return itemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(locks);

                // todo 远程锁库存
                R r = wmsFeignService.orderLockStock(lockVo);

                if (r.getCode() == 0){
                    // 库存锁定成功
                    response.setOrder(order.getOrder());
                    response.setCode(0);
//                    int i = 1/0;
                    return response;
                }else {
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                }

            }else {
                response.setCode(2);
                return response;
            }

        }


    }




// 锁定库存
@Transactional(rollbackFor = NoStockException.class)
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {

        /**
         * 保存库存工作单的详情。
         * 追溯
         */
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(vo.getOrderSn());

        wareOrderTaskService.save(taskEntity);






        // 1. 理论上,因为有多个仓库可能都会有这个sku,所以我们要找到一个就近仓库,锁定库存

        // 1. 我们在这儿用最简单的,
        // 找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();

        List<SkuWareHasStock> collect = locks.stream().map(item -> {

            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());

            // 查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());

        Boolean allLock = true;
        // 2. 锁定库存
        for (SkuWareHasStock hasStock : collect) {

            Boolean skuStocked = false;

            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();

            if (wareIds == null || wareIds.size() == 0){
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }

            // 1. 如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给mq
            // 2. 锁定失败,前面保存的工作单信息就回滚了。
            //      发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
            //
            for (Long wareId:wareIds){
                // 成功就返回1,否则就是0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1){

                    skuStocked = true;

                    break;
                }else {
                    // 当前仓库锁定失败,重试下一个仓库
                }
            }


            if (skuStocked == false){
                // 当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }


        }

        // 3. 能走到这一步,肯定全部都是锁定成功的,
        // 上面有任何问题的,我们都抛异常了

        return true;
    }

举个例子,订单服务的执行流程是,订单服务的逻辑、远程调用库存服务、远程调用优惠券服务。

事务不会出问题

  1. 假设订单服务出异常,代码都执行不到仓储服务锁库存、优惠券服务扣积分的远程调用步骤,不存在整体回滚的问题,事务不会出问题。

  2. 如果是远程调用的库存服务出异常,首先库存服务自己会回滚,订单服务会在库存服务调用完成之后获取返回的锁库存的结果,得知发生了异常,订单服务也会抛异常,那么订单服务也会回滚。

事务会出问题

  1. 假设我们订单服务是成功的,在调用库存服务的时候,出现了假失败的情况,例如服务器故障、超时等原因,库存锁成功并且库存服务事务提交了之后,一直没有给订单服务返回,远程调用有一个超时机制,远程调用会抛一个类似readTimeout的异常,就会出现一个问题,库存锁成功了,订单却回滚了

  2. 如果远程调用库存服务成功,远程调用优惠券服务出异常,那么优惠券服务会自动回滚,订单服务获取到异常信息也会自动回滚,但是库存服务感知不到异常就不会回滚

  3. 如果订单服务在成功调用各远程服务,最后在整合结果的时候出现异常,那么订单服务会回滚,远程服务则不会回滚

在这里插入图片描述

本地事务

数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)
和持久性(Durabilily),简称就是 ACID;

  • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败

  • 一致性:数据在事务的前后,业务整体一致。
    转账。A:1000;B:1000; 转 200 事务成功; A:800 B:1200

  • 隔离性:事务之间互相隔离。
    例如100个人下单,就会有100个事务,有一个失败了,它的事务回滚不会影响其他事务。

  • 持久性:一旦事务成功,数据一定会落盘在数据库。

事务的隔离级别

  • READ UNCOMMITTED(读未提交)
    该隔离级别的事务会读到其它未提交事务的数据(那就是错误的数据),此现象也称之为脏读。

  • READ COMMITTED(读提交)
    一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。

  • REPEATABLE READ(可重复读)
    该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。

  • SERIALIZABLE(序列化)
    在该隔离级别下事务都是串行顺序执行的(也就没有并发能力了),MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

越大的隔离级别并发能力越弱。

事务的传播行为和@Transactional的坑

  1. PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置
  2. PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
  3. PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
  4. PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  6. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
/**
     *
     * 本地事务,在分布式系统,只能控制住自己的回滚,控制不了其它事务的回滚
     * 分布式事务,最大原因,网络问题
     *
     * isolation = Isolation.SERIALIZABLE 事务的隔离级别
     * propagation = Propagation.REQUIRED 事务的传播行为
     * timeout = 30 事务的超时时间,该事务30s没有执行成功就回滚
     *
     *
     * REQUIRED,REQUIRES_NEW
     *
     * 传播行为(bc两个小事务要不要和a共用一个事务):
     *      a,b,c方法都标注了@Transactional,a调用了bc,bc事务是否要与a共用一个事务
     *      如果此时b的传播行为是REQUIRED,c的传播行为是REQUIRES_NEW
     *          那么b与a共用一个事务,c新建一个事务
     *              此时如果a在调用bc之后发生了异常,则ab事务回滚,c不会回滚
     *
     *              因为ab共用了一个事务(也能说b加入了a的事务),
     * 					如果此时ab事务都设置了timeout,那么b的timeout将失效
     *
     *  springboot事务的坑(本地事务失效问题):
     *      如果abc方法都加了@Transactional,abc在同一个service类(同一个对象)中,a调用了bc,那么bc任何事务的设置都无效,都是和a共用一个事务。
     *
     *      因为事务是用代理对象来控制的,如果像上面那样调用方法相当于把bc方法的代码直接粘贴到了a方法中
     *          同一个对象内事务方法互调默认失效,原因 绕过了代理对象
     *
     *      解决:
     *          使用代理对象来调用事务方法
     *              1. 引入aop-stater模块,帮我们引入了aspectj
     *              2. @EnableAspectJAutoProxy(exposeProxy = true),开启aspectj动态代理功能。
     *
     *                  exposeProxy,对外暴露代理对象
     *
     *                  以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)
     *                  默认是使用jdk接口的动态代理
     *
     *               3. 本类对象用代理对象来调用
     *                  Object o = AopContext.currentProxy();
     *                  强转成我们的本类对象即可,然后就可以调用对象的方法
						
						如果没有本类方法互调的话就不需要使用这个
     *
     *
     *
     * @param vo
     * @return
     */

分布式事务

在这里插入图片描述

business(下单)远程调用库存(storage),保存订单(order),扣减积分(account),只有这三个步骤全部成功,我们的下订单才算成功。

如果是单体应用,我们将三处代码全部写在一个系统里面,而且我们全部连向的是一个数据库,这样的话我们使用本地事务就可以控制,只要有一个失败则全体回滚。

但是正是由于我们分布式系统的出现,由于我们业务太大,我们不可能将业务全写到一个系统里面,我们就拆分成了好多微服务,比如库存服务、订单服务、用户账户服务,而且每个服务还是连自己的数据库,操作自己的数据,还互相没有关系,
分布式系统之间部署还可能不在一块儿,库存服务在1号机器,订单服务在2号机器,用户账户服务在3号机器,这样我们想要完成下单逻辑,就需要远程调用这三个机器的各个方法,但由于我们分布式系统中经常会出现异常

机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失…

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。

分布式事务出现的原因,就是节点之间互相的状态不能同步,包括网络状况互相感知不到。

cap定理(cap和base都是理论)

CAP 原则又称 CAP 定理,指的是在一个分布式系统中

  1. 一致性(Consistency):
    在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

  2. 可用性(Availability)
    在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

  3. 分区容错性(Partition tolerance)
    多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。
    分区容错的意思是,区间(服务器之间)通信可能失败。

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

原因是:
123三台机器,我们让三个节点都保存a数据,这就满足了一致性

假设我们不是由于节点故障而是通信故障,13之间的网线断了,我们保存数据肯定只给某一个机器发了请求,1号机器让23同步,因为13之间网线断了,死活连不上,此时就发生了分区错误
分区错误之后,假设我们想让它满足可用性,我们让3号机器恢复,比如我们负载均衡去3号机器读数据,但由于3号之前通信故障,数据没有同步过来,读到的数据自然就不一样了,所以一满足可用性就发现又不一致了。想要满足一致性,就必须让3号机器不能访问,不能访问就不可用了。所以一致性和可用性只能二选一

在分布式系统中分区容错必须满足,因为网络肯定会出现问题;那么一致性与可用性就只能二选一,满足可用的话就得容忍业务能访问到不一致的数据,满足一致的话,就不能让整个集群可用了,如果让整个集群可用的话,数据就会不一致。

如何取舍

对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到99.99999%(N 个 9),即保证P 和 A,舍弃 C(一致性)。

base理论

是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性
BASE 是指

  • 基本可用(Basically Available)

    • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。

      • 响应时间上的损失:正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2 秒。

      • 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。

  • 软状态( Soft State)
    软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。

  • 最终一致性( Eventual Consistency)
    最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

  • 强一致性、弱一致性、最终一致性
    从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性

seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

AT(auto transaction)模式的 整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:
    提交异步化,非常快速地完成。
    回滚通过一阶段的回滚日志进行反向补偿。

AT模式不适用于高并发

AT模式下的事务会加很多锁,加锁之后相当于把并发变成了串行化,所有人都得等待上一个订单下完,再来下订单,那就不适用于高并发了。

下订单使用seata的AT模式适用的场景不适用高并发场景,是适合我们商品服务中的后台保存商品的场景,也远程调用了很多服务,同时并发量不是很大

而我们的下单是高并发场景。

术语

  • TC (Transaction Coordinator) - 事务协调者(协调器)
    维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器
    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

在这里插入图片描述
流程

  1. tm要执行下单业务,tm会告诉tc要开启一个全局事务,协调器就知道我们可能要跨服务开启一个全局的事务了
  2. 资源管理器告诉协调器这儿有一个分支事务,并且要实时汇报它的事务状态(无论是成功或者回滚协调器都知道)。每个资源管理器都需要向协调器实时汇报它的事务状态。
  3. 任意一个分支事务发生异常,tc会将所有资源管理器的事务回滚。

每个微服务都需要有一个undo_log表,用于自动补偿,例如一个事务已经提交了,但这时发生了异常,此时该事务已经无法回滚了。
在提交事务之前,会查询一下要执行事务涉及到的表数据,并记录在undo_log表中,如果发生了上面的情况,则将数据自动改回去,而不需要我们敲代码来执行这个操作。

使用seata DataSourceProxy代理自己的数据源

// DataSourceAutoConfiguration springboot默认的数据源配置
// Hikari 是 springboot默认的数据源
@Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, 
Generic.class, DataSourceJmxConfiguration.class})





// DataSourceConfiguration
// springboot将Hikari配置为数据源
@ConditionalOnMissingBean({DataSource.class}) // 容器中没有我们配置的数据源的时候,才会启动这段代码配置来配置Hikari来作为我们的数据源
static class Hikari {
        Hikari() {
        }

        @Bean
        @ConfigurationProperties(
            prefix = "spring.datasource.hikari"
        )
        HikariDataSource dataSource(DataSourceProperties properties) {
            HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(properties, HikariDataSource.class);
            if (StringUtils.hasText(properties.getName())) {
                dataSource.setPoolName(properties.getName());
            }

            return dataSource;
        }
    }




// DataSourceConfiguration
// 上面的createDataSource其实就是这么创建的
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
        return properties.initializeDataSourceBuilder().type(type).build();
    }






// 根据以上源码编写自己的数据源配置,
// ware服务也可以把这段代码直接粘到mybatis.config中
package com.atlinxi.gulimall.order.config;

import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;

@Configuration
public class MySeataConfig {

    @Autowired
    DataSourceProperties dataSourceProperties;


    /**
     * DataSourceAutoConfiguration
     *
     * @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) 只要有数据源,就开启数据源的自动配置
     * @ConditionalOnMissingBean(
     *     type = {"io.r2dbc.spi.ConnectionFactory"}
     * )
     *
     * 开启DataSourceProperties进行属性绑定,封装了数据源的属性,例如连接地址等
     * @EnableConfigurationProperties({DataSourceProperties.class})
     * @param dataSourceProperties
     * @return
     */
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder()
                .type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())){
            dataSource.setPoolName(dataSourceProperties.getName());
        }

        // 数据源交给seata
        return new DataSourceProxy(dataSource);
    }
}


部署、使用seata完整流程

package com.atlinxi.gulimall.order;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;


/**
 * 使用rabbitmq
 *
 * 1. 引入amqp依赖,RabbitAutoConfiguration就会自动生效
 * 2. 给容器中自动配置了
 *      RabbitTemplate,AmqpAdmin,CachingConnectionFactory,RabbitMessagingTemplate
 *
 *      所有属性都是 @ConfigurationProperties(
 *          prefix = "spring.rabbitmq"
 *          )
 *          public class RabbitProperties
 *
 * 3. 给配置文件中配置spring.rabbitmq.xxx
 * 4. @EnableRabbit,开启功能
 * 5. 监听消息,使用@RabbitListener,必须有@EnableRabbit
 *      @RabbitListener:类/方法上(监听哪些队列)
 *      @RabbitHandler:标在方法上(重载区分不同的消息)
 *          不同的消息实际上就是不同的实体类
 *
 *
 *
 *  seata控制分布式事务,
 *  1. 每一个微服务先必须创建undo_log(mysql的表)
 *  2. 安装事务协调器 https://github.com/seata/seata/releases
 *  3. 整合
 *      1. 导入依赖 spring-cloud-starter-alibaba-seata
 *          seata-all:1.3.0(这个版本和事务协调器的版本是一致的)
 *      2. 解压并启动seata-server
 *          registry.conf:注册中心配置
 *              将seata注册到nacos中
 *                  registry.type = "nacos"
 *                  config.type = "file"
 *      启动成功后在nacos的服务列表中就能看到seata-server
 *
 *      3. 所有想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源
 * 			 seata想要控制事务,自己默认的数据源必须让seata代理,seata才能控制事务
 * 
 *      4. 每个微服务,都必须导入registry.conf和file.conf(下面地址有具体内容)
 *              https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata/account-service
 *         registry.conf
 *              registry.type = nacos    seata的注册中心使用nacos
 *              config.type = file      seata的配置使用file
 *         file.conf(gulimall-order是微服务名,其他均为固定写法)
 *              每个事务都要注册到tc中,tc的名字就是当前应用的名字 + 固定写法
 *              service.vgroupMapping.gulimall-order-seata-service-group = "default"
 *
 *      5. 给分布式大事务的入口标注 @GlobalTransactional
 *          每一个远程的小事务用 @Transactional
 */
@EnableRabbit
@SpringBootApplication
@EnableDiscoveryClient
@EnableRedisHttpSession
@EnableFeignClients
@EnableAspectJAutoProxy(exposeProxy = true)
public class GulimallOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallOrderApplication.class, args);
    }

}

可靠消息 + 最终一致性方案

RabbitMQ延时队列(实现定时任务)

解锁库存的时间是40min是因为,关闭订单的时间是30min
在这里插入图片描述

  • 场景
    比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。

  • 常用解决方案:
    spring的 schedule 定时任务轮询数据库

  • 缺点:
    消耗系统内存、增加了数据库的压力、存在较大的时间误差
    定时任务每隔一段时间就要做一个全表扫描,就会增加数据库的压力。
    解决:rabbitmq的消息TTL和死信Exchange结合

在这里插入图片描述
定时任务设置的话肯定是每隔30min执行一次,不可能每下一个订单就设置一次定时任务,所以就会出现时效性的问题。

例如上面的订单,实际上59分钟才能被关闭。

消息的TTL(Time To Live)

消息的TTL就是消息的存活时间。

  • RabbitMQ可以对队列和消息分别设置TTL。

  • 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信

  • 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。
    这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x- message-ttl属性来设置时间,两者是一样的效果。

设置消息的目的就是TTL的时间内,消息没有被消费,就会被服务器认为是一个死信(也就是说消息就死亡了),将它丢弃。

Dead Letter Exchanges(DLX)死信

  • 一个消息在满足如下条件下,会进死信路由(消息被确认是死信后进入根据路由进入执行的交换机),记住这里是路由而不是队列,一个路由可以对应很多队列。(什么是死信)

    1. 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)requeue=false
    2. 上面的消息的TTL到了,消息过期了。
    3. 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
  • Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。

  • 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列

  • 手动ack&异常消息统一放在一个队列处理建议的两种方式

    • catch异常后,手动发送到指定队列,然后使用channel给rabbitmq确认消息已消费
    • 给Queue绑定死信队列,使用nack(requque为false)确认消息消费失败

延时队列实现方式

  • x-message-ttl:单位ms

  • x-dead-letter-exchange:指定死信交换机

  • x-dead-letter-routing-key-delay:指定路由键,交换机通过路由键将消息发送给指定的队列
    在这里插入图片描述
    在这里插入图片描述

  • 建议给队列设置过期时间,如果给消息设置过期时间,rabbitmq采用的是惰性检查机制(懒检查),假设队列中存了第一个消息5min过期,第二个消息1min过期,第三个消息1s过期,按照正常情况应该是第三个消息优先弹出队列,但服务器不是这么检查的,

  • 服务器从队列中拿第一个消息,发现5min过期,就放回去了,服务器5min以后再来拿,就会把第一个消息拿出来,第一个消息就过期了。

  • 我们第二、第三个消息需要等第一个消息5min过期以后才能过期,服务器拿到第一个消息以后,就来拿第二个消息,这时第二个消息不需要等1min,因为它发消息有一个时间,服务器一算早过期了,就将它拿出来(第三个消息也一样)。

    这就导致了第二三个消息得等第一个消息5min之后才能被拿到。

所以我们应该给整个队列设置一个过期时间(队列中的每个消息都是按收到的时间来计算这个过期时间的),这样整个队列所有的消息都是这个过期时间,服务器直接拿出来就行了。

延时队列实现 关闭订单和解锁库存

库存解锁的场景

以下都有个前提,就是下订单成功,

  1. 订单过期30min没有支付被系统自动取消

  2. 用户手动取消

  3. 库存锁定成功,订单接下来的业务调用失败,导致订单回滚,之前锁定的库存就要自动解锁

订单服务定时关闭订单

在这里插入图片描述
订单服务下单成功,就给rabbitmq发一个消息,路由键是order_delay,将消息发送给userr.order.delay.exchange,交换机按照路由键发送到user.order.delay.queue队列中,队列设置了如图三个参数,
并且这个队列没有人去消费,没有任何人从队列中拿数据,只要超过30min(图上的1min不作数),rabbitmq就会把消息从队列中拿出来,这个消息变成死信
然而我们这里设置将死信发送给user.order.exchange交换机,再通过order路由键发送给指定的队列,那么这个队列中就都是过期30min的消息,这个队列收到内容了就可以判断,订单只要没支付就可以关单。

在这里插入图片描述

和上面的相比实际上就是,利用路由代替了两个交换机,设计变的简单了,最终的结果和上面是一样的。

基于我们业务的消息队列设计,遵循每一个微服务有它自己的交换机,以当前微服务名-event-exchange为名感知当前微服务各种事件的交换机,交换机会绑定很多的队列。

// 创建exchange、queue、binding
package com.atlinxi.gulimall.order.config;

import com.atlinxi.gulimall.order.entity.OrderEntity;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class MyMQConfig {







    /**
     *
     * 使用api创建交换机和队列太麻烦了
     * spring中允许直接使用@Bean的方式,可以把Binding,Queue,Exchange放到容器中就会生效
     * spring会帮我们连接rabbitmq创建出来(没有的情况下)
     *
     * rabbitmq只要有,@Bean声明属性发生变化也不会覆盖
     *
     * @return
     */
    // 死信队列
    @Bean
    public Queue orderDelayQueue(){
        /**
         *
         * public Queue(String name, boolean durable, 持久化
         *              boolean exclusive, 排他(只允许一个connection连接)
         *      boolean autoDelete, 是否自动删除,如果是的话交换机在没有绑定任何东西的情况下就会自动删除
         *      @Nullable Map<String, Object> arguments) {
         *
         *
         * x-dead-letter-exchange: order-event-exchange
         * x-dead-letter-routing-key: order.release.order
         * x-message-ttl: 60000
         *
         *
         */

        Map<String,Object> arguments = new HashMap<>();

        arguments.put("x-dead-letter-exchange","order-event-exchange");
        arguments.put("x-dead-letter-routing-key","order.release.order");
        arguments.put("x-message-ttl",60000);



        Queue queue = new Queue("order.delay.queue",true,false,false,arguments);

        return queue;
    }


    @Bean
    public Queue orderReleaseOrderQueue(){

        // order.release.order.queue
        Queue queue = new Queue("order.release.order.queue",true,false,false);

        return queue;


    }


    @Bean
    public Exchange orderEventExchange(){


        // public TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        TopicExchange topicExchange = new TopicExchange("order-event-exchange",true,false);
        return topicExchange;

    }


    @Bean
    public Binding orderCreateOrder(){
        /**
         *
         * public Binding(String destination,  目的地
         *                Binding.DestinationType destinationType,
         *              String exchange, String routingKey, @Nullable Map<String, Object> arguments) {
         *
         *
         *
         */
        Binding binding = new Binding("order.delay.queue",Binding.DestinationType.QUEUE,"order-event-exchange",
                "order.create.order",null);

        return binding;
    }


    @Bean
    public Binding orderReleaseOrderBinding(){

        Binding binding = new Binding("order.release.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange",
                "order.release.order",null);

        return binding;

    }


    /**
     * 订单释放直接和库存释放进行绑定
     * @return
     */
    @Bean
    public Binding orderReleaseOtherBinding(){

        Binding binding = new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"order-event-exchange",
                "order.release.other.#",null);

        return binding;

    }

}

  1. 订单创建成功后,发送消息到rabbitmq延时队列,只要超过30min,就会按设定好的路由通过交换机到达订单队列

  2. 订单的状态只有待付款才需要关单,关闭订单的延时队列是30min,解锁库存的延时队列是50min,关闭订单和解锁库存就是这么联动的。

  3. 为了解决上述联动可能出现的问题,我们在关闭订单后,给解锁库存的消息队列发送消息,立即执行解锁库存。

问题

在这里插入图片描述

但是这样联动是有大问题的,假设订单创建成功,发消息的时候由于机器卡顿,网络慢等原因,订单创建成功没有及时的把消息发出去,
而此时库存服务的延时队列的消息到期了,这样就导致解锁库存的消息比关闭订单的消息先到期了,
解锁库存的服务收到消息要解锁库存,查询订单是否支付成功,而此时订单还是新建状态,就不解锁,消息同时被消费了,最后订单服务因为没有支付把订单关闭了,库存就永远得不到释放了。

解决
我们在关闭订单后,给解锁库存的消息队列发送消息,立即执行解锁库存。

库存服务定时解锁库存

package com.atlinxi.gulimall.ware.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class MyRabbitConfig {


    /**
     * rabbitmq在发送消息时,如果消息是实体类的话,就会进行序列化,我们是看不懂的
     *
     * 指定消息转换器为Jackson,就会帮我们转换成json
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }


//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void handle(Message message){
//
//    }



    @Bean
    public Exchange stockEventExchange(){


        // public TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        TopicExchange topicExchange = new TopicExchange("stock-event-exchange",true,false);
        return topicExchange;

    }




    @Bean
    public Queue stockReleaseStockQueue(){

        // order.release.order.queue
        Queue queue = new Queue("stock.release.stock.queue",true,false,false);

        return queue;


    }




    /**
     *
     * 使用api创建交换机和队列太麻烦了
     * spring中允许直接使用@Bean的方式,可以把Binding,Queue,Exchange放到容器中就会生效
     * spring会帮我们连接rabbitmq创建出来(没有的情况下)
     *
     * rabbitmq只要有,@Bean声明属性发生变化也不会覆盖
     *
     * @return
     */
    // 死信队列
    @Bean
    public Queue stockDelayQueue(){
        /**
         *
         * public Queue(String name, boolean durable, 持久化
         *              boolean exclusive, 排他(只允许一个connection连接)
         *      boolean autoDelete, 是否自动删除,如果是的话交换机在没有绑定任何东西的情况下就会自动删除
         *      @Nullable Map<String, Object> arguments) {
         *
         *
         * x-dead-letter-exchange: order-event-exchange
         * x-dead-letter-routing-key: order.release.order
         * x-message-ttl: 60000
         *
         *
         */

        Map<String,Object> arguments = new HashMap<>();

        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",120000);



        Queue queue = new Queue("stock.delay.queue",true,false,false,arguments);

        return queue;
    }




    @Bean
    public Binding stockReleaseBinding(){
        /**
         *
         * public Binding(String destination,  目的地
         *                Binding.DestinationType destinationType,
         *              String exchange, String routingKey, @Nullable Map<String, Object> arguments) {
         *
         *
         *
         */
        Binding binding = new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,
                "stock-event-exchange", "stock.release.#",null);

        return binding;
    }


    @Bean
    public Binding stockLockedBinding(){

        Binding binding = new Binding("stock.delay.queue",Binding.DestinationType.QUEUE,"stock-event-exchange",
                "stock.locked",null);

        return binding;

    }



}


这里需要引入库存工作单和库存工作单详情,库存工作单是订单的详情,库存工作单详情是每一个sku的id,购买数量,仓库id、锁定状态等。只要有一个商品没有锁定成功就抛出异常。

发送消息到延时队列

  1. 在下订单远程调用库存服务锁定库存的时候,库存服务先保存工作单(其实就是记录一下具体锁了哪些库存,回滚的时候需要)。

  2. 在该sku锁定库存成功的时候,rabbitmq将库存工作单详情保存并(sku在哪个仓库锁定了几件)发送给延时队列。此时库存工作单详情的状态为锁定
    如果此时锁定失败,数据库事务会回滚,但是我们消息仍然被发送出去,但是没关系,监听消息解锁的时候我们查不到工作单详情,就证明无需解锁。

监听消息,库存解锁队列(延时队列的消息到期后会进入该队列)

  1. 库存锁定成功,订单接下来的业务调用失败,导致订单回滚,之前锁定的库存就要自动解锁

  2. 库存服务锁库存本身失败。

如果锁库存本身失败的话,库存工作单就会回滚,所以在监听消息的时候,需要判断库存工作单是否存在,如果不存在则无需解锁(这种情况一定会抛异常的,订单服务也可以收到库存服务抛出的异常,订单服务自然也就回滚了)。

存在的话说明锁库存肯定是成功了,订单服务是否成功不知道。此时有两种情况,

  1. 没有订单,证明订单服务回滚了,需要解锁。
  2. 有订单,查询订单状态
    1. 已取消(手动取消和30min定时到期),解锁库存
    2. 只要不是取消状态,就不能解锁。

解锁库存的时候,需要判断库存工作单的状态,只有已锁定才可以解锁,已解锁和已扣减都不行(好像不会出现这种情况,属于小概率事件?)。

解锁库存的时候rabbitmq一定要手动消息确认,如果出现任何异常都需要拒绝并重新放入队列。

// 发送消息
@Transactional(rollbackFor = NoStockException.class)
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {

        /**
         * 保存库存工作单的详情。
         * 追溯
         */
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(vo.getOrderSn());

        wareOrderTaskService.save(taskEntity);






        // 1. 理论上,因为有多个仓库可能都会有这个sku,所以我们要找到一个就近仓库,锁定库存

        // 1. 我们在这儿用最简单的,
        // 找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();

        List<SkuWareHasStock> collect = locks.stream().map(item -> {

            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());

            // 查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());


        // 2. 锁定库存
        for (SkuWareHasStock hasStock : collect) {

            Boolean skuStocked = false;

            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();

            if (wareIds == null || wareIds.size() == 0){
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }

            // 1. 如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给mq
            // 2. 锁定失败,前面保存的工作单信息就回滚了。
            //      发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
            //
            for (Long wareId:wareIds){
                // 成功就返回1,否则就是0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1){

                    skuStocked = true;
                    // sku锁定成功,就没必要去锁定其他仓库了

                    // todo 告诉mq库存锁定成功
                    WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(),
                            wareId, 1);
                    wareOrderTaskDetailService.save(entity);

                    StockLockedTo stockLockedTo = new StockLockedTo();
                    stockLockedTo.setId(taskEntity.getId());
                    // 只发id不行,防止回滚以后找不到数据
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(entity,stockDetailTo);
                    stockLockedTo.setDetailTo(stockDetailTo);

                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",
                            stockLockedTo);
                    break;
                }else {
                    // 当前仓库锁定失败,重试下一个仓库
                }
            }


            if (skuStocked == false){
                // 当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }


        }

        // 3. 能走到这一步,肯定全部都是锁定成功的,
        // 上面有任何问题的,我们都抛异常了

        return true;
    }












// 监听消息
package com.atlinxi.gulimall.ware.listener;

import com.alibaba.fastjson.TypeReference;
import com.atlinxi.common.to.mq.OrderTo;
import com.atlinxi.common.to.mq.StockDetailTo;
import com.atlinxi.common.to.mq.StockLockedTo;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.ware.dao.WareSkuDao;
import com.atlinxi.gulimall.ware.entity.WareOrderTaskDetailEntity;
import com.atlinxi.gulimall.ware.entity.WareOrderTaskEntity;
import com.atlinxi.gulimall.ware.feign.OrderFeignService;
import com.atlinxi.gulimall.ware.service.WareOrderTaskDetailService;
import com.atlinxi.gulimall.ware.service.WareOrderTaskService;
import com.atlinxi.gulimall.ware.service.WareSkuService;
import com.atlinxi.gulimall.ware.vo.OrderVo;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {


    @Autowired
    WareSkuService wareSkuService;



    /**
     * 1. 库存自动解锁
     *
     *  下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
     *      之前锁定的库存就要解锁
     *
     *
     *  2. 订单失败
     *  锁库存失败,整体都会回滚,也就不存在工作单了
     *
     *
     *  只要解锁库存的消息失败,一定要告诉服务器解锁失败。
     *
     * @param to
     * @param message
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {

        System.out.println("收到解锁库存的消息。。。。。。");

        try {
            // 当前消息是否被第二次及以后(重新)派发过来的
            // 但是这样做太暴力了,万一是重试的呢
//            Boolean redelivered = message.getMessageProperties().getRedelivered();
            wareSkuService.unLockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }


    }







    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {

        System.out.println("订单关闭准备解锁库存。。。。。。");

        try {
        	// 这个service方法如果一切正常就手动确认消息
        	// 有任何问题抛异常就好,这边catch住然后拒绝消息

			// 在解锁库存的同时需要更新库存工作单的状态为已解锁
            wareSkuService.unLockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }


    }



}

解锁库存总流程图

在这里插入图片描述
在这里插入图片描述

远程调用没有携带登录信息被拦截

package com.atlinxi.gulimall.order.interceptor;

import com.atlinxi.common.constant.AuthServerConstant;
import com.atlinxi.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {


    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //

        //

        /**
         * 在库存服务解锁库存的时候需要远程调用订单服务查询订单状态,以此来判断该订单是否需要解锁库存,
         *      因为这是服务之间的请求,没有携带session,进入订单服务的时候会因为没有获取到登录信息而被拦截
         *      而我们在库存服务的时候已经判断了是否登录,所以这里是不需要判断登录的,放行就是了
         *
         * uri 就是类似于上面那个路径,url是完整的请求地址
         *
         * order/order/status/{orderSn}  orderSn是动态的
         *
         *
         */
        boolean match = new AntPathMatcher().match("/order/order/status/**", request.getRequestURI());

        if (match){
            return true;
        }


        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);

        if (attribute!=null){

            loginUser.set(attribute);

            return true;
        }else {
            // 没登录就去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return  false;
        }

    }
}

如何保证消息可靠性(消息不可靠的常见因素)

柔性事务-可靠消息+最终一致性方案(异步确保型)

最应该强调的就是可靠消息,因为任何不可靠的因素导致消息出现了一些问题故障,消息没发出去等等,我们可能会面临很大的损失,

如何保证消息可靠性-消息丢失

  • 消息发送出去,由于网络问题没有抵达服务器

    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制(一般情况网络中断短时间可能不会恢复),可记录到数据库,采用定期扫描重发的方式

    • 做好日志记录,每个消息都可以做好日志记录(给数据库保存每一个消息的详细信息),采用定期扫描重发

  • 消息抵达Broker,Broker要将消息写入磁盘(交给队列才是持久化)才算成功。此时Broker尚未持久化完成,宕机。

    • publisher也必须加入确认回调机制,如果消息失败,修改数据库消息状态,然后数据库扫描定期重发。
  • 自动ACK的状态下(消费者一拿到消息就自动ack了)。消费者收到消息,但没来得及消息然后宕机

    • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

总结

  1. 做好消息两端(publisher,consumer【手动ack】)确认,
  2. 每一个发送的消息都在数据库做好记录,定期将失败的消息再次发送

如何保证消息可靠性-消息重复(一个消息给消费者发了两遍)

  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者。
    这就导致了该业务执行了两遍。

  • 消息消费失败,由于重试机制,自动又将消息发送出去,这是允许的。

解决

  1. 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
    例如我们的解锁库存,在解锁的时候会判断库存工作单的状态,如果是已解锁就不会再次解锁了。

  2. 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理

  3. rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的。
    这个有bug,万一上一次消费失败,是重试的呢?

如何保证消息可靠性-消息积压

队列中如果存了太多的消息,肯定会影响mq性能。

  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大

解决

  1. 上线更多的消费者,进行正常消费

  2. 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

总结

如果我们想要使用柔性事务-可靠消息+最终一致性方案(异步确保性)来做分布式事务,我们一定要保证可靠消息,最核心的就是消息丢失,积压的话可以慢一些,重复我们可以写成幂等性。

最害怕的就是丢失,最终一致性方案就是要防止消息丢失(解决方案在上面已经陈述过了)。

我们发现,如果我们每一个业务每发一个消息都要这么写一段代码是很麻烦的,我们可以将消息写成一个微服务,需要的时候调用这个微服务即可。

别人家有位在MIT读博士的小孩对我说,他们没有讲义,也没有课本;老师上来讲一通,黑板上写一通,指定一批参考读物;几名教授讲课时互相批判,各有各的观点,各自推荐一些参考书指导你去读;然后你写,写文章不需要结果,只要思维逻辑是合适的,老师就给你打分了,这就是科学。老师讲的是他正在研究的前沿,他自己也未完全明白,科学是一代一代去努力的。博士只要有思想方法,不一定会有结果,有些科学太遥远,一步很难证明,过程正确也是好的。我们火花奖也不一定要有“结果”才行,否则怎么叫“青出于蓝而胜于蓝”呢?如果教育总是追求结果,学生思想就会被约束。所以,不能太实用主义,以需求为导向,牵引中国前进是不够的。教育应该放开让学生“胡思乱想”,只要他想的逻辑相吻合,就不要去约束他。读书是为了拿到开门的“钥匙”,关键是读一个方法学,运用知识的方法比知识更重要。因为知识可以在互联网上获取,怎么组合、怎么拼接,这就是大学要学习的。

https://baijiahao.baidu.com/s?id=1760664270073856317&wfr=spider&for=pc
擦亮花火、共创未来——任正非在“难题揭榜”花火奖座谈会上的讲话
任正非

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值