【Solution】商品秒杀之Redis缓存与MQ异步优化以及超卖一人一单等问题的解决

news2024/11/29 1:47:09

目录

一、Demo开始前准备

1、数据库准备

2、项目准备

3、全局唯一id生成器

二、秒杀业务基本实现

1、秒杀基本业务逻辑

2、秒杀接口设计

3、秒杀业务代码实现

4、超卖问题产生

三、保证线程安全解决超卖少卖问题

1、超卖产生的原因

2、加锁方案:乐观锁

3、使用乐观锁少卖问题产生

4、少卖问题产生原因

5、解决少卖问题

四、一人一单基本实现

1、一人一单业务逻辑

2、代码实现

3、一人多买问题产生原因

4、加锁解决一人多买问题时注意点

5、事务未提交锁提前释放问题

五、Redis缓存与MQ异步优化

1、优化思路

2、保证原子性

3、封装Java调用Redis执行lua脚本API

4、MQ相关配置

1.配置文件

2.配置类创建队列

3.封装消费者

4.封装生产者

4、最终代码实现


一、Demo开始前准备

1、数据库准备

create database super_mall;
user super_mall;

create table orders(
  id bigint not null primary key,
  user_id bigint not null,
  product_id bigint not null,
  pay_type int default 1 comment '支付方式 1:余额支付 2:支付宝支付 3:微信支付',
  status int not null default 1 comment '订单状态 1:未支付 2:已支付 3:已退款 4:已核销',
  pay_time timestamp default current_timestamp,
  use_time timestamp default current_timestamp,
  ref_time timestamp default current_timestamp,
  update_time timestamp default current_timestamp
);

create table product(
  id bigint not null primary key,
  shop_id bigint not null,
  stock int not null comment '商品库存',
  product varchar(1024) not null,
  start_time timestamp default current_timestamp,
  end_time timestamp default current_timestamp,
  status int not null default 1 comment '商品状态 1上架 2下架 3缺货',
  price bigint not null,
  photo varchar(255) default null
);

create table userInfo(
  id bigint not null primary key,
  openid varchar(255) not null,
  nickname varchar(255) not null,
  sex int not null,
  photo varchar(255) not null,
  status int default 1 comment '用户状态 1注册 2禁止'
);
insert into userInfo(id,openid,nickname,sex,photo) values(1,"1","用户222",1,"defualt.jpg");

主要有三张表:用户表、商品表、订单表,将上述sql脚本执行一遍即可

2、项目准备

在准备好数据库之后,我们需要创建一个SpringBoot项目

【Java】两张图帮你的社区版IDEA创建SpringBoot项目_idea社区版不支持springboot_西瓜霜润喉片的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/qq_61903414/article/details/130174514?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169771109016800227471663%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=169771109016800227471663&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-130174514-null-null.nonecase&utm_term=%E7%A4%BE%E5%8C%BA&spm=1018.2226.3001.4450

3、全局唯一id生成器

它是一种在分布式系统下用来生成全局唯一ID的工具,它具有唯一性,高可用,高性能,递增性,安全性。如果我们使用数据库中的自增主键则不能保证安全性。如在订单系统中,我们在数据库中有订单表,如果在该订单表中使用数据库的自增主键,它的id规律性太明显且受单表数量的限制,如果订单数量日益增多,后续添加新的订单表时,他的主键又会重新开始。此处我们使用31位时间戳+32位递增数字组合而成,一个long类型8个字节刚好64比特,64位表示符合位,接下来31位表示时间戳最后32位拼接递增的数字,递增数字基于redis实现

@Component
public class RedisIdWorker {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public long nextId(String prefixKey) {
        // 1. 生成时间戳
        long timestamp = System.currentTimeMillis();;

        // 2. 生成序列号
        String day = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        Long count = stringRedisTemplate.opsForValue().increment("icr:" + prefixKey + ":" + day);

        // 3.拼接后返回
        return timestamp << 32 | count;
    }
}

二、秒杀业务基本实现

1、秒杀基本业务逻辑

首先我们需要从前端传回的参数中获取要购买的商品id,然后根据商品id进行查询信息,看库存是否足够,如果足够则扣减库存、生成订单进行下单

2、秒杀接口设计

controller层代码

