Zookeeper + Redisson [realize data coordination + distributed lock]

Table of contents

1. Add pom dependencies

2. Configure application.yml

3. zookeeper module

4. Add RedissonConfig configuration file

5. The reference core code is as follows


1. Add pom dependencies

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.jmh</groupId>
    <artifactId>seckill</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seckill</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <!--<spring-boot.version>2.4.1</spring-boot.version>-->
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.44</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- mybatis plus依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--mybatis-plus生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- MD5依赖 -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <!-- valid验证依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--hariki-->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        <!--rabbitmq 消息队列-->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--zookeeper客户端-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <!-- 需要与集群中版本号一致 -->
            <version>3.6.2</version>
        </dependency>
        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.5.0</version>
        </dependency>
        <!--commons-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

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

</project>

2. Configure application.yml

#配置redission
spring:
  redis:
    #服务端IP
    host: 127.0.0.1
    #端口
    port: 6379
    #密码
    password: 1234
    #选择数据库
    database: 0
    #超时时间
    timeout: 10000ms
    #Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问
    #Lettuce线程安全,Jedis线程非安全
    lettuce:
      pool:
        #最大连接数,默认8
        max-active: 8
        #最大连接阻塞等待时间,默认-1
        max-wait: 10000ms
        #最大空闲连接,默认8
        max-idle: 200
        #最小空闲连接,默认0
        min-idle: 5
#配置zookeeper
zookeeper:
  #连接IP地址
  server: 192.168.119.128:2181
  #会话超时时间
  sessionTimeoutMs: 60000
  #连接超时时间
  connectionTimeoutMs: 60000
  #最大重试次数
  maxRetries: 3
  #重试间隔时间
  baseSleepTimeMs: 1000

3. zookeeper module

  • ZookeeperClient 

package com.jmh.seckill.zookeeper;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;

import java.util.List;

@Data
@Slf4j
@SuppressWarnings("all")
public class ZookeeperClient {

    /*
    zookeepernode节点的类型:
    1.持久化目录节点(PERSISTENT), 创建好之后就永久存在
    2.持久化顺序编号节点(PERSISTENT_SEQUENTIAL),创建好节点后还可以默认带个自增的编号
    3.临时目录节点(EPHEMERAL),和sessionId绑定的,当客户端被关闭之后,对于的临时目录节点会被删除
    4.历史顺序编号目录节点(EPHEMERAL_SEQUENTIAL),临时节点带个自增的编号
    5.容器节点(Container),3.5.3新增的特性,没有子节点的容器节点会被清除掉。
    6.TTL节点,3.5.3新增的特性,位节点设定了失效时间。具体失效时间却决于后台检测失效线程的轮询频率。
    */

    private CuratorFramework client;
    //NodeCache	监听节点对应增、删、改操作
    private NodeCache nodeCache;
    //PathChildrenCache	监听节点下一级子节点的增、删、改操作
    private PathChildrenCache pathChildrenCache;
    //TreeCache	可以将指定的路径节点作为根节点,对其所有的子节点操作进行监听,呈现树形目录的监听
    private TreeCache treeCache;

    //CuratorCache用于替换NodeCache/TreeCache/PathChildrenCache
    //private CuratorCache curatorCache;

    private String zookeeperServer;
    private int sessionTimeoutMs;
    private int connectionTimeoutMs;
    private int baseSleepTimeMs;
    private int maxRetries;

