Grain Mall 21 Order Service Distributed Transaction

stock lock

// 提交订单
@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;
    }

For example, the execution process of the order service is the logic of the order service, the remote call to the inventory service, and the remote call to the coupon service.

business will not go wrong

  1. Assuming that the order service is abnormal, the code cannot execute the remote call steps of warehousing service lock inventory and coupon service deducting points. There is no overall rollback problem, and the transaction will not go wrong.

  2. If there is an exception in the inventory service called remotely, the inventory service will roll back first, and the order service will get the result of the lock inventory returned after the inventory service call is completed. Knowing that an exception has occurred, the order service will also throw an exception, then the order The service is also rolled back.

business will go wrong

  1. Assuming that our order service is successful, when calling the inventory service, there is a false failure, such as server failure, timeout, etc. After the inventory lock is successful and the inventory service transaction is submitted, the order service has not been returned. Remote call There is a timeout mechanism, and the remote call will throw an exception similar to readTimeout, and there will be a problem, 库存锁成功了,订单却回滚了.

  2. If the remote call to the inventory service is successful and the remote call to the coupon service is abnormal, the coupon service will automatically roll back, and the order service will automatically roll back when the abnormal information is obtained.但是库存服务感知不到异常就不会回滚

  3. If the order service calls each remote service successfully, and an exception occurs when the result is integrated, then订单服务会回滚,远程服务则不会回滚

insert image description here

local affairs

Several characteristics of database transactions: atomicity (Atomicity), consistency (Consistency), isolation or independence (Isolation)
and persistence (Durabilily), referred to as ACID;

  • Atomicity: A series of operations cannot be split as a whole, either succeed at the same time or fail at the same time

  • Consistency: The data is consistent before and after the transaction, and the business as a whole is consistent.
    transfer. A: 1000; B: 1000; transfer to 200, the transaction is successful; A: 800 B: 1200

  • Isolation: Transactions are isolated from each other.
    For example, if 100 people place an order, there will be 100 transactions. If one fails, its transaction rollback will not affect other transactions.

  • Persistence: Once the transaction is successful, the data will definitely be placed in the database.

transaction isolation level

  • READ UNCOMMITTED (read uncommitted)
    transactions at this isolation level will read data from other uncommitted transactions (that is, wrong data). This phenomenon is also called dirty reading.

  • READ COMMITTED (read commit)
    A transaction can read another committed transaction, and multiple reads will cause different results. This phenomenon is called non-repeatable read problem, the default isolation level of Oracle and SQL Server.

  • REPEATABLE READ (repeatable read)
    This isolation level is the default isolation level of MySQL. In the same transaction, the result of select is the state at the beginning of the transaction. Therefore, the results read by the same select operation will be consistent. However, there will be phantom reading phenomenon.

  • SERIALIZABLE (serialization)
    Under this isolation level, transactions are executed in serial order (there is no concurrency capability). The InnoDB engine of the MySQL database will implicitly add a shared read lock to the read operation, thereby avoiding dirty reads , Non-repeatable repetition and phantom reading problems.

The larger the isolation level, the weaker the concurrency capability.

Transaction propagation behavior and the pit of @Transactional

  1. PROPAGATION_REQUIRED: If there is no current transaction, create a new transaction, if there is a current transaction, join the transaction, 该设置是最常用的设置.
  2. PROPAGATION_SUPPORTS: Support the current transaction, if there is a transaction currently, join the transaction, if there is no transaction currently, execute it as a non-transaction.
  3. PROPAGATION_MANDATORY: Supports the current transaction. If there is a transaction, it will join the transaction. If there is no transaction, an exception will be thrown.
  4. PROPAGATION_REQUIRES_NEW: Create a new transaction, regardless of whether the current transaction exists or not, a new transaction will be created.
  5. PROPAGATION_NOT_SUPPORTED: Perform operations in a non-transactional manner. If there is a current transaction, suspend the current transaction.
  6. PROPAGATION_NEVER: Execute in a non-transactional manner, and throw an exception if a transaction currently exists.
  7. PROPAGATION_NESTED: Execute within a nested transaction if there is a current transaction. If there is no current transaction, perform a similar operation to 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
     */

distributed transaction

