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

问题:

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

思路:

系统初始化,把商品库存数量stock加载到Redis上面来。

后端收到秒杀请求,Redis预减库存,如果库存已经到达临界值的时候,就不需要继续请求下去,直接返回失败,即后面的大量请求无需给系统带来压力。

判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品,判断是否重复秒杀。

库存充足,且无重复秒杀,将秒杀请求封装后消息入队,同时给前端返回一个code (0),即代表返回排队中。(返回的并不是失败或者成功,此时还不能判断)

5, 前端接收到数据后,显示排队中,并根据商品id轮询请求服务器(考虑200ms轮询一次)。

后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这名字的通道,如果有消息过来,获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(秒杀事务是一个原子操作:库存减1,下订单,写入秒杀订单)。

此时,前端根据商品id轮询请求接口MiaoshaResult,查看是否生成了商品订单,如果请求返回-1代表秒杀失败,返回0代表排队中,返回>0代表商品id说明秒杀成功。

返回结果说明:

前端会根据后端返回的值来判断是秒杀结果。

-1 :库存不足秒杀失败

0 :排队中,继续轮询

>0 :返回的是商品id ,说明秒杀成功

代码实现

1.后端接收秒杀请求的接口doMiaosha。

@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)

@ResponseBody

public Result doMiaosha(Model model,MiaoshaUser user,

@RequestParam(value="goodsId",defaultValue="0") long goodsId,

@PathVariable("path")String path) {

model.addAttribute("user", user);

//1.如果用户为空,则返回至登录页面

if(user==null){

return Result.error(CodeMsg.SESSION_ERROR);

}

//2.预减少库存,减少redis里面的库存

long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);

//3.判断减少数量1之后的stock,区别于查数据库时候的stock<=0

if(stock<0) {

return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);

}

//4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品

MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);

if (order != null) {// 查询到了已经有秒杀订单,代表重复下单

return Result.error(CodeMsg.REPEATE_MIAOSHA);

}

//5.正常请求,入队,发送一个秒杀message到队列里面去,入队之后客户端应该进行轮询。

MiaoshaMessage mms=new MiaoshaMessage();

mms.setUser(user);

mms.setGoodsId(goodsId);

mQSender.sendMiaoshaMessage(mms);

//返回0代表排队中

return Result.success(0);

}

//MiaoshaMessage 消息的封装 MiaoshaMessage Bean

public class MiaoshaMessage {

private MiaoshaUser user;

private long goodsId;

public MiaoshaUser getUser() {

return user;

}

public void setUser(MiaoshaUser user) {

this.user = user;

}

public long getGoodsId() {

return goodsId;

}

public void setGoodsId(long goodsId) {

this.goodsId = goodsId;

}

}

注意:消息队列这里,消息只能传字符串,MiaoshaMessage 这里是个Bean对象,是先用beanToString方法,将转换为String,放入队列,使用AmqpTemplate传输。

@Autowired

RedisService redisService;

@Autowired

AmqpTemplate amqpTemplate;

public void sendMiaoshaMessage(MiaoshaMessage mmessage) {

// 将对象转换为字符串

String msg = RedisService.beanToString(mmessage);

log.info("send message:" + msg);

// 第一个参数队列的名字,第二个参数发出的信息

amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);

}

/**

* 将Bean对象转换为字符串类型

* @param

*/

public static String beanToString(T value) {

//如果是null

if(value==null) return null;

//如果不是null

Class> clazz=value.getClass();

if(clazz==int.class||clazz==Integer.class) {

return ""+value;

}else if(clazz==String.class) {

return ""+value;

}else if(clazz==long.class||clazz==Long.class) {

return ""+value;

}else {

return JSON.toJSONString(value);

}

}

2.监控该消息队列,一旦有消息进入,从该消息中获取对象进行秒杀操作

@RabbitListener(queues=MQConfig.MIAOSHA_QUEUE)//指明监听的是哪一个queue

public void receiveMiaosha(String message) {

log.info("receiveMiaosha message:"+message);

//通过string类型的message还原成bean,拿到了秒杀信息之后。开始业务逻辑秒杀,

MiaoshaMessage mm=RedisService.stringToBean(message, MiaoshaMessage.class);

MiaoshaUser user=mm.getUser();

long goodsId=mm.getGoodsId();

GoodsVo goodsvo=goodsService.getGoodsVoByGoodsId(goodsId);

int stockcount=goodsvo.getStockCount();

//1.判断库存不足

if(stockcount<=0) {

return;

}

//2.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品

MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);