@Api(tags = "商品API")
@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @ApiOperation(value = "秒杀")
    @PostMapping("/order")
    public Return order(@RequestParam("id") @NotNull Long id) {
        if (id <= 0) {
            return Return.fail(Code.REQUEST_FAIL);
        }

        return productService.order(id);
    }
}

3、秒杀业务代码实现


@Slf4j
@Service
public class ProductService {

    @Autowired
    private TokenUtil tokenUtil;

    @Autowired
    private RabbitMqProduct rabbitMqProduct;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Transactional
    public Return order(Long id) {
        // 1. 根据id查询商品信息
        Product product = productMapper.queryById(id);

        // 2. 判断库存是否足够
        Integer stock = product.getStock();
        if (stock <= 0) {
            return Return.fail(Code.ORDER_STOCK);
        }

        // 3. 扣减库存
        int subtract = productMapper.subtract(id);
        if (subtract != 1) {
            return Return.fail(Code.ORDER_FAIL);
        }

        // 4. 生成订单信息
        Order order = new Order();
        Long orderId = redisIdWorker.nextId("order");
        order.setId(orderId);
        order.setProductId(id);
        Long userId = 1L;  // todo: 后续从会话中获取用户信息
        order.setUserId(userId);
        orderMapper.add(order);

        // 5. 返回订单号
        return Return.success(Code.ORDER_SUCCESS,orderId);
    }
}