insert image description here

The business (order) remotely calls the inventory (storage), saves the order (order), and deducts the points (account). Only when these three steps are all successful, can our order be considered successful.

If it is a single application, we write all three codes in one system, and we all connect to a database, so we can use local transactions to control it, and if one fails, all will be rolled back.

But precisely because of the emergence of our distributed system, because our business is too large, we can't write all the business into one system, so we split it into many micro-services, such as inventory service, order service, user account service, Moreover, each service still connects to its own database and operates its own data, and it has nothing to do with each other. The
deployment of distributed systems may not be together. The inventory service is on the 1st machine, the order service is on the 2nd machine, and the user account service is on the 2nd machine. No. 3 machine, so that if we want to complete the order logic, we need to remotely call the methods of these three machines, but because we 分布式系统中经常会出现异常,

Machine downtime, network abnormality, message loss, message out-of-order, data error, unreliable TCP, loss of stored data...

Distributed transaction is a technical difficulty in enterprise integration, and it is also something involved in every distributed system architecture, especially in microservice architecture, it can almost be said to be unavoidable.

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

cap theorem (cap and base are both theories)

The CAP principle, also known as the CAP theorem, refers to the

  1. Consistency:
    Whether all data backups in a distributed system have the same value at the same time. (equivalent to all nodes accessing the same latest data copy)

  2. Availability (Availability)
    After some nodes in the cluster fail, whether the cluster as a whole can still respond to the client's read and write requests. (high availability for data updates)

  3. Partition tolerance (Partition tolerance)
    Most distributed systems are distributed in multiple sub-networks. Each subnet is called a partition.
    Partition tolerance means that interval (between servers) communication can fail.

The CAP principle means that these three elements can only achieve at most two points at the same time, and it is impossible to take care of all three.

The reason is:
123 three machines, we let all three nodes save a data, which is满足了一致性

Assuming that we are not due to a node failure but a communication failure, the network cable between 13 is broken, we must only send a request to a certain machine for saving data, and machine 1 makes 23 synchronize, because the network cable between 13 is broken, life and death cannot be connected Above, it happened at this time 分区错误.
After the partition error, assuming that we want it to meet the availability, we let the No. 3 machine recover. For example, we load balance and go to the No. 3 machine to read data, but due to the communication failure before No. 3, the data has not been synchronized. , the data read is naturally different, 所以一满足可用性就发现又不一致了. If you want to meet the consistency, you must make the No. 3 machine inaccessible, 不能访问就不可用了. so 一致性和可用性只能二选一.

Partition fault tolerance in a distributed system 必须满足, because there will definitely be problems in the network; then consistency and availability can only be chosen one of the two, if it is available, it must be satisfied, if it is 容忍业务能访问到不一致的数据consistent, the entire cluster cannot be made available, if the entire cluster is made available Otherwise, the data will be inconsistent.

How to choose

For most large-scale Internet application scenarios, there are many hosts and scattered deployments, and the current cluster scale is getting larger and larger, so node failures and network failures are normal, and service availability must be guaranteed to reach 99.99999% (N 9), that is, to ensure P and A, discarding C (consistency).

base theory

It is an extension of the CAP theory. The idea is that even if strong consistency cannot be achieved (the consistency of CAP is strong consistency), appropriate weak consistency can be adopted, ie 最终一致性.
BASE means

  • Basically Available

    • Basic availability means that when a distributed system fails, some availability (such as response time, functional availability) is allowed to be lost, and some availability is allowed to be lost. It should be noted that basic availability is by no means equivalent to system unavailability.

      • Loss in response time: Under normal circumstances, search engines need to return corresponding query results to users within 0.5 seconds, but due to failures (such as power outages or network failures in some computer rooms of the system), the response time of query results increases to 1~2 seconds.

      • Functional loss: In order to protect the stability of the system, some consumers may be directed to a downgrade page during shopping peaks (such as Double Eleven).

  • Soft state (Soft State)
    Soft state refers to allowing the system to exist in an intermediate state, and the intermediate state will not affect the overall availability of the system. In distributed storage, there are generally multiple copies of a piece of data, and the delay in allowing synchronization of different copies is the embodiment of soft state. The asynchronous replication of mysql replication is also a manifestation.

  • Eventual Consistency (Eventual Consistency)
    Final consistency means that all data copies in the system can finally reach a consistent state after a certain period of time. Weak consistency is the opposite of strong consistency, and eventual consistency is a special case of weak consistency.

  • Strong consistency, weak consistency, and eventual consistency
    From the perspective of the client, when multiple processes access concurrently, different strategies for how to obtain updated data in different processes determine different consistency. For relational databases, it is required that updated data can be seen by subsequent visits, which is strong consistency. If so 容忍后续的部分或者全部访问不到, it is weak consistency. If access to updated data is required after a period of time, it is eventual consistency

