秒杀业务

运行流程:

  1. 秒杀频道首页列出秒杀商品,点击秒杀商品图片可以跳转到秒杀商品详细页面
  2. 商品详细页面显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存,当库存为0或者不存在活动时间范围内时无法秒杀
  3. 秒杀下单成功,直接跳转到支付页面(扫码),支付成功,跳转到成功页面,填写收货、电话、收件人等信息,完成订单。
  4. 当用户秒杀下单5分钟内未支付,取消预订单,调用支付的关闭订单接口,恢复库存。

A:查询模块(查询库存)并发查询,库存数存在缓存中,(商品信息和图片信息等)静态化处理(生成静态页)和库存剩余数量缓存化处理。
B: 下订单模块(秒杀关键部分),队列控制异步化处理,首先判断这个队列是否已满, 如果没满就将请求放入队列中排队,队列满以后的所有请求直接返回秒杀失败。
C: 支付模块,异步付款,等待付款成功结果。(付款成功更新库存,也可下单的时候扣库存)。

技术特点

  • 读多写少 (一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存)
  • 使用(Redis)缓存解决
    ​ 高并发
  • 限流 (限制同一时间访问的流量)
  • 负载均衡 (单体tomcat并发200完美胜任,突破五,六百就力不从心)
  • 缓存 (预先把秒杀商品加载进内存)
  • 异步 (将同步的并发请求转换为异步,多线程处理)
  • 队列 (使用redis队列,因为pop操作是原子的,即使有很多用户同时到达,也是依次执行)

架构思想

限流

只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端

削峰

对于秒杀系统,瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。

异步处理

秒杀系统是一个高并发系统,采用异步处理模式可以极大的提高系统并发量,其实异步处理就是削峰的一种方式

内存缓存

秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大的提升。

问题解决

一. 秒杀页面(恶意刷新页面)

秒杀活动开始前,其实就有很多用户访问该页面了。如果这个页面的一些资源,比如 CSS、JS、图片、商品详情等,都访问后端服务器,甚至 DB 的话,服务肯定会出现不可用的情况。

解决方案: 所以要创建静态页面让这个页面整体进行静态化,并将页面静态化之后的页面分发到 CDN(类似于资源服务器) 边缘节点上,起到压力分散的作用。

生成的静态页面会遇到的问题: 由于我们以后开发的系统肯定不是给自己用的,用户可能处于不同的时区,他们的当前系统时间也是不同的,所以我们写一个通用的时间规范:就是当前服务器的时间;

二. 防止提前下单

在之前我们做的后端项目中,跳转到某个详情页一般都是:根据ID查询该详情数据,然后将页面跳转到详情页并将数据直接渲染到页面上。但是秒杀系统不同,它也不能就这样简单的定义;

解决方案:首先要保证该商品处于秒杀状态。也就是

  1. 秒杀开始时间要<当前时间;
  2. 秒杀截止时间要>当前时间。

要保证一个用户只能抢购到一件该商品,应做到商品秒杀接口对应同一用户只能有唯一的一个URL秒杀地址,不同用户间秒杀地址应是不同的,且配合订单表seckill_order中联合主键的配置实现。

可以根据正在进行秒杀的商品ID生成秒杀地址值(md5混合值, 避免用户抓包拿到秒杀地址)
通过MD5加密以后,用户在秒杀之前模拟不出真实的地址

三. Nginx优化

动态资源与静态资源进行分离,获取静态资源时不走服务器

限制某个IP同一时间段的访问次数,即针对某一个IP,限制单位时间内发起请求数量。

四. 库存超卖

原子性(atomicity):一个事务是一个不可分割的最小工作单位,要么都成功要么都失败
Redis所有单个命令的执行都是原子性的

秒杀的商品只有10个库存,可能一秒钟有1k个订单;核心思想就是保证库存递减是原子性操作

当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。

解决方案:

1.数据库:

update seckill set num=num-1 where num>0 and goodsId=1

数据库查询、更新的时候有用到行级锁,是可以保证更新操作的原子性的。数据库性能较差,不建议使用

2.Redis分布式锁:

reids->setnx(‘lock’, 1)

设置一个锁,程序执行完成再解锁。锁定的过程,不利于并发执行,所有线程都在等待锁解开,不建议使用。

3.消息队列:

将订单请求全部放入消息队列,然后另外一个后台程序监听消息一个个处理队列中的订单请求。

并发不受影响,但是用户等待的时间较长,进入队列的订单也会很多,体验上不好,不建议使用。

4.redis递减:

redis->incrby('product', -1)

edis的incr和decr 可以实现原子性的递增递减

Java代码的实现

/ 根据抢购的商品id
Seckill seckill = (Seckill) redisTemplate.boundHashOps("seckill").get(goodsId);
if (seckill.getStockCount() < 0) {
    // 商品已售完
    throw new SeckillException("商品已售完");
}
// 减少抢购商品的库存信息
Long count = redisTemplate.boundHashOps("seckill_count").increment(goodsId,-1);
// 更新库存信息
seckill.setStockCount(count);
if (count <= 0) {
    // 库存不足
    // 更新数据库库存数据
    seckill.setStockCount(count);
    seckillMapper.update(seckill);
    // 秒杀结束,把订单信息更新到MySQL中
    throw new SeckillException("库存不足,谢谢参与!");
} else {
    // 更新库存信息
    redisTemplate.boundHashOps("seckill").put(goodsId,seckill);
    // 存储订单信息,暂时存储到redis中

}

5. 解决高并发

活动周期短,瞬间流量大(高并发),大量的人短期涌入服务器抢购,但是数量有限,最终只有少数人能成功下单。

解决方案:

1.redis队列

2.消息中间件

6. 多个集群的数据怎么保持一致性

不要做多集群的数据同步,而是用散列,每个集群的数据是独立存在的。

假设,有10个商品,每个商品有1w库存,规划用10个集群,那么每个集群有10个商品,每个商品是1k库存。

每个集群只需要负责把自己的库存卖掉即可,至于说,会不会有用户知道有10个集群,然后每个集群都去抢。

这种情况就不要用程序来处理了,利用运营规则,活动结束后汇总订单的时候再去处理

比如:某个集群用户访问量特别少,那么可以引入一个中控服务,来监控各个集群的库存,然后再做平衡。


版权声明:文章转载请注明来源,如有侵权请联系博主删除!
最后修改:2020 年 06 月 05 日 09 : 15 AM
如果觉得我的文章对你有用,请随意赞赏
评论打卡也可以哦,您的鼓励是我最大的动力!