商品秒杀核心功能及优化

1. 正常秒杀流程

  1. 在商品详情页面等待秒杀倒计时–http://localhost:8080/goodsDetail.htm?goodsId=2
  2. 倒计时为0,开始秒杀,点【秒杀】按钮开始秒杀 --http://localhost:8080/seckill/doSeckill?goodsId=2
  3. 服务端收到秒杀请求,首先判断是不是在秒杀期间,再判断秒杀商品是否有库存
  4. 上面条件都满足,则进入下单流程,下单流程分3步,1.减库存 2.新增订单信息 3.新增秒杀订单信息

2. 秒杀存在问题

1.商品超卖 ,库存减为负数

  1. 商品超卖:10个商品库存,会出现超卖200多的订单
  2. 秒杀商品库存数会变成负数:因为进入下单流程后,扣减库存是直接减1,大量请求过来,就容易被减为负数
  3. 解决办法:1.通过下面的接口优化,减少过滤服务端的请求压力 2.优化sql,避免出现商品超卖,库存为负数的情况
  4. 如果用户秒杀成功,把秒杀订单信息放在redis里面(key-order:userId:goodsId),这样可以防止重复下单

2.商品库存全部秒杀之后,快速响应秒杀请求

  1. 通过在redis中设置 isStockEmpty:goodsId的值,有值,表示该商品已经全部秒杀结束,直接返回
  2. 通过内存标记:Map<Long,Boolean> EmptyStockMap,在SeckillController初始化时,把秒杀商品<goodsId,false>放在内存中,如果商品库存为空,设置EmptyStockMap<goodsId,true>,后面直接查这个HashMap的值,如果为true,直接返回,不需要再次访问redis
  3. 在SeckillController初始化时,把每个秒杀商品的库存放在redis的seckillGoods:goodsId中,以方便后面在redis预减库存

商品超卖,通过sql优化,同时放在同一用户反复抢购,设置唯一索引

OrderServiceImpl.seckill

boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().setSql("stock_count=stock_count-1").eq("id", seckillGoods.getId()).gt("stock_count", 0));if(!result){return null;}

唯一索引 t_seckill_order

3.功能要点(接口优化)

1.redis预减库存

SeckillController afterPropertiesSet()

SeckillController初始化时,把每个秒杀商品的库存放在redis的seckillGoods:goodsId中