mapper层:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ProductMapper">
    <resultMap id="product" type="com.example.demo.pojo.entity.Product">
        <id property="id" column="id"/>
        <result property="photo" column="photo"/>
        <result property="price" column="price"/>
        <result property="product" column="product"/>
        <result property="shopId" column="shop_id"/>
        <result property="startTime" column="start_time"/>
        <result property="endTime" column="end_time"/>
        <result property="status" column="status"/>
        <result property="stock" column="stock"/>
    </resultMap>
    <insert id="insert">
        insert into product(id,shop_id,stock,product,price,photo,start_time,end_time) values(#{id},#{shopId},#{stock},#{product},#{price},#{photo},#{startTime},#{endTime})
    </insert>

    <select id="queryById" resultMap="product">
        select * from product where id=#{id}
    </select>

    <update id="subtract">
        update product set stock=stock-1 where id=#{id}
    </update>
</mapper>

4、超卖问题产生

在上面的代码中,如果商品A在某一时刻的库存仅为1了,此时多个用户的线程访问下单接口,第一个线程查询商品信息后发现库存足够,但是还没有进行扣减库存生成订单操作。这个时候另外的一些线程也去查询了商品信息发现库存足够,于是也去进行下单操作。于是使得库存为负,导致商品超卖

三、保证线程安全解决超卖少卖问题

1、超卖产生的原因

由此可见上述产生线程安全问题是因为判断库存与扣减操作不是原子性的,那么该如何去解决呢?如果使用悲观锁,给判断与扣减库存操作进行加锁操作,那么所有的下单操作都是串行,该接口性能极差用户体验不佳。我们可以使用乐观锁

2、加锁方案:乐观锁

乐观锁主要有两种方式,首先可以使用版本号法,维护一个版本号,每次修改都使得版本号+1,在进行修改时判断一下版本号是否相同,如果不同则修改失败。比如有两个线程,第一个线程查询库存为1版本号为1可以进行扣减库存操作,于是在修改时判断一些版本号是否一致,此时发现都是1,于是扣减成功版本号+1变为2,这个时候第二个线程在第一个线程扣减之前查询到库存为1版本号为1,于是也去进行扣减操作,判断版本号时线程2查询的版本号为1但是由于被线程1修改了所以真正的版本号不再是1而是2于是扣减失败。还有一种就是CAS方法,与上述类似,在扣减库存操作时判断查询到的库存与数据库中的库存是否相同,相同的成功反之失败。比如此时有两个线程都查询到数据库中该商品额库存为1,此时线程1执行扣减库存操作,这个时候会比较他当时查询出来的库存1是否与数据库中库存1一样,此处一致则扣减成功,库存变为0,此时线程2再去进行扣减操作的时候进行比较,线程2查询时的库存为1但此时数据库中的库存已经为0了,于是扣减失败。这里我们实现时采用第二种方式,他不需要引入额外的字段:版本号。我们在实现时只需要将扣减库存的sql语句进行修改即可

update product set stock=stock-1 where id=#{id} and stock=#{stock}

3、使用乐观锁少卖问题产生

在上述实现中我们解决了超卖问题,但是新的问题又来了,由于这个秒杀商品所以该接口一定会被大量的线程所访问,如果此时商品库存有200个或者刚开始秒杀。当两个用户访问该接口时,他们都同时查询到了库存为200于是都去进行扣减操作,线程1进行扣减操作时数据库中的库存200与查询出的库存200相同则扣减成功,库存变为199,这个时候线程2再去进行扣减操作时发现他查询出来的库存为200但是数据库中的库存确是199于是下单失败。由此可见库存足够却下单失败

4、少卖问题产生原因

在上述描述中我们可以了解到是由于乐观锁实现时导致库存足够却下单失败的原因

5、解决少卖问题

商品只要库存足够就可以进行下单,在这里我们可以对上述乐观锁进行修改,将条件判断不在是判断库存是否相同,而是判断库存此时是否大于0也就是是否足够,这个时候就能解决超卖少卖问题

update product set stock=stock-1 where id=#{id} and stock > 0

四、一人一单基本实现

1、一人一单业务逻辑

在上述秒杀代码的基础上我们需要对下单操作进行限制,一个人只能下单一次,所以我们需要在上述扣减库存操作之前进行判断,判断该用户是否已经下过单,如果已经下单则返回下单失败

2、代码实现

package com.example.demo.service;

import com.example.demo.component.RedisIdWorker;
import com.example.demo.enums.Code;
import com.example.demo.mapper.OrderMapper;
import com.example.demo.mapper.ProductMapper;
import com.example.demo.pojo.entity.Order;
import com.example.demo.pojo.entity.Product;
import com.example.demo.util.Return;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
    public class ProductService {

        @Autowired
        private OrderMapper orderMapper;

        @Autowired
        private ProductMapper productMapper;

        @Autowired
        private RedisIdWorker redisIdWorker;

        @Transactional
        public Return order(Long id) {
            // 1. 查询商品
            Product product = productMapper.queryById(id);


            // 4. 判断库存是否足够
            if (product.getStock() <= 0) {
                return Return.fail(Code.ORDER_STOCK);
            }

            // 5. 判断订单是否存在(用户是否已下单)
            // 5.1 获取用户id
            Long userId = 1L;  // TODO: 2023/10/14 后续从Token获取
            // 5.2 根据商品id与用户id查询订单表
            int count = orderMapper.queryByUserIdAndId(userId,product.getId());
            if (count != 0) {
                return Return.fail(Code.ORDER_TWO);
            }

            // 6. 扣减库存
            int subtract = productMapper.subtract(id);
            if (subtract != 1) {
                return Return.fail(Code.ORDER_FAIL);
            }

            // 7. 生成订单
            // 7.1 订单id
            Order order = new Order();
            order.setId(redisIdWorker.nextId("order"));
            // 7.2 用户id
            order.setUserId(userId);
            // 7.3 商品id
            order.setProductId(product.getId());
            // 7.4 入库
            int isSuccess = orderMapper.add(order);
            if (isSuccess != 1) {
                return Return.fail(Code.ORDER_FAIL);
            }

            // 9. 返回订单id
            return Return.success(Code.ORDER_SUCCESS,order.getId());
        }
    }

3、一人多买问题产生原因

上述代码的实现如果有用户的多个线程来访问该接口,此时同一个用户有两个线程来访问该接口,线程1查询完订单表没有该用户购买该商品订单信息去进行扣减库存生成订单操作之前,线程2也查询完订单表也没有该用户购买该商品的订单,于是也去进行扣减库存生成订单,于是同一个用户购买了多次,并没有达到一人一单的效果。产生这一问题是因为查询订单与生成订单操作并不是原子性的,于是这里我们可以采用加锁的办法去实现

4、加锁解决一人多买问题时注意点

那么我们如何去加锁呢?我们需要对查询订单信息与生成订单的代码进行加锁操作,那么锁对象如何是什么呢?这里如果直接使用类对象或者类属性进行加锁,那么不同用户的线程访问时也需要串行执行,所以我们不能无脑加锁,此处产生线程安全问题的原因是同一用户的不同线程,所以我们可以对该用户的id进行加锁,只有同一个用户的不同线程访问时才会有锁竞争。此处还要注意的是用户的id他是一个Long类型的数据,同一用户的不同线程每次访问时他的id在堆中的地址并不是一致的,每次都会发生变化,那么锁对象也就没有意义,我们可以将用户id转为字符串并使用intern()方法将他存入字符串常量池,这样同一个用户锁对象的地址就不会发送变化。此处我们将用户下单操作抽取为方法,在上述代码中进行完库存判断后直接调用该方法即可

@Transactional
    private Return createOrder(Long id) {
        // 5. 判断订单是否存在(用户是否已下单)
        // 5.1 获取用户id
        Long userId = 1L;  // TODO: 2023/10/14 后续从Token获取
        synchronized (userId.toString().intern()) {
            // 5.2 根据商品id与用户id查询订单表
            int count = orderMapper.queryByUserIdAndId(userId, id);
            if (count != 0) {
                return Return.fail(Code.ORDER_TWO);
            }

            // 6. 扣减库存
            int subtract = productMapper.subtract(id);
            if (subtract != 1) {
                return Return.fail(Code.ORDER_FAIL);
            }

            // 7. 生成订单
            // 7.1 订单id
            Order order = new Order();
            order.setId(redisIdWorker.nextId("order"));
            // 7.2 用户id
            order.setUserId(userId);
            // 7.3 商品id
            order.setProductId(id);
            // 7.4 入库
            int isSuccess = orderMapper.add(order);
            if (isSuccess != 1) {
                return Return.fail(Code.ORDER_FAIL);
            }

            // 9. 返回订单id
            return Return.success(Code.ORDER_SUCCESS, order.getId());
        }
    }

5、事务未提交锁提前释放问题

上述代码存在一个新的问题,当方法执行完成锁会被释放,但是此时事务还没有提交,数据库中还是没有订单信息,此时该用户的其他线程就会获取到锁,判断订单表中没有该用户购买该商品的信息,于是进行下单操作,产生 问题。我们只需要让事务提交发生在锁释放之前即可,将锁的粒度进行修改

package com.example.demo.service;

import com.example.demo.component.RedisIdWorker;
import com.example.demo.enums.Code;
import com.example.demo.mapper.OrderMapper;
import com.example.demo.mapper.ProductMapper;
import com.example.demo.pojo.entity.Order;
import com.example.demo.pojo.entity.Product;
import com.example.demo.util.Return;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
public class ProductService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private RedisIdWorker redisIdWorker;

    public Return order(Long id) {
        // 1. 查询商品
        Product product = productMapper.queryById(id);

        // 2. 判断秒杀是否开始
        if (product.getStartTime().isAfter(LocalDateTime.now())) {
            return Return.fail(Code.ORDER_START);
        }


        // 5. 创建订单
        Long userId = 1L;  // TODO: 2023/10/14 后续从Token获取
        synchronized (userId.toString().intern()) {
            ProductService proxy = (ProductService) AopContext.currentProxy();
            return proxy.createOrder(userId,id);
        }
    }

    @Transactional
    private Return createOrder(Long userId, Long id) {
        // 5. 判断订单是否存在(用户是否已下单)
        // 5.1 获取用户id
        // 5.2 根据商品id与用户id查询订单表
        int count = orderMapper.queryByUserIdAndId(userId, id);
        if (count != 0) {
            return Return.fail(Code.ORDER_TWO);
        }

        // 6. 扣减库存
        int subtract = productMapper.subtract(id);
        if (subtract != 1) {
            return Return.fail(Code.ORDER_FAIL);
        }

        // 7. 生成订单
        // 7.1 订单id
        Order order = new Order();
        order.setId(redisIdWorker.nextId("order"));
        // 7.2 用户id
        order.setUserId(userId);
        // 7.3 商品id
        order.setProductId(id);
        // 7.4 入库
        int isSuccess = orderMapper.add(order);
        if (isSuccess != 1) {
            return Return.fail(Code.ORDER_FAIL);
        }

        // 9. 返回订单id
        return Return.success(Code.ORDER_SUCCESS, order.getId());

    }

}

