提示:海量数据设计方案、高并发设计方案、宽表是什么、什么是写扩散、微博这种高并发写高并发读怎么设计、什么是对冲请求、什么是重写轻读
`
业内常说高并发问题,高并发设计,可能部分小伙伴接触的比较少,不太清晰,今天正好梳理一下,也是为了方便后续自己查阅。本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!
不管是什么系统,我们以开发者的角度,都可以概括为读、写。其实再复杂的系统,也都是由这两部分组成的。因此,我们可以针对于不同的操作,去设计不同的数据模型或数据结构来使对应的操作更加的高效。
好不夸张的说,但凡做过几年程序员的,几乎都知道:流量快撑不住的时候,可以“加缓存”。这没什么可说的。缓存通常有两种思路:本地缓存,集中式缓存。本地缓存就是在客户端本地加缓存。集中式缓存就比如redis、mongo之类的。对于缓存,我们需要注意以下几点:
如果你不回源。只查缓存,缓存没有直接返回前端,这种方法肯定是主动更新缓存,并且不设置缓存的过期时间,就不会有缓存穿透、缓存雪崩的问题。如果你回源,缓存没有,要先查库,在更新缓存,就需要考虑上面的问题
如果是数据结构比较简单的,那直接
CDN、静态文件加速、动静分离等这一类,我们称为:边缘计算的方案。比如你有一个网页,大家都需要访问,如果所有人都访问你的服务器,那压力很大,你服务器承受不住,那么你就可以把这个网页主动分发到整个CDN网络中,CDN网络会进行很多的备份,然后根据客户的地址就近的去找一个节点去读这个网页。就不用非得挤着一个节点排着队去访问了,大家各自找离自己最近的节点去拿数据
注意:这三种方案(redis、mysql的master/slave、cdn/静态文件加速)虽然从技术上看完全不一样,但是从策略上看,都是“缓存/加副本”的形式,都是通过对数据进行冗余,达到空间换时间的效果。
比如说,一个接口要做3件事。如果是串行的话,总耗时是 T=T1+T2+T3,如果是并行的话,那么总耗时变成了 T = Max(T1,T2,T3)。当然前提是这三个事是没有耦合的关系。如果做完1才能做第二件事的话,就不能异步了。而且异步以后,怎么获取异步的结果也需要考虑。(比较常见的有mq、dubbo之类的。你完成了以后,调个dubbo接口或者发个mq通知一下,把结果传过去之类的回调)
冗余请求很好理解。就是多发几个请求,那为啥要多发几个请求呢。比如说你一个请求,可以访问节点a、b、c。其中节点a是0.5秒,节点b是2秒,节点3是1.5秒。现在客户端希望每个请求的相应时间都不超过1秒。那客户端怎么知道那个请求更快呢?那同时给节点a、b、c都发同一个请求,谁先响应,我就先用谁,这就是冗余请求。
2013年谷歌公司eaf Dean在论文《The Tail at Scale》中讲过这样一个案例:假设一个用户的请求需要100台机器同时处理,每台服务器有1%的概率发生延迟调用(假设响应大于1秒为延迟调用)。那么对于c端用户来说,相应时间大于1秒的概率是63%。怎么算出来的呢?反过来算,如果用于的请求要小于1秒,那么就需要这100台服务器的响应同时小于1秒。那么概率是100个99%相乘。99%^100=0.366032… 100-0.366=63.3%。这意味着,虽然每个机器都只有1%的延迟率,c端用户延迟率却有63%,机器数越多,问题越严重。论文中给出了解决方案:同时给多个服务器发请求,哪个快,就用哪个(冗余请求)。但这会让整个系统的调用量翻倍。
对冲请求:把这个方法调整一下就变成了:客户端首先给服务器端发送一个请求,并等待服务器端给的响应,如果客户端在一定时间内没收到服务器端给的相应,则马上给另一台(或多台)服务器发同样的请求,客户端等待第一个响应到达后,终止其他请求的处理。上面的“一定时间”定义为:内部服务95%请求的响应时间。这种方法在论文中称为“对冲请求”。论文中提到了在谷歌公司的一个测试数据:采用这种方法可以仅用2%的额外请求将系统99%的请求响应从1800ms降低到74ms。
这个的原理其实就是,将比较耗时的操作前移。(比如说查库比较耗时,用户登录的时候要用这个接口的数据,那么你可以在启动项目时先查出来,用户登录时直接看)。 将比较耗时的操作,从查询的时候往前移。我们有很多类似的原理:缓存预热(查库比较耗时,查缓存快,项目启动时直接查好,省的用户请求的时候再去查。
比如说微博或者微信。场景:用户关注了n个人(或者有n个好友),每个人都在不断的发微博,然后系统需要把这n个人的微博按时间排序成一个列表,也就是Feeds流展示给用户。同时,用户也需要查看自己发布的微博列表。
关注的人表:
发布的微博(文章)表:
要想实现场景中的效果(查看这个人关注的人的发布的微博,时间排序),可以:
select followings from Following where user_id = 1 //查询user_id = 1的用户的关注的用户列表
select msg_id from msg where user_id in (followings) limit offset,count //查询关注的所有用户的微博列表
很显然,这种模型无法满足高并发的查询请求,那么怎么处理呢?
改成重写轻读:不是查询的时候再去聚合,而提前为每一个user,准备一个feeds流,或者叫收件箱
原理很简单:假设一个人拥有100个粉丝,那么这个人发布1条微博后,只写入自己的收件箱,就返回成功。然后程序异步的把这条微博去推送给他的这1000个粉丝,也就是“写”扩散。这样,用户读取feeds流的时候就不需要实时的聚合了,直接读取各自的收件箱即可。这就是“重写轻读”,把计算逻辑从“读”的一端移动到了“写”的一端。(当然,这个的前提是微博或者朋友圈这种对写的实时性没那么高的场景下,想微信、qq这种实时聊天平台就不太适合了)。
原理知道了以后,还是会有写其他问题的。
多表的关联查询:宽表与搜索引擎。后端经常需要对业务数据进行多表关联查询,可以通过增加slave来解决。但是这种方法只适用于没有salve的场景。如果数据分库了,那么需要从多个库去查询然后聚合,无法使用原生的join功能。就会存在这么一个问题:如果需要把聚合出来的数据按某个维度排序并分页展示,并且这个维度并不在数据库的字段中,那怎么实现呢?无法使用数据库的排序和分页功能,也无法在内存中通过实时计算来排序、分页(数据量太大了)。
解决方案:类似于微博重写轻读的思路,提前把关联的数据计算好,存在一个地方,读的时候直接去读聚合好的数据,而不是读取的时候再去做join。(比如说,你2个分片,要通过一个数据库不存在的字段去分组,那么你可以写一个定时,每天执行一次,去查数据、聚合,然后存到一个新的表中,这个表就是宽表。宽表意思是:包含了你业务查询所需要的所有的结果,而不是一条条的记录,就是宽表)。也可以用ES类的搜索引擎来实现:把多张表的Join结果做成一个个的文档,放在搜索引擎里面,也可以灵活地实现排序和分页查询功能。
其实这些方案的核心思想都是,让读写分离(CQRS架构。Command Query Responsibility Separation)。让读的处理、写的处理分到不同的两个地方。比如说写,我直接写到库了。读的话,我加缓存啊,加宽表啊之类的.读写分离架构有几个典型特征:
拿库存系统举例:假设用户读到的某个商品的库存是9件,实际上可能是8件(某个用户刚买了一件),也可能是10件(某个用户取消了订单),但等到用户真正下单的那一刻,会实时的扣减数据库里面的库存,也就是左边的写是“实时、完全准确的”,即使右边的读有一定的延迟也没关系。
同样的,拿微博系统举例:博主发了一个博客,他并没有要求粉丝立马,实时的能看到他的文章,延迟几秒钟看到这个文章也没关系。当然了,粉丝也不会感知到自己看到的博客是几秒钟前的。这并不用影响业务。
当然,有些业务,比如说涉及到钱的,或者用户自己的数据(账户里的钱,用户下的订单),在用户体验上肯定要保证,自己修改的数据要马上看到。这种在实现上,读和写可能是完全同步的,也可能是异步的,但是要控制读写比的延迟非常小,用户感知不到。
数据分片其实就是对要处理的数据或请求分成多份并行处理。数据库为了分摊读的压力,可以加缓存、加salve。为了应对高并发写的压力,就需要分库分表了。分表后,还在同一个数据库,一个机器上。但是可以更加充分的利用cpu、内存。分库后,可以利用多台机器的资源。redis cluster也是一样。es的分布式索引,在搜索引擎中有一个基本策略是分布式索引,比如10亿的商品,如果在一个倒序索引中,则索引很大,也不能并发的查询。如果是把这10亿个商品分成n份。建成n个小的索引,一个请求来了以后,并行的在n个索引上查询,再把结果聚合。
异步就太常见了。短信注册啊,订单系统下单啊等等。对于客户端来讲:异步就是请求服务器做一个事,客户端不等结果,直接干别的事。回头再去轮询或者让服务器回调通知。对于服务器来说,是接受到客户端的一个请求后,不立马返回结果,而是“后台慢慢的处理”,稍后返回结果。
这个也挺好理解的。就是将小的消息,合并为一个大的消息,然后去处理这个大的消息。举个例子就是:假设有10个用户,对同一个广告都点击了一次。也就是意味着,对同一个广告主,要扣10次钱。假设一次扣费1块,扣10次,改成批量写,就变成了,一次扣10块,扣一次。如果要模拟一下的话:从持久化的消息队列中取多条消息,对多条消息按广告主的账号id进行分组,同一个组内的消息的扣费金额累加合并,然后从数据库里扣除。
mysql的事务实现上,存在着小事务合并机制。比如扣库存,对同一个SKU,本来是扣10次、每次扣1个,也就是10个事务;在MySQL内核里面合并成1次扣10个,也就是10个事务变成了1个事务。同样,在多机房的数据库多活(跨数据中心的数据库复制)场景中,事务合并也是加速数据库复制的一个重要策略。
今天主要是分享了下,从大的方向上有哪些可以优化的方案方法。很多时候如果我们从最开始的设计合理的话,后续的工作量就会少很多。避免了一些无谓的优化、重构、修复工作。后续有时间的话,会从具体实现的角度来继续分享。