set

Seata is an open source distributed transaction solution dedicated to providing high-performance and easy-to-use distributed transaction services. Seata will provide users with AT, TCC, SAGA and XA transaction modes to create a one-stop distributed solution for users.

The overall mechanism of AT (auto transaction) mode

Evolution of the two-phase commit protocol:

  • Phase 1: Business data and rollback log records are committed in the same local transaction, releasing local locks and connection resources.

  • Phase 2:
    Committing is asynchronous and completes very quickly.
    Rollbacks are reverse compensated through a one-stage rollback log.

AT mode is not suitable for high concurrency

Transactions in AT mode will add a lot of locks. After locking, it is equivalent to turning concurrency into serialization. Everyone has to wait for the previous order to be placed before placing an order, which is not suitable for high concurrency.

The applicable scenario of using seata's AT mode to place an order is not suitable for high-concurrency scenarios. It is suitable for us 商品服务中的后台保存商品的场景, and it also calls a lot of services remotely, and the amount of concurrency is not very large.

And our order is a high-concurrency scenario.

the term

  • TC (Transaction Coordinator) - The transaction coordinator (coordinator)
    maintains the state of global and branch transactions, and drives global transaction commit or rollback.

  • TM (Transaction Manager) - Transaction Manager
    defines the scope of global transactions: start global transactions, commit or rollback global transactions.

  • RM (Resource Manager) - Resource Manager
    manages resources for branch transactions, talks to TC to register branch transactions and report the status of branch transactions, and drives branch transactions to commit or rollback.

insert image description here
process

  1. tm wants to execute the order business, tm will tell tc to start a global transaction, and the coordinator will know that we may have to start a global transaction across services
  2. The resource manager tells the coordinator that there is a branch transaction and reports its transaction status in real time (whether it is successful or rolled back to the coordinator). Each resource manager needs to report its transaction status to the coordinator in real time.
  3. If an exception occurs in any branch transaction, tc will roll back the transactions of all resource managers.

Each microservice needs to have a undo_logtable for, for 自动补偿example, a transaction has been submitted, but an exception occurs at this time, and the transaction cannot be rolled back at this time.
Before committing the transaction, the table data involved in the execution of the transaction will be queried and recorded in undo_logthe table. If the above situation occurs, the data will be automatically changed back, without requiring us to type code to perform this operation.

Use seata DataSourceProxy to proxy your own data source

// 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);
    }
}


The complete process of deploying and using 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);
    }

}

Reliable message + eventual consistency scheme

RabbitMQ delay queue (implementing timing tasks)

The time to unlock the inventory is 40 minutes because the time to close the order is 30 minutes
insert image description here

  • Scenarios
    such as unpaid orders, after a certain period of time, the system will automatically cancel the order and release the possession.

  • Common solutions:
    spring's schedule timed task polling database

  • Disadvantages:
    consume system memory, increase the pressure on the database, and have large time errors.
    Timing tasks need to do a full table scan every once in a while, which will increase the pressure on the database.
    solve:rabbitmq的消息TTL和死信Exchange结合

insert image description here
If the scheduled task is set, it must be executed every 30 minutes. It is impossible to set a scheduled task every time an order is placed, so there will be timeliness problems.

For example, the order above can actually be closed after 59 minutes.

Message TTL (Time To Live)

