【Java秒杀方案】11.功能开发-【商品秒杀及优化】防止超卖 接口优化(redis预减库存,内存标记减少redis访问,RabbitMQ异步下单) 安全优化(隐藏秒杀接口,验证码,接口防刷)
商品秒杀核心功能及优化
1. 正常秒杀流程
- 在商品详情页面等待秒杀倒计时–http://localhost:8080/goodsDetail.htm?goodsId=2
- 倒计时为0,开始秒杀,点【秒杀】按钮开始秒杀 --http://localhost:8080/seckill/doSeckill?goodsId=2
- 服务端收到秒杀请求,首先判断是不是在秒杀期间,再判断秒杀商品是否有库存
- 上面条件都满足,则进入下单流程,下单流程分3步,1.减库存 2.新增订单信息 3.新增秒杀订单信息
2. 秒杀存在问题
1.商品超卖 ,库存减为负数
- 商品超卖:10个商品库存,会出现超卖200多的订单
- 秒杀商品库存数会变成负数:因为进入下单流程后,扣减库存是直接减1,大量请求过来,就容易被减为负数
- 解决办法:1.通过下面的接口优化,减少过滤服务端的请求压力 2.优化sql,避免出现商品超卖,库存为负数的情况
- 如果用户秒杀成功,把秒杀订单信息放在redis里面(key-order:userId:goodsId),这样可以防止重复下单
2.商品库存全部秒杀之后,快速响应秒杀请求
- 通过在redis中设置 isStockEmpty:goodsId的值,有值,表示该商品已经全部秒杀结束,直接返回
- 通过内存标记:Map<Long,Boolean> EmptyStockMap,在SeckillController初始化时,把秒杀商品<goodsId,false>放在内存中,如果商品库存为空,设置EmptyStockMap<goodsId,true>,后面直接查这个HashMap的值,如果为true,直接返回,不需要再次访问redis
- 在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异步下单) 安全优化(隐藏秒杀接口,验证码,接口防刷)相关推荐
- 接口优化:Redis预减库存,减少对数据库访问方案
欢迎关注方志朋的博客,回复"666"获面试宝典 Redis预减库存:主要思路减少对数据库的访问,之前的减库存,直接访问数据库,读取库存,当高并发请求到来的时候,大量的读取数据有可能 ...
- java 消息队列 秒杀_【IDEA+SpringBoot+Java商城秒杀实战21】高并发秒杀系统接口优化 RabbitMQ异步下单...
问题: 针对秒杀的业务场景,在大并发下,仅仅依靠页面缓存.对象缓存或者页面静态化等还是远远不够.数据库压力还是很大,所以需要异步下单,如果业务执行时间比较长,那么异步是最好的解决办法,但会带来一些额外 ...
- 实战 Java 第12天:开发商品点赞接口
实战 Java 第12天:开发商品点赞接口 前言 一.新建praise点赞表 二.新建Praise实体类 三.新建 PraiseMapper 接口 四.新建PraiseMapper.xml文件 五.新 ...
- 实战 Java 第8天:开发商品详情查询接口
实战 Java 第8天:开发商品详情查询接口 前言 一.在 ProductService 类中添加接口 二.在 ProductMapper 类中添加接口 三.增加 sql 语句 四.在 Product ...
- 实战 Java 第5天:开发商品查询(模糊查询与条件查询)接口
实战 Java 第5天:开发商品查询接口 前言 一.在 ProductService 类中添加接口 二.在 ProductMapper 类中添加接口 三.增加 sql 语句 四.在 ProductCo ...
- Java实现库存防超卖_高并发场景-订单库存防止超卖
背景 在电商系统中买商品过程,先加入购物车,然后选中商品,点击结算,即会进入待支付状态,后续支付. 过程需要检验库存是否足够,保证库存不被超卖. 场景一:买家需要购买数量可以多件 场景二:秒杀活动,到 ...
- java支付宝对账功能开发_java后台实现支付宝对账功能
完成支付宝支付.查询的接口之后,我们应该还需要定时与支付宝进行对账,以确保商户系统的订单信息是正确的,想知道支付宝支付.查询接口实现过程的亲们,可移步到上一篇有详细过程. 现在我们来讲一下支付宝对账的 ...
- JAVA关于搜索附近功能开发探讨
最近项目开发项目.小程序增加了搜索附近的任务功能.原来使用redisGEO实现了此功能.此后增加需求按条件查询附近的功能.想看看使用MySQL去实现该需要.经过分析和查询后可以有两种实现方案,在此记录 ...
- Java实现语音阅读功能开发(输入文字,转语音播放)
第一步 导入maven坐标 <!-- https://mvnrepository.com/artifact/com.jacob/jacob 文字转语音 --><dependency& ...
最新文章
- 规格选择_Axure教程:实现商品规格选择功能
- 【Django】ORM操作#2
- Drbd+Pacemaker实现高可用
- Facebook开源动画库 POP-POPBasicAnimation运用
- js list删除指定元素_删除js数组中的指定元素,有这两步就够了
- 高精度(压位+判负数+加减乘+读写)
- IntelliJ IDEA如何导入Gradle项目
- jsp页面页面post传值_在Js页面通过POST传递参数跳转到新页面详解
- 接口自动化测试 返回html,接口自动化测试实战(更新完毕)
- 日均请求量1.6万亿次背后,DNSPod的秘密-国密DoH篇
- springboot 项目里使用spring.xml文件
- 七层网络协议详细解释
- 两步教你在安卓中快速使用矢量图
- freeswitch呼叫中心之百度MRCP语音合成识别环境搭建
- Killer Names( 容斥定理,快速幂 )
- 打印机 打印机驱动 打印机如何与PC通信 什么通信协议 蓝牙打印机项目 蓝牙协议栈
- Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. (解決)
- 模型优化之模型融合|集成学习
- 数学英语不好能学php吗,英语和数学基础不好,还能学好编程吗?
- 计算机能换显卡吗,联想台式机可以更换显卡