@Overridepublic void afterPropertiesSet() throws Exception {//初始化执行方法 商品库存数量加载到redisList<GoodsVo> list = goodsService.findGoodsVo();if(CollectionUtils.isEmpty(list)){return;}list.forEach(goodsVo ->{redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());EmptyStockMap.put(goodsVo.getId(),false);});}

SeckillController.doSeckill() seckillGoods:goodsId

原来是先decrement,如果库存<=0,再increment,防止redis中库存为0 ,为了保证redsi事务一致,用lua脚本

Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
log.info("stock:"+stock);
if(stock<=0){EmptyStockMap.put(goodsId,true);valueOperations.increment("seckillGoods:" + goodsId);return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}

lua脚本可以防止redis中库存变成-1

//lua脚本可以防止redis中库存变成-1
Long stock=(Long)redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId),Collections.emptyList());
log.info("stock:"+stock);
if(stock<=0){EmptyStockMap.put(goodsId,true);//valueOperations.increment("seckillGoods:" + goodsId);return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}

stock.lua

放在resources目录下面,与static 同级

if (redis.call('exists',KEYS[1]) == 1) thenlocal stock = tonumber(redis.call('get',KEYS[1]));if (stock > 0) thenredis.call('incrby',KEYS[1],-1);return stock;end;return 0;
end;

2. 内存标记减少redis访问

初始化以及更新

见SeckillController afterPropertiesSet()和 SeckillController.doSeckill() 关于EmptyStockMap.put操作
校验场景

SeckillController.doSeckill

//内存标记,减少读取redis的次数  当redis中库存为0,在内存中吧map对应goodsid的库存为空状态设置为trueif(EmptyStockMap.get(goodsId)){return RespBean.error(RespBeanEnum.EMPTY_STOCK);}

3. RabbitMQ异步下单

SeckillController.doSeckill 中校验库存,判断是否重复抢购,以及预减库存之后,发送消息,再通过MQ接受消息,异步下单

准备下单消息,发送

 //下单SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);mqSender.sendSeckillMessage(JSON.toJSONString(seckillMessage));//正在排队中 0return RespBean.success(0);

接受消息,校验判断后,下单

MQReceiver

@RabbitListener(queues = "seckillQueue")public void receive(String message){}

4.功能要点(安全优化)

1. 隐藏秒杀地址

通过准备每个商品,每个用户一个秒杀地址,方式代刷
seckillPath:userId:goodsId redis中的值,已经有效期

通过 /seckill/path 获取秒杀地址

SeckillController.path()

再通过 /seckill/"+path+"/doSeckill 这个真正的秒杀地址,秒杀

2. 验证码

通过验证码图片,校验每次提交操作是不是人手动操作,屏蔽机器人自动提交

<img src=""id="captchashow" alt="验证码">
<script>
$("#captchashow").click(function (){getCaptcha();})function getCaptcha(){console.log("getCaptcha...");$("#captchashow").attr("src","/seckill/captcha?goodsId="+goodsId+"&time="+(new Date()).getTime());}
</script>

SeckillController.captcha()

3. 接口防刷

@AccessLimit(second=5,maxCount=5,needLogin=true)

通过设置接口AccessLimit注解,设置每5秒钟,最大请求次数为5次,需要有用户登录信息

通过redis计数器实现

String key=request.getRequestURI()+ user.getId();
通过设置5秒杀的有效期,累加当前5秒钟内的总的访问次数

 /*** 获取秒杀地址* */@RequestMapping(value = "/path",method = RequestMethod.GET)@ResponseBody@AccessLimit(second=5,maxCount=5,needLogin=true)//通用接口限流public RespBean path(Model model, User user,Long goodsId,Long captcha,HttpServletRequest request) {}

AccessLimitInterceptor.preHandle()

Integer count = (Integer)valueOperations.get(key);if(null==count){//第一次 设置count值 设置超时时间 5秒valueOperations.set(key ,1,5, TimeUnit.SECONDS);}else if(count>5){log.info("AccessLimitInterceptor:"+"短时间之内请求次数太多");render(response,RespBeanEnum.REQUEST_LIMITED);return false;}else {valueOperations.increment(key);}

5.代码实现

SeckillController

@RequestMapping(value = “/path”,method = RequestMethod.GET)
path()

@RequestMapping(value = “/{path}/doSeckill”,method = RequestMethod.POST)
doSeckill()

@RequestMapping("/getResult")
getResult()

@RequestMapping("/captcha")
captcha()

package com.example.miaosha.controller;import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.miaosha.config.AccessLimit;
import com.example.miaosha.exception.GlobalException;
import com.example.miaosha.pojo.Order;
import com.example.miaosha.pojo.SeckillMessage;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.rabbitmq.MQSender;
import com.example.miaosha.service.IGoodsService;
import com.example.miaosha.service.IOrderService;
import com.example.miaosha.service.ISeckillOrderService;
import com.example.miaosha.vo.GoodsVo;
import com.example.miaosha.vo.RespBean;
import com.example.miaosha.vo.RespBeanEnum;
import com.wf.captcha.ArithmeticCaptcha;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;@Controller
@Slf4j
@RequestMapping("/seckill")
public class SeckillController implements InitializingBean {@Autowiredprivate IGoodsService goodsService;@Autowiredprivate ISeckillOrderService seckillOrderService;@Autowiredprivate IOrderService orderService;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate MQSender mqSender;@Autowiredprivate RedisScript<Long>   redisScript;private Map<Long,Boolean> EmptyStockMap=new HashMap<>();/*** 获取秒杀地址* */@RequestMapping(value = "/path",method = RequestMethod.GET)@ResponseBody@AccessLimit(second=5,maxCount=5,needLogin=true)//通用接口限流public RespBean path(Model model, User user,Long goodsId,Long captcha,HttpServletRequest request) {log.info("captchaPage:"+captcha);if(null==user){return RespBean.error(RespBeanEnum.SESSION_ERROR);}ValueOperations valueOperations = redisTemplate.opsForValue();//现在访问次数 5秒内,访问5次 为了便于次数,验证码默认为0captcha=0L;/*String captchaRedisStr=(String) valueOperations.get("captcha:"+user.getId()+":"+goodsId);Long captchaRedis=Long.valueOf(captchaRedisStr);log.info("captchaRedis:"+captchaRedis);*/Long captchaRedis=0L;if(captchaRedis!=captcha){return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);}String str=orderService.createPath(user,goodsId);return RespBean.success(str);}/*** 获取秒杀地址* */@RequestMapping(value = "/path0",method = RequestMethod.GET)@ResponseBody//通用接口限流public RespBean path0(Model model, User user,Long goodsId,Long captcha,HttpServletRequest request) {log.info("captchaPage:"+captcha);if(null==user){return RespBean.error(RespBeanEnum.SESSION_ERROR);}ValueOperations valueOperations = redisTemplate.opsForValue();String  url=request.getRequestURI();log.info("url:"+url);//现在访问次数 5秒内,访问5次 为了便于次数,验证码默认为0captcha=0L;/*String captchaRedisStr=(String) valueOperations.get("captcha:"+user.getId()+":"+goodsId);Long captchaRedis=Long.valueOf(captchaRedisStr);log.info("captchaRedis:"+captchaRedis);*/Long captchaRedis=0L;if(captchaRedis!=captcha){return RespBean.error(RespBeanEnum.CAPTCHA_ERROR);}//Integer count = (Integer)valueOperations.get(url + ":" + user.getId());if(null==count){//第一次 设置count值 设置超时时间 5秒valueOperations.set(url + ":" + user.getId(),1,5,TimeUnit.SECONDS);}else if(count>5){return RespBean.error(RespBeanEnum.REQUEST_LIMITED);}else {valueOperations.increment(url + ":" + user.getId());}String str=orderService.createPath(user,goodsId);return RespBean.success(str);}//秒杀静态化,在商品详情页,直接ajax请求秒杀,成功后,跳转秒杀成功静态页面@RequestMapping(value = "/{path}/doSeckill",method = RequestMethod.POST)@ResponseBodypublic RespBean doSeckill(Model model, User user,Long goodsId,@PathVariable String path) {//判断秒杀库存 判断是否重复秒杀 秒杀(减库存 添加订单信息,添加秒杀订单信息)if(null==user){return RespBean.error(RespBeanEnum.SESSION_ERROR);}ValueOperations valueOperations = redisTemplate.opsForValue();Boolean check=orderService.checkPath(user,goodsId,path);if(!check){return RespBean.error(RespBeanEnum.REQUEST_ILLEGAL);}//判断库存是否>0//预减库存//内存标记,减少读取redis的次数  当redis中库存为0,在内存中吧map对应goodsid的库存为空状态设置为trueif(EmptyStockMap.get(goodsId)){return RespBean.error(RespBeanEnum.EMPTY_STOCK);}//Long stock = valueOperations.decrement("seckillGoods:" + goodsId);//redis lua脚本返回的库存 stokc是减之前的库存,如果库存是8,那返回的是8,实际上redis里面已经变成7,如果redis里面stock 是0,那直接返回0//lua脚本可以防止redis中库存变成-1Long stock=(Long)redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods:" + goodsId),Collections.emptyList());log.info("stock:"+stock);if(stock<=0){EmptyStockMap.put(goodsId,true);//valueOperations.increment("seckillGoods:" + goodsId);return RespBean.error(RespBeanEnum.EMPTY_STOCK);}// 判断是否重复抢购SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:" + user.getId() + ":" + goodsId);if(null!=seckillOrder){return RespBean.error(RespBeanEnum.REPEATE_ERROR);}//下单SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);mqSender.sendSeckillMessage(JSON.toJSONString(seckillMessage));//正在排队中 0return RespBean.success(0);/*GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);if(goodsVo.getStockCount()<1){model.addAttribute("errorMsg", RespBeanEnum.EMPTY_STOCK.getMessage());return RespBean.error(RespBeanEnum.EMPTY_STOCK);}SeckillOrder seckillOrder = (SeckillOrder)redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);//SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("goods_id", goodsId).eq("user_id", user.getId()));if(null!=seckillOrder){model.addAttribute("errorMsg", RespBeanEnum.REPEATE_ERROR.getMessage());return RespBean.error(RespBeanEnum.REPEATE_ERROR);}Order order= orderService.seckill(goodsVo,user);return RespBean.success(order);*/}@RequestMapping("/getResult")@ResponseBodypublic RespBean getResult(Model model, User user, @RequestParam Long goodsId){if(null==user){return RespBean.error(RespBeanEnum.SESSION_ERROR);}//从秒杀订单表中搜索当前用户对应的goodsId有没有订单,有订单表示秒杀成功Long orderId=orderService.getResult(user,goodsId);return RespBean.success(orderId);}@RequestMapping("/captcha")public void captcha(HttpServletRequest request, HttpServletResponse response,User user,Long goodsId) throws Exception {if(user==null||goodsId<0){throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);}// 设置请求头为输出图片类型response.setContentType("image/gif");response.setHeader("Pragma", "No-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);// 算术类型ArithmeticCaptcha captcha = new ArithmeticCaptcha(130, 48,3);//captcha.setLen(3);  // 几位数运算,默认是两位//captcha.getArithmeticString();  // 获取运算的公式:3+2=?//captcha.text();  // 获取运算的结果:5redisTemplate.opsForValue().set("captcha:"+user.getId()+":"+goodsId,captcha.text(),300, TimeUnit.SECONDS);log.info("captcha公式:"+captcha.getArithmeticString());log.info("captcha结果:"+captcha.text());captcha.out(response.getOutputStream());  // 输出验证码}//原始秒杀@RequestMapping("/doSeckill0")public String doSeckill0(Model model, User user,Long goodsId){//判断秒杀库存 判断是否重复秒杀 秒杀(减库存 添加订单信息,添加秒杀订单信息)GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);if(goodsVo.getStockCount()<1){model.addAttribute("errorMsg", RespBeanEnum.EMPTY_STOCK.getMessage());return "seckillFail";}SeckillOrder seckillOrder = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("goods_id", goodsId).eq("user_id", user.getId()));if(null!=seckillOrder){model.addAttribute("errorMsg", RespBeanEnum.REPEATE_ERROR.getMessage());return "seckillFail";}Order order= orderService.seckill(goodsVo,user);model.addAttribute("order",order);return "orderDetail";}@Overridepublic void afterPropertiesSet() throws Exception {//初始化执行方法 商品库存数量加载到redisList<GoodsVo> list = goodsService.findGoodsVo();if(CollectionUtils.isEmpty(list)){return;}list.forEach(goodsVo ->{redisTemplate.opsForValue().set("seckillGoods:"+goodsVo.getId(),goodsVo.getStockCount());EmptyStockMap.put(goodsVo.getId(),false);});}
}

IOrderService

seckill
getResult
createPath
checkPath

package com.example.miaosha.service;import com.example.miaosha.pojo.Order;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.vo.GoodsVo;/*** <p>*  服务类* </p>** @author cch* @since 2021-11-17*/
public interface IOrderService extends IService<Order> {Order seckill(GoodsVo goodsVo, User user);Long getResult(User user, Long goodsId);String createPath(User user, Long goodsId);Boolean checkPath(User user, Long goodsId,String path);
}

OrderServiceImpl

seckill
getResult
createPath
checkPath

package com.example.miaosha.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.example.miaosha.mapper.SeckillOrderMapper;
import com.example.miaosha.pojo.Order;
import com.example.miaosha.mapper.OrderMapper;
import com.example.miaosha.pojo.SeckillGoods;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.service.IOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.miaosha.service.ISeckillGoodsService;
import com.example.miaosha.service.ISeckillOrderService;
import com.example.miaosha.utils.MD5Util;
import com.example.miaosha.utils.UUIDUtil;
import com.example.miaosha.vo.GoodsVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.Date;
import java.util.concurrent.TimeUnit;/*** <p>*  服务实现类* </p>** @author cch* @since 2021-11-17*/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {@Autowiredprivate ISeckillGoodsService seckillGoodsService;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate SeckillOrderMapper seckillOrderMapper;@Autowiredprivate RedisTemplate redisTemplate;@Override@Transactionalpublic Order seckill(GoodsVo goodsVo, User user) {SeckillGoods seckillGoods = seckillGoodsService.getOne(new QueryWrapper<SeckillGoods>().eq("goods_id",goodsVo.getId()));seckillGoods.setStockCount(seckillGoods.getStockCount()-1);//seckillGoodsService.updateById(seckillGoods);//单独更新库存 设置条件 id= stock_count>0/*boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().set("stock_count", seckillGoods.getStockCount()).eq("id", seckillGoods.getId()).gt("stock_count", 0));if(!result){return null;}*/boolean result = seckillGoodsService.update(new UpdateWrapper<SeckillGoods>().setSql("stock_count=stock_count-1").eq("id", seckillGoods.getId()).gt("stock_count", 0));if(!result){return null;}Order order = new Order();order.setUserId(user.getId());order.setGoodsId(goodsVo.getId());order.setDeliveryAddrId(1L);order.setGoodsName(goodsVo.getGoodsName());order.setGoodsCount(1);order.setGoodsPrice(goodsVo.getSeckillPrice());order.setOrderChannel(1);order.setStatus(0);order.setCreateDate(new Date());orderMapper.insert(order);SeckillOrder seckillOrder = new SeckillOrder();seckillOrder.setUserId(user.getId());seckillOrder.setOrderId(order.getId());seckillOrder.setGoodsId(goodsVo.getId());seckillOrderMapper.insert(seckillOrder);//秒杀成功,表秒杀订单存入redis uid+gidredisTemplate.opsForValue().set("order:"+user.getId()+":"+goodsVo.getId(),seckillOrder);return order;}/** return orderId 成功 id  /秒杀失败-1 /排队中 0* */@Overridepublic Long getResult(User user, Long goodsId) {//改为从redis中获取秒杀成功信息SeckillOrder seckillOrder =(SeckillOrder)redisTemplate.opsForValue().get("order:"+user.getId()+":"+goodsId);/*SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder>().eq("goods_id", goodsId).eq("user_id", user.getId()));*/if(null !=seckillOrder){return seckillOrder.getOrderId();}//如果没有redis中没有查到订单消息,看是否秒杀结束,如果已经秒杀结束,由于本次秒杀结束,所以判断没有秒杀到//特殊情况:如果秒杀100,只有10人参与,那肯定都会秒杀到,所以不存在活动没有结束,一直查询秒杀结果的情况if(redisTemplate.hasKey("isStockEmpty:"+goodsId)){//秒杀结束return  -1L;}return 0L;}@Overridepublic String createPath(User user, Long goodsId) {//获取秒杀地址String str = MD5Util.md5(UUIDUtil.uuid() + "123456");ValueOperations valueOperations = redisTemplate.opsForValue();valueOperations.set("seckillPath:"+user.getId()+":"+goodsId,str,60, TimeUnit.SECONDS);return str;}@Overridepublic Boolean checkPath(User user, Long goodsId,String path) {ValueOperations valueOperations = redisTemplate.opsForValue();String redisPath = (String)valueOperations.get("seckillPath:" + user.getId() + ":" + goodsId);if(user==null||goodsId<0||StringUtils.isEmpty(redisPath)){return false;}if(redisPath.equals(path)){return true;}return false;}
}

RedisConfig

redisTemplate
redisScript

package com.example.miaosha.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisTemplate<String,Object> redisTemplate= new RedisTemplate<>();redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}@Beanpublic DefaultRedisScript<Long> script(){DefaultRedisScript<Long> redisScript =new DefaultRedisScript<>();//lock.lua脚本位置和application.yml 同级目录redisScript.setLocation(new ClassPathResource("stock.lua"));redisScript.setResultType(Long.class);return redisScript;}}

SeckillMessage

异步下单,发送消息体

package com.example.miaosha.pojo;import com.example.miaosha.vo.GoodsVo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {private User user;private Long goodsId;
}

MQSender

异步下单,发送消息

package com.example.miaosha.rabbitmq;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
@Slf4j
public class MQSender {@Autowiredprivate RabbitTemplate rabbitTemplate;public void sendSeckillMessage(String message){log.info("sendSeckill发送消息:"+message);rabbitTemplate.convertAndSend("seckillExchange","seckill.message",message);}/*public void send(Object msg){log.info("发送消息:"+msg);rabbitTemplate.convertAndSend("q1",msg);}public void sendFanout(Object msg){log.info("fanout发送消息:"+msg);rabbitTemplate.convertAndSend("fanoutExchange","",msg);}public void sendDirect01(Object msg){log.info("directExchange发送消息:"+msg);rabbitTemplate.convertAndSend("direct_Exchange","queue.red",msg);}public void sendDirect02(Object msg){log.info("directExchange发送消息:"+msg);rabbitTemplate.convertAndSend("direct_Exchange","queue.green",msg);}public void sendTopic01(Object msg){log.info("topic_Exchange发送消息:"+msg);rabbitTemplate.convertAndSend("topic_Exchange","aaa.queue.bbb",msg);}public void sendTopic02(Object msg){log.info("topic_Exchange发送消息:"+msg);rabbitTemplate.convertAndSend("topic_Exchange","queue.red.message",msg);}public void sendHead01(String msg){log.info("topic_Exchange发送消息:"+msg);MessageProperties properties=new MessageProperties();properties.setHeader("color","red");properties.setHeader("speed","low");Message message=new Message(msg.getBytes(),properties);rabbitTemplate.convertAndSend("head_Exchange","",message);}public void sendHead02(String msg){log.info("topic_Exchange发送消息:"+msg);MessageProperties properties=new MessageProperties();properties.setHeader("color","red");properties.setHeader("speed","fast");Message message=new Message(msg.getBytes(),properties);rabbitTemplate.convertAndSend("head_Exchange","",message);}*/}

MQReceiver

异步下单,接受消息,进入下单流程
@RabbitListener(queues = “seckillQueue”)
public void receive(String message){}

package com.example.miaosha.rabbitmq;import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.miaosha.pojo.SeckillMessage;
import com.example.miaosha.pojo.SeckillOrder;
import com.example.miaosha.pojo.User;
import com.example.miaosha.service.IGoodsService;
import com.example.miaosha.service.IOrderService;
import com.example.miaosha.service.ISeckillOrderService;
import com.example.miaosha.utils.JsonUtil;
import com.example.miaosha.vo.GoodsVo;
import com.example.miaosha.vo.RespBean;
import com.example.miaosha.vo.RespBeanEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;@Service
@Slf4j
public class MQReceiver {@Autowiredprivate IGoodsService goodsService;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate ISeckillOrderService seckillOrderService;@Autowiredprivate IOrderService orderService;@RabbitListener(queues = "seckillQueue")public void receive(String message){log.info("seckillQueue接受消息:"+message);SeckillMessage seckillMessage = JSON.parseObject(message, SeckillMessage.class);Long goodsId = seckillMessage.getGoodsId();User user = seckillMessage.getUser();//下单操作GoodsVo goodsVo = goodsService.findGoodsVoById(goodsId);if(goodsVo.getStockCount()<1){//如果有key,表示,没有库存,结束redisTemplate.opsForValue().set("isStockEmpty:"+goodsId,"0");return;}//1. 判断是否重复抢购 从redis中判断ValueOperations valueOperations = redisTemplate.opsForValue();SeckillOrder seckillOrder = (SeckillOrder)valueOperations.get("order:" + user.getId() + ":" + goodsId);if(null!=seckillOrder){return;}//重复抢购//2.从t_seckill_order中获取SeckillOrder seckillOrderDB = seckillOrderService.getOne(new QueryWrapper<SeckillOrder>().eq("user_id", user.getId()).eq("goods_id", goodsId));if(null!=seckillOrderDB){return;}//重复抢购orderService.seckill(goodsVo,user);}/*@RabbitListener(queues = "q1")public void receive(Object msg){log.info("接受消息:"+msg);}@RabbitListener(queues = "queue_fanout01")public void receive1(Object msg){log.info("queue_fanout01接受消息:"+msg);}@RabbitListener(queues = "queue_fanout02")public void receive2(Object msg){log.info("queue_fanout02接受消息:"+msg);}@RabbitListener(queues = "direct_queue01")public void receive3(Object msg){log.info("direct_queue01接受消息:"+msg);}@RabbitListener(queues = "direct_queue02")public void receive4(Object msg){log.info("direct_queue02接受消息:"+msg);}@RabbitListener(queues = "topic_queue01")public void receive5(Message msg){log.info("topic_queue01接受消息:"+msg);}@RabbitListener(queues = "topic_queue02")public void receive6(Message msg){log.info("topic_queue02接受消息:"+msg);}@RabbitListener(queues = "head_queue01")public void receive7(Message msg){log.info("head_queue01接受消息:"+new String(msg.getBody()));}@RabbitListener(queues = "head_queue02")public void receive8(Message msg){log.info("head_queue02接受消息:"+new String(msg.getBody()));}*/}

RabbitMQConfig

初始化 队列,交换机 路由键,已经绑定

QUEUE
EXCHANGE
ROUTINGKEY

package com.example.miaosha.config;import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration
public class RabbitMQConfig {private static final  String QUEUE="seckillQueue";private static final  String EXCHANGE="seckillExchange";private static final  String ROUTINGKEY="seckill.#";@Beanpublic Queue seckillQueue(){return new Queue(QUEUE);}@Beanpublic TopicExchange seckillExchange(){return new TopicExchange(EXCHANGE);}@Beanpublic Binding binding(){return BindingBuilder.bind(seckillQueue()).to(seckillExchange()).with(ROUTINGKEY);}}

【Java秒杀方案】11.功能开发-【商品秒杀及优化】防止超卖 接口优化(redis预减库存,内存标记减少redis访问,RabbitMQ异步下单) 安全优化(隐藏秒杀接口,验证码,接口防刷)相关推荐

  1. 接口优化:Redis预减库存,减少对数据库访问方案

    欢迎关注方志朋的博客,回复"666"获面试宝典 Redis预减库存:主要思路减少对数据库的访问,之前的减库存,直接访问数据库,读取库存,当高并发请求到来的时候,大量的读取数据有可能 ...

  2. java 消息队列 秒杀_【IDEA+SpringBoot+Java商城秒杀实战21】高并发秒杀系统接口优化 RabbitMQ异步下单...

    问题: 针对秒杀的业务场景,在大并发下,仅仅依靠页面缓存.对象缓存或者页面静态化等还是远远不够.数据库压力还是很大,所以需要异步下单,如果业务执行时间比较长,那么异步是最好的解决办法,但会带来一些额外 ...

  3. 实战 Java 第12天:开发商品点赞接口

    实战 Java 第12天:开发商品点赞接口 前言 一.新建praise点赞表 二.新建Praise实体类 三.新建 PraiseMapper 接口 四.新建PraiseMapper.xml文件 五.新 ...

  4. 实战 Java 第8天:开发商品详情查询接口

    实战 Java 第8天:开发商品详情查询接口 前言 一.在 ProductService 类中添加接口 二.在 ProductMapper 类中添加接口 三.增加 sql 语句 四.在 Product ...

  5. 实战 Java 第5天:开发商品查询(模糊查询与条件查询)接口

    实战 Java 第5天:开发商品查询接口 前言 一.在 ProductService 类中添加接口 二.在 ProductMapper 类中添加接口 三.增加 sql 语句 四.在 ProductCo ...

  6. Java实现库存防超卖_高并发场景-订单库存防止超卖

    背景 在电商系统中买商品过程,先加入购物车,然后选中商品,点击结算,即会进入待支付状态,后续支付. 过程需要检验库存是否足够,保证库存不被超卖. 场景一:买家需要购买数量可以多件 场景二:秒杀活动,到 ...

  7. java支付宝对账功能开发_java后台实现支付宝对账功能

    完成支付宝支付.查询的接口之后,我们应该还需要定时与支付宝进行对账,以确保商户系统的订单信息是正确的,想知道支付宝支付.查询接口实现过程的亲们,可移步到上一篇有详细过程. 现在我们来讲一下支付宝对账的 ...

  8. JAVA关于搜索附近功能开发探讨

    最近项目开发项目.小程序增加了搜索附近的任务功能.原来使用redisGEO实现了此功能.此后增加需求按条件查询附近的功能.想看看使用MySQL去实现该需要.经过分析和查询后可以有两种实现方案,在此记录 ...

  9. Java实现语音阅读功能开发(输入文字,转语音播放)

    第一步 导入maven坐标 <!-- https://mvnrepository.com/artifact/com.jacob/jacob 文字转语音 --><dependency& ...

最新文章

  1. 规格选择_Axure教程:实现商品规格选择功能
  2. 【Django】ORM操作#2
  3. Drbd+Pacemaker实现高可用
  4. Facebook开源动画库 POP-POPBasicAnimation运用
  5. js list删除指定元素_删除js数组中的指定元素,有这两步就够了
  6. 高精度(压位+判负数+加减乘+读写)
  7. IntelliJ IDEA如何导入Gradle项目
  8. jsp页面页面post传值_在Js页面通过POST传递参数跳转到新页面详解
  9. 接口自动化测试 返回html,接口自动化测试实战(更新完毕)
  10. 日均请求量1.6万亿次背后,DNSPod的秘密-国密DoH篇
  11. springboot 项目里使用spring.xml文件
  12. 七层网络协议详细解释
  13. 两步教你在安卓中快速使用矢量图
  14. freeswitch呼叫中心之百度MRCP语音合成识别环境搭建
  15. Killer Names( 容斥定理,快速幂 )
  16. 打印机 打印机驱动 打印机如何与PC通信 什么通信协议 蓝牙打印机项目 蓝牙协议栈
  17. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. (解決)
  18. 模型优化之模型融合|集成学习
  19. 数学英语不好能学php吗,英语和数学基础不好,还能学好编程吗?
  20. 计算机能换显卡吗,联想台式机可以更换显卡

热门文章

  1. 大数据平台开发大作业
  2. “食”在网络:传统餐饮行业亮剑互联网
  3. 自动推送天气信息(Python+腾讯云函数+qmsg酱+和风天气)
  4. web安全之路的规划
  5. 深度学习应用实例--对话机器人--简介
  6. druid监控记录mysql_使用Druid监控SQL执行状态
  7. 2022.4.30 五一假期快乐的第一天
  8. js和jsp中遍历list对象
  9. 日期与时间函数与夏令时的关系
  10. 华为3面已过,面议薪资要价10K,面试官说我不尊重华为?