Java Seckill Module:Redis pre-reduced inventory and Rabbitmq asynchronous order

mac2024-05-12  34

上期回顾:Java Seckill Module:Interface optimization and Rabbitmq integration

秒杀接口:首先看看用户有没有登录,接着判断下库存,如果有库存,则判断是否秒杀过了,如果还没秒杀过,则进行秒杀:减库存,写订单。

这里我们将之前的同步下单,利用rabbitmq做成一个异步下单。

总之,我们的目的就是减少数据库的访问:

1、系统初始化,把商品库存数量加载到redis。

2、收到请求,redis预减库存,库存不足,直接返回,否则进入3

3、请求入队,立即返回排队中

4、请求出队,生成订单,减少看库存

5、客户端轮询,是否秒杀成功

详细解析:在系统初始化的时候,首先将商品库存数量加载到redis,接着服务端收到请求的时候并不是去访问数据库,而是进行下redis的预减库存,就是在redis中去减库存,如果redis中只有10个库存,来了10个请求,redis中的库存预减为0后,不管后面来了几个请求,都是直接返回失败,后面的数据对于数据库基本上是没有任何压力的,有什么好处这样,例如过来10个请求,将预减到0,如果第11个请求过来的时候就不进行往下走了,直接返回秒杀失败,如果有库存,也不是直接访问数据库,通过异步下单,将请求放入队列中,接着服务端立即返回排队中,这里的返回并不是成功和失败,而是排队中,因为是在排队,所以是不知道能不能秒杀成功的,失败也暂时还不能判断出来,类似于12306买完票后进行排队中,有个排队出票过程,这个过程的出票结果有可能是成功也有可能失败,相当于异步下单,异步化之后,客户端发送请求接收响应后,客户端就会进行轮询来询问服务端是否秒杀成功,接着就是进行请求出队(异步的),出队成功才会生成订单,生成订单会写入redis中去,再对数据库减少库存,记住这里不是预减库存而是真正的在数据库中减少库存,随后客户端轮询就能知道是否秒杀成功,整个操作是没有阻塞的。


如何在系统初始化的时候就将商品给加载进来呢?

***** public class MiaoshaController implements InitializingBean {} @ResponseBody public Result<Integer> miaosha(Model model,MiaoshaUser user,         @RequestParam("goodsId")long goodsId,         @PathVariable("path") String path) {     model.addAttribute("user", user);     if(user == null) {         return Result.error(CodeMsg.SESSION_ERROR);     }     boolean check = miaoshaService.checkPath(user, goodsId, path);     if(!check){         return Result.error(CodeMsg.REQUEST_ILLEGAL);     }     boolean over = localOverMap.get(goodsId);     if(over) {         return Result.error(CodeMsg.MIAO_SHA_OVER);     }     long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10     if(stock < 0) {         localOverMap.put(goodsId, true);         return Result.error(CodeMsg.MIAO_SHA_OVER);     }     MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);     if(order != null) {         return Result.error(CodeMsg.REPEATE_MIAOSHA);     }     MiaoshaMessage mm = new MiaoshaMessage();     mm.setUser(user);     mm.setGoodsId(goodsId);     sender.sendMiaoshaMessage(mm);     return Result.success(0); }

解读:首先收到请求之后,不是直接去查询数据库而是先去缓存中预减库存,减少后返回的是客户端的值,如果库存小于0,则直接返回失败,有库存则进行判断是否重复秒杀,没有秒杀过则进行入队操作,需要一个秒杀的message来存放用户秒杀请求信息,接着将消息给发送出去,表示正在排队中:

***** 接着实现该接口方法:(实现这个方法,系统在初始化的时候会帮我们自动回调该方法) public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsList = goodsService.listGoodsVo(); if(goodsList == null) { return; } for(GoodsVo goods : goodsList) { redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); localOverMap.put(goods.getId(), false); } }

解读:首先查询出所有的商品,然后加载到缓存里面。

 

