system desing 系统设计(八):视频流videos stream和优惠券coupon/秒杀/抢红包等系统设计

  1、印象中从2017年开始抖音火了,直接带动了视频流这种UGC的火爆,江湖传闻抖音的DAU和平均用户时长已经超过了wechat,实现了弯道超车,实在是后生可畏!对于这种视频站点,可能的架构如下(这是上传的流程,从站点下载视频流程类似):

  

   (1)整个流程大致如下:

  •  用户的请求现到web server,由web server指派encode server,这里web server充当了load balancer的角色
  •     用户把视频分片后挨个把chunk上传到encode server,这里把用户上传的文件统一编码;上传的协议大致是这样的;
  •     encode server再把视频最终存放在专门的服务器,同时把视频的metadata存放到dataBase
  •     web server把用户视频数据插入dataBase

        我个人认为,整个架构中最核心的莫过于视频文件的存储系统了,这里有些市面上常见文件系统的对比,供参考!

  

      (2)除了视频,另一类最重要的数据就数元数据metadata、用户数据userInfo、视频切片video chunk、视频缩略图Thumbnail等数据,这些都是结构化的数据,是完全可以存放在sql或Nosql种的!但由于涉及到数据库之间的关联,所以还是建议用sql数据库!

  (3)视频站点用户最关心的就是观看的流畅度了!理论上讲,怎么提高用户观看的流畅度了?

  •  多缓存几十秒:大家都有这么一种经历:视频刚打开的时候会有一段内容,这段内容快要看完的时候突然又多了几十秒,这就是缓存的预加载preload;大致流程如下:
  •  耳熟能详的CDN

     

     视频行业2/8定理也适用:20%的视频共享了80%的流量!为了加快访问速度,可以把hot video同步到CDN去!

   2、视频站点的成功,收获了海量的流量,这些流量不能就这样白白“浪费”了啊,肯定要想办法变现的嘛,不然怎么做高公司市值了!从90年代末互联网在国内起步,到现在发展了20多年,变现路径还是老三样:

  •   广告
  •        电商以及附加的增值服务
  •        游戏以及附加的增值服务

    国内在电商和游戏两个细分领域分别诞生了阿里和腾讯两家龙头,说明这两个赛道的容量是足够大的,所以抖音开始在电商、O2O生活服务等发力。新上线的服务在冷启动阶段,为了尽快在市场立足,多抢地盘,各种优惠券、秒杀、团购等促销方案都是最基本的运营套路,这些业务的后台又是怎么设计的了?

  (1)先看看秒杀、抢红包的需求

  • Large flow and high concurrency瞬时大流量和高并发:服务器、数据库等能承载的 QPS 有限,如数据库一般是4C8G的单机1000 QPS,需要根据业务预估并发量。
  • Over Sale 有限库存:不能超卖 库存是有限的,需要精准地保证,就是卖掉了 N 个商品。不能超卖,当然也不能少卖了。
  • Malicious ticket Grab黄牛恶意请求:使用脚本模拟用户购买,模拟出十几万个请求去抢购。
  • 固定时间开启 :时间到了才能购买,提前一秒都不可以(以商家「京东」「淘宝」的时间为准)。
  • Purchase limit严格限购:一个用户,只能购买 1 个或 N 个。

   (2)整个系统设计流程如下:

         

  •   因为数据库的QPS和承载能力有限,秒杀可能在秒级内突发10thousands~100thousands的QPS,很明显仅靠数据库可能扛不住,所以需要redis做cache,直接接受用户的请求
  •      查询库存和扣减库存是两个不同的步骤(库表设计见下面):
    •  SELECT stock FROM `stock_info` WHERE commodity_id = 189 AND seckill_id = 28;
    •    UPDATE `stock_info` SET stock = stock - 1 WHERE commodity_id = 189 AND seckill_id = 28 AND stock>0;

           因为是高并发的,所以这两句话就需要依靠transaction来保障了,简而言之:对于某个请求,这两个sql要么都执行,要么都不执行,否则容易引发超卖的问题!对于关系型数据库而言,   transaction是基本的功能,但是redis这种缓存就不同了,需要借助lua脚本来实现,保证多行命令在执行的时候不会被其他命令插队,可以完成一些 Redis 事务性的操作

  •  对于秒杀少量商品(一般都是单价比较贵的,如iphone),因为最终下订单付款的毕竟是少数,所以其实不需要MessageQueue来削锋;但是如果秒杀的商品数量非常多,下单付款的用户量巨大,此时就必须要MessageQueus来缓冲了!虽说有一定的延时,但此刻已经通过了redis,说明已经抢到了商品,付款和数据库扣减有个几秒、十几秒的延迟不碍事,用户完全能接受

  上面的流程图总结一下,本质就是数据在不同的系统流转,流转的顺序或诱因如下:

      

  高并发导致数据库奔溃,于是增加redis做cache;但是并发量超高,redis必须“放心”,数据库又挂了,此时就要使用MQ来削锋、Asyncronize传输数据了!我个人观点:MessageQueue本质上也是一种缓存!

  (3)为了配合上述功能,数据库表设计如下:

      

   为什么要设计4张表? 很明显是为了松耦合,每张表各司其职!但是问题又来了,这些数据库表如果在不同的系统,怎么保证分布式一致性了?又涉及到分布式事务了!需要一个全局的coornidator在不同的数据库系统之间协调,做到全局的transaction!

      

   3、秒杀搞定了,还有优惠券了!各种服务拆解如下:

       

  需要解决的问题:

    

  (1)先看看发券的系统设计:

   

  •    商家通过管理服务器发送优惠券生成和注册的消息到优惠券服务区【如果搞优惠券的商家数量不多,并发量不大,可以省略MessageQueue和优惠券服务器,把优惠券服务器的功能放管理服务器也行】
  •         优惠券服务器在数据库生成优惠券的数据
  •         一旦有用户申请优惠券,优惠券服务器把消息通过触达系统(短信、邮件、站内消息)发送给用户

  (2)再看看领券系统的设计:

  

 

   整个流程也简单:client给优惠券服务器发请求,优惠券服务器先检查优惠券的剩余数量,如果还有就插入一条记录,把user_id注册一下,同时把优惠券的数据量减1;

  (3)为了配合上述流程设计,数据库表设计如下:

  •    优惠券剩余数量的检查:SELECT total_count FROM coupon_batch WHERE batch_id = 1111;
  •  用户抢到优惠券后:
    •  把用户和优惠券做个关联:INSERT INTO coupon (user_id, coupon_id,batch_id) VALUES(1001, 66889, 1111);
    •  更新优惠券的剩余数量:UPDATE coupon_batch SET total_count = total_count - 1, assign_count =assign_count + 1 WHERE batch_id = 1111 AND total_count > 0;

  那么现在问题又来了,和秒杀的问题如出一辙:在高并发的环境下,否则容易产生如下后果:

  •   insert成功但updata失败:超发
  •        insert失败但updata成功:少发

  所以这3个sql查询是需要transaction支持的,尤其是后面的insert和updata,必须要保证要么都成功,要么都失败!所以数据库选型只能是关系型sql数据库了,比如mysql!

  (4)原则上讲,一个用户只能领有限数量的券,不可能让一个用户把所有券都领完!怎么限制用户领券的数量了,也就是防止用户超领?秒杀同样存在这个问题!

     

     这里借用秒杀的思路:用户一旦成功申请到优惠券,就把user_id放入redis集合中!用户每次申请优惠券,都需要先在redis中查看是否已经存在!如果是,说明已经申请过了,此时可以直接拒绝,或把用户界面的申请按钮至灰,就不让用户点击和发请求了!

  4、 幂等性idempotency&防重放攻击

  做渗透测试时,有个常见的攻击手法:支付下单payment的时候抓包,然后对抓到的包做分析,理论上应该有id、price、amount、count类的字段,此时可以尝试更改这些字段,然后发包过去,看看服务器返回啥!也可以先把原来的包放过去,再更改这些关键字段的值后重复发包(业界俗称重放攻击),看看服务器怎么应对!为了应对这种重放攻击,业务上可以采用的幂等性方式如下:

  • 利用数据库的唯一约束实现幂等:比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号(检查这个编号id是不是已经存在了)就可以保证幂等
  • 去重表:本质也是根据数据库的唯一性约束来实现。思路是:首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重现重复消费,数据库会抛唯一约束异常,操作就会回滚
  • 利用redis的原子性:每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中
  • 多版本(乐观锁)控制:此方案多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1
  • 状态机机制:此方案多用于更新且业务场景存在多种状态流转的场景
  • token机制:生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。

  

参考:

1、https://juejin.cn/post/6970999336020738085  消息队列详解

2、https://www.bilibili.com/video/BV1Za411Y7rz?p=2&vd_source=241a5bcb1c13e6828e519dd1f78f35b2  系统设计

 

posted @ 2022-08-21 11:36  第七子007  阅读(355)  评论(0编辑  收藏  举报