硬核讲解:秒杀设计

原文出自:公众号 sowhat1412
原文链接:
https://mp.weixin.qq.com/s/rbXrhzIJG2NtYt_61OmzTA
1 秒杀场景秒杀场景
  1. 登陆12306进行火车票抢座
  2. 1599元购入飞天茅台
  3. 周董演唱会的门票
  4. 双十一秒杀活动
秒杀场景关注点
  1. 严格防止超卖:库存1000件卖了1020件 , 要杀个码农祭天了!防止超卖是秒杀系统设计最核心的部分 。
  2. 防止黑产:防止不怀好意的羊毛党薅羊毛 。
  3. 保证用户体验:高并发下 , 给用户提供友善的购物体验 , 尽可能支持比较高的QPS等等 。
接下来就让我们按照关注点 , 不断细化秒杀场景 。
2 第1版-裸奔
硬核讲解:秒杀设计

文章插图
裸奔秒杀
不加思考 , 上来直接按照 SpringBoot + MyBatis 模式进行秒杀系统的设计 , 流程如下:
  1. Controller层获得用户秒杀请求后调用Service层 。
  2. Service层获得请求后要要检查已售数据跟库存总量是否一致 , 一致说明商品卖没了 , 不一致说明还有库存 , 那就调用DAO层对已售数量进行加1 。
  3. DAO层获得请求后直接通过MyBatis操作数据库实现已售数量加1跟订单创建 。
如果你用Postman去测试会发现是OK的 , 但如果你用专业的并发测试工具JMeter模式多用户并发请求会发现订单创建数量 > 库存量 - 已售量 。原因解释下 , 比如用户A、B并发进行秒杀请求 , 此时库存=100 , 已售=64 。
  1. A用户进行描述请求 , 此时调用到了Service层 , 发现已售不等于库存 , 此时拿到库存数是64 , A将库存更新为63 , 然后创建订单 。
  2. B用户进行描述请求 , 此时调用到了Service层 , 发现已售不等于库存 , 此时拿到库存数是64 , B将库存更新为63 , 然后创建订单 。
  3. 此时库存减少了1个但是订单创建多个 , 卖超了!
    无锁并发请求 , 卖超了
3 第2版-悲观锁
硬核讲解:秒杀设计

文章插图
syn悲观锁
遇见 并发问题 很容易想到以前学过并发编程嘛 , 既然Controller默认是单例模式 , 那我用 synchronized 将Controller层调用Service层的代码进行加锁同步即可 。
这样就可以解决卖超问题了 , 但是须知 , 既然是悲观锁 , 如果有1000个并发请求 , 那只有1个拿到锁了 。有999个会去竞争这个锁的 。
@Transactional
@Service
@Transactional
@Slf4j
public class OrderServiceImpl implements OrderService
{
//校验库存
Stock stock = checkStock(id);
//更新库存
updateSale(stock);
//创建订单
return createOrder(stock);
}
当然了你也可以用Spring自带的事务注解来实现悲观锁的操作 , 因为用了@Transactional就可以实现通过事务来控制 , 要么全部成功 , 要么全部失败 , 用事务时有两点需注意:
  1. 尽可能将MySQL执行语句往方法体后面靠 , 因为MySQL事务的commit语句是在第一次执行MySQL相关语句开始 , 一直到方法的结束 。
  2. 设置事务的超时时间 , 如果不设置默认是-1是无限长 。并且事务中设置的耗时timeout = 最后一个MySQL语句耗时 + 以及最后一个MySQL之前的所有耗时 。
需注意:悲观锁状态下会保证商品卖出去 , 如果没拿到锁的线程会阻塞的等待拿锁 。但是他的阻塞也会给用户带来非常不良好的体验 。
4 第3版-乐观锁
硬核讲解:秒杀设计

文章插图
MySQL版本号
我们为每个数量的已售数据配备个版本号 , 在Service层调用时获得用户的已售数跟对应版本号 , 然后更新时将已售数跟版本号同时更新 。因为 MySQL在更新时会自带乐观加速机制 , 如果更新成功则表示抢购成功 , 更新失败则表示抢购失败 , 此时你会发现不是手速越快就一定能抢到的哦 , 但起码保证了不会超卖 , 


推荐阅读