    /**
     * 初始化
     */
    public void init() {
        client = CuratorFrameworkFactory.builder()
                .connectString(zookeeperServer)
                .sessionTimeoutMs(sessionTimeoutMs)
                .retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries))
                .build();
        client.start();
        log.info("===============> zookeeper init");
    }

    /**
     * 销毁
     */
    public void destory() {
        if (null != client)
            CloseableUtils.closeQuietly(client);
        if (null != nodeCache)
            CloseableUtils.closeQuietly(nodeCache);
        if (null != treeCache)
            CloseableUtils.closeQuietly(treeCache);
        if (null != pathChildrenCache)
            CloseableUtils.closeQuietly(pathChildrenCache);
       /* if(null!=curatorCache)
            CloseableUtils.closeQuietly(curatorCache);*/
        log.info("===============> zookeeper destory");
    }

    /**
     * 创建永久Zookeeper节点
     *
     * @param nodePath  节点路径(如果父节点不存在则会自动创建父节点),如:/curator
     * @param nodeValue 节点数据
     * @return 返回创建成功的节点路径
     */
    public String createPersistentNode(String nodePath, String nodeValue) {
        try {
            return client.create().creatingParentsIfNeeded()
                    .forPath(nodePath, nodeValue.getBytes());
        } catch (Exception e) {
            log.error("创建永久Zookeeper节点失败,nodePath:{},nodeValue:{},e={}", nodePath, nodeValue, e.getMessage());
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 创建永久有序Zookeeper节点
     *
     * @param nodePath  节点路径(如果父节点不存在则会自动创建父节点),如:/curator
     * @param nodeValue 节点数据
     * @return 返回创建成功的节点路径
     */
    public String createSequentialPersistentNode(String nodePath, String nodeValue) {
        try {
            return client.create().creatingParentsIfNeeded()
                    .withMode(CreateMode.PERSISTENT_SEQUENTIAL)
                    .forPath(nodePath, nodeValue.getBytes());
        } catch (Exception e) {
            log.error("创建永久有序Zookeeper节点失败,nodePath:{},nodeValue:{}", nodePath, nodeValue);
        }
        return null;
    }

    /**
     * 创建临时Zookeeper节点
     *
     * @param nodePath  节点路径(如果父节点不存在则会自动创建父节点),如:/curator
     * @param nodeValue 节点数据
     * @return 返回创建成功的节点路径
     */
    public String createEphemeralNode(String nodePath, String nodeValue) {
        try {
            return client.create().creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .forPath(nodePath, nodeValue.getBytes());
        } catch (Exception e) {
            log.error("创建临时Zookeeper节点失败,nodePath:{},nodeValue:{}", nodePath, nodeValue);
        }
        return null;
    }

    /**
     * 创建临时有序Zookeeper节点
     *
     * @param nodePath  节点路径(如果父节点不存在则会自动创建父节点),如:/curator
     * @param nodeValue 节点数据
     * @return 返回创建成功的节点路径
     */
    public String createSequentialEphemeralNode(String nodePath, String nodeValue) {
        try {
            return client.create().creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(nodePath, nodeValue.getBytes());
        } catch (Exception e) {
            log.error("创建临时有序Zookeeper节点失败,nodePath:{},nodeValue:{}", nodePath, nodeValue);
        }
        return null;
    }

    /**
     * 检查Zookeeper节点是否存在
     *
     * @param nodePath 节点路径
     * @return 如果存在则返回true
     */
    public boolean checkExists(String nodePath) {
        try {
            Stat stat = client.checkExists().forPath(nodePath);
            return stat != null;
        } catch (Exception e) {
            log.error("检查Zookeeper节点是否存在出现异常,nodePath:{}", nodePath);
        }
        return false;
    }

    /**
     * 获取某个Zookeeper节点的所有子节点
     *
     * @param nodePath 节点路径
     * @return 返回所有子节点的节点名
     */
    public List<String> getChildren(String nodePath) {
        try {
            return client.getChildren().forPath(nodePath);
        } catch (Exception e) {
            log.error("获取某个Zookeeper节点的所有子节点出现异常,nodePath:{}", nodePath);
        }
        return null;
    }

    /**
     * 获取某个Zookeeper节点的数据
     *
     * @param nodePath 节点路径
     * @return
     */
    public String getData(String nodePath) {
        try {
            return new String(client.getData().forPath(nodePath));
        } catch (Exception e) {
            log.error("获取某个Zookeeper节点的数据出现异常,nodePath:{}", nodePath);
        }
        return null;
    }

    /**
     * 设置某个Zookeeper节点的数据
     *
     * @param nodePath 节点路径
     */
    public void setData(String nodePath, String newNodeValue) {
        try {
            client.setData().forPath(nodePath, newNodeValue.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("设置某个Zookeeper节点的数据出现异常,nodePath:{}", nodePath);
        }
    }

    /**
     * 删除某个Zookeeper节点
     *
     * @param nodePath 节点路径
     */
    public void delete(String nodePath) {
        try {
            client.delete().guaranteed().forPath(nodePath);
        } catch (Exception e) {
            log.error("删除某个Zookeeper节点出现异常,nodePath:{}", nodePath);
        }
    }

    /**
     * 级联删除某个Zookeeper节点及其子节点
     *
     * @param nodePath 节点路径
     */
    public void deleteChildrenIfNeeded(String nodePath) {
        try {
            client.delete().guaranteed().deletingChildrenIfNeeded().forPath(nodePath);
        } catch (Exception e) {
            log.error("级联删除某个Zookeeper节点及其子节点出现异常,nodePath:{}", nodePath);
        }
    }

    /**
     * <p><b>注册节点监听器</b></p>
     * NodeCache: 对一个节点进行监听,监听事件包括指定路径的增删改操作
     *
     * @param nodePath 节点路径
     * @return void
     */
    public NodeCache registerNodeCacheListener(String nodePath) {
        try {
            //1. 创建一个NodeCache
            nodeCache = new NodeCache(client, nodePath);

            //2. 添加节点监听器
            nodeCache.getListenable().addListener(() -> {
                ChildData childData = nodeCache.getCurrentData();
                if (childData != null) {
                    System.out.println("Path: " + childData.getPath());
                    System.out.println("Stat:" + childData.getStat());
                    System.out.println("Data: " + new String(childData.getData()));
                }
            });

            //3. 启动监听器
            nodeCache.start();

            //4. 返回NodeCache
            return nodeCache;
        } catch (Exception e) {
            log.error("注册节点监听器出现异常,nodePath:{}", nodePath);
        }
        return null;
    }

    /**
     * <p><b>注册子目录监听器</b></p>
     * PathChildrenCache:对指定路径节点的一级子目录监听,不对该节点的操作监听,对其子目录的增删改操作监听
     *
     * @param nodePath 节点路径
     * @param listener 监控事件的回调接口
     * @return PathChildrenCache
     */
    public PathChildrenCache registerPathChildListener(String nodePath,
                                                       PathChildrenCacheListener listener) {
        try {
            //1. 创建一个PathChildrenCache
            pathChildrenCache = new PathChildrenCache(client, nodePath, true);

            //2. 添加目录监听器
            pathChildrenCache.getListenable().addListener(listener);

            //3. 启动监听器
            pathChildrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);

            //4. 返回PathChildrenCache
            return pathChildrenCache;
        } catch (Exception e) {
            log.error("注册子目录监听器出现异常,nodePath:{}", nodePath);
        }
        return null;
    }

    /**
     * <p><b>注册目录监听器</b></p>
     * TreeCache:综合NodeCache和PathChildrenCahce的特性,可以对整个目录进行监听,同时还可以设置监听深度
     *
     * @param nodePath 节点路径
     * @param maxDepth 自定义监控深度
     * @param listener 监控事件的回调接口
     * @return TreeCache
     */
    public TreeCache registerTreeCacheListener(String nodePath, int maxDepth, TreeCacheListener listener) {
        try {
            //1. 创建一个TreeCache
            treeCache = TreeCache.newBuilder(client, nodePath)
                    .setCacheData(true)
                    .setMaxDepth(maxDepth)
                    .build();

            //2. 添加目录监听器
            treeCache.getListenable().addListener(listener);

            //3. 启动监听器
            treeCache.start();

            //4. 返回TreeCache
            return treeCache;
        } catch (Exception e) {
            log.error("注册目录监听器出现异常,nodePath:{},maxDepth:{1}", nodePath);
        }
        return null;
    }


}
  •  ZookeeperConfiguration
package com.jmh.seckill.zookeeper;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
@SuppressWarnings("all")
//@ConfigurationProperties(prefix = "zookeeper")
public class ZookeeperConfiguration {

    @Value("${zookeeper.server}")
    private String zookeeperServer;
    @Value("${zookeeper.sessionTimeoutMs}")
    private int sessionTimeoutMs;
    @Value("${zookeeper.connectionTimeoutMs}")
    private int connectionTimeoutMs;
    @Value("${zookeeper.maxRetries}")
    private int maxRetries;
    @Value("${zookeeper.baseSleepTimeMs}")
    private int baseSleepTimeMs;

    @Bean(initMethod = "init", destroyMethod = "destory")
    public ZookeeperClient zookeeperClient() {
        ZookeeperClient zookeeperClient = new ZookeeperClient();
        zookeeperClient.setZookeeperServer(zookeeperServer);
        zookeeperClient.setSessionTimeoutMs(sessionTimeoutMs);
        zookeeperClient.setConnectionTimeoutMs(connectionTimeoutMs);
        zookeeperClient.setBaseSleepTimeMs(baseSleepTimeMs);
        zookeeperClient.setMaxRetries(maxRetries);
        return zookeeperClient;
    }

}

4. Add RedissonConfig configuration file

package com.jmh.seckill.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
 
    @Value("${spring.redis.host}")
    private String host;
 
    @Value("${spring.redis.port}")
    private String port;
 
    @Value("${spring.redis.password}")
    private String password;
 
    @Bean
    public RedissonClient redissonClient(){
        Config config=new Config();
        String url="redis://"+host+":"+port;
        config.useSingleServer().setAddress(url).setPassword(password).setDatabase(0);
        return Redisson.create(config);
    }
}

 5. The reference core code is as follows

