14.高并发读写


  • 侧重于“高并发读”的系统
    • 搜索引擎
    • 电商的商品搜索
    • 电商系统的商品描述、图片和价格
  • 侧重于“高并发写”的系统
    • 广告扣费系统
  • 同时有“高并发读”和“高并发写”的系统
    • 电商的库存系统和秒杀系统
    • 支付系统和微信红包
    • IM、微博和朋友圈

高并发读

  • 加缓存/读副本
    • 本地缓存或集中式缓存(主动更新/被动更新)
      • 高可用问题
      • 不回源策略:缓存为空时直接返回空,不设置缓存的过期时间
      • 回源策略:缓存为空时查库更新缓存
        • 缓存穿透/缓存击穿/热Key过期/缓存雪崩
    • MySQL 的 Master/Slave
    • CDN/静态文件加速/动静分离
  • 并发读
    • 异步RPC
    • 冗余请求:100台机器每台延迟率为1%,则C端延迟率为 (1-99%100)=63.4%
      • 客户端同时向多台服务器发送请求,哪个返回得快就用哪个(但是会调用量翻倍)
      • 对冲请求
        • 如果客户端在一定的时间内(内部服务95%请求的响应时间)没有收到服务端的响应,则马上给另一台(或多台)服务器发送同样的请求
        • 采用这种方法,可以仅用2%的额外请求将系统99.9%的请求响应时间从1800ms 降低到74ms
      • 捆绑请求
        • 当一个上游系统向下游系统分发请求时,将请求分发给任务队列较短,负载较轻的服务器,因为服务抖动一个常见的来源是请求被执行前在服务端的排队延迟
        • 但是探测负载后服务器负载会发生变化,而且多个服务探测到一个低负载服务时,会使该服务瞬间过热
          • 上游系统同时发送两个一样的请求给下游多个服务器,当下游服务器处理完成后,通知其他服务器不用再处理该请求。
          • 为了防止由于任务队列为空而导致的所有服务器同时处理任务的情况,上游系统需要在发给下游多个系统的两个请求之间引入一个延迟,该延迟时间要足够第一个系统处理完任务并通知其他系统。
      • 不要在底层设置太长的任务队列,而由上层基于任务的优先级进行动态的下发
        • 自己维护一个队列允许服务器跳过那些更早到来的非延迟敏感的批处理操作,优先往下传那些高优先级的交互请求
      • 将需要长时间运行的任务拆成多个短时间运行的任务
      • 控制好定时任务和后台运行的任务
        • 将后台的大任务拆分成一系列的小任务,在后台任务运行的时候首先确认系统的负载,如果负载太高,则延迟运行后台任务等
  • 重写轻读
    • 微博Feeds流:不是查询的时候再去聚合,而是提前为每个user_id准备一个Feeds流,或者叫收件箱,每个用户都有一个发件箱和收件箱
      • 写扩散:发布1条微博后,只写入自己的发件箱就返回成功。然后后台异步地把这条微博推送到1000个粉丝的收件箱
      • 限制数量:Redis最多保存2000条,2000条以外的丢弃。2000个以前的数据持久化存储并且支持分页查询
        • DB分库分表同时按user_id和时间范围进行分片
        • DB二级索引:记录<user_id,月份,count>,快速地定位到offset = 5000的微博发生在哪个月份,也就是哪个数据库的分片
      • 推拉结合:大V的粉丝太多了
        • 对于粉丝数多的用户,只推送给在线的粉丝们,离线用户上线时主动去拉
    • 多表的关联查询:宽表与搜索引擎
      • 分库分表下的关联查询:提前把关联数据计算好,存在一个地方,读的时候直接去读聚合好的数据,而不是读取的时候再去做Join
      • 可以另外准备一张宽表:把要关联的表的数据算好后保存在宽表里
        • 可以定时算,也可能任何一张原始表发生变化之后就触发一次宽表数据的计算
        • ES搜索引擎:把多张表的Join结果做成一个个的文档
  • 读写分离(CQRS 架构)
    • 分别为读和写设计不同的数据结构
    • 写-分库分表;读-缓存;join宽表存ES
    • 读和写的串联
      • 定时任务定期把业务数据库中的数据转换成适合高并发读的数据结构
      • 写的一端把数据的变更发送到消息中间件,然后读的一端消费消息
      • 直接监听业务数据库中的 Binlog,监听数据库的变化来更新读的一端的数据
    • 读比写有延迟(最终一致性)
      • 但是对对于用户自己的数据,自己写自己读,在用户体验上肯定要保证自己修改的数据马上能看到
        • 读和写完全同步
        • 异步的,但要控制读比写的延迟非常小,用户感知不到
      • 保证数据不能丢失、不能乱序