The TTL of the message is the lifetime of the message.

  • RabbitMQ can set TTL for queues and messages separately.

  • The queue setting is the retention time that the queue has no consumer connection, and it can also be set separately for each individual message. Beyond this time, we consider the message dead and call it 死信.

  • If the queue is set and the message is also set, then the smaller one will be taken. So if a message is routed to different queues, the death time of the message may be different (different queue settings).
    Here we only talk about the TTL of a single message, because it is the key to implementing the delayed task. You can set the time by setting expiration字段或者x- message-ttlthe properties of the message, both have the same effect.

The purpose of setting the message is that within the TTL time, if the message is not consumed, it will be considered a dead letter by the server (that is to say, the message is dead), and it will be discarded.

Dead Letter Exchanges (DLX)

  • A message will enter if the following conditions are met 死信路由(the message is confirmed to be a dead letter and enters the switch for execution according to the route). Remember that this is a route rather than a queue. One route can correspond to many queues. (what is dead letter)

    1. A message is rejected by the Consumer, and requeue is false in the parameter of the reject method. That is to say, it will not be placed in the queue again and used by other consumers. (basic.reject/basic.nack) requeue=false
    2. The TTL of the above message has been reached and the message has expired.
    3. The queue length limit is full. The first messages will be discarded or thrown to the dead letter route
  • Dead Letter Exchange is actually an ordinary exchange, just like creating other exchanges. It's just that if there is a message expired in a queue that is set to Dead Letter Exchange, it will automatically trigger the forwarding of the message and send it to Dead Letter Exchange.

  • We can not only control the message to become a dead letter after a period of time, but also control the message that becomes a dead letter to be routed to a specified switch. 结合二者In fact, a延时队列

  • Manual ack & exception messages are put in one queue to deal with the suggested two ways

    • After the catch is abnormal, manually send it to the specified queue, and then use the channel to confirm to rabbitmq that the message has been consumed
    • Bind the dead letter queue to Queue, use nack (requque is false) to confirm message consumption failure

Delay Queue Implementation

  • x-message-ttl: unit ms

  • x-dead-letter-exchange: Specifies the dead letter exchange

  • x-dead-letter-routing-key-delay: specify the routing key, the switch sends the message to the specified queue through the routing key
    insert image description here
    insert image description here

  • It is suggested 队列设置过期时间that if you set an expiration time for the message, rabbitmq adopts a lazy checking mechanism (lazy checking), assuming that the first message expires in 5 minutes in the queue, the second message expires in 1 minute, and the third message expires in 1 second, according to the normal The situation should be that the third message pops out of the queue first, but the server does not check this way,

  • The server takes the first message from the queue, and puts it back when it finds that it has expired in 5 minutes. When the server comes back to get it after 5 minutes, it will take out the first message, and the first message will expire.

  • Our second and third messages need to wait 5 minutes for the first message to expire before they expire. After the server gets the first message, it will come to get the second message. At this time, the second message does not need to wait for 1 minute, because it There is a time for sending a message, and once the server has expired, it will be taken out (the third message is the same).

    This caused the second and third messages to be received 5 minutes after the first message.

So we should set an expiration time for the entire queue (each message in the queue is calculated according to the time it is received), so that all messages in the entire queue have this expiration time, and the server can just take it out .

Delay queue realizes closing orders and unlocking inventory

Inventory Unlocked Scenarios

There is a prerequisite for the following, that is, the order is placed successfully.

  1. The system automatically cancels the order without payment within 30 minutes

  2. User manually canceled

  3. If the inventory is locked successfully, the next business call of the order fails, causing the order to be rolled back, and the previously locked inventory will be automatically unlocked

The order service closes the order regularly

insert image description here
When the order service places an order successfully, it sends a message to rabbitmq. The routing key is order_delay, and the message is sent to userr.order.delay.exchange. The exchange sends it to the user.order.delay.queue queue according to the routing key. The queue is set as follows There are three parameters in the picture,
and there is no one to consume in this queue, and no one takes data from the queue. As long as it exceeds 30 minutes (the 1 minute in the picture does not count), rabbitmq will take the message out of the queue, and the message becomes dead. However,
here we set the dead letter to be sent to the user.order.exchange switch, and then sent to the specified queue through the order routing key, then this queue is full of 30-minute expired messages, and the queue can be judged after receiving the content. Orders can be closed as long as they are not paid.

insert image description here

