思路:减少数据库访问
1、系统初始化,把商品库存加载到redis
2、收到请求在redis预减库存,库存不足,直接返回,
3. 可以在redis里面做内存标记(这一步是很大的优化,只要库存减成零,后面的请求无论是100个还是一万个都是直接失败,压力很小),否则进入3 下一步
4、异步下单:如果有库存,不是直接连接数据库写入,而是对RabbitMQ操作,请求入队,立即返回排队中,类似我们在12306买火车票,并不会马上返回下单成功或者失败,而是显示排队中
5、客户端正常做轮询,判断是否秒杀成功。 服务端在入队之后,会请求出队,生成订单 减少库存
实现方法:
redis预减库存
系统初始化的时候,查询商品数量,把商品库存数量加载到redis缓存里面,
/**
* 系统初始化
* */
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);
}
}
执行秒杀的时候,不是直接在数据库中减库存,为了提高并发能力,在redis缓存里预减库存,如果库存不足,就返回指定的错误,如果库存充足,执行预减库存操作。
根据user和goodsid到redis里面判断订单是否已经存在,判断是否重复秒杀,如果重复了就返回不能能重复秒杀。
//预减库存
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);
}
RabbitMQ消息队列把下单异步化
如果没有,就执行入队操作,把user和goodsid信息发送出去。调用rabbitmq的sender方法,rabbitmq使用默认的direct模式。然后返回一个状态,表示排队中。
//入队
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排队中
秒杀到之后,发送消息,在接收端MqReciiver
里面,判断库存,判断如果不是重复秒杀,就调用MiaoshaService里面的方法,往下对数据库进行 减库存,下订单,写入秒杀订单。
@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);
}
在MiaoshaService
里面,减库存,下订单,写入秒杀订单是一个事务操作,要么全部成功,要么全部失败,所以需要给方法添加@Transactional
注解。 数据库写入订单之后会把订单信息写入redis里面来。
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//减库存 下订单 写入秒杀订单
boolean success = goodsService.reduceStock(goods);
if(success) {
//order_info maiosha_order
return orderService.createOrder(user, goods);
}else {
setGoodsOver(goods.getId());
return null;
}
}
在页面里做轮询,查询秒杀结果,如果秒杀失败,直接返回失败。如果成功,就接着查询是否下单成功,如果没成功就接着轮询,如果成功,点击查看订单,就返回订单信息。
内存标记优化:
在MIaoshaController里吗,新建一个HashMap,把商品id和标记值初始化为false存进去。
在预减库存的时候,如果商品库存小于零,做一个标记true,后续的请求不再去访问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);
}
}
//内存标记,减少redis访问
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);
}