高并发读怎么破解?

251 阅读7分钟

高并发读的场景

侧重于高并发读的应用

(1)搜索引擎

image.png

这类系统的主要的特点为 C端用户的数量级非常高 可能是数亿或者是数十亿的数量级的 作为写的一端 网页的发布者的数量级可能是百万或者是千万的 也就是说读的人是远远多于写的,第二个特点就是响应时间C端用户的响应时间要尽可能的快 最慢也绝不能超过1-2s 但是对于发布者来说可能发布一片文章 好一点的情况可能几分钟后就可以被检索到 坏一点的情况可能就是几小时甚至几天后才能被检索到,第三个特点就是读的频率要远远大于写的频率

(2)电商的商品搜索

和上面的模式其实是一样的卖家发布商品 用户在商城进行商品的检索 同样的存在读写规模的差异 并且写的频率肯定是远远小于读的

对于高并发读的应用的优化策略

1.动静分离和CDN加速

(1)静态内容 数据不变 对于用户来说数据基本是一致的 如图片,html,JS,CSS,文件等 各种直播系统 内容生成端产生的视频内容 对于消费端来说看到的内容都是一样的 (2)动态内容 需要根据用户的信息或者其他的信息 进行动态的生成并返回给客户

对于静态的内容 一个最常用的处理策略就是CDN 一个静态资源文件缓存到了全网的各个节点 当一个用户访问的时候 离用户就近的节点还没有数据 CDN就去源系统抓取文件并缓存到该节点 当后面的用户访问的时候只需要从这个节点访问即可 无需再去源系统抓取 接下来我们画一张CDN简单的工作图

image.png

其中一个比较经典的例子就是我们在微信里面发送图片视频的时候 这种就属于典型的静态数据 就会被上传到CDN服务器中 并返回对应的资源ID 为后续的访问加速 因为这种数据一般来说是一旦发布不可修改的

2.加缓存
我们可以使用本地或者是集中式的缓存 或者可以采用多级缓存的架构 但是我们需要注意的是 
假如我们采用的是 多级缓存的这种方式 我们需要确保我们的系统对于数据的一致性没有特别大的要求的时候 
才可以采用这种设计 或者是 我们在数据变更的时候需要同步到各个节点中 
这无疑会增加我们系统的复杂性 比如我们可以 通过 canal监听binlog 服务启动的时候我们注册到zookeeper上
进行元数据的管理 然后当数据发生变化的时候推送到各个节点但是这就有点得不偿失了

对于我们要是使用边路缓存的话比如Redis或者是Memcache的话如果我们的缓存策略是回源(缓存未命中查询DB)的话 我们还需要考虑以下的问题

缓存雪崩 一般是我们需要考虑缓存宕机的问题 这些海量的请求是不是会压垮我们的数据库 这个时候我们其实就可以使用单级缓存作为最后一道屏障

缓存穿透 大量请求不存在的数据导致大量请求打到DB

热点Kye的失效

3.并发读和pipline

1.案例1 异步RPC 假设我们的一个读接口依赖三个RPC接口 耗时分别为T1 T2 T3 如果是同步调用的时候耗时就是T1+T2+T3 如果我们采用的是异步调用的时候 我们起三个线程分别调用三个接口 当然前提是 这三个调用之间没有上下文的依赖关系 可以进行并行的处理 同时因为我们这种场景其实是IO密集型的操作 我们的线程池的参数设计其实可以参考 这个公式: ThreadN=N(1+wt/ct) 其中N为CPU核数 wt为IO等待时间 ct为CPU处理时间(计算时间)

2.pipline 对于我们的缓存系统来说我们单个的命令慢的原因是我们通常在发起一个Get请求之后接收到返回值 再发起下一个请求 这个时候 我们可以采用pipline的时候方式所有的请求流入管道 然后在有序的返回给客户端 无需等待返回

4.批量读

这种策略在我们日常的工作中是非常的常见的 比如说我们使用缓存是的批量获取MulitGet 或者是 查询/插入数据库的时候的批量操作都是batch的一种提现

5.重写轻读

其实这种方案某种情况下也是一种空间换时间的一种体现 我们接下来举两个例子来理解一下这种场景

案例一 微博Feeds流

相信我们在日常使用微信朋友圈或者是微博的时候都会有类似的场景: 用户关注了N个人(或者是有N个好友) 每个人在不断的发布朋友圈或者是微博 系统需要将这N个人的微博或者是朋友圈按照时间排序形成一个列表(也就是feed流) 然后展示给用户 同时用户也需要查看自己的微博列表

所以对于用户来说最基本的需求有两个 查看关注的人的微博列表和查看自己的微博列表

我们先来考虑一下最原始的设计方案 然后看看这种设计的不足之处以及可能出现的瓶颈问题

image.png

假设我们的表中只存储有关文章的ID 关于文章的正文以及相关的资源都存储在NoSql数据库 这个时候 看看我们上面需要实现的两个需求

(1) 查看自己的文章列表

select msg_id from Msg where user_id=xxx offset count

(2) 查看user_id=1 的用户的关注的人的文章列表 这个时候我们就需要两个SQL来处理

select followings from Following where user_id=1 // 查询关注的人 select msg_id from Msg where user_id in (followings) offset count //查看文章

这个时候我们就会发现这种设计查询自己的文章的时候还是可以满足的 但是当我们使用这种模型设计来进行用户的Feeds流的查询的时候必然无法满足高并发的查询需求! 那么这种场景的设计如何优化呢?

这就需要用到我们这一小结的标题所提到的架构设计方案了 "重写轻读" 其实也就是写扩散的逻辑 具体到实现方案就是为我们的每个用户维护一个类似于收件箱的数据结构 当我们的内容发布者进行内容的发布之后 他只需要写到自己的"发件箱" 即可 然后进行异步的扩散 分发到关注者的收件箱中 其实这种思想也是前面所说的空间换时间的思想 如果我们还是采用上面的架构设计的话 那么一些粉丝基数比较大的博主的msg数据无疑会变成热点数据 其实也是通过这种方式来避免了热点数据的出现

其实上面还是遗留了一个小问题 就是关于这个收件箱的设计 我们如何实现(理论上来说应该是个无限长的列表) 我们会在后面为大家单独的讲解

案例二 宽表&搜索引擎

其实我们在日常的开发中难免会遇到对于多张表的join操作 比如我们常见的优化操作可以通过单独查询然后内存聚合 但是假设我们的数据量大或者是一些分页的操作这种实现方案就有些力不从心了

我们还可以通过异步的方式将多表join的结果生成一张宽表又或者是将结果转存到ES等搜索引擎中 其实本质也是通过数据冗余以及多副本的机制来进行读请求的优化