【RocketMQ】(五)利用Redssion分佈式鎖和RocketMQ消息的最終一致性 實現併發場景下單扣減庫存

一、項目結構: 

1、 父工程

pom.xml:(父工程只有一個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>

    <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>Greenwich.SR1</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.2.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2、base-framework-mysql-support:數據庫相關的配置

   2.1 MybatisPlusConfig :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();
    }
}

2.2 RedissonConfig: redisson相關配置

package com.lucifer.config;

import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration
public class RedissonConfig {

    @Resource
    private RedissonProperties redissonProperties;

    /**
     * 單機模式自動裝配
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix ="redisson",name="single-is",havingValue="true")
    public RedissonClient getSingleRedisson(){
        Config config = new Config();
        String singlePassword = redissonProperties.getSinglePassword();
        SingleServerConfig serverConfig = config.useSingleServer().setAddress("redis://" + redissonProperties.getSingleAddress());
        System.out.println("redis:=================="+serverConfig.getAddress());
        if(StringUtils.isNotBlank(singlePassword)){
            serverConfig.setPassword(singlePassword);
        }
        return Redisson.create(config);
    }

    /**
     * 哨兵模式自動裝配
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix ="redisson",name="sentinel-is",havingValue="true")
    public RedissonClient getSentinelRedisson(){
        Config config = new Config();
        SentinelServersConfig serverConfig = config.useSentinelServers().addSentinelAddress(redissonProperties.getSentinelAddresses()).setMasterName(redissonProperties.getSentinelMasterName());
        String sentinelPassword = redissonProperties.getSentinelPassword();
        if(StringUtils.isNotBlank(sentinelPassword)) {
            serverConfig.setPassword(sentinelPassword);
        }
        return Redisson.create(config);
    }
}

2.3 RedissonProperties:讀取application.yml的自定義配置

package com.lucifer.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


/**
 * @author lucifer
 * @date 2020/4/21 14:09
 * @description TODO
 */
@Data
@Component
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties  {

    private String singleAddress;

    private String singlePassword;

    private String sentinelMasterName;

    private String sentinelAddresses;

    private String sentinelPassword;

}

2.4 pom.xml: jar包

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

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>
</project>

3、order-service 訂單微服務

3.1 OrderController:控制層 用於測試

package com.lucifer.controller;

import com.lucifer.pojo.Order;
import com.lucifer.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.UUID;

/**
 * @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;
    }
}

 3.2 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) throws Exception;

}

3.2.1 service 實現類: 

package com.lucifer.service.impl;

import com.alibaba.fastjson.JSON;
import com.lucifer.mapper.OrderMapper;
import com.lucifer.mapper.TxLogMapper;
import com.lucifer.pojo.Order;
import com.lucifer.pojo.Storage;
import com.lucifer.pojo.TxLog;
import com.lucifer.service.OrderService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.redisson.api.RBucket;
import org.redisson.api.RFuture;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;


/**
 * @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;
    @Resource
    private RedissonClient redissonClient;


    @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) throws Exception {
        //創建鎖對象
        RLock lock = redissonClient.getLock("placeOrder:" + order.getUserId() + order.getCommodityCode());
        try {
            //嘗試去獲取鎖
            RFuture<Boolean> booleanRFuture = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
            Boolean aBoolean = booleanRFuture.get();
            //如果獲取到了鎖
            if (aBoolean) {
                //獲取redis中的庫存
                RBucket<Storage> storageBucket = redissonClient.getBucket("placeOrder");
                Storage storage = storageBucket.get();
                System.out.println("剩餘庫存:=================" + storage.getCount());
                if (storage.getCount() <= 0) {
                    throw new RuntimeException("商品:" + order.getCommodityCode() + ",庫存爲空");
                }
                //用事務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);
            }
        } finally {
            //釋放鎖
            lock.unlock();
        }
    }
}

3.3. mapper接口:

package com.lucifer.mapper;

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

public interface TxLogMapper extends BaseMapper<TxLog> {
}
package com.lucifer.mapper;

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

public interface OrderMapper extends BaseMapper<Order> {
}

 3.4. 實體類:

package com.lucifer.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 訂單表
 */
@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;
}
package com.lucifer.pojo;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;

import java.io.Serializable;

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

    private Long id;
    private String commodityCode;
    private Long count;
}
import java.util.Date;

