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

目录

一、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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate TokenUtil tokenUtil;@Autowiredprivate RabbitMqProduct rabbitMqProduct;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate ProductMapper productMapper;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate ObjectMapper objectMapper;@Transactionalpublic 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;@Servicepublic class ProductService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate ProductMapper productMapper;@Autowiredprivate RedisIdWorker redisIdWorker;@Transactionalpublic Return order(Long id) {// 1. 查询商品Product product = productMapper.queryById(id);// 4. 判断库存是否足够if (product.getStock() <= 0) {return Return.fail(Code.ORDER_STOCK);}// 5. 判断订单是否存在(用户是否已下单)// 5.1 获取用户idLong 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 订单idOrder order = new Order();order.setId(redisIdWorker.nextId("order"));// 7.2 用户idorder.setUserId(userId);// 7.3 商品idorder.setProductId(product.getId());// 7.4 入库int isSuccess = orderMapper.add(order);if (isSuccess != 1) {return Return.fail(Code.ORDER_FAIL);}// 9. 返回订单idreturn Return.success(Code.ORDER_SUCCESS,order.getId());}}

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

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

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

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

@Transactionalprivate Return createOrder(Long id) {// 5. 判断订单是否存在(用户是否已下单)// 5.1 获取用户idLong 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 订单idOrder order = new Order();order.setId(redisIdWorker.nextId("order"));// 7.2 用户idorder.setUserId(userId);// 7.3 商品idorder.setProductId(id);// 7.4 入库int isSuccess = orderMapper.add(order);if (isSuccess != 1) {return Return.fail(Code.ORDER_FAIL);}// 9. 返回订单idreturn 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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate ProductMapper productMapper;@Autowiredprivate 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);}}@Transactionalprivate 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 订单idOrder order = new Order();order.setId(redisIdWorker.nextId("order"));// 7.2 用户idorder.setUserId(userId);// 7.3 商品idorder.setProductId(id);// 7.4 入库int isSuccess = orderMapper.add(order);if (isSuccess != 1) {return Return.fail(Code.ORDER_FAIL);}// 9. 返回订单idreturn 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-- 库存不足 返回1return 1;
end-- 判断是否下过单
if (redis.call('sismember',orderKey,userId) == 1) then-- 存在 返回2return 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. 初始化DefaultRedisScriptDefaultRedisScript<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";// 队列与交换机@Beanpublic Queue orderQueue() {// 创建队列,并设置持久化return QueueBuilder.durable(ORDER_KILL_QUEUE).build();}@Beanpublic DirectExchange orderExchange() {// 创建直连交换机,并设置持久化return ExchangeBuilder.directExchange(ORDER_KILL_EXCHANGE).durable(true).build();}// 绑定队列与交换机@Beanpublic 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 {@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate RabbitTemplate rabbitTemplate;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate 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服务器发生acklog.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;@Autowiredpublic 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("消息重送成功");};// 设置 ConfirmCallbackthis.rabbitTemplate.setConfirmCallback(confirmCallback);// 当消息无法路由时返回this.rabbitTemplate.setMandatory(true);this.rabbitTemplate.setReturnsCallback(returnedMessage -> {// 如果消息无法路由,则重新发送send(RabbitMQConfig.ORDER_KILL_EXCHANGE, RabbitMQConfig.ORDER_KILL_KEY,returnedMessage.getMessage());});}@SneakyThrowspublic <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、最终代码实现

@SneakyThrowspublic 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.rhkb.cn/news/163923.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SSM的传统文化网站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

使用Portainer图形化工具轻松管理远程Docker环境并实现远程访问

文章目录 前言1. 部署Portainer2. 本地访问Portainer3. Linux 安装cpolar4. 配置Portainer 公网访问地址5. 公网远程访问Portainer6. 固定Portainer公网地址 前言 Portainer 是一个轻量级的容器管理工具&#xff0c;可以通过 Web 界面对 Docker 容器进行管理和监控。它提供了可…

【C/PTA】顺序结构专项练习

本文结合PTA专项练习带领读者掌握顺序结构&#xff0c;刷题为主注释为辅&#xff0c;在代码中理解思路&#xff0c;其它不做过多叙述。 7-1 是不是太胖了 据说一个人的标准体重应该是其身高&#xff08;单位&#xff1a;厘米&#xff09;减去100、再乘以0.9所得到的公斤数。已…

搜维尔科技:“虚实结合” 体验式人机验证技术,助力通用汽车开启研发新篇章

虚拟现实(VR)技术为制造业带来了巨大的可能性。它使工程师能够以真实世界的比例完整体验他们的设计,就像身临其境一样。通过在VR中模拟制造过程,可以发现并解决许多问题,从而避免在实际生产中投入大量资源后才发现问题。VR模拟使不同团队之间的沟通和协作变得比较直观和高效。这…

【数据结构】830+848真题易错题汇总(自用)

【数据结构】830848易错题汇总(10-23) 文章目录 【数据结构】830848易错题汇总(10-23)选择题填空题判断题简答题&#xff1a;应用题&#xff1a;算法填空题&#xff1a;算法设计题&#xff1a;(待补) 选择题 1、顺序栈 S 的 Pop(S, e)操作弹出元素 e&#xff0c;则下列(C )是正…

互联网Java工程师面试题·Java 总结篇·第十弹

目录 82、JDBC 能否处理 Blob 和 Clob&#xff1f; 83、简述正则表达式及其用途。 84、Java 中是如何支持正则表达式操作的&#xff1f; 85、获得一个类的类对象有哪些方式&#xff1f; 86、如何通过反射创建对象&#xff1f; 87、如何通过反射获取和设置对象私有字段的值…

基于SSM+Vue的体育馆管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

紫光同创FPGA实现UDP协议栈网络视频传输,带录像和抓拍功能,基于YT8511和RTL8211,提供2套PDS工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐我这里已有的以太网方案紫光同创FPGA精简版UDP方案紫光同创FPGA带ping功能UDP方案紫光同创FPGA精简版UDP视频传输方案 3、设计思路框架OV5640摄像头配置及采集数据缓冲FIFOUDP协议栈详解MAC层发送MAC发送模式MAC层接收ARP发送ARP接收ARP缓…

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

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

学信息系统项目管理师第4版系列32_信息技术发展

1. 大型信息系统 1.1. 大型信息系统是指以信息技术和通信技术为支撑&#xff0c;规模庞大&#xff0c;分布广阔&#xff0c;采用多级 网络结构&#xff0c;跨越多个安全域&#xff1b;处理海量的&#xff0c;复杂且形式多样的数据&#xff0c;提供多种类型应用 的大系统 1.1.…

【计算机网络笔记】OSI参考模型基本概念

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

LeetCode 799. 香槟塔【数组,模拟,简单线性DP】1855

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

日志回滚工作原理剖析及在文件系统的作用

日志回滚原理 当涉及到崩溃恢复和一致性保护时&#xff0c;日志回滚是一种常见的机制。它通过记录写入操作到一个事务日志中&#xff0c;而不是直接应用到文件系统&#xff0c;以保护文件系统的一致性。下面是日志回滚的一般工作原理&#xff1a; 日志记录&#xff1a;在进行写…

uni-app实现拍照功能

直接些这样的组件代码 <template><view><button click"takePhoto">拍照</button><image :src"photoUrl" v-if"photoUrl" mode"aspectFit"></image></view> </template><script&g…

阿里云服务器x86计算架构ECS规格大全

阿里云企业级服务器基于X86架构的实例规格&#xff0c;每一个vCPU都对应一个处理器核心的超线程&#xff0c;基于ARM架构的实例规格&#xff0c;每一个vCPU都对应一个处理器的物理核心&#xff0c;具有性能稳定且资源独享的特点。阿里云服务器网aliyunfuwuqi.com分享阿里云企业…

Mybatis对数据库进行增删查改以及单元测试

这篇写的草率了&#xff0c;是好几天前学到&#xff0c;以后用来自己复习 UserInfo import lombok.Data;Data public class UserInfo {private int id;private String name;private int age;private String email;//LocalDateTime可用于接收 时间}Mapper UserMapper pack…

【word技巧】word页眉,如何禁止他人修改?

我们设置了页眉内容之后&#xff0c;不想其他人修改自己的页眉内容&#xff0c;我们可以设置加密的&#xff0c;设置方法如下&#xff1a; 先将页眉设置好&#xff0c;退出页眉设置之后&#xff0c;我们选择布局功能&#xff0c;点击分隔符 – 连续 设置完之后页面分为上下两节…

sqoop 脚本密码管理

1&#xff1a;背景 生产上很多sqoop脚本的密码都是铭文&#xff0c;很不安全&#xff0c;找了一些帖子&#xff0c;自己尝试了下&#xff0c;记录下细节&#xff0c;使用的方式是将密码存在hdfs上然后在脚本里用别名来替代。 2&#xff1a;正文 第一步&#xff1a;创建密码对…

11. 机器学习 - 评价指标2

文章目录 混淆矩阵F-scoreAUC-ROC 更多内容&#xff1a; 茶桁的AI秘籍 Hi, 你好。我是茶桁。 上一节课&#xff0c;咱们讲到了评测指标&#xff0c;并且在文章的最后提到了一个矩阵&#xff0c;我们就从这里开始。 混淆矩阵 在我们实际的工作中&#xff0c;会有一个矩阵&am…

Cocos Creator3.8 项目实战(十)使用 protobuf详细教程

在 Cocos Creator 中使用 protobuf.js 库可以方便地进行协议的序列化和反序列化。 下面是使用 protobuf.js 的详细说明&#xff1a; 一、protobuf环境安装 1、安装 npm protobuf环境安装安装需要使用 npm 命令进行&#xff0c;因此首先需要安装 npm 。 如果你还没安装 npm …