高并发写

  • 数据分片:对要处理的数据或请求分成多份并行处理
    • 分库分表
    • ES分布式索引:并行地在n个索引上查询,再把查询结果进行合并
  • 异步化:凡是不阻碍主流程的业务逻辑,都可以异步化,放到后台去做
    • 短信验证码注册或登录:60s之后没有收到短信,用户会再次点击
    • 电商的订单系统:拆单
      • 支付完成后,服务器会立即返回成功,而不是等1个拆成3个之后再返回成功
    • 广告计费系统
      • 在扣费之前其实还有一系列的业务逻辑要处理,比如判断是否为机器人在刷单
      • 实际上用户的点击请求或浏览请求首先会以日志的形式进行落盘。一般是支持持久化的消息队列
      • 落盘之后,立即给客户端返回数据。后续的所有处理,当然也包括扣费,全部是异步化的,而且会使用流式计算模型完成后续的所有工作。
    • 写内存 + Write-Ahead 日志:MySQL 中为了提高磁盘IO 的写性能,使用了Write-Ahead 日志,也就是Redo Log。
      • 高并发地扣减 MySOL 中的账户余额,或者电商系统中扣库存,如果直接在数据库中扣,数据库会扛不住,则可以在Redis 中扣,同时落一条日志(日志可以在一个高可靠的消息中间件或数据库中插入一条条的日志)。当Redis 宕机,把所有的日志重放完毕,再用数据库中的数据初始化Redis 中的数据。
  • 批量写
    • 广告计费系统的合并扣费
      • 扣费模块一次性地从持久化消息队列中取多条消息,对多条消息按广告主的账号ID进行分组,同一个组内的消息的扣费金额累加合并,然后从数据库里扣除
    • MySQL 的小事务合并机制
      • 比如扣库存,对同一个SKU,本来是扣10 次、每次扣1 个,也就是10 个事务;在MySQL 内核里面合并成1 次扣10 个,也就是10 个事务变成了1 个事务。
      • 我们同样可以借鉴这一点,比如我们的Canal 同步代码中,首页广告内容的更新就采用同样的机制
      • 在多机房的数据库多活(跨数据中心的数据库复制)场景中,事务合并也是加速数据库复制的一个重要策略

总结梳理

