【RocketMQ】(四)解決分佈式事務-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)數據庫截圖:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章