秒杀功能实现

  1. 前端秒杀按钮的处理,判断其是否在秒杀时间内,如果在秒杀时间内,那么按钮可以按,如果不行按钮就变成灰通过(disable)来处理
  2. 秒杀按钮按下之后给后端发送一个请求,这时候搞明白前端需要的返回数据是什么?(订单编号)
  3. 后端接受到请求,先考虑情况
    1. 判断其是否有登录? 如果没有抛出异常
    2. 判断其是否有重复下单?
    3. 判断其是否是在秒杀时间内下单
    4. 判断是否有库存
    5. 查看是否有该商品
  4. 减少库存
  5. 生成订单
  6. 生成订单记录
  7. 返回订单编号(订单编号要是有序的,使用推特的雪花算法)

Tip:记得贴@Transactional标签

订单详情

  1. 前端获取订单编号,发起请求,因为显示的数据是订单信息的,所以需要数据就是订单对象
  2. 后端接受请求,先判断条件
    1. 是否有登录
    2. 查询订单编号,看是否存在,如果存在返回订单对象,如果不在就抛出异常
    3. 判断订单对象中的用户账号,和前台接收的用户账号是否相同,不相同就抛出异常.ps:用户只能看到自己的订单
  3. 用订单编号查询订单是否存在,存在返回订单对象.

秒杀和超卖问题

image.png

为什么代码中做了这个操作,还是会出现重复下单的问题?

因为多线程在执行这行代码时,A线程和B线程同时去访问数据库中是否有该订单,两个线程得到的状态都是没有,所以相当于一个用户发了多次请求.

image.png

解决:

从数据库层面:
可以添加索引,用其索引唯一的特性,来解决
image.png

为什么会出现超卖的情况?

因为真正减少库存的的操作是下面的代码,所以多个线程过来访问的时候,数据库还没有进行更改,线程获取的还库存大于0,所以出现了超卖的情况.
image.png

解决:

从数据库层面:
我们可以通过sql语句,where关键字后面添加库存大于0时才可以执行.同时返回影响行数,在影响行数等于0的时候抛出异常
ps:mysql中innodb是有行锁的,所以不用担心多线程同时访问库存等于1时,同时进行更改

优化代码

优化思想:减少对mysql数据库的访问,让更多请求到redis.

使用redis实现控制访问人数

为什么要使用redis控制访问人数?

比如说:秒杀商品只有十份,有1w个人来访问,只有十个人能成功,而剩下的9000+的人都要去频繁查询数据库,这样就会让数据库压力变大,所以我们可以将商品库存放入redis中,以减少数据库的压力,做一个预库存的处理d

那什么时候将库存放入在redis中呢?

库存放入redis中,其实就是一个初始化,搞清楚要些什么, key的前缀,key,以及库存数量.商品服务大多都有一个后台管理系统,我们可以在点击上架的时候,就将库存初始化到redis中
image.png
image.png

做了预库存的问题就是即使放在redis中,如果有1w人来访问,那么还是只有十个人能买到,其他的人则会造成网络开销.这个问题如何去解决呢?

我们可以通过给每个秒杀的产品的库存添加本地标识符,通过判断标志符来看秒杀商品是否全部卖完.

  1. - **这里又带来一个问题, 标识符如何去设呢?用什么数据结构?**

由于秒杀商品不止是一种,所以如果A商品被卖完,标识符设为true,那么B商品也没有办法卖了,所以我们可以考虑使用map的数据结构,key为商品id,value为boolean

  1. - **那么问题又来了,使用hashmap,hashtable,还是concurrenthashmap?为什么?**

hashmap由于其不加锁,会造成hash冲突,所以不选用,而hashtable相当于给整个数组加锁,性能不高,而ConcurrentMap其特点是分段锁,可以同时进行,所以我们选用concurrentma

具体代码:

  1. 1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/2937743/1606200625310-067e84e4-cd7a-4422-a759-6aa2413c4541.png#align=left&display=inline&height=18&margin=%5Bobject%20Object%5D&name=image.png&originHeight=35&originWidth=919&size=48333&status=done&style=none&width=459.5)
  2. 1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/2937743/1606200717231-4c01ed79-a0a5-49da-b1a5-ae9eabfd6014.png#align=left&display=inline&height=174&margin=%5Bobject%20Object%5D&name=image.png&originHeight=347&originWidth=1069&size=440459&status=done&style=none&width=534.5)

优化重复下单

  1. 重复下单本质上就是保持唯一,然后进行判断,之前使用mysql索引的唯一性进行解决,但是本着优化数据库,尽量将请求到redis中
  2. 我们在其将订单保存到数据库中的时候,存入redis,保持key的唯一,让key为seckill_order:userid:seckillid,其中value是什么都可以,但是我们将value弄为true方便我们秒杀时判断

image.png

  1. 我们秒杀的时候就可以通过前端传过来的userid和seckillid拼接的key来判断订单是否已经存在,如果已经存在了的话,那么就是重复下单

image.png

优化查询秒杀商品数据