if (order != null) {// 重复下单

return;

}

//原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务

miaoshaService.miaosha(user,goodsvo);

}

@Transactional

public OrderInfo miaosha(MiaoshaUser user, GoodsVo goodsvo) {

//1.减少库存,即更新库存

boolean success=goodsService.reduceStock1(goodsvo);//考虑减少库存失败的时,不进行写入订单

if(success) {

//2.下订单,其中有两个订单: order_info miaosha_order

OrderInfo orderinfo=orderService.createOrder_Cache(user, goodsvo);

return orderinfo;

}else {//减少库存失败,做一个标记,代表商品已经秒杀完了。

setGoodsOver(goodsvo.getId());

return null;

}

}

//写入缓存

private void setGoodsOver(Long goodsId) {

redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true);

}

//查看缓存中是否有该key

private boolean getGoodsOver(Long goodsId) {

return redisService.exitsKey(MiaoshaKey.isGoodsOver, ""+goodsId);

}

注意:秒杀操作是一个事务,使用@Transactional注解来标识,如果减少库存失败,则回滚。

3.前端根据商品id轮询请求接口MiaoshaResult,查看是否生成了商品订单,后端处理秒杀逻辑,并向前端返回请求结果。

/**

* 客户端做一个轮询,查看是否成功与失败,失败了则不用继续轮询。

* 秒杀成功,返回订单的Id。

* 库存不足直接返回-1。

* 排队中则返回0。

* 查看是否生成秒杀订单。

*/

@RequestMapping(value = "/result", method = RequestMethod.GET)

@ResponseBody

public Result doMiaoshaResult(Model model, MiaoshaUser user,

@RequestParam(value = "goodsId", defaultValue = "0") long goodsId) {

long result=miaoshaService.getMiaoshaResult(user.getId(),goodsId);

System.out.println("轮询 result:"+result);

return Result.success(result);

}

public long getMiaoshaResult(Long userId, long goodsId) {

//先去缓存里面取得

MiaoshaOrder order=orderService.getMiaoshaOrderByUserIdAndGoodsId(userId, goodsId);

//秒杀成功

if(order!=null) {

return order.getOrderId();

}

else {

//查看商品是否卖完了

boolean isOver=getGoodsOver(goodsId);

if(isOver) {//商品卖完了

return -1;

}else {//商品没有卖完

return 0;

}

}

}

注意:然后轮询访问 doMiaoshaResult这个接口,从数据库中拿订单,如果有。返回商品id,说明秒杀成功,通过从redis中拿到isOver标记来判断失败还是在请求,商品卖完了返回-1,商品没有卖完返回0,继续请求,前端拿到返回的数据,通过判断,进行显示,成功就跳转订单页面。

前端轮询业务代码:

function doMiaosha(path) {

alert(path);

alert("秒杀!");

$.ajax({

url : "/miaosha/" + path + "/do_miaosha",

type : "POST",

data : {

goodsId : $("#goodsId").val()

},

success : function(data) {

if (data.code == 0) {

//秒杀成功,跳转详情页面

//window.location.href="order_detail.htm?orderId="+data.data.id;

//轮询

getMiaoshaResult($("#goodsId").val());

} else {

layer.msg(data.msg);

}

},

error : function() {

layer.msg("请求有误!");

}

});

}

//做轮询

function getMiaoshaResult(goodsId) {

$.ajax({

url : "/miaosha/result",

type : "GET",

data : {

goodsId : $("#goodsId").val()

},

success : function(data) {

if (data.code == 0) {

var result = data.data;

if (result < 0) {

layer.msg("抱歉,秒杀失败!");

} else if (result == 0) {

//继续轮询

setTimeout(function() {

getMiaoshaResult(goodsId);

}, 200);//200ms之后继续轮询

layer.msg(data.msg);

} else {

layer.confirm("恭喜你,秒杀成功!查看订单?", {

btn : [ "确定", "取消" ]

}, function() {

//秒杀成功,跳转详情页面

window.location.href = "order_detail.htm?orderId="

+ result;

}, function() {

layer.closeAll();

});

}

} else {

layer.msg(data.msg);

}

},

error : function() {

layer.msg("请求有误!");

}

});

}

注意setTimeout的用法。

setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。

1000 毫秒= 1 秒。

如果你只想重复执行可以使用 setInterval() 方法。

使用 clearTimeout() 方法来阻止函数的执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值