五、Redis缓存与MQ异步优化

1、优化思路

上述代码中我们解决了线程安全的问题,但是由于秒杀接口是一个被高并发访问的接口,而上述的实现中数据库读写操作太多,这样在高并发的情况下对数据库的压力太大,此时我们可以对该代码进行分析优化,上述代码其实主要分为两步:1.进行数据库读操作判断用户是否有下单的权限 2.如果有则进行数据库写操作扣减库存插入订单 这个时候我们可以将数据库读操作使用redis做缓存处理来减缓数据库的压力,将库存信息与订单信息进行缓存处理,请求到达服务器去查询缓存,如果有下单权限,我们可以采用MQ异步地进行数据库写操作来减缓数据库压力。

首先我们需要思考在redis中我们需要做什么?首先是对商品库存的查询,判断商品的库存是否足够其次需要判断该用户是否已经下单。在商品库存查询时我们可以使用redis中的string类型来处理,那如何判断用户是否已经下过单,我们可以使用set数据类型,他的特点是value都是唯一的,我们可以以商品的id作为key的组成,以下单用户的id为value存入其中,我们只需要根据商品id去查询该set中是否有该用户的id如果有就是已经购买,则不能继续购买,没有则将redis缓存中的库存扣减并在set集合中添加该用户的id,那么在redis中判断库存是否足够、判断用户是否下单与扣减库存插入用户id四个命令不是原子性的,会存在线程安全问题。这个时候我们可以通过lua来保证这四个命令的原子性。最后我们需要通过MQ异步的将扣减库存与生成订单操作入库