同样是减少数据库的压力,我们要将秒杀商品的信息也放到redis中,放到redis中我们要用怎么样的一个数据结构呢?
image.png
我们用map的格式放入我们的秒杀商品数据,由于redis是我们自己用springboot原理封装的,所以我们还要添加方法set(设置),get(获取),getAll(获取全部),
image.pngimage.png
image.png

  1. 做完上面的操作,我们就可以初始化秒杀商品数据到我们的redis中image.png
  2. 初始化完成之后,我们更改controller中查询商品列表和查询商品详情的方法,直接在redis里面查询image.png

    预库存和库存如何保持一致的问题

    由于我们做了一个预库存,预库存在redis中,这个时候预库存和库存的数值是不一致的,如果我们为了保持一致,每次查询秒杀商品的时候去通过数据库查询是否和redis中的数值相等不相等的话,这样我们优化的目的就达不到了.所以我们只要保证redis中的预库存不要为负数就行了,防止redis中的预库存不为负数就行了
    1.在查询秒杀商品列表和查询秒杀商品详情的时候从redis中取出预库存数量,判断是否小于等于0如果是的话,那么将库存设置为0
    image.png

    优化秒杀功能(异步下单)

    流程

    13 秒杀操作.png
    每次用户秒杀商品的时候,在下单的时候,都会去操作数据库,进行一个下单,那么这会很浪费性能,也浪费时间,所以我们可以采用一个异步下单的方式.这时候就要用到rocketMQ消息中间来做处理了

    1. 用户从前端发来请求,我们不直接进入service,先进行一个本地标识的一个判断,判断秒杀商品是否已经卖完了,如果没卖完,就继续往下走
    2. 然后到了预库存也是判断是否已经卖完了,如果卖完了就返回卖完,没卖完就要往MQ发送消息
    3. MQ接受到消息,直接返回”正在抢购中”, service监听消息9,如果得到消息就去操作数据库做秒杀操作
      1. 那么这一步就有一个问题,抢购成功后或者失败后你要将结果返回给用户,怎么返回呢?因为用户的请求已经结束了,在得到正在抢购中就已经结束了
        1. 我们可以定时像数据库发送请求,查询是否成功.
          1. 但是这样子,本来秒杀数据库压力就大, 一大堆请求定时去请求,压力更加大,所以我们可以用websocket给用户推送
    4. 然后为了要让用户知道是否抢购成功,我们用websocket给用户去推送秒杀结果
      1. websocket怎么做呢?
        1. 客户端生成uuid在发送秒杀请求的时候,将用户数据和uuid一起发送到MQ中,
        2. 在得到MQ返回的”正在抢购的回馈时”,客户端会与websocket建立一个长连接,并将uuid发送给websocket,然后websocket会将uuid对应哪个客户端一一对应起来
        3. 而MQ在做秒杀操作之后,也会发送秒杀结果成功或者失败的消息,而websocket会监听这个消息并且消费.
        4. 得到秒杀成功或失败的结果,用uuid对应的关系,将结果返回到该客户端中,这样用户就知道秒杀的结果了
    5. 这时候有来了一个问题,如果高并发情况,有两个消息是同一个人发的话,因为我们数据库做了一个唯一索引的操作,所以两个请求肯定是会有一个是失败的,那么这个时候要做什么操作呢?
      1. 预库存要回补
      2. 修改本地标识
        1. 现在秒杀服务做了集群两个服务甚至三个服务,那么多个服务本地标识都要修改,那要如何去做呢?
          1. 我们可以发出消息,然后消费者用广播模式同时消费这个消息,做到修改本地标识
    6. 如果秒杀成功又要做什么操作呢?
      1. 我们要发出一个延时消息,去判断订单状况是否已付款
      2. 如果付款了那么就发送秒杀成功的消息
      3. 如果超时未付款我们要做
        1. 将订单状态改成超时未支付
        2. 将预库存回补
        3. 修改本地标识
        4. 库存回补

具体步骤

  1. 1. 引入RocketMQ依赖
  2. 1. 配置RocketMQ
  3. 1. RocketMQ需要的字段,设置为常量创建一个类
  4. 1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/2937743/1606467370346-33f7b1c1-7784-4efd-817e-6af9c072a86b.png#align=left&display=inline&height=213&margin=%5Bobject%20Object%5D&name=image.png&originHeight=426&originWidth=1121&size=488022&status=done&style=none&width=560.5)
  5. 4. Controller发出消息,给RocketMQ.所以还要创建一个RocketMQListener接受消息
  6. 1. rocketMQ要贴@RocketMQMessageListener标签然后配置标签需要的参数
  7. 1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/2937743/1606467791357-89460597-fba0-4b7e-b568-2977a9bd5373.png#align=left&display=inline&height=63&margin=%5Bobject%20Object%5D&name=image.png&originHeight=126&originWidth=669&size=114773&status=done&style=none&width=334.5)
  8. 1. 然后贴上@Component交给spring管理
  9. 1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/2937743/1606467839884-0047f64b-aa55-4643-9cc8-466ac21873ba.png#align=left&display=inline&height=148&margin=%5Bobject%20Object%5D&name=image.png&originHeight=296&originWidth=707&size=256882&status=done&style=none&width=353.5)
  10. 5. 这样子基本的秒杀业务就完成了