/**
 * @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;
}

3.5、ProducerTransactionListener 是去實現 RocketMQLocalTransactionListener接口(重要

package com.lucifer.transaction;

import com.alibaba.fastjson.JSON;
import com.lucifer.mapper.TxLogMapper;
import com.lucifer.pojo.Order;
import com.lucifer.pojo.Storage;
import com.lucifer.pojo.TxLog;
import com.lucifer.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * @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;
    @Resource
    private RedissonClient redissonClient;

    /**
     * 事務消息發送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);
         
            //扣減redis中庫存
            RBucket<Storage> storageBucket = redissonClient.getBucket("placeOrder");
            Storage storage = storageBucket.get();
            long count = storage.getCount() - order.getCount();
            storage.setCount(count);
            storageBucket.set(storage);

            //當返回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;
        }
    }
}

3.6. 啓動類 

package com.lucifer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

}

3.7.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

redisson:
  single-is: true
  single-address: 192.168.160.131:6379
  single-password:
  sentinel-is: false
  sentinel-master-name: business-master
  sentinel-addresses: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
  sentinel-password:


3.8. 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>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


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

4.storage-service 庫存微服務

4.1 項目啓動就會將商品 "product-1"的庫存信息查詢出來,放到redis當中,這裏用於測試

package com.lucifer.config;

import com.lucifer.pojo.Storage;
import com.lucifer.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Slf4j
@Component
public class StorageInitApplicationRunner implements ApplicationRunner {

    @Resource
    private RedissonClient redissonClient;
    @Resource
    private StorageService storageService;

    @Override
    public void run(ApplicationArguments applicationArguments) throws Exception {
        //從數據庫查詢搶購秒殺商品信息
        Storage storage = storageService.getStorage("product-1");
        //獲取redis中key爲storage對象信息
        RBucket<Storage> storageBucket = redissonClient.getBucket("placeOrder");
        //如果key存在,就設置key的值爲新值value
        //如果key不存在,就設置key的值爲value
        storageBucket.set(storage);
        log.info("商品數據初始化完成!");
    }
}

4.2 service接口

package com.lucifer.service;

import com.lucifer.pojo.Storage;

import java.util.concurrent.ExecutionException;

public interface StorageService {
    /**
     * 扣減庫存
     *
     * @param commodityCode
     * @param count
     * @param txNum         事務id
     */
    void deduct(String commodityCode, int count, String txNum) throws ExecutionException, InterruptedException, Exception;

    /**
     * 獲取商品信息
     *
     * @param commodityCode
     * @return
     */
    Storage getStorage(String commodityCode);
}

4.2.1 實現類

package com.lucifer.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lucifer.mapper.StorageMapper;
import com.lucifer.mapper.TxLogMapper;
import com.lucifer.pojo.Storage;
import com.lucifer.pojo.TxLog;
import com.lucifer.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * @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;
    @Resource
    private RedissonClient redissonClient;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deduct(String commodityCode, int count, String txNum) throws Exception {
        RLock lock = redissonClient.getLock("placeOrder:" + commodityCode);
        Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

        try {
            if (res.get()) {
                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 + ",不存在");
                }
                //扣減MySQL中的庫存
                storage.setCount(storage.getCount() - count);
                System.out.println("剩餘庫存數量:" + storage.getCount());
                storageMapper.updateById(storage);
                //添加事務記錄,用於冪等
                TxLog tLog = new TxLog();
                tLog.setTxNum(txNum);
                tLog.setCreateTime(new Date());
                txLogMapper.insert(tLog);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public Storage getStorage(String commodityCode) {
        QueryWrapper<Storage> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("commodity_code", commodityCode);
        Storage storage = storageMapper.selectOne(queryWrapper);
        System.out.println("剩餘庫存數:" + storage.getCount());
        return storage;
    }
}

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

4.4 實體類

   Order、Storage、TxLog 三個實體類同order-service中;

4.5 ConsumerTransactionListener 實現RocketMQListener接口,消費消息

package com.lucifer.transaction;

import com.alibaba.fastjson.JSON;
import com.lucifer.pojo.Order;
import com.lucifer.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @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);
        if(order!=null){
            //扣減庫存
            try {
                storageService.deduct(order.getCommodityCode(), order.getCount(), order.getTxNum());
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("扣減庫存失敗");
            }
        }
    }
}

