秒杀功能实现
- 前端秒杀按钮的处理,判断其是否在秒杀时间内,如果在秒杀时间内,那么按钮可以按,如果不行按钮就变成灰通过(disable)来处理
- 秒杀按钮按下之后给后端发送一个请求,这时候搞明白前端需要的返回数据是什么?(订单编号)
- 后端接受到请求,先考虑情况
- 判断其是否有登录? 如果没有抛出异常
- 判断其是否有重复下单?
- 判断其是否是在秒杀时间内下单
- 判断是否有库存
- 查看是否有该商品
- 减少库存
- 生成订单
- 生成订单记录
- 返回订单编号(订单编号要是有序的,使用推特的雪花算法)
Tip:记得贴@Transactional标签
订单详情
- 前端获取订单编号,发起请求,因为显示的数据是订单信息的,所以需要数据就是订单对象
- 后端接受请求,先判断条件
- 是否有登录
- 查询订单编号,看是否存在,如果存在返回订单对象,如果不在就抛出异常
- 判断订单对象中的用户账号,和前台接收的用户账号是否相同,不相同就抛出异常.ps:用户只能看到自己的订单
- 用订单编号查询订单是否存在,存在返回订单对象.
秒杀和超卖问题
为什么代码中做了这个操作,还是会出现重复下单的问题?
因为多线程在执行这行代码时,A线程和B线程同时去访问数据库中是否有该订单,两个线程得到的状态都是没有,所以相当于一个用户发了多次请求.
解决:
为什么会出现超卖的情况?
因为真正减少库存的的操作是下面的代码,所以多个线程过来访问的时候,数据库还没有进行更改,线程获取的还库存大于0,所以出现了超卖的情况.
解决:
从数据库层面:
我们可以通过sql语句,where关键字后面添加库存大于0时才可以执行.同时返回影响行数,在影响行数等于0的时候抛出异常
ps:mysql中innodb是有行锁的,所以不用担心多线程同时访问库存等于1时,同时进行更改
优化代码
优化思想:减少对mysql数据库的访问,让更多请求到redis.
使用redis实现控制访问人数
为什么要使用redis控制访问人数?
比如说:秒杀商品只有十份,有1w个人来访问,只有十个人能成功,而剩下的9000+的人都要去频繁查询数据库,这样就会让数据库压力变大,所以我们可以将商品库存放入redis中,以减少数据库的压力,做一个预库存的处理d
那什么时候将库存放入在redis中呢?
库存放入redis中,其实就是一个初始化,搞清楚要些什么, key的前缀,key,以及库存数量.商品服务大多都有一个后台管理系统,我们可以在点击上架的时候,就将库存初始化到redis中
做了预库存的问题就是即使放在redis中,如果有1w人来访问,那么还是只有十个人能买到,其他的人则会造成网络开销.这个问题如何去解决呢?
我们可以通过给每个秒杀的产品的库存添加本地标识符,通过判断标志符来看秒杀商品是否全部卖完.
- **这里又带来一个问题, 标识符如何去设呢?用什么数据结构?**
由于秒杀商品不止是一种,所以如果A商品被卖完,标识符设为true,那么B商品也没有办法卖了,所以我们可以考虑使用map的数据结构,key为商品id,value为boolean
- **那么问题又来了,使用hashmap,hashtable,还是concurrenthashmap?为什么?**
hashmap由于其不加锁,会造成hash冲突,所以不选用,而hashtable相当于给整个数组加锁,性能不高,而ConcurrentMap其特点是分段锁,可以同时进行,所以我们选用concurrentma
具体代码:
1. 
1. 
优化重复下单
- 重复下单本质上就是保持唯一,然后进行判断,之前使用mysql索引的唯一性进行解决,但是本着优化数据库,尽量将请求到redis中
- 我们在其将订单保存到数据库中的时候,存入redis,保持key的唯一,让key为seckill_order:userid:seckillid,其中value是什么都可以,但是我们将value弄为true方便我们秒杀时判断
- 我们秒杀的时候就可以通过前端传过来的userid和seckillid拼接的key来判断订单是否已经存在,如果已经存在了的话,那么就是重复下单
优化查询秒杀商品数据
同样是减少数据库的压力,我们要将秒杀商品的信息也放到redis中,放到redis中我们要用怎么样的一个数据结构呢?
我们用map的格式放入我们的秒杀商品数据,由于redis是我们自己用springboot原理封装的,所以我们还要添加方法set(设置),get(获取),getAll(获取全部),
- 做完上面的操作,我们就可以初始化秒杀商品数据到我们的redis中
初始化完成之后,我们更改controller中查询商品列表和查询商品详情的方法,直接在redis里面查询
预库存和库存如何保持一致的问题
由于我们做了一个预库存,预库存在redis中,这个时候预库存和库存的数值是不一致的,如果我们为了保持一致,每次查询秒杀商品的时候去通过数据库查询是否和redis中的数值相等不相等的话,这样我们优化的目的就达不到了.所以我们只要保证redis中的预库存不要为负数就行了,防止redis中的预库存不为负数就行了
1.在查询秒杀商品列表和查询秒杀商品详情的时候从redis中取出预库存数量,判断是否小于等于0如果是的话,那么将库存设置为0优化秒杀功能(异步下单)
流程
每次用户秒杀商品的时候,在下单的时候,都会去操作数据库,进行一个下单,那么这会很浪费性能,也浪费时间,所以我们可以采用一个异步下单的方式.这时候就要用到rocketMQ消息中间来做处理了- 用户从前端发来请求,我们不直接进入service,先进行一个本地标识的一个判断,判断秒杀商品是否已经卖完了,如果没卖完,就继续往下走
- 然后到了预库存也是判断是否已经卖完了,如果卖完了就返回卖完,没卖完就要往MQ发送消息
- MQ接受到消息,直接返回”正在抢购中”, service监听消息9,如果得到消息就去操作数据库做秒杀操作
- 那么这一步就有一个问题,抢购成功后或者失败后你要将结果返回给用户,怎么返回呢?因为用户的请求已经结束了,在得到正在抢购中就已经结束了
- 我们可以定时像数据库发送请求,查询是否成功.
- 但是这样子,本来秒杀数据库压力就大, 一大堆请求定时去请求,压力更加大,所以我们可以用websocket给用户推送
- 我们可以定时像数据库发送请求,查询是否成功.
- 那么这一步就有一个问题,抢购成功后或者失败后你要将结果返回给用户,怎么返回呢?因为用户的请求已经结束了,在得到正在抢购中就已经结束了
- 然后为了要让用户知道是否抢购成功,我们用websocket给用户去推送秒杀结果
- websocket怎么做呢?
- 客户端生成uuid在发送秒杀请求的时候,将用户数据和uuid一起发送到MQ中,
- 在得到MQ返回的”正在抢购的回馈时”,客户端会与websocket建立一个长连接,并将uuid发送给websocket,然后websocket会将uuid对应哪个客户端一一对应起来
- 而MQ在做秒杀操作之后,也会发送秒杀结果成功或者失败的消息,而websocket会监听这个消息并且消费.
- 得到秒杀成功或失败的结果,用uuid对应的关系,将结果返回到该客户端中,这样用户就知道秒杀的结果了
- websocket怎么做呢?
- 这时候有来了一个问题,如果高并发情况,有两个消息是同一个人发的话,因为我们数据库做了一个唯一索引的操作,所以两个请求肯定是会有一个是失败的,那么这个时候要做什么操作呢?
- 预库存要回补
- 修改本地标识
- 现在秒杀服务做了集群两个服务甚至三个服务,那么多个服务本地标识都要修改,那要如何去做呢?
- 我们可以发出消息,然后消费者用广播模式同时消费这个消息,做到修改本地标识
- 现在秒杀服务做了集群两个服务甚至三个服务,那么多个服务本地标识都要修改,那要如何去做呢?
- 如果秒杀成功又要做什么操作呢?
- 我们要发出一个延时消息,去判断订单状况是否已付款
- 如果付款了那么就发送秒杀成功的消息
- 如果超时未付款我们要做
- 将订单状态改成超时未支付
- 将预库存回补
- 修改本地标识
- 库存回补
具体步骤
1. 引入RocketMQ依赖
1. 配置RocketMQ
1. 将RocketMQ需要的字段,设置为常量创建一个类
1. 
4. Controller发出消息,给RocketMQ.所以还要创建一个RocketMQListener接受消息
1. rocketMQ要贴@RocketMQMessageListener标签然后配置标签需要的参数
1. 
1. 然后贴上@Component交给spring管理
1. 
5. 这样子基本的秒杀业务就完成了