Compared with the above, in fact, using routing instead of two switches, the design becomes simpler, and the final result is the same as above.

Based on the message queue design of our business, each microservice has its own switch, and the current microservice name-event-exchange is used to perceive the switch of various events of the current microservice. The switch will bind many queues.

// 创建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. After the order is successfully created, send a message to the rabbitmq delay queue. As long as it exceeds 30 minutes, it will reach the order queue through the switch according to the set route

  2. The status of the order needs to be closed only when it is pending payment. The delay queue for closing the order is 30 minutes, and the delay queue for unlocking the inventory is 50 minutes. This is the linkage between closing the order and unlocking the inventory.

  3. In order to solve the problems that may arise in the above linkage, after closing the order, we send a message to the message queue for unlocking the inventory, and immediately execute the unlocking of the inventory.

question

insert image description here

However, there is a big problem with such linkage. Assuming that the order is successfully created, when the message is sent, due to reasons such as machine freezes and slow network, the order is successfully created and the message is not sent in time. At this time, the delay queue message of the inventory
service Expires, which causes the message to unlock the inventory to expire earlier than the message to close the order.
The service to unlock the inventory receives the message to unlock the inventory and check whether the order is paid successfully. At this time, the order is still in the new state, so it will not be unlocked. The message is consumed at the same time, and finally the order service closes the order because there is no payment, and the inventory will never be released.

Solution
After closing the order, we send a message to the message queue for unlocking the inventory, and execute the unlocking of the inventory immediately.

Inventory service unlocks inventory regularly

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;

    }



}


It needs to be introduced here 库存工作单和库存工作单详情, the inventory work order is the details of the order, and the inventory work order details are the id of each sku, the purchase quantity, warehouse id, lock status, etc. An exception is thrown as long as one item is not locked successfully.

Send message to delayed queue

  1. When placing an order and remotely calling the inventory service to lock the inventory, the inventory service first saves the work order (in fact, it is to record which inventory is locked, which is needed when rolling back).

  2. When the sku locks the inventory successfully, rabbitmq saves the details of the inventory work order and sends it to the delay queue (how many items are locked in which warehouse the sku is in). The status of the inventory work order details is now 锁定.
    If the lock fails at this time, the database transaction will be rolled back, but our message is still sent out, but it doesn’t matter, we can’t find the details of the work order when listening to the message to unlock, which proves that there is no need to unlock.

Listen to messages, inventory unlock queue (messages in the delayed queue will enter the queue after it expires)

  1. If the inventory is locked successfully, the next business call of the order fails, causing the order to be rolled back, and the previously locked inventory will be automatically unlocked

  2. The inventory service failed to lock the inventory itself.

If the lock inventory itself fails, the inventory work order will be rolled back, so when listening to the message, it is necessary to determine whether the inventory work order exists, and if it does not exist, there is no need to unlock it (this case will definitely throw an exception, and the order service can also After receiving an exception thrown by the inventory service, the order service will naturally roll back).

If it exists, it means that the lock inventory must have been successful, and it is not known whether the order service is successful. At this point there are two situations,

  1. If there is no order, it proves that the order service is rolled back and needs to be unlocked.
  2. There is an order, check the status of the order
    1. Canceled (manual cancellation and 30min timer expiration), unlocked inventory
    2. As long as it is not canceled, it cannot be unlocked.

When unlocking the inventory, it is necessary to judge the status of the inventory work order. Only when it is locked can it be unlocked, and neither unlocked nor deducted (it seems that this situation will not happen, is it a small probability event?).

Rabbitmq must manually confirm the message when unlocking the inventory. If there is any abnormality, it needs to be rejected and put back into the queue.

// 发送消息
@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);
        }


    }



}

Unlock the general flow chart of inventory

insert image description here
insert image description here

Remote calls without login information are intercepted

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;
        }

    }
}

How to ensure message reliability (common factors of unreliable messages)

Flexible transaction - reliable message + eventual consistency scheme (asynchronous guarantee type)

The most important thing to emphasize is that 可靠消息because of any unreliable factors, there are some problems and failures in the message, the message is not sent, etc., we may face great losses,