2、保证原子性

-- 获取参数
-- 1.商品id
local productId = ARGV[1];
-- 2.用户id
local userId = ARGV[2];

-- 构造缓存的key
-- 1.订单key
local orderKey = "order:order:" .. productId;
-- 2.库存id
local stockKey = "order:stock:" .. productId;

-- 判断库存是否足够
if (tonumber(redis.call('get', stockKey)) <= 0) then
    -- 库存不足 返回1
    return 1;
end

-- 判断是否下过单
if (redis.call('sismember',orderKey,userId) == 1) then
    -- 存在 返回2
    return 2;
end

-- 满足扣减库存
redis.call('incrby',stockKey,-1);
-- 下单:缓存订单中加入该用户
redis.call("sadd",orderKey,userId);
-- 返回0
return 0

3、封装Java调用Redis执行lua脚本API

/**
 * 封装Java调用redis执行lua脚本的API
 */
public class LuaUtil {
    /**
     *
     * @param type 返回类型
     * @param luaScriptPath 脚本路径
     * @param stringRedisTemplate redisTemplate
     * @param keys lua脚本所需要的keys
     * @param args lua脚本所需要的args
     * @param <T> 返回值T
     * @return 返回lua执行结果
     */
    public static <T> T execute(Class<T> type,
                                String luaScriptPath,
                                StringRedisTemplate stringRedisTemplate,
                                List<String> keys,Object... args) {
        // 1. 初始化DefaultRedisScript
        DefaultRedisScript<T> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(type);
        redisScript.setLocation(new ClassPathResource(luaScriptPath));

        // 2. 执行lua脚本
        T result = stringRedisTemplate.execute(redisScript, keys, args);

        // 3. 返回结果
        return result;
    }

4、MQ相关配置

1.配置文件

配置文件中开启confirm、return、ack模式确保消息可靠性

rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    virtual-host: /super_mall
    publisher-confirm-type: correlated #??????
    publisher-returns: true            #??????
    listener:
      simple:
        acknowledge-mode: manual       #??????
        prefetch: 10                   #???????10????????????????10?
        retry:
          enabled: true                #????
          max-attempts: 4              #??????
          max-interval: 1000s          #??????

2.配置类创建队列

package com.example.demo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    public static final String ORDER_KILL_QUEUE = "order:kill";
    public static final String ORDER_KILL_EXCHANGE = "order:change";
    public static final String ORDER_KILL_KEY = "order:kill:async";

    // 队列与交换机
    @Bean
    public Queue orderQueue() {
        // 创建队列,并设置持久化
        return QueueBuilder.durable(ORDER_KILL_QUEUE).build();
    }

    @Bean
    public DirectExchange orderExchange() {
        // 创建直连交换机,并设置持久化
        return ExchangeBuilder.directExchange(ORDER_KILL_EXCHANGE).durable(true).build();
    }

    // 绑定队列与交换机
    @Bean
    public Binding dlxBind(@Qualifier("orderQueue") Queue dlxQueue, @Qualifier("orderExchange") DirectExchange dlxExchange) {
        return BindingBuilder.bind(dlxQueue).to(dlxExchange).with(ORDER_KILL_KEY);
    }
}

3.封装消费者

消费者开启了ack模式

@Slf4j
@Component
public class RabbitMqConsumer {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private ProductMapper productMapper;

