RocketMQ实现可靠消息最终一致性

 RocketMQ实现可靠消息最终一致性的原理图:

    废话不多说,直接上代码,这个案例用了RocketMQ、Spring cloud Alibaba组件中的nacos来实现服务的注册与发现、mybatis-plus等等,案例中使用到了两个数据库;

一、项目结构: 

           rocketmq-transaction工程分为三个子模块,base-framework-mysql-support模块(作为基础模块,被其它服务模块引用)存放数据库相关jar包和配置类,order-service模块是订单微服务,storage-service是库存微服务模块;

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lucifer</groupId>
    <artifactId>rocketmq-transaction</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>order-service</module>
        <module>storage-service</module>
        <module>base-framework-mysql-support</module>
    </modules>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.3.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>


</project>

二、模块介绍

(1)base-framework-mysql-support:

此模块只有一个关于mybatis-plus的配置:代码如下:

package com.lucifer.config;

import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author lucifer
 * @date 2020/4/14 21:54
 * @description mybatis-plus 配置
 */
@Configuration
@MapperScan(value = {"com.lucifer.mapper"})
public class MybatisPlusConfig {
    
    /**
     * SQL执行效率插件
     */
    @Bean
   // @Profile({"dev", "test"})// 设置 dev test 环境开启
    public PerformanceInterceptor performanceInterceptor() {
        return new PerformanceInterceptor();
    }
}

pom.xml: 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>rocketmq-transaction</artifactId>
        <groupId>com.lucifer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>base-framework-mysql-support</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.39</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
    </dependencies>

</project>

 (1)order-service:订单模块

 pojo:存放实体类的包

/**
 * 订单表
 */
@Data
@NoArgsConstructor
@TableName("order_tbl")
public class Order {

    @TableId(type = IdType.AUTO)
    private Integer id;
    private String userId;
    private String commodityCode;
    private Integer count;
    private BigDecimal money;
    @TableField(exist = false)
    private String txNum;
}
/**
 * @author lucifer
 * @date 2020/4/15 13:04
 * @description 事务日志表
 */
@Data
//@Builder
@NoArgsConstructor
//@Accessors(chain = true)
@TableName("tx_log")
public class TxLog {
    @TableId
    private String txNum;
    private Date createTime;
}

mapper:

package com.lucifer.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.Order;

public interface OrderMapper extends BaseMapper<Order> {
}
package com.lucifer.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.TxLog;

public interface TxLogMapper extends BaseMapper<TxLog> {
}

service:接口 

package com.lucifer.service;


import com.lucifer.pojo.Order;

public interface OrderService {


    /**
     * 发送订单消息
     *
     * @param order
     */
    void sendOrder(Order order);


    /**
     * 新增订单
     *
     * @param order
     */
    void insertOrder(Order order);

}

 实现类:

/**
 * @author lucifer
 * @date 2020/4/14 19:31
 * @description
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private RocketMQTemplate rocketMQTemplate;
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private TxLogMapper txLogMapper;


    @Override
    public void sendOrder(Order order) {
        String str = JSON.toJSONString(order);
        Message<String> message = MessageBuilder.withPayload(str).build();
        /**
         * 发送一条事务消息
         * String txProducerGroup: 生产组
         * String destination:topic
         * Message<?> message: 消息内容
         * Object arg: 参数
         */
        rocketMQTemplate.sendMessageInTransaction("producer_group_tx1", "topic_tx", message, null);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void insertOrder(Order order) {
        //用事务id幂等处理
        if (txLogMapper.selectById(order.getTxNum()) != null) {
            return;
        }
        orderMapper.insert(order);
        //插入事务日志
        TxLog txLog = new TxLog();
        txLog.setTxNum(order.getTxNum());
        System.out.println("order.getTxNum():" + order.getTxNum());
        txLog.setCreateTime(new Date());
        txLogMapper.insert(txLog);

        //模拟异常,检查事务是否回滚
        QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("commodity_code", "product-1");
        if (orderMapper.selectList(queryWrapper).size()== 6) {
            throw new RuntimeException("人为模拟异常");
        }
    }

}

 rocketmq的事务监听器:(重要)

/**
 * @author lucifer
 * @date 2020/4/15 0:59
 * @description TODO
 */
@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "producer_group_tx1")
public class ProducerTransactionListener implements RocketMQLocalTransactionListener {

    @Resource
    private OrderService orderService;
    @Resource
    private TxLogMapper txLogMapper;

    /**
     * 事务消息发送mq成功后的回调方法
     *
     * @param msg
     * @param arg
     * @return 返回事务状态
     * RocketMQLocalTransactionState.COMMIT:提交事务,提交后broker才允许消费者使用
     * RocketMQLocalTransactionState.ROLLBACK:回滚事务,回滚后消息将被删除,并且不允许别消费
     * RocketMQLocalTransactionState.Unknown:中间状态,表示MQ需要核对,以确定状态
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {

        try {
            String str = new String((byte[]) msg.getPayload());
            Order order = JSON.parseObject(str, Order.class);

            orderService.insertOrder(order);
            //当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit,mq将消息的状态改为可消费
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            e.printStackTrace();
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 事务状态回查,查询是否下单成功
     *
     * @param msg
     * @return 返回事务状态
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String str = new String((byte[]) msg.getPayload());
        Order order = JSON.parseObject(str, Order.class);
        //事务id
        String txNo = order.getTxNum();
        TxLog txLog = txLogMapper.selectById(txNo);
        if (txLog != null) {
            return RocketMQLocalTransactionState.COMMIT;
        } else {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
}

 application.yml: 配置文件

server:
  port: 8081

spring:
  application:
    name: order-service
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.160.131:3306/order?autoReconnect=true&useUnicode=true&createDatabaseIfNotExist=true&characterEncoding=utf8&serverTimezone=UTC
      username: root
      password: 123456
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.160.131:8848
#  main:
#    allow-bean-definition-overriding: true
logging:
  level:
    com.lucifer.mapper: debug

rocketmq:
  producer:
    group: producter_tx
  name-server: 192.168.160.131:9876

 springboot启动类:

/**
 * @author lucifer
 * @date 2020/4/14 19:28
 * @description TODO
 */
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