How to Ensure Message Reliability - Message Loss

  • The message was sent, but did not reach the server due to network problems

    • Do a good job of fault tolerance (try-catch). Sending messages may cause network failures. After failures, there must be a retry mechanism (generally, network interruptions may not recover for a short period of time), which can be recorded in the database, and regular scans and resends are used.

    • Do a good job of logging, each message can be logged (save the detailed information of each message to the database), and use regular scanning to resend

  • When the message arrives at the Broker, the Broker must write the message to the disk (deliver it to the queue for persistence) to be considered successful. At this time, Broker has not yet completed persistence and is down.

    • The publisher must also add a confirmation callback mechanism. If the message fails, the database message status will be modified, and then the database scan will be resent periodically.
  • In the state of automatic ACK (consumers will automatically ack as soon as they get the message). The consumer receives the message, but fails to get the message and then crashes

    • Be sure to enable manual ACK, remove it only if the consumption is successful, noAck if it fails or has not had time to process it and re-enter the queue

Summarize

  1. Do a good job of confirmation at both ends of the message (publisher, consumer [manual ack]),
  2. Every sent message is recorded in the database, and the failed messages are sent again regularly

How to ensure message reliability - message duplication (a message is sent to the consumer twice)

  • The message consumption is successful, the transaction has been submitted, and when the ack occurs, the machine is down. As a result, no ack is successful, and the Broker's message is changed from unack to ready again, and sent to other consumers.
    This results in the business being executed twice.

  • If the message consumption fails, the message will be sent out automatically due to the retry mechanism, which is allowed.

solve

  1. The consumer's business consumption interface should be designed as 幂等性. For example, there is a status flag of the work order in the deducted inventory.
    For example, our unlocked inventory will judge the status of the inventory work order when it is unlocked. If it is unlocked, it will not be unlocked again.

  2. Using 防重表(redis/mysql), each message sent has a unique identifier for the business, and it does not need to be processed after processing

  3. Each message of rabbitMQ has redelivereda field, you can get whether it was re-delivered, not the first time.
    There is a bug in this. What if the last consumption fails and is retried?

How to Ensure Message Reliability - Message Backlog

If too many messages are stored in the queue, it will definitely affect the performance of mq.

  • Consumer Downtime Backlog
  • Insufficient spending power of consumers
  • The sender sends too much traffic

solve

  1. Online more consumers for normal consumption

  2. Launch a dedicated queue consumption service, take out messages in batches first, record them in the database, and process them slowly offline

Summarize

If we want to use it 柔性事务-可靠消息+最终一致性方案(异步确保性)to do distributed transactions, we must ensure 可靠消息that the most important thing is 消息丢失that the backlog can be slower, and we can write idempotence for repetition.

The most feared thing is loss, and the final consistency solution is what it is 防止消息丢失(the solution has been stated above).

We found that it would be cumbersome to write a piece of code every time we send a message for each business. We can write the message as a microservice and call this microservice when needed.

A child who is a doctoral student at MIT told me that they have no lecture notes and no textbooks; Viewpoints, recommend some reference books to guide you to read; then you write, writing articles does not need results, as long as the thinking logic is appropriate, the teacher will give you a grade, this is science. What the teacher was talking about was the frontier of his research, and he himself did not fully understand that science is the work of generations. As long as a doctor has a way of thinking, there may not be results. Some sciences are too far away, and it is difficult to prove it in one step. It is good to have a correct process. Our Spark Award does not necessarily have to have "results", otherwise how can it be called "green out of blue and better than blue"? If education always pursues results, students' thinking will be constrained. Therefore, it is not enough to be too pragmatic and demand-oriented to lead China forward. Education should allow students to "think wildly", as long as the logic of his thinking is consistent, don't restrain him. Reading is to get the "key" to open the door. The key is to read a methodology. The method of using knowledge is more important than knowledge. Because knowledge can be obtained on the Internet, how to combine and how to stitch together, this is what the university should learn.

https://baijiahao.baidu.com/s?id=1760664270073856317&wfr=spider&for=pc
Shine the Fireworks and Create the Future Together—Ren Zhengfei’s Speech at the Symposium on the Fireworks Awards of "Unveiling the Problems"
Ren Zhengfei

Guess you like

Origin blog.csdn.net/weixin_44431371/article/details/129991239