    @RabbitListener(queues = RabbitMQConfig.ORDER_KILL_QUEUE)
    public void createOrder(Message message, Channel channel) throws IOException {
        long tag = message.getMessageProperties().getDeliveryTag();
        try {
            // 1. 获取消息
            Order order = objectMapper.readValue(message.getBody(), Order.class);
            if (order == null) {
                log.error("消息为空发送失败");
                throw new Exception("消息格式错误");
            }

            // 2. 消费消息
            int subtract = productMapper.subtract(order.getProductId());
            int add = orderMapper.add(order);
            if (subtract != 1 || add != 1) {
                throw new Exception("入库失败,消息重发");
            }

            // 3. 向MQ服务器发生ack
            log.info("订单创建成功:{}",order.toString());
            channel.basicAck(tag, true);
        } catch (Exception e) {
            // 4. 应答消息处理失败,允许重复投递
            channel.basicNack(tag, true, true);
        }
    }
}

4.封装生产者

在这里需要注意可能会报出一下错误

java.lang.IllegalStateException: Only one ConfirmCallback is supported by each RabbitTemplate
    at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-5.3.26.jar:5.3.26]
    at org.springframework.amqp.rabbit.core.RabbitTemplate.setConfirmCallback(RabbitTemplate.java:469) ~[spring-rabbit-2.4.11.jar:2.4.11]
    at com.example.demo.component.RabbitMqProduct.send(RabbitMqProduct.java:34) ~[classes/:na]
    at com.example.demo.service.ProductService.order(ProductService.java:68) ~[classes/:na]
    at com.example.demo.controller.ProductController.order(ProductController.java:23) ~[classes/:na]
    at sun.reflect.GeneratedMethodAccessor14.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_192]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_192]
 

报错"每个RabbitTemplate只支持一个ConfirmCallback"的原因是在 `send()` 方法中多次设置了相同的 ConfirmCallback 实例给同一个 RabbitTemplate 对象。

