总体介绍

为了提升下单速度,我们将订单数据存入到Redis缓存中,以及一些中间形成的缓存信息都保存在redis中,不去访问mysql。
只有三次访问mysql

  1. 查询到用户订单已支付,将redis里面的订单信息删掉,将订单信息更新插入到mysql当中去
  2. 检测到秒杀商品卖完了,将秒杀商品数量和信息更新到mysql数据库的秒杀商品信息表中。
  3. 超时订单是某个秒杀商品的最后一个,发现订单延迟时已经在redis中删除了商品信息,需要从mysql取出商品信息。

全程使用高性能的redis保证高并发。

秒杀整体过程

  1. 商品数据定时压入redis缓存
  2. 客户开始抢购,用户的请求经过openResty(Nginx)到redis中获取秒杀商品(到达秒杀列表页)
  3. 用户如果想抢某个商品,也需要经过openResty到达秒杀详情页(从redis获取数据生成秒杀商品详情页)
  4. 用户进行秒杀抢购,但抢购之前需要先登录,登陆前请求还需要先到达openresty(判断当前秒杀商品还有没有库存,如果没有库存就直接拦截了,也不用调用后面的微服务了减轻压力)
  5. openResty通过之后判断用户是否登录(没登录就先去oauth2.0进行登录)
  6. 登录成功访问秒杀微服务,但还不能直接抢单(因为抢单要检查的内容很多,很费时,高并发的时候就都堵在那里了)。
  7. 排队下单
    1. 如何防止重复排队(有人下两次单怎么办)
  8. 多线程生成订单
    1. 如何不使用行锁解决并发超卖问题
  9. 秒杀订单支付

    多线程生成订单

    下单的方法用@async注解修饰
    一旦订单创建成功了就说明抢到了
    这里以队列的形式控制超卖来说明

  10. 如果能从商品数量队列取出东西来说明没有超卖,可以生成订单

  11. 清理用户的排队次数信息
  12. 从redis获取商品信息
  13. 创建秒杀订单
  14. 订单存到redis中
  15. redis中的对应商品库存-1
  16. 判断当前库存是否为0
    1. 如果为0将信息同步到mysql当中
    2. 不为0将商品信息放回redis中
  17. 更新抢单状态(抢单状态由一个redis hash保存)
  18. 发送消息给延时队列,以解决超时订单问题

    商品数据定时压入redis缓存

  19. 首先进行秒杀商品的入库审核,审核通过存入mysql

  20. 秒杀服务定时将秒杀商品转入redis(定时载入因为中途可能有商品上架,所以不能一次性转入)
  • 使用@Scheduled注解(cron = “0/5 ?”) 实现反复执行,里面cron表达式表示从第0秒开始,每5秒执行一次,将数据从mysql里面查询出来存入redis缓存当中
    • 存入的商品,符合以下几个条件
      • 判断审核状态为通过,
      • 参与秒杀时间在计算出的秒杀时间段里面
      • 原来不在redis中
      • 库存个数>0
  • 有5个时间段,只载入参与秒杀时间在这5个时间段的数据,载入的形式为hash形式,命名空间为参与秒杀开始时间,key为秒杀商品id,value为秒杀商品信息

    秒杀订单支付

  1. 排队完成后进入秒杀支付页面(这里跳转和调用支付系统是前端实现的),支付页面自动调用支付系统
  2. 从微信支付获取二维码数据,并在页面生成二维码
  3. 用户扫码支付后,微信支付服务器会进行验证,并返回调用前预留的回调地址(用户付款界面)。这是携带了支付状态信息
  4. 收到回调信息后会将该信息发送到秒杀系统的消息队列中
  5. 秒杀微服务监听支付回调信息消息队列
    1. 监听到支付状态成功,
      1. redis中根据用户名删除订单,
      2. 将订单信息完善,新增交易流水号等信息,将订单信息保存在mysql中
      3. 删除用户重复排队信息标识和排队标识
    2. 支付状态失败,
      1. 关闭微信支付
      2. redis中删除订单
      3. 回滚:
        1. 在对应商品的防止超卖队列加一个占位对象
        2. 回滚redis中的对应商品库存,如果这时候查不到对应商品了,说明出问题的是最后一个,需要从数据库里面查出来,并进行一个自增
      4. 删除用户排队信息和重复排队标识
  6. 监听延时队列
    1. 监听到内容进行查询,查不到订单说明已经支付成功了,订单已经删除了
    2. 能监听到内容,说明订单超时了,应当按照支付失败处理。

排队下单

  1. 排队信息封装,pojo SeckillStatus
    1. 包括用户名,创建时间,订单状态,订单金额,订单号等字段
  2. 采用redis的list队列,用户点击抢购的时候,生成用户抢购信息(seckillStatus)并存入到redis队列当中
  3. 多线程生成订单时从队列依次获取SeckillStatus
    1. 多线程生成订单时要修改订单状态
    2. 支付时候,要清理订单状态
  4. 排队过程中可以查询当前订单状态:用username获取SeckillStatus,查看其中的status信息

如何防止重复排队

redis维护一个hash类型的表,表示用户抢单次数
key是用户名,value是一个自增值,每次都自增1
用自增值不会产生并发问题,因为redis是单线程的。
在生成排队信息之前,查询当前用户抢单次数,如果大于1,就说明存在重复抢单,抛出不能重复抢单异常

如何解决并发超卖问题

在日常订单处理中使用mysql行锁来解决订单超卖
但秒杀这种高并发情况下再使用mysql行锁会比较低效,秒杀过程中应当降低mysql的压力,这里设计了两种方案:

  1. 使用list,给每件商品都创建一个队列(队列key唯一,应该设为商品id),创建队列时往队列里面加入商品数量个占位对象,用户抢单时依次从队列取占位对象,能取到就说明还有库存
  2. 使用hash和自增操作,创建一个秒杀订单数量hash表,key是秒杀商品订单,value是商品数量,每次抢单后自增-1,如果返回值>=0,则可以下单,否则说明已经卖完

两种方案分析,使用自增好一些,如果使用list,一方面需要每次迭代添加,另一方面也需要再redis生成大量的队列。