***** private MiaoshaUser user; private long goodsId; ***** 这是发送方法: public void sendMiaoshaMessage(MiaoshaMessage mm) { String msg = RedisService.beanToString(mm); log.info("send message:"+msg); amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg); } ***** public static final String MIAOSHA_QUEUE = "miaosha.queue"; ***** 接收消息这里我们用Direct模式,在接收消息这里我们需要将接收到的string类型的数据转化为bean对象,然后将信息放入MiaoshaMessage中,接着查库存,查订单,没有订单,则在miaoshaService.miaosha(user, goods);中去判断减库存是否成功,成功的话就生成订单。 @RabbitListener(queues=MQConfig.MIAOSHA_QUEUE) public void receive(String message) { log.info("receive message:"+message); 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); } ***** 在秒杀的时候有可能减库存失败,需要判断下: public boolean reduceStock(GoodsVo goods) { MiaoshaGoods g = new MiaoshaGoods(); g.setGoodsId(goods.getId()); int ret = goodsDao.reduceStock(g); return ret > 0; } ***** 只有减库存成功了才能生成订单,生成订单将id插入对象里面: orderDao.insert(orderInfo);

 

 

回到页面轮询:

function doMiaosha(path){ $.ajax({ url:"/miaosha/"+path+"/do_miaosha", type:"POST", data:{ goodsId:$("#goodsId").val() }, success:function(data){ if(data.code == 0){ getMiaoshaResult($("#goodsId").val()); }else{ layer.msg(data.msg); } }, error:function(){ layer.msg("客户端请求有误"); } }); } ***** 轮询 function getMiaoshaResult(goodsId){ g_showLoading(); $.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); }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("客户端请求有误"); } }); }

在轮询的时候需要查询下是否生成了订单:

@ResponseBody public Result<Long> miaoshaResult(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); }

返回一个结果,秒杀成功或者还是在排队中。

*****

public long getMiaoshaResult(Long userId, long goodsId) { MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId); if(order != null) { return order.getOrderId(); }else { boolean isOver = getGoodsOver(goodsId); if(isOver) { return -1; }else { return 0; } } } 如果买完了,就往redis中写入一个值 private void setGoodsOver(Long goodsId) { redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true); } 判断是否买完 private boolean getGoodsOver(long goodsId) { return redisService.exists(MiaoshaKey.isGoodsOver, ""+goodsId); } 永久不过期 public static MiaoshaKey isGoodsOver = new MiaoshaKey(0, "go"); public <T> boolean exists(KeyPrefix prefix, String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); String realKey = prefix.getPrefix() + key; return jedis.exists(realKey); }finally { returnToPool(jedis); } }

解读:首先判断是否秒杀成功,秒杀成功则直接返回orderId,如果没有成功则判断是否买完,没有买完则接着进行轮询,就是还在排队中。

还有一点需要优化:

我们预减库存是在redis中去减的,例如有10个,来了11个请求,前面已经减了10个了,刚好减完了,当收到的请求大于商品存储值时,stock就会变为-2,-3,-4等等下去,也就是后面的操作始终为失败,失败的话,后面就没必要继续访问redis数据库的,以减少网络开销。

如何解决呢?

private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>();

在系统初始化的时候,再写入个东西来标识秒杀为未结束:

public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsList = goodsService.listGoodsVo(); if(goodsList == null) { return; } for(GoodsVo goods : goodsList) { redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); localOverMap.put(goods.getId(), false); } }

也就是说,在系统初始化的时候给商品标识个未结束。

boolean over = localOverMap.get(goodsId); if(over) { return Result.error(CodeMsg.MIAO_SHA_OVER); } long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId); if(stock < 0) { localOverMap.put(goodsId, true); return Result.error(CodeMsg.MIAO_SHA_OVER); }

解读:创建一个内存标记来减少redis访问,如果结束了,则没必要再去访问redis,如果库存小于0,则设置over为true,接着第11个请求,请求到本方法,进行判断的时候,就直接返回错误信息。

 

 

最新回复(0)