高并发读

  • 加缓存/读副本

    • 本地缓存或集中式缓存(主动更新/被动更新)

      • 高可用问题

      • 不回源策略:缓存为空时直接返回空,不设置缓存的过期时间

      • 回源策略:缓存为空时查库更新缓存

        • 缓存穿透/缓存击穿/热Key过期/缓存雪崩
    • MySQL 的 Master/Slave

    • CDN/静态文件加速/动静分离

  • 并发读

    • 异步RPC

    • 冗余请求:100台机器每台延迟率为1%,则C端延迟率为 (1-99%100)=63.4%

      • 客户端同时向多台服务器发送请求,哪个返回得快就用哪个(但是会调用量翻倍)

      • 对冲请求

        • 如果客户端在一定的时间内(内部服务95%请求的响应时间)没有收到服务端的响应,则马上给另一台(或多台)服务器发送同样的请求

        • 采用这种方法,可以仅用2%的额外请求将系统99.9%的请求响应时间从1800ms 降低到74ms

      • 捆绑请求

        • 当一个上游系统向下游系统分发请求时,将请求分发给任务队列较短,负载较轻的服务器,因为服务抖动一个常见的来源是请求被执行前在服务端的排队延迟

        • 但是探测负载后服务器负载会发生变化,而且多个服务探测到一个低负载服务时,会使该服务瞬间过热

          • 上游系统同时发送两个一样的请求给下游多个服务器,当下游服务器处理完成后,通知其他服务器不用再处理该请求。

          • 为了防止由于任务队列为空而导致的所有服务器同时处理任务的情况,上游系统需要在发给下游多个系统的两个请求之间引入一个延迟,该延迟时间要足够第一个系统处理完任务并通知其他系统。

      • 不要在底层设置太长的任务队列,而由上层基于任务的优先级进行动态的下发

        • 自己维护一个队列允许服务器跳过那些更早到来的非延迟敏感的批处理操作,优先往下传那些高优先级的交互请求
      • 将需要长时间运行的任务拆成多个短时间运行的任务

      • 控制好定时任务和后台运行的任务

        • 将后台的大任务拆分成一系列的小任务,在后台任务运行的时候首先确认系统的负载,如果负载太高,则延迟运行后台任务等
  • 重写轻读

    • 微博Feeds流:不是查询的时候再去聚合,而是提前为每个user_id准备一个Feeds流,或者叫收件箱,每个用户都有一个发件箱和收件箱

      • 写扩散:发布1条微博后,只写入自己的发件箱就返回成功。然后后台异步地把这条微博推送到1000个粉丝的收件箱

      • 限制数量:Redis最多保存2000条,2000条以外的丢弃。2000个以前的数据持久化存储并且支持分页查询

        • DB分库分表同时按user_id和时间范围进行分片

        • DB二级索引:记录<user_id,月份,count>,快速地定位到offset = 5000的微博发生在哪个月份,也就是哪个数据库的分片

      • 推拉结合:大V的粉丝太多了

        • 对于粉丝数多的用户,只推送给在线的粉丝们,离线用户上线时主动去拉
    • 多表的关联查询:宽表与搜索引擎

      • 分库分表下的关联查询:提前把关联数据计算好,存在一个地方,读的时候直接去读聚合好的数据,而不是读取的时候再去做Join

      • 可以另外准备一张宽表:把要关联的表的数据算好后保存在宽表里

        • 可以定时算,也可能任何一张原始表发生变化之后就触发一次宽表数据的计算

        • ES搜索引擎:把多张表的Join结果做成一个个的文档

  • 读写分离(CQRS 架构)

    • 分别为读和写设计不同的数据结构

    • 写-分库分表;读-缓存;join宽表存ES

    • 读和写的串联

      • 定时任务定期把业务数据库中的数据转换成适合高并发读的数据结构

      • 写的一端把数据的变更发送到消息中间件,然后读的一端消费消息

      • 直接监听业务数据库中的 Binlog,监听数据库的变化来更新读的一端的数据

    • 读比写有延迟(最终一致性)

      • 但是对对于用户自己的数据,自己写自己读,在用户体验上肯定要保证自己修改的数据马上能看到

        • 读和写完全同步

        • 异步的,但要控制读比写的延迟非常小,用户感知不到

      • 保证数据不能丢失、不能乱序

高并发写

  • 数据分片:对要处理的数据或请求分成多份并行处理

    • 分库分表

    • ES分布式索引:并行地在n个索引上查询,再把查询结果进行合并

  • 异步化:凡是不阻碍主流程的业务逻辑,都可以异步化,放到后台去做

    • 短信验证码注册或登录:60s之后没有收到短信,用户会再次点击

    • 电商的订单系统:拆单

      • 支付完成后,服务器会立即返回成功,而不是等1个拆成3个之后再返回成功
    • 广告计费系统

      • 在扣费之前其实还有一系列的业务逻辑要处理,比如判断是否为机器人在刷单

      • 实际上用户的点击请求或浏览请求首先会以日志的形式进行落盘。一般是支持持久化的消息队列

      • 落盘之后,立即给客户端返回数据。后续的所有处理,当然也包括扣费,全部是异步化的,而且会使用流式计算模型完成后续的所有工作。

    • 写内存 + Write-Ahead 日志:MySQL 中为了提高磁盘IO 的写性能,使用了Write-Ahead 日志,也就是Redo Log。

      • 高并发地扣减 MySOL 中的账户余额,或者电商系统中扣库存,如果直接在数据库中扣,数据库会扛不住,则可以在Redis 中扣,同时落一条日志(日志可以在一个高可靠的消息中间件或数据库中插入一条条的日志)。当Redis 宕机,把所有的日志重放完毕,再用数据库中的数据初始化Redis 中的数据。
  • 批量写

    • 广告计费系统的合并扣费

      • 扣费模块一次性地从持久化消息队列中取多条消息,对多条消息按广告主的账号ID进行分组,同一个组内的消息的扣费金额累加合并,然后从数据库里扣除
    • MySQL 的小事务合并机制

      • 比如扣库存,对同一个SKU,本来是扣10 次、每次扣1 个,也就是10 个事务;在MySQL 内核里面合并成1 次扣10 个,也就是10 个事务变成了1 个事务。

      • 我们同样可以借鉴这一点,比如我们的Canal 同步代码中,首页广告内容的更新就采用同样的机制

      • 在多机房的数据库多活(跨数据中心的数据库复制)场景中,事务合并也是加速数据库复制的一个重要策略


文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录