1. 减少数据库的操作
判断是否重复抢购这个操作可以优化,大致思路是把用户订单放到Redis里,键中加上用户,抢购时判断是否已存在信息。来代替查询数据库
具体操作
java //生成订单时 redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goods.getId(), JsonUtil.object2JsonStr(seckillOrder));
java //秒杀操作前 SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" +user.getId() + ":" + goodsId); //如果取出的信息不为空代表已经购买过此产品,不能再参与(限购为1情况下) if(seckillOrder != null){ return RespBean.error(RespBeanEnum.REPEATE_ERROR); }
库存可以在初始化时候就加入到Redis里,秒杀的时候直接减去Redis里的库存,然后再使用RabbitMQ消息队列分别进行处理
具体操作
- 配置初始化库存
```java //接口调用implements InitializingBean @RestController @RequestMapping("/seckill") public class SecKillController implements InitializingBean{ //实现这个接口的方法 //系统初始化,把商品库存数量加载到Redis @Override public void afterPropertiesSet() throws Exception{ //从数据库找出有秒杀商品 List list = goodsService.findGoodsVo(); if(CollectionUtils.isEmpty(list)){ return; } //把每个秒杀产品的库存信息加载到Redis里 list.forEach(goodsVo -> redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount())); }
} ```
- 秒杀接口里配置库存减少操作
java @RequestMapping("/doSeckill",method = RequestMethod.POST) public RespBean doSeckill(User user,Long goodsId){ //判断用户是否存在 //一堆代码判断是否重复抢购 //判断是否重复抢购后 ValueOperations valueOperations = redisTemplate.opsForValue(); //预减库存,每次执行自动-1 Long stock = valueOperations.decrement("seckillGoods:" + goodsId); //如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题 if (stock < 0){ //每次执行自动加一,防止库存出现-1的情况 valueOperations.increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTY_STOCK); } //生成订单,以后会替换成SeckillMessage Order order = orderService.seckill(user,goods); return RespBean.success(Order); }
把秒杀请求封装成一个对象发送给RabbitMQ。把操作异步掉,用消息队列达到流量削锋的目的。
具体操作
- 创建封装对象SeckillMessage
java public class SeckillMessage{ private User user; private Long goodId; //getter、setter、构造器省略 }
- 选择使用RabbitMQ的Topic模式,进行config配置
```java @Configuration public class RabbitMQTopicConfig{ private static final String QUEUE = "seckillQueue"; private static final String EXCHANGE = "seckillExchange";
@Bean public Queue queue(){ return new Queue(QUEUE); } @Bean public TopicExchange topicExchange(){ return new TopicExchange(EXCHANGE); } @Bean public Binding binding(){ return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#"); }
} ```
- MQSender配置
```java @Service //lombok的注解 @Slf4j public class MQSender { @Autowired private RabbitTemplate rabbitTemplate;
public void sendSeckillMessage(String message){ log.info("发送消息:" + message); rabbitTemplate.converAndSend("seckillExchange", "seckill.message", message); }
}
```
- 配置完后回到Controller接口修改,加入MQSender
```java @Autowired private MQSender mqSender;
@RequestMapping("/doSeckill",method = RequestMethod.POST) public RespBean doSeckill(User user,Long goodsId){ //判断用户是否存在 //一堆代码判断是否重复抢购 //判断是否重复抢购后 ValueOperations valueOperations = redisTemplate.opsForValue(); //预减库存,每次执行自动-1 Long stock = valueOperations.decrement("seckillGoods:" + goodsId); //如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题 if (stock < 0){ //每次执行自动加一,防止库存出现-1的情况 valueOperations.increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTY_STOCK); } //替换处 SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); //使用RabbitMQ实现异步,把订单生成放入另一部分操作,这样能快速返回信息。这里是JsonUtil是自己编写的工具类,在下文 mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage)); //前端收到0,要作出"排队中"的响应 return RespBean.success(0); } ```
- MQReceiver,接受sender消息,异步掉操作
```java @Service //lombok的注解 @Slf4j public class MQReceiver { @Autowired private RabbitTemplate rabbitTemplate;
@Autowired private IGoodsService goodsService; @Autowired private RedisTemplate redisTemplate; @Autowired private OrderService orderService; @RabbitListener(queues = "seckillQueue") public void receive(String message){ //先打印下看看能不能拿到正确的message log.info("接受到的消息:" + message); //转换消息从JSON到对象 SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class); //获取goodId和用户 User user = seckillMessage.getUser(); Long goodsId = seckillMessage.getGoodId(); //获取商品信息 GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId); //判断库存 if(goodsVo.getStockCount() < 1){ return; } //判断是否重复抢购 SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId); if(seckillOrder != null){ return; } //下单操作 orderService.seckill(user,goodsVo); }
} ```
- -
内存标记,减少库存卖光之后对redis的访问
具体操作
- 在Controller里设置个内存标记
java //Long对应不同的商品id private Map<Long,Boolean> EmptyStockMap = new HashMap<>();
- 之前的初始化方法重写,加入内存标记初始化
```java @Override public void afterPropertiesSet() throws Exception{ //从数据库找出有秒杀商品 List list = goodsService.findGoodsVo(); if(CollectionUtils.isEmpty(list)){ return; } //把每个秒杀产品的库存信息加载到Redis里 list.forEach(goodsVo -> { redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(), goodsVo.getStockCount())); EmptyStockMap.put(goodsVo.getId(),false); }
}
```
- 接口里具体实现内存标记判断
```java @Autowired private MQSender mqSender;
@RequestMapping("/doSeckill",method = RequestMethod.POST) public RespBean doSeckill(User user,Long goodsId){ //判断用户是否存在 //一堆代码判断是否重复抢购 //判断是否重复抢购后 ValueOperations valueOperations = redisTemplate.opsForValue(); //预减库存,每次执行自动-1 Long stock = valueOperations.decrement("seckillGoods:" + goodsId); //内存标记判断,减少Redis访问 if(EmptyStockMap.get(goodsId)){ return RespBean.error(RespBeanEnum.EMPTYSTOCK); } //如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题 if (stock < 0){ //修改处,标记内存 EmptyStockMap.put(goodsId,true); //每次执行自动加一,防止库存出现-1的情况 valueOperations.increment("seckillGoods:" + goodsId); return RespBean.error(RespBeanEnum.EMPTYSTOCK); } SeckillMessage seckillMessage = new SeckillMessage(user, goodsId); //这里是JsonUtil是自己编写的工具类,在下文 mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage)); //前端收到0,要作出"排队中"的响应 return RespBean.success(0); } ```
- -
轮询判断是否秒杀成功,因为之前只返回了一个排队中
具体操作
在秒杀的Controller里再新建一个接口,专门获取秒杀结果,返回一个orderId代表成功,返回-1代表秒杀失败,返回0代表排队中
Controller
java @RequestMapping("/result",method = RequestMethod.POST) public RespBean getResult(User user, Long goodsId){ if(user == null){ return RespBean.error(RespBeanEnum.SESSION_ERROR); } Long orderId = seckillOrderService.getResult(user,goodsId); return RespBean.success(orderId); }
- Service层创建接口并实现(相关自动加载注解就不写了)
java @Override public Long getResult(User user, Long goodsId){ SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder)().eq("user_id", user.getId()).eq("goods_id",goodsId); //如果订单表里有这个用户下了这个产品的订单,就说明秒杀成功,返回订单编号。 if(seckillOrder != null){ return seckillOrder.getOrderId(); }else if(redisTemplate.hasKey("isStockEmpty:" + goodsId)){ //如果Redis里这个产品的isStockEmpty标记为是,就返回库存为空的标记 return -1L; }else{ //如果没有这个订单,就代表秒杀失败 return 0L; } }
- 在生成订单的时候要加上判断库存为空
```java @Transactional @Override public Order seckill(User user,GoodsVo goods){ ValueOperations valueOperations = redisTemplate.opsForValue(); //秒杀商品表减库存操作...
//判断是否还有库存 if(seckillGoods.getStockCount() < 1){ valueOperations.set("isStockEmpty:" + goods.getId(), "0"); return null; }
} ```
- 接下来就是前端写轮询发送请求判断了,若返回-1就是秒杀失败不用轮询,若返回0排队中就要写设置间隔几秒发送请求到getResult接口。
脚本优化
经历以上操作,超卖问题是解决了,但是存在一个问题,单个用户多次秒杀会使得redis库存多次减少并且超出限购。还会让redis库存扣完但是数据库没有,所以需要优化。(这个方法好像没有用,自己预想是用用户标记)
lua脚本实现分布式锁,具体作用看视频
RedisConfig调用
在配置一个stock.lua判断库存
RedisConfig配置
Controller里判断库存修改
java @Autowired private RedsiScript<Long> script;
JSONUtil 或者Maven添加fastJson依赖
```java package com.bank.seckill2022.utils;
import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.List;
/**
- 功能描述:
- @param: Json工具类
- @return:
- @author 来自于网络
- @date: 2022/3/25 23:36 */
public class JSONUtil { private static ObjectMapper objectMapper = new ObjectMapper(); /* * 将对象转换成json字符串 * * @param obj * @return */ public static String object2JsonStr(Object obj) { try { return objectMapper.writeValueAsString(obj); } catch (JsonProcessingException e) { //打印异常信息 e.printStackTrace(); } return null; } /* * 将字符串转换为对象 * * @param 泛型 / public static T jsonStr2Object(String jsonStr, Class clazz) { try { return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz); } catch (JsonParseException e) { e.printStackTrace(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } / * * 将json数据转换成pojo对象list *
Title: jsonToList
*Description:
* * @param jsonStr * @param beanType * @return */ public static List jsonToList(String jsonStr, Class beanType) { JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType); try { List list = objectMapper.readValue(jsonStr, javaType); return list; } catch (Exception e) { e.printStackTrace(); } return null; } }```