package com.jmh.seckill.controller;

import com.jmh.seckill.exception.BusinessException;
import com.jmh.seckill.pojo.SeckillGoods;
import com.jmh.seckill.pojo.SeckillOrder;
import com.jmh.seckill.pojo.User;
import com.jmh.seckill.service.IRedisService;
import com.jmh.seckill.service.ISeckillGoodsService;
import com.jmh.seckill.service.ISeckillOrderService;
import com.jmh.seckill.utils.JsonResponseBody;
import com.jmh.seckill.utils.JsonResponseStatus;
import com.jmh.seckill.zookeeper.ZookeeperClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>
 * 秒杀订单信息表 前端控制器
 * </p>
 *
 * @author jmh
 * @since 2023-01-12
 */
@RestController
@RequestMapping("/seckillOrder")
@Slf4j
public class SeckillOrderController  implements InitializingBean {

    @Autowired
    private ISeckillOrderService seckillOrderService;

    @Autowired
    private IRedisService redisService;

    @Autowired
    private ISeckillGoodsService seckillGoodsService;

    @Autowired
    private ZookeeperClient zookeeperClient;

    @Autowired
    private RedissonClient redissonClient;

    //定义内存标记
    private Map<String,Boolean> stockMap=new ConcurrentHashMap<>();