 controller层:

/**
 * @author lucifer
 * @date 2020/4/14 19:32
 * @description TODO
 */
@RestController
@RequestMapping(value = "order")
public class OrderController {

    @Resource
    OrderService orderService;

    /**
     * 下单:插入订单表、扣减库存,模拟回滚
     *
     * @return
     */
    @GetMapping("/placeOrder/commit")
    public Boolean placeOrderCommit() {
        //将uuid作为事务id,发送到mq
        String uuid = UUID.randomUUID().toString();
        Order order = new Order();
        order.setCommodityCode("product-1");
        order.setUserId("1");
        order.setCount(1);
        order.setTxNum(uuid);
        order.setMoney(new BigDecimal(12.5));
        System.out.println("准备下单了=======》" + order);
        orderService.sendOrder(order);
        return true;
    }
}

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <artifactId>rocketmq-transaction</artifactId>
        <groupId>com.lucifer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>order-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.lucifer</groupId>
            <artifactId>base-framework-mysql-support</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <!-- nacos -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

(3)storage-service:库存微服务模块

mapper:

package com.lucifer.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.Storage;

public interface StorageMapper extends BaseMapper<Storage> {
}
package com.lucifer.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucifer.pojo.TxLog;

public interface TxLogMapper extends BaseMapper<TxLog> {
}

pojo:

/**
 * 库存表
 */
@Data
@Accessors(chain = true)
@TableName("storage_tbl")
public class Storage {

    private Long id;
    private String commodityCode;
    private Long count;
}

ps:order、txlog两个实体类从order-service中复制过来即可;

service:

public interface StorageService {
    /**
     * 扣减库存
     *
     * @param commodityCode
     * @param count
     * @param txNum 事务id
     */
    void deduct(String commodityCode, int count,String txNum);
}

实现类:

/**
 * @author lucifer
 * @date 2020/4/14 20:07
 * @description TODO
 */
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    @Resource
    private StorageMapper storageMapper;
    @Resource
    private TxLogMapper txLogMapper;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deduct(String commodityCode, int count, String txNum) {
        log.info("扣减库存,商品编码:{},数量:{}", commodityCode, count);
        TxLog txLog = txLogMapper.selectById(txNum);
        if (txLog != null) {
            return;
        }
        //扣减库存
        QueryWrapper<Storage> wrapper = new QueryWrapper<>();
        wrapper.setEntity(new Storage().setCommodityCode(commodityCode));
        Storage storage = storageMapper.selectOne(wrapper);
        if (storage == null) {
            throw new RuntimeException("商品" + commodityCode + ",不存在");
        }
        storage.setCount(storage.getCount() - count);

        storageMapper.updateById(storage);

        //添加事务记录,用于幂等
        TxLog tLog = new TxLog();
        tLog.setTxNum(txNum);
        tLog.setCreateTime(new Date());
        txLogMapper.insert(tLog);

        //模拟异常,检查事务是否回滚
        if(storageMapper.selectById(1).getCount()==4996){
            throw new RuntimeException("人为模拟异常");
        }
    }
}

rocketmq监听类: 

/**
 * @author lucifer
 * @date 2020/4/15 0:59
 * @description TODO
 */
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "consumer_group_tx2", topic = "topic_tx")
class ConsumerTransactionListener implements RocketMQListener<String> {

    @Resource
    private StorageService storageService;

    @Override
    public void onMessage(String message) {
        log.info("开始消费消息:{}", message);
        //解析消息
        Order order = JSON.parseObject(message, Order.class);

        //扣减库存
        storageService.deduct(order.getCommodityCode(), order.getCount(), order.getTxNum());
    }
}

springboot启动类:

/**
 * @author lucifer
 * @date 2020/4/14 20:23
 * @description 库存服务
 */
@EnableDiscoveryClient
@SpringBootApplication
public class StorageApplication {
    public static void main(String[] args) {
        SpringApplication.run(StorageApplication.class, args);
    }
}

application.yml: 

server:
  port: 8082

spring:
  application:
    name: storage-service
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.160.131:3306/storage?autoReconnect=true&useUnicode=true&createDatabaseIfNotExist=true&characterEncoding=utf8&serverTimezone=UTC
      username: root
      password: 123456
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.160.131:8848
#  main:
#    allow-bean-definition-overriding: true
logging:
  level:
    com.lucifer.mapper: debug

rocketmq:
  producer:
    group: consumer_tx
  name-server: 192.168.160.131:9876

pom.xml: 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>rocketmq-transaction</artifactId>
        <groupId>com.lucifer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>storage-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.lucifer</groupId>
            <artifactId>base-framework-mysql-support</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.2</version>
        </dependency>
        <!-- nacos -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>0.2.2.RELEASE</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

启动order-service服务,启动storage-service服务;

测试场景:

(1)order-service 本地事务失败,order-service不会发送下订单消息

(2)storage-service 接收到下单的消息,扣减库存失败,会不断重试扣减库存(当然这个尝试次数有限制的),控制台会不断打印重试信息:如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列,此时需要人工干预了。

场景(2)数据库截图:

 
发布了187 篇原创文章 · 获赞 146 · 访问量 49万+

猜你喜欢

转载自blog.csdn.net/qq_37495786/article/details/105536552