问题:
针对秒杀的业务场景,在大并发下,仅仅依靠页面缓存、对象缓存或者页面静态化等还是远远不够。数据库压力还是很大,所以需要异步下单,如果业务执行时间比较长,那么异步是最好的解决办法,但会带来一些额外的程序上的复杂性。
思路:
1.后端接收秒杀请求的接口doMiaosha。
@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)
@ResponseBody
public Result<Integer> 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 <T> 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<Long> 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() 方法来阻止函数的执行。