    /**
     * 服务预热方法
     * @throws Exception 异常
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        seckillGoodsService.list().forEach(e->{
            redisService.setSeckillGoods(e.getGoodsId(),Long.valueOf(e.getStockCount()));
            //创造内存标记
            String key="/seckillGoodsId/"+e.getGoodsId();
            //设置内存标记相应的key和值,false代表没卖完,true代表已卖完
            stockMap.put(key,false);
            //给zookeeper制造key和值
            if (zookeeperClient.checkExists(key))
                zookeeperClient.setData(key, "false");
            else
                zookeeperClient.createPersistentNode(key, "false");
            log.warn(e.getGoodsId() + "/" + e.getStockCount());
        });
        //注册zookeeper目录监听器,用于监听指定目录下的节点值变化
        zookeeperClient.registerPathChildListener("/seckillGoodsId", new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework client,
                                   PathChildrenCacheEvent event) throws Exception {
                ChildData data = event.getData();
                String flag = new String(data.getData());
                switch (event.getType()) {
                    case CHILD_ADDED:
                        log.info("子节点增加, path={}, data={}", data.getPath(), flag);
                        break;
                    case CHILD_UPDATED:
                        log.info("子节点更新, path={}, data={}", data.getPath(), flag);
                        // 如果zookeeper中的商品节点标记为true,则代表秒杀商品已经秒杀完毕
                        // 修改内存标记为true(代表秒杀商品库存不足)
                        if ("true".equals(flag)) {
                            stockMap.put(data.getPath(), true);
                        }
                        break;
                    case CHILD_REMOVED:
                        log.info("子节点删除, path={}, data={}", data.getPath(), flag);
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /**
     * 秒杀商品订单生成方法
     * @param goodsId 商品编号
     * @param user 用户对象
     * @return 结果
     */
    @RequestMapping("/addOrder")
    public JsonResponseBody<?> addOrder(Long goodsId, User user){
        //制造Jvm内存标记判断秒杀商品是否已经卖完
        if (stockMap.get("/seckillGoodsId/"+goodsId))
            throw new BusinessException(JsonResponseStatus.ORDER_COUNT);
        //限购:判断用户是否重复抢购[redis]
        SeckillOrder byUidAndGoodsIdToSeckillOrder = redisService.getByUidAndGoodsIdToSeckillOrder(user.getId(), goodsId);
        if (null!=byUidAndGoodsIdToSeckillOrder)
            throw new BusinessException(JsonResponseStatus.ORDER_RESP);
        //使用redisson分布式锁
        RLock clientLock = redissonClient.getLock("/seckillGoodsId/"+goodsId);
        clientLock.lock();
        try {
            //判断秒杀商品库存是否充足[redis]
            if (redisService.decrement(goodsId) < 0) {
                redisService.increment(goodsId);
                //将zookeeper监听的值改为true
                zookeeperClient.setData("/seckillGoodsId/" + goodsId, "true");
                throw new BusinessException(JsonResponseStatus.ORDER_COUNT);
            }
        }catch (Exception e){
            if (e instanceof BusinessException){
                throw new BusinessException(JsonResponseStatus.ORDER_COUNT);
            }
                throw new BusinessException(JsonResponseStatus.ERROR);
        } finally {
            //解锁
            clientLock.unlock();
        }
        //生成秒杀订单
        SeckillOrder msOrder=new SeckillOrder();
        msOrder.setUserId(user.getId());
        msOrder.setGoodsId(goodsId);
        //将订单数据交给rabbitMQ进行异步处理订单
        seckillOrderService.sendSeckillOrderToRabbitMQ(msOrder);
        return new JsonResponseBody<>();
    }



}

Guess you like

Origin blog.csdn.net/m0_63300795/article/details/128739461