在每次调用 send()方法时,都会创建一个新的 ConfirmCallback实例并设置给 RabbitTemplate` ,这导致了多个 ConfirmCallback 被设置到同一个 RabbitTemplate上,从而触发了错误。

为了解决这个问题,可以将 ConfirmCallback的设置提取到类的构造函数中,确保每次创建 RabbitMqProduct 对象时都会创建一个新的 ConfirmCallback实例,并将其设置给相应的 RabbitTemplate 对象。这样每个 RabbitMqProduct 对象都会有自己独立的 ConfirmCallback 。

package com.example.demo.component;

import com.example.demo.config.RabbitMQConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@Scope("prototype")
@Slf4j
public class RabbitMqProduct {
    private final RabbitTemplate rabbitTemplate;
    private final ObjectMapper objectMapper;
    private Object message;

    @Autowired
    public RabbitMqProduct(RabbitTemplate rabbitTemplate, ObjectMapper objectMapper) {
        this.rabbitTemplate = rabbitTemplate;
        this.objectMapper = objectMapper;
        // 创建一个 ConfirmCallback 实例
        RabbitTemplate.ConfirmCallback confirmCallback = (correlationData, ack, cause) -> {
            if (!ack) {
                // 如果消息发送失败,则重新发送
                send(RabbitMQConfig.ORDER_KILL_EXCHANGE, RabbitMQConfig.ORDER_KILL_KEY,message);
            }
            log.info("消息重送成功");
        };
        // 设置 ConfirmCallback
        this.rabbitTemplate.setConfirmCallback(confirmCallback);
        // 当消息无法路由时返回
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setReturnsCallback(returnedMessage -> {
            // 如果消息无法路由,则重新发送
            send(RabbitMQConfig.ORDER_KILL_EXCHANGE, RabbitMQConfig.ORDER_KILL_KEY,returnedMessage.getMessage());
        });
    }

    @SneakyThrows
    public <T> void send(String exchange,String routingKey, T message) {
        // 将消息内容转化为JSON格式并发送
        String json = objectMapper.writeValueAsString(message);
        rabbitTemplate.convertAndSend(exchange, routingKey, json);
    }

    public void setMessage(Object message) {
        this.message = message;
    }
}

4、最终代码实现

@SneakyThrows
    public Return order(Long id) {
        // 1. 执行lua脚本
        Long userId = 1L; // TODO: 2023/10/15 后续修改为会话获取
        Long result = LuaUtil.execute(Long.class, "./lua/order.lua",
                stringRedisTemplate, Collections.emptyList(),
                id.toString(), userId.toString());

        // 2. 判断lua鉴权结果
        int isSuccess = result.intValue();
        if (isSuccess != 0) {
            // 2.1 下单权限不足
            return Return.fail(isSuccess == 1 ? Code.ORDER_STOCK : Code.ORDER_TWO);
        }

        // 3. MQ异步入库
        // 3.1 构造Order对象
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(id);
        Long orderId = redisIdWorker.nextId("order");
        order.setId(orderId);
        product.send(RabbitMQConfig.ORDER_KILL_EXCHANGE,RabbitMQConfig.ORDER_KILL_KEY,order);
        log.info("下单成功,消息进入队列准备入库:{}",order.toString());

        // 4. 返回订单号
        return Return.success(Code.ORDER_SUCCESS,orderId);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1111346.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Go 函数的健壮性、panic异常处理、defer 机制

Go 函数的健壮性、panic异常处理、defer 机制 文章目录 Go 函数的健壮性、panic异常处理、defer 机制一、函数健壮性的“三不要”原则1.1 原则一&#xff1a;不要相信任何外部输入的参数1.2 原则二&#xff1a;不要忽略任何一个错误1.3 原则三&#xff1a;不要假定异常不会发生…

【Linux】如何判断RS-232串口是否能正常使用

1.RS-232串口短接 使用RS-232协议的串口引脚一般如图下所示 为了让串口能够接收到自己发出的串口数据&#xff0c;需要将输出端和输入端&#xff08;RXD和TXD&#xff09;进行短接操作&#xff1a; 短接完成后&#xff0c;才能实现自发自收的功能&#xff08;走其他协议的串口清…

开发者职场“生存状态”大调研报告分析 - 第四版

听人劝、吃饱饭,奉劝各位小伙伴,不要订阅该文所属专栏。 作者:不渴望力量的哈士奇(哈哥),十余年工作经验, 跨域学习者,从事过全栈研发、产品经理等工作,现任研发部门 CTO 。荣誉:2022年度博客之星Top4、博客专家认证、全栈领域优质创作者、新星计划导师,“星荐官共赢计…

Swift使用Embassy库进行数据采集:热点新闻自动生成器

概述 爬虫程序是一种可以自动从网页上抓取数据的软件。爬虫程序可以用于各种目的&#xff0c;例如搜索引擎、数据分析、内容聚合等。本文将介绍如何使用Swift语言和Embassy库编写一个简单的爬虫程序&#xff0c;该程序可以从新闻网站上采集热点信息&#xff0c;并生成一个简单…

【MyBatis】MyBatis日志信息配置

目录 什么是MyBatis相关的日志&#xff1f; 标准日志信息配置&#xff1a; 配置logback日志信息&#xff1a; 什么是MyBatis相关的日志&#xff1f; 首先什么叫做与MyBatis相关的日志呢&#xff1f;就是我们在执行sql语句的时候&#xff0c;如果没有MyBatis相关的日志&…

SpringBoot项目访问后端页面404

检查项目的路径和mapper映射路径没问题后&#xff0c;发现本级pom文件没有加入web启动模块的pom文件中 maven做项目控制时&#xff0c;要注意将maven模块加入到web启动模块中

IDEA项目import报错清理缓存

1、清理maven缓存 2、整个项目清理缓存

易点易动上线招标管理模块:提升企业高效招标管理的解决方案

在当今竞争激烈的商业环境下&#xff0c;招标管理对于企业的成功至关重要。为了帮助企业实现高效的招标管理&#xff0c;易点易动固定资产管理系统上线了全新的招标管理模块。该模块涵盖了供应商资质审核、采购询价单、重新报价单、招标结果单、招标作废单等功能&#xff0c;为…

【动态规划】583. 两个字符串的删除操作、72. 编辑距离

提示&#xff1a;努力生活&#xff0c;开心、快乐的一天 文章目录 583. 两个字符串的删除操作&#x1f4a1;解题思路&#x1f914;遇到的问题&#x1f4bb;代码实现&#x1f3af;题目总结 72. 编辑距离&#x1f4a1;解题思路&#x1f914;遇到的问题&#x1f4bb;代码实现&…

知识分享:如何制作一个电子名片二维码?

参加国际展会、寻找合作商、线下客户拜访、渠道开发、商务对接、行业交流大会……在这些场合中&#xff0c;商务名片都是必不可少的。随着二维码应用的流行&#xff0c;名片上使用二维码已经非常普遍了。你也可以在商务名片上使用一个自己设计的电子名片二维码&#xff0c;扫描…

基于 KubeSphere 部署 KubeBlocks 实现数据库自由

作者&#xff1a;尹珉&#xff0c; KubeSphere Contributor & Ambassador&#xff0c;KubeSphere 社区用户委员会杭州站站长。 KubeSphere 是什么&#xff1f; KubeSphere 是在 Kubernetes 之上构建的面向云原生应用的分布式操作系统&#xff0c;完全开源&#xff0c;支持…

使用HTML制作一个摊煎饼小游戏

铁打的行业流水的人&#xff0c;与其被动等待35岁的到来&#xff0c;不如主动出击探索副业&#xff0c;实现一个HTML摊煎饼小游戏&#xff0c;结合传统方法和现代技术&#xff0c;致力于制作出既美味又有创意的煎饼。 目录 引言简介游戏背景 所需材料和工具HTML基础知识CSS样式…

python作图

最重要的一张图了&#xff0c;有助于了解一下图的各个组成部分。最重要的一句话就是 Figure包含至少一个Axes&#xff0c;每个Axes可以被认为是一个模块&#xff08;包含坐标轴&#xff0c;标题&#xff0c;图像内容等&#xff09;。因此&#xff0c;创建单图的时候就是在Figur…

BAT028:批量将文件修改日期后缀更新为最新修改日期

引言&#xff1a;编写批处理程序&#xff0c;实现批量将文件修改日期后缀更新为最新修改日期。 一、新建Windows批处理文件 参考博客&#xff1a; CSDNhttps://mp.csdn.net/mp_blog/creation/editor/132137544 二、写入批处理代码 1.右键新建的批处理文件&#xff0c;点击【…

文本识别工具 TextSniper for Mac有哪些特点

TextSniper 是一款 macOS 平台上的文本提取工具&#xff0c;它可以将屏幕上的文字内容快速转换为可编辑的文本。无论是从图像、视频、PDF 文件还是其他类型的文档中提取文字&#xff0c;TextSniper 都提供了便捷的功能。 以下是 TextSniper的一些主要特点和功能&#xff1a; …

智慧粮仓粮库解决方案:视频+AI识别技术赋能,守护大国粮仓

一、需求背景 我国作为世界人口大国&#xff0c;农产品需求量庞大&#xff0c;保障粮食等重要农产品安全是经济社会稳定发展的重要基础。粮食安全涉及到生产、收购、加工、储藏、销售等全产业链&#xff0c;既涉及新型基础设施建设&#xff0c;也有对软件技术等专业能力要求。…

我的128创作纪念日

我的创作128纪念日 &#x1f388;1.机缘&#x1f388;2.收获&#x1f388;3.反思与憧憬 &#x1f388;1.机缘 2023年6月13日的那天&#xff0c;开始了CSDN的创作之旅&#xff0c;那时的我还是一名大一的学生&#xff0c;机缘巧合下开始了创作&#xff0c;主要也是分享自己在学习…

Apache Doris (四十二): RECOVER数据删除恢复

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你大数据的个人空间-豹哥教你大数据个人主页-哔哩哔哩视频 目录

web前端面试-- IEEE754标准JS精度丢失问题0.1+0.2!=0.3、0.2+0.3==0.5 十进制转二进制讲解

本人是一个web前端开发工程师&#xff0c;主要是vue框架&#xff0c;整理了一些面试题&#xff0c;今后也会一直更新&#xff0c;有好题目的同学欢迎评论区分享 ;-&#xff09; web面试题专栏&#xff1a;点击此处 背景 WEB前端面试官&#xff1a;0.20.3是否等于0.5&#xff…

文心一言 VS 讯飞星火 VS chatgpt (116)-- 算法导论10.3 1题

一、用go语言&#xff0c;画图表示序列(13&#xff0c;4&#xff0c;8&#xff0c;19&#xff0c;5&#xff0c;11)&#xff0c;其存储形式为多数组表示的双向链表。同样画出单数组表示的形式。 文心一言&#xff1a; 在Go语言中&#xff0c;我们无法直接画图&#xff0c;但我…