1、秒杀商品1.0
(1)先从数据库查询秒杀商品数量,如果秒杀商品大于0,就执行下面的步骤。
(2)根据memberId和orderId查询用户是否存在此商品的秒杀订单,如果存在就不能再次秒杀,可以防止重复秒杀。
(3)减库存,下订单,写入秒杀订单(直接操作数据库)。这三步需要在同一个事务中执行,只要有一个步骤执行失败,就会导致回滚
该版本存在一个问题,就是减库存的时候会出现超卖的现象?
采用5000个线程进行压力测试就会出现超卖的现象。如何解决?
- 首先采用了悲观锁的思路,对记录进行加锁for update , 在每次对某条记录进行操作,就给该条记录上锁,这样就保证每个时刻都只能有一个线程访问了。但是压力测试发现此时QPS只能达到500多,大大影响了性能。
- 后面就改用乐观锁的思路,对秒杀商品增加了一个版本号(version)字段,就解决了超发的问题,QPS能够达到800多。
update miaosha_goods set stock_count = stock_count - 1 where goods_id=#{goodsId} and version=#{version}
- 第三种方式:sql语句上加上对库存的判断
update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0
2、秒杀商品2.0(Redis和RabbitMQ)
为了提供秒杀的性能,我们采用了Redis和RabbitMQ对秒杀功能进行了优化。优化的思路主要就是:减少数据库的访问。
(1)缓存预热:通过一个定时任务,每天凌晨三点上架未来三天即将进行的秒杀活动信息以及对应的商品信息。
(2)验证时效:超出秒杀场次的规定时间返回秒杀结束。
(3)判断是否重复秒杀:直接查询redis是否存在memberId-skuId的字段。如果有则占位失败,直接返回。没有则占位成功,继续秒杀。
//3.1 通过在redis中使用 用户id-skuId 来占位看是否买过
Boolean occupy = redisTemplate.opsForValue().setIfAbsent(memberResponseVo.getId()+"-"+redisTo.getSkuId(), num.toString(), ttl, TimeUnit.MILLISECONDS);
(4)获取信号量:在秒杀请求到达时,先获取Redis的信号量。如果足够再预减库存(直接挡住了很多请求,不会继续往下执行,大大提高了效率,这样就可以不访问数据库了)。
//4. 校验库存和购买量是否符合要求
if (num <= redisTo.getSeckillLimit()) {
//4.1 尝试获取库存信号量
RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + redisTo.getRandomCode());
boolean acquire = semaphore.tryAcquire(num,100,TimeUnit.MILLISECONDS);
RSemaphore基于 Redis 的Semaphore开发。使用RSemaphore获取资源的顺序是不可预测的,所以它是一种非公平锁。可以理解为分布式的信号量,它的作用是用来限制同时访问共享区域的线程数量。
(5)异步下单:创建订单信息,发送给消息队列,将秒杀成功的消息直接返回给用户。
//4.2 获取库存成功
if (acquire) {
//5. 发送消息创建订单
//5.1 创建订单号
orderSn = IdWorker.getTimeId();
//5.2 创建秒杀订单to
SeckillOrderTo orderTo = new SeckillOrderTo();
orderTo.setMemberId(memberResponseVo.getId());
//orderTo.setMemberId(555L);
orderTo.setNum(num);
orderTo.setOrderSn(orderSn);
orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
orderTo.setSeckillPrice(redisTo.getSeckillPrice());
orderTo.setSkuId(redisTo.getSkuId());
//5.3 发送创建订单的消息,减少并发的压力
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo);
}
(6)订单服务监听并处理消息:订单服务一直监听秒杀消息队列,发现队列中有消息就会进行处理。在订单服务里,还需要判断库存,是否重复秒杀,再进行下订单减库存,为了防止消息队列中添加了过多消息。
@Component
@RabbitListener(queues = "order.seckill.order.queue")
public class SeckillOrderListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void createOrder(SeckillOrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("***********接收到秒杀消息");
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
orderService.createSeckillOrder(orderTo);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
channel.basicReject(deliveryTag,true);
}
}
}
通过对秒杀功能进行了优化,我们再次进行压力测试,QPS能够达到1500左右。