公众号上线啦!
搜一搜【国服冰】
使命:尽自己所能给自学后端开发的小伙伴提供一个少有弯路的平台
回复:国服冰,即可领取我为大家准备的资料,里面包含整体的Java学习路线,电子书,以及史上最全的面试题!
为何需要异步下单
在秒杀系统用户进行抢购的过程中,由于在同一时间会有大量请求涌入服务器,如果每个请求都立即访问数据库进行扣减库存+写入订单的操作,对数据库的压力是巨大的。
如何减轻数据库的压力呢,我们将每一条秒杀的请求存入消息队列(例如RabbitMQ
)中,放入消息队列后,给用户返回类似“抢购请求发送成功”的结果。而在消息队列中,我们将收到的下订单请求一个个的写入数据库中,比起多线程同步修改数据库的操作,大大缓解了数据库的连接压力,最主要的好处就表现在数据库连接的减少:
- 同步方式:大量请求快速占满数据库框架开启的数据库连接池,同时修改数据库,导致数据库读写性能骤减。
- 异步方式:一条条消息以顺序的方式写入数据库,连接数几乎不变(当然,也取决于消息队列消费者的数量)。
这种实现可以理解为是一种流量削峰:让数据库按照他的处理能力,从消息队列中拿取消息进行处理。
简洁逻辑处理
具体分为4个流程:
1、获取库存标记,预减库存时,当库存小于0
的情况下,库存标记变为true
,当库存小于零则直接结束,不操作redis
。
2、Redis
预减库存,在系统初始化时将商品库存信息加载到缓存中,如果预减库存小于零则直接结束。
3、判断用户是否是第一次秒杀,在创建订单时会在redis
中生成一个副本,同样是操作缓存,避免用户重复秒杀。
4、秒杀入队,将用户与商品ID传递给消息队列进行处理。
继承InitializingBean
并实现afterPropertiesSet()
方法, 凡是继承该接口的类,在初始化bean
的时候都会执行该方法。
/**
* 系统每次初始化时,将秒杀商品的库存数量加载到redis
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsList> goodsList = goodsService.getGoodsList();
if(goodsList == null){
return;
}
for(GoodsList good : goodsList){
isOver.put(good.getId(),false);
Integer stock_count = good.getStock_count();
redisService.set(GoodListKey.miaoshaGoodCountKey,good.getId()+"",stock_count);
}
}
秒杀接口:
@RequestMapping("/miaosha_static")
@ResponseBody
public Result<Integer> miaoSha_static(@RequestParam("good_id") int good_id, @UserParameter User user){
//库存标记,如果库存小于零则直接结束,不操作redis
if(isOver.get(good_id)){
return Result.error(CodeMsg.OUT_OF_STOCK);
}
//判断是否还有库存
//预减库存
Long decr = redisService.decr(GoodListKey.miaoshaGoodCountKey, good_id + "");
if(decr < 0){
isOver.put(good_id,true);
return Result.error(CodeMsg.OUT_OF_STOCK);
}
//判断该用户是第一次秒杀,不可重复秒杀
//查找redis缓存,绕开数据库
Miaosha_order miaosha_order = redisService.get(MiaoSha_OrderKey.orderKey,user.getId()+":"+good_id,Miaosha_order.class);
if(miaosha_order != null){
return Result.error(CodeMsg.NO_REPEAT_MIAOSHA);
}
MiaoshaMsg msg = new MiaoshaMsg();
msg.setUser(user);
msg.setGood_id(good_id);
//秒杀入队
provider.miaoshaProvider(msg);
return Result.success(0); //0代表排队中
}
接口将用户信息与商品ID
传递给消息的Provider
,然后消费者监听到队列里有了消息,进行消费。
provider:
public void miaoshaProvider(MiaoshaMsg msg){
String message = redisService.beanToString(msg);
System.out.println("miaoshaProvider");
amqpTemplate.convertAndSend(MqConfig.MIAOSHA_QUEUE,message);
}
Consumer:
@RabbitListener(queues = MqConfig.MIAOSHA_QUEUE)
public void miaoshaConsumer(String msg){
MiaoshaMsg message = redisService.StringToBean(msg, MiaoshaMsg.class);
GoodsList good = goodsService.getGoodsListById(message.getGood_id());
//判断该商品是否还有库存
int stock_count = miaoSha_goodsService.getMiaosha_goodsByid(good.getId());
if(stock_count <= 0){
return;
}
//判断该用户是第一次秒杀,不可重复秒杀
//查找redis缓存,绕开数据库
Miaosha_order miaosha_order = redisService.get(MiaoSha_OrderKey.orderKey,message.getUser().getId()+":"+good.getId(),Miaosha_order.class);
if(miaosha_order != null){
return;
}
//允许秒杀
//库存-1 生成order_info订单 生成秒杀订单
miaoShaService.miaoSha(message.getUser(), good);
}
注意:消息队列生产和消费时接收的消息都必须是
String
类型,所以在执行逻辑前需要BeanToString
和StringToBean
进行转换
事务进行订单生成:
@Transactional
public Integer miaoSha(User user, GoodsList good){
//减少库存
int i = miaoShaGoodsService.reduceStockCount(good.getId());
if(i == 1){
//减库存成功
//创建Order_info订单,秒杀订单
int order_id = order_infoService.createOrder_Info(user, good);
return order_id;
}else {
//减库存失败,无库存
setGoodsCountIsOver(good.getId());
return null;
}
}
减库存失败时调用setGoodsCountIsOver()
将该商品ID
存放到redis
中代表此商品已无库存
public void setGoodsCountIsOver(int good_id){
redisService.set(GoodListKey.miaoshaGoodCountIsOver,good_id+"",true);
}
创建订单的过程中同样在Redis
中生成一个副本
@Transactional
public int createOrder_Info(User user, GoodsList good){
Order_info order_info = new Order_info();
order_info.setUser_id(user.getId());
order_info.setGoods_id(good.getId());
order_info.setDelivery_addr_id(1);
order_info.setGoods_name(good.getGoods_name());
order_info.setGoods_count(good.getStock_count());
order_info.setGoods_price(good.getMiaosha_price());
order_info.setOrder_channel(1);
order_info.setStatus(0);
order_info.setCreate_date(new Date());
order_info.setPay_date(null);
//创建订单
order_infoDao.createOrder_info(order_info);
int order_id = order_info.getId();
//创建秒杀订单
miaoSha_orderService.createMiaoSha_Order(user, good, order_id);
Miaosha_order miaosha_order = new Miaosha_order();
miaosha_order.setUser_id(user.getId());
miaosha_order.setOrder_id(order_id);
miaosha_order.setGoods_id(good.getId());
//订单存入缓存
redisService.set(MiaoSha_OrderKey.orderKey,user.getId()+":"+good.getId(),miaosha_order);
return order_id;
}
前端逻辑处理的过程中,当秒杀接口返回的code
为0
时,代表秒杀逻辑执行成功,然后ajax
去异步执行一个请求判断订单是否已经生成,若订单已生成则返回订单的ID
代表秒杀成功,否则查询该商品是否已经无库存,若已无库存则返回-1
代表秒杀失败,若还有库存则返回0
代表该请求还在排队中。
/**
* orderId 秒杀成功
* 0 排队中
* -1 失败
*/
@RequestMapping("/getmiaoshadetail")
@ResponseBody
public Result<Integer> getMiaoshaDetail(@UserParameter User user,@RequestParam("good_id") int good_id){
int detail = miaoShaService.getMiaoshaDetail(user, good_id);
return Result.success(detail);
}
public int getMiaoshaDetail(User user,int good_id){
Miaosha_order order = redisService.get(MiaoSha_OrderKey.orderKey,user.getId()+":"+good_id,Miaosha_order.class);
if(order != null){
//秒杀成功
return order.getOrder_id();
}else {
boolean over = getGoodsCountIsOver(good_id);
if(over){
//无库存,秒杀失败
return -1;
}else {
//排队中
return 0;
}
}
}
public boolean getGoodsCountIsOver(int good_id){
return redisService.exist(GoodListKey.miaoshaGoodCountIsOver,good_id+"");
}
前端反馈给用户的信息就可以自己设计,如何给用户的良好体验感,贴出来篇幅过长,这里就不展示啦!