4.6啓動類

package com.lucifer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

4.7application.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


redisson:
  single-is: true
  single-address: 192.168.160.131:6379
  single-password:
  sentinel-is: false
  sentinel-master-name: business-master
  sentinel-addresses: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
  sentinel-password:

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

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <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>
            <exclusions>
                <exclusion>
                    <artifactId>fastjson</artifactId>
                    <groupId>com.alibaba</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.2</version>
        </dependency>
    </dependencies>


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

</project>

二、測試

(1)場景一:正常情況:使用jmter測試:用20個線程去模擬20個用戶同時去購買某個商品:

目前庫存只有10個: 

查看控制檯: (當10個訂單下單成功後,第11個訂單。。。。)

準備下單了=======》Order(id=null, userId=1, commodityCode=product-1, count=1, money=12.5, txNum=7a0702f8-8814-4d6a-a492-04ccd1bda31b)
剩餘庫存:=================0
java.lang.RuntimeException: 商品:product-1,庫存爲空

查看數據庫:只有10個訂單下單成功,並且庫存爲0,並沒有成爲負數

 

(2)場景2:模擬異常的發生,在order-service中:

刪除redis數據、清空數據庫剛產生的數據,將庫存數調回10,重啓庫存微服務;

模擬20個用戶去訪問,order-service 微服務控制檯:

剩餘庫存:=================6
2020-04-21 19:20:59.502 DEBUG 22056 --- [nio-8081-exec-1] c.lucifer.mapper.TxLogMapper.selectById  : ==>  Preparing: SELECT tx_num,create_time FROM tx_log WHERE tx_num=? 
2020-04-21 19:20:59.509 DEBUG 22056 --- [nio-8081-exec-1] c.lucifer.mapper.TxLogMapper.selectById  : ==> Parameters: 4dadbd02-735f-43f3-b10e-5863c833406c(String)
2020-04-21 19:20:59.520 DEBUG 22056 --- [nio-8081-exec-1] c.lucifer.mapper.TxLogMapper.selectById  : <==      Total: 0
 Time:17 ms - ID:com.lucifer.mapper.TxLogMapper.selectById
Execute SQL:SELECT tx_num,create_time FROM tx_log WHERE tx_num='4dadbd02-735f-43f3-b10e-5863c833406c'

java.lang.RuntimeException: 人爲異常

當訂單下了4單,剩餘庫存爲6時,人爲異常拋出,此時數據庫:

(2)場景3:模擬異常的發生,在storage-service中:

庫存微服務 控制檯:一直打印,去扣減MySQL的庫存,不過因爲此處人爲製造異常,庫存只要爲5,就會去拋異常

java.lang.RuntimeException: 扣減庫存失敗
	at com.lucifer.transaction.ConsumerTransactionListener.onMessage(ConsumerTransactionListener.java:37) ~[classes/:na]
	at com.lucifer.transaction.ConsumerTransactionListener.onMessage(ConsumerTransactionListener.java:18) ~[classes/:na]
	at org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer$DefaultMessageListenerConcurrently.consumeMessage(DefaultRocketMQListenerContainer.java:308) ~[rocketmq-spring-boot-2.0.2.jar:2.0.2]
	at org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService$ConsumeRequest.run(ConsumeMessageConcurrentlyService.java:417) [rocketmq-client-4.4.0.jar:4.4.0]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_192]
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) [na:1.8.0_192]
	at java.util.concurrent.FutureTask.run(FutureTask.java) [na:1.8.0_192]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_192]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_192]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_192]

2020-04-21 19:31:31.243 DEBUG 15160 --- [essageThread_12] c.lucifer.mapper.TxLogMapper.selectById  : ==> Parameters: f60a6c44-292b-47b7-8bfd-6aedaeb26a8e(String)
java.lang.RuntimeException: 人爲異常

此時,order數據庫中有10個訂單,而庫存數據庫中庫存爲6,有6單是扣減失敗的,所以此時庫存微服務會不停的去嘗試扣減庫存(嘗試次數好像是16次),一般情況下,不會讓rocketmq重試那麼多次,重試幾次差不多了,還是無法扣減只能人工扣減了。

此時將IDEA中的人爲異常註釋掉,重新編譯,會發現,庫存微服務扣減成功了,然後數據庫中訂單數爲10,庫存數扣減爲0 了。

 

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