【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性

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

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第1张图片

    废话不多说,直接上代码,这个案例用了RocketMQ、Spring cloud Alibaba组件中的nacos来实现服务的注册与发现、mybatis-plus等等,案例中使用到了两个数据库,流程就是用户在订单微服务中下单,然后在库存微服务中扣减库存;

一、项目结构: 

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第2张图片

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



    4.0.0

    com.lucifer
    rocketmq-transaction
    pom
    1.0-SNAPSHOT
    
        order-service
        storage-service
        base-framework-mysql-support
    


    
        UTF-8
        UTF-8
        1.8
        Finchley.RELEASE
    

    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            

            
                org.springframework.boot
                spring-boot-dependencies
                2.1.3.RELEASE
                pom
                import
            

        
    


二、模块介绍

(1)base-framework-mysql-support:

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第3张图片

此模块只有一个关于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: 



    
        rocketmq-transaction
        com.lucifer
        1.0-SNAPSHOT
    
    4.0.0

    base-framework-mysql-support

    
        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        
        
            mysql
            mysql-connector-java
            5.1.39
        

        
            com.baomidou
            mybatis-plus-boot-starter
            3.1.0
        
        
            org.projectlombok
            lombok
            1.18.12
        
    

 (1)order-service:订单模块

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第4张图片

 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 {
}
package com.lucifer.mapper;

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

public interface TxLogMapper extends BaseMapper {
}

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 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 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:



    4.0.0

    
        rocketmq-transaction
        com.lucifer
        1.0-SNAPSHOT
    

    order-service

    
        
            com.lucifer
            base-framework-mysql-support
            1.0-SNAPSHOT
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.apache.rocketmq
            rocketmq-spring-boot-starter
            2.0.2
        
        
            com.alibaba
            fastjson
            1.2.62
        
        
        
            org.springframework.cloud
            spring-cloud-starter-alibaba-nacos-discovery
            0.2.2.RELEASE
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

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

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第5张图片

mapper:

package com.lucifer.mapper;

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

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

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

public interface TxLogMapper extends BaseMapper {
}

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 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 {

    @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: 



    4.0.0
    
        rocketmq-transaction
        com.lucifer
        1.0-SNAPSHOT
    

    storage-service

    
        
            com.lucifer
            base-framework-mysql-support
            1.0-SNAPSHOT
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.apache.rocketmq
            rocketmq-spring-boot-starter
            2.0.2
        
        
        
            org.springframework.cloud
            spring-cloud-starter-alibaba-nacos-discovery
            0.2.2.RELEASE
        
    


    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

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

测试场景:

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

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

场景(2)数据库截图:

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第6张图片

【RocketMQ】(四)解决分布式事务-RocketMQ实现可靠消息最终一致性_第7张图片

 

 

你可能感兴趣的:(分布式事务,微服务实战篇,高并发中间件专题)