在众多抢购活动中,在有限的商品数量的限制下如何保证抢购到商品的用户数不能大于商品数量,也就是不能出现超卖的问题;还有就是抢购时会出现大量用户的访问,如何提高用户体验效果也是一个问题,也就是要解决秒杀系统的性能问题。
每一个用户只能抢购一件商品的限制;在数据库减库存时加上库存数量判断,库存数量为0时阻止秒杀订单的生成。
总体思路就是要减少对数据库的访问,尽可能将数据缓存到Redis缓存中,从缓存中获取数据。
Controller 实现 InitializingBean 接口,并重写 afterPropertiesSet方法,这样可以保证系统初始化的时候讲物品库存加载到Redis缓存中
@Override
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);// 设置商品是否卖完的内存标志,初始化时为false,没有卖完
}
}
//利用map定位某件商品是否已经卖完,卖完了就可以不用去访问Redis缓存了,分别放置<商品ID,是否卖完标志>
private HashMap<Long, Boolean> localOverMap = new HashMap<>();
public Result<Integer> miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.error(CodeMsg.SESSION_ERROR);
}
//做了一个内存标记,减少Redis缓存访问,一旦对于商品库存没有了,over为真,将直接返回秒杀失败,不进行后面的Redis缓存访问。
boolean over = localOverMap.get(goodsId);
if (over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//从Redis缓存中进行预减库存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
// 预减库存时库存不足,直接返回秒杀失败
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);
//在异步队列中写入用户信息和对应商品ID,因为服务端可以根据用户ID和商品ID定位用户是否秒杀过对应商品,并且服务端需要根据商品ID去查询数据库中对应商品的库存是否足够。
sender.sendMiaoshaMessage(mm);
//并非直接返回下单结果,而是分步骤进行,相当于将提交操作变成两段式,先申请后确认。
//申请之后进入排队中,确认是否可以秒杀成功由服务端进行确认。
return Result.success(0);//0代表正在排队中
}
// 配置文件,构建异步队列
@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";
// Direct模式
@Bean
public Queue miaoShaQueue(){
return new Queue(MIAOSHA_QUEUE,true);
}
}
// 发送
public class MQSender {
@Autowired
AmqpTemplate amqpTemplate;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
// 将对象转化为String进行发送
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
// 接收到请求,进行处理
public class MQReceiver {
// 监听一个名为MQConfig.MIAOSHA_QUEUE的队列
@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
log.info("receive message:"+message);
// 将接收到的String转化回Object对象,可以从中获取相应信息
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);
}
}
// SQL加库存数量判断:利用stock_count > 0限制,防止出现超卖,只有stock_count > 0时才能更新库存操作
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")
public int reduceStock(MiaoshaGoods g);
// 判断是否可以对数据库进行减库存
public boolean reduceStock(GoodsVo goods) {
MiaoshaGoods g = new MiaoshaGoods();
g.setGoodsId(goods.getId());
int res = goodsDao.reduceStock(g);
return res > 0;
}
//减少库存 下订单 秒杀订单 这三个操作必须是原子性,利用@Transactional注解将这3个操作放在一个事务里面,保证同时成功,否则失败
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
//判断是否可以减少库存
boolean success = goodsService.reduceStock(goods);
// 库存足够
if (success) {
//下订单,秒杀订单
return orderService.creatOrder(user, goods);
} else { //无法减少库存,说明该对应商品库存不足了
return null;
}
}
将存库MySQL迁移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。
优点:解决性能问题
缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。
引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
优点:解决超卖问题,略微提升性能。
缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。
将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作(相比较MySQL的自增来说没有空洞),同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。
优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。