针对秒杀的业务场景,在高并发下,仅仅依靠页面缓存、对象缓存或者页面静态化等还是远远不够,数据库压力还是很大,所以需要异步下单,如果业务执行时间比较长,那么异步是最好的解决办法,但会带来一些额外的程序上的复杂性
具体思路:
- 系统初始化,把商品库存数量加载到Redis里面去
- 后端收到秒杀请求,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败,即后面的大量请求无需给系统带来压力
- 判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品,判断是否重复秒杀
- 库存充足,且无重复秒杀,将秒杀请求封装后放入消息队列,同时给前端返回一个code (0),即代表正在排队中(返回的并不是失败或者成功,此时还不能判断)
- 前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(200ms轮询一次)
- 后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这个名字的通道,如果有消息过来就获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单、写入订单详情)
- 此时,前端根据商品id轮询请求result接口查看是否生成了商品订单,如果返回-1代表秒杀失败,返回0代表排队中,返回>0代表秒杀成功
返回结果说明:
前端可根据后端返回的值来判断是否秒杀成功
-1 :库存不足,秒杀失败
0 :排队中,需继续轮询
>0 :返回的是商品id ,说明秒杀成功
监听rabbitmq,一旦有消息进入,就从该消息中获取对象进行秒杀操作
接收消息下单具体代码(MQReceiver.java):
package com.javaxl.miaosha_05.rabbitmq;
import com.javaxl.miaosha_05.domain.MiaoshaOrder;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
import com.javaxl.miaosha_05.redis.RedisService;
import com.javaxl.miaosha_05.service.GoodsService;
import com.javaxl.miaosha_05.service.MiaoshaService;
import com.javaxl.miaosha_05.service.OrderService;
import com.javaxl.miaosha_05.vo.GoodsVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@Autowired
RedisService redisService;
@Autowired
GoodsService goodsService;
@Autowired
OrderService orderService;
@Autowired
MiaoshaService miaoshaService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = MQConfig.MIAOSHA_QUEUE,
durable = "true"),
exchange = @Exchange(value = MQConfig.MIAOSHA_EXCHANGE,
durable = "true",
type = "topic",
ignoreDeclarationExceptions = "true"),
key = "miaosha.#")
)
public void receive(String message) {
log.info("receive message:" + message);
//将string类型的message还原成bean,拿到了秒杀信息之后开始秒杀业务逻辑
MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser user = mm.getUser();
long goodsId = mm.getGoodsId();
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
//库存不足
if (stock <= 0) {
return;
}
//判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
return;
}
//减库存 下订单 写入秒杀订单
miaoshaService.miaosha(user, goods);
}
}
下单相应的service层代码:
/**
* 减库存、下订单、写入秒杀订单
*/
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
try {
//减数据库里该商品的库存
goodsService.reduceStock(goods);
//下订单、写入秒杀订单
return orderService.createOrder(user, goods);
}catch (Exception e){
e.printStackTrace();
//还原redis里的库存
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goods.getId());
throw new RuntimeException();
}
}
执行秒杀事务的时候,先生成订单详情,然后生成秒杀订单,为了进一步确保秒杀过程中一个用户只能秒杀一件商品,可以给秒杀订单表miaosha_order添加一个唯一索引,如果再次插入相同的user_id与goods_id,那么将不会被允许,从而在事务中插入失败而回滚:
reduceStock方法代码:
相应的dao层代码:
createOrder方法代码:
相应的dao层代码:
注:秒杀操作是一个事务,需使用@Transactional注解来标识,如果减少库存失败,则回滚
前端根据商品id轮询请求result接口,查看是否生成了商品订单并判断是否秒杀成功
获取秒杀结果接口代码:
/**
* orderId:成功
* -1:秒杀失败
* 0: 排队中
*/
@RequestMapping(value = "/result", method = RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(HttpServletRequest request, HttpServletResponse response, Model
model, MiaoshaUser user, @RequestParam("goodsId") long goodsId) {
model.addAttribute("user", user);
if (user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
long result = miaoshaService.getMiaoshaResult(user.getId(), goodsId);
return Result.success(result);
}
getMiaoshaResult方法代码:
getMiaoshaOrderByUserIdGoodsId方法代码:
前端轮询result接口的代码:
result这个接口,从缓存中拿订单(将订单信息录入数据库的同时还会往redis放一份),如果有则返回商品id,说明秒杀成功;商品卖完了则返回-1,说明秒杀失败;商品没有卖完则返回0,说明正在排队中,需继续轮询,前端拿到返回的数据,通过判断,进行显示,成功就跳转订单页面