高并发系统设计:读写分离提升MySQL的并发量

为什么要读写分离

  • 大部分系统的访问模型是读多写少,读写请求量的差距可能达到几个数量级。因此,我们需要优先考虑数据库如何抗住更高的查询请求,所以,首先需要把读写流量分开,因为这样才方便针对读流量做单独的扩展,这就是我们所说的主从读写分离
  • 能不能用redis作为MySQL的前置缓存,帮助MySQL挡住大部分的查询请求,降低查询的并发量
    • 对于类似电商中的商品系统这样与用户关联不大的系统,效果特别好,因为在这些系统中,每个人看到的内容都是一样的,这时,redis缓存的命中率非常高,几乎全部的请求都可以命中缓存,几乎没有多少请求能够穿透到MySQL。
    • 但是,和用户相关的系统,比如订单系统、账户系统等使用缓存的效果就没有这么好了,因为在这些系统中,每个用户需要查询的信息都是和用户相关的,即使是同一个功能界面,每个人看到的数据都是不一样的
  • 那随着系统用户数量越来越多,达到MySQL上的读写请求也就越来越多,当单台MySQL支撑不了这么多的并发请求时,怎么办?读写分离是提高MySQL并发的首选方案
    • 当单台MySQL无法满足要求时,只能用多个MySQL实例来承担大量的读写请求。MySQL和大部分常用的关系数据库一样,都是典型的单机数据库,不支持分布式部署
      • 用一个单机数据库的多个实例来组成一个集群,提供分布式数据库服务,是一个非常困难的事情。
      • 在部署集群的时候,需要做很多额外的工作,而且很难做到对应用透明,导致应用程序也需要为此做较大的架构调整。
      • 所以,除非系统规模真的大到只有这一条路可以走,不建议对数据进行分片,自行构建MySQL集群,代价非常大。
    • 解决方案,我们不对数据进行分片,而是使用多个具有相同数据的MySQL实例来分担大量的查询请求,这种方法叫做“读写分离”
      • 读写分离之所以能够解决问题,是因为很多系统,特别面向公众用户的互联网系统,都是读多写少的。也就是说,数据库需要应对的绝大部分是只读查询请求。
      • 一个分布式存储系统,想要做分步写是非常困难的,因为很难解决好数据一致性的问题。但是实现分布式读就相对简单很多,只需要增加一些只读的实例,把数据实时的同步到这些只读实例上,这些只读的实例就可以分担大量的查询请求。
      • 读写分离的另一个好处就是,它实施起来相对比较简单。把使用单机MySQL的系统升级为读写分离的多实例架构非常容易,一般不需要修改业务逻辑,只需要简单修改DAOD代码,把对数据的读写请求分开,请求不同的MySQL实例就好了
      • 读写分离依靠主从同步技术使得数据库实现了数据复制为多份,增加了抵抗大量并发读请求的能力,提升了数据库的查询性能的同时,也提升了数据的安全性,当某一个数据库节点,无论是主库还是从库发生故障时,我们还有其他的节点中存储全量的数据,保证数据不会丢失。
  • 那么,如何实施MySQL的读写分离方案呢?需要做两件事:
    • 部署一主多从MySQL实例,并让它们之间保持数据实时同步。
      • 也就是说对数据间拷贝,我们可以将一个数据库的数据拷贝一份或者多份,并且写入其他的数据库服务器中。这个过程也叫做主从复制
      • 原始的数据库我们称为主库,主要负责数据的写入;拷贝的目标数据库叫做从库,主要负责数据查询
    • 分离应用程序对数据库的读写请求,分别发送给主库和从库
      • 也就是说,在主从分离的情况下,我们如何屏蔽主从分离带来的访问数据库方式的变化,让开发同学像是在使用单一数据库一样

在这里插入图片描述
ps:啥叫做数据的分片

  • 读写分离是为了解决大量数据查询问题的
  • 分片在MySQL中体现为分库分表。分库分表是主要是为了解决数据库的海量存储,以及海量数据带来的大量写压力的

两个技术关键点

主从复制

我们以MySQL为例介绍一下主从复制。

MySQL 自带主从同步的功能,经过简单的配置就可以实现一个主库和几个从库之间的数据同步,部署和配置的方法

MySQL的主从复制是依赖于binlog的,也就是记录MySQL上的所有变化并以二进制式保存在磁盘上二进制日志文件。主从复制就是将binlog中的数据从主从传输到从库上,一般这个过程是异步的,即主库上的操作不会等待binlog同步的完成。

主从复制的过程是这样的

  • 首先从库在连接到主节点时会创建一个IO线程,用以请求主库更新的binlog,并且把接收到的binlog信息写入到一个叫做relay log的日志文件中,而主库也会创建一个log dumo线程来发送binlog给从库
  • 同时,从库也会创建一个SQL线程读取relay log中的内容,并且在从库中做回放,最终实现主从的一致性

在这个方案中,使用独立的log dump线程是一种异步的方式,可以避免对主库的主体更新流程产生影响,而从库在接受到信息后并不是写入从库的存储中,而是写入一个relay log。是避免写入从库实际存储会比较耗时,最终造成主库和从库延迟变长。

在这里插入图片描述
你会发现,基于性能的考虑,主库的写入流程并没有等待主从同步完成就会返回结果,那么在极端的情况下,比如说主库上binlog还没有来得及刷新到磁盘上就出现了磁盘损坏或者机器掉电,就会导致binlog的丢失,最终造成主从数据的不一致。不过,这种情况出现的概率很低,对于互联网项目来说是可以容忍的。

做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。同时,在读流量比较大的情况下,我们可以部署多个从库共同承担读流量,这就是”一主多从“部署方式。另外,从库也可以当作一个备库来使用,以避免主库故障导致数据丢失。

那么,是不是无限制的增加从库的数量就可以抵抗住大量的并发呢?不是。因为随着从库数量的增加,从库连接上来的IO线程比较多,主库也需要创建同样多的log dump线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带宽,所以在实际使用时,一般一个主库最多3~5个从库。

主从复制的缺陷:

  • 部署复杂
  • 会有一定的主从同步的延迟
    • 原因是,数据库中的数据在主库完成更新后,是异步同步到每个从库上,这个过程有一个微小的时间差,这个时间差叫做主从同步延迟
    • 正常情况下,主从延迟非常小,不超过1ms。但是即使这个非常小的延迟,也会导致在某一个时刻,主库和从库上的数据是不一致的。
    • 应用程序需要能够接受并克服这种主从不一致的情况,否则就会引发一些由于主从延迟导致的数据错误。
    • 主从同步的延迟,是我们排查问题时很容易忽略的一个问题
    • 有时候我们遇到从数据库中获取不到信息的诡异问题,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。
    • 所以,我们一般会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间应该是在毫秒级别,一旦落到秒级别就需要告警了

但是我们应该怎么设计方案来容忍这个主从同步延迟呢?

  • 这种问题其实没什么好的技术手段来解决,所以你看大的电商,它支付完成后是不会自动跳回到订单页的,它增加了一个无关紧要的“支付完成”页面,其实这个页面没有任何有效的信息,就是告诉你支付成功,然后再放一些广告什么的。你如果想再看刚刚支付完成的订单,需要手动点一下,这样就很好地规避了主从同步延迟的问题。也就是说需要手动刷新

  • 我们需要特别注意的,是那些数据更新后,立刻需要查询更新后的数据,然后再更新其他数据这种情况。比如说在购物车页面,如果用户修改了某个商品的数量,需要重新计算优惠和总价。更新了购物车的数据后,需要立即调用计价服务,这个时候如果计价服务去读购物车的从库,非常可能读到旧数据而导致计算的总价错误。对于这个例子,可以把“更新购物车、重新计算总价”这两个步骤合并成一个微服务,然后放在一个数据库事务中去,同一个事务中的查询操作也会被路由到主库,这样来规避主从不一致的问题

对于这种主从延迟带来的数据不一致的问题,没有什么简单方便而且通用的技术方案可以解决,我们需要重新设计业务逻辑,尽量规避更新数据后立即去从库查询刚刚更新的数据核心思想就是尽量不去从库中查询信息

比如有个场景,在发微博的过程中会有些同步的操作,像是更新数据库的操作,也有一些异步的操作,比如说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的ID写入消息队列,再由队列处理机根据ID在从库中获取微博信息再发送给审核系统。此时如果主从数据库存在延迟,会导致再从库中获取不到微博信息,整个流程就会出现异常。

在这里插入图片描述

具体怎么做呢?

  • 第一种方案是数据的冗余。可以再发送消息队列时不仅仅发送微博ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中查询数据
    • 优先考虑这种方案,因为它足够简单
    • 缺点是可能造成单条消息比较大,从而增加了消息发送的带宽和时间
  • 第二种方案是使用缓存。可以再同步写数据库时,也把微博的数据写入到memcached缓存里面,这样队列处理机在获取微博信息时会优先查询缓存,这样也可以保证数据的一致性
    • 适合新增数据的场景
    • 在更新数据的场景下,先更新缓存可能会造成数据的不一致。比方说两个线程同时更新数据,线程 A 把缓存中的数据更新为 1,此时另一个线程 B 把缓存中的数据更新为 2,然后线程 B 又更新数据库中的数据为 2,此时线程 A 更新数据库中的数据为 1,这样数据库中的值(1)和缓存中的值(2)就不一致了
  • 第三方方案时查询主库。可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,负责会对主库造成比较大的压力。
    • 不推荐这种方案
    • 因为一旦主库承担了大量的读请求导致崩溃,那么对整体系统的影响是极大的

如何访问数据库

我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库的读写分离。这时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,复杂度提升了很多。

为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。

  • 第一类:淘宝的TDDL,Sharding-JDBC像这种内嵌在代码中的第三方组件中。
    • 原理:这些组件集成在你的应用程序内,代理应用程序的所有数据库请求,自动把请求路由到对应数据库实例上
      • 可以把它看成是一个数据源的代理,它的配置管理多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。
      • 当有一个数据库请求时,中间件将SQL语句发给某一个指定的数据源来处理,然后将处理结果返回
    • 优缺点:
      • 优点是简单易用,没有多余的部署成本,因为它是植入到应用程序内部,于应用程序一起运行的。
      • 缺点是缺乏多语言的支持,目前业界这一类的主流方案除了 TDDL,还有早期的网易 DDB,它们都是 Java 语言开发的,无法支持其他的语言。另外,版本升级也依赖使用方更新,比较困难。
    • 推荐使用。这种方式代码侵入非常少,并且兼顾了性能和稳定性
  • 另一类:在应用程序和数据库实例之间部署一组数据库代理实例。如早期阿里巴巴开源的Cobar,基于 Cobar 开发出来的 Mycat,360 开源的 Atlas,美团开源的基于 Atlas 开发的 DBProxy 等等
    • 原理:
      • 对应用程序来说,数据库代理把自己伪装成一个单节点的MySQL实例,应用程序的所有数据库请求被发送给代理,代理分离读写请求,然后转发给对应的数据库实例。
        • 这一类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它在内部管理着很多数据源
        • 当有数据库请求时,它会对SQL语句做必要的改写,然后发往指定的数据源
    • 优缺点:
      • 优点:
        • 它一般使用标准的MySQL通信协议,所以可以很好的支持多语言。由于它是独立部署的,所以也比较方便的维护升级。
        • 但是,代理有一个好处是,它对应用程序是完全透明的。所以,只有在不方便修改应用程序代码这一种情况下,才需要使用代理方式
      • 缺点:因为使用代理加长了你的系统运行时数据库请求的调用链路(所有的SQL语句都需要跨两次网络:从应用到代理层和从代理层到数据源),有一定的性能损失,并且代理服务本身也可能出现故障和性能瓶颈等问题。

在这里插入图片描述

当然,你可以纯手动方式分离持续的读写请求:

  • 纯手动方式:修改应用程序的 DAO 层代码,定义读写两个数据源,指定每一个数据库请求的数据源。
    • 什么时候用?
      • 如果你的应用程序是一个逻辑非常简单的微服务,简单到只有几个SQL
      • 你的应用程序使用的编程语言没有合适的读写分离组件

如果你配置了多个从库,推荐使用“HAProxy+Keepalived”,来给所有的从节点做一个高可用负载均衡方案,既可以避免某个从节点宕机导致业务可用率降低,也方便你后续随时扩容从库的实例数量。

总结

  • 随着系统的用户增长,当单个MySQL实例快要杠不住大量并发的时候,读写分离是首选的数据库扩容方案。读写分离的方案不需要对系统做太大的改动,就可以让系统支撑的并发提升几倍到十几倍。主从读写分离以及部署一主多从可以解决突发的数据库读流量,是一种数据库横向扩展的方法。
  • 读写分离后,主从的延迟是一个关键的监控指标,可能会造成写入数据之后立即读的时候读取不到的情况。我们的应用程序应该能兼容主从延迟,避免因为主从延迟而导致出现数据错误。规避这个问题最关键的一点是,我们在实际系统的业务流程时,尽量不要在更新数据之后立即取查询更新后的数据
  • 推荐使用集成在应用内的读写分离组件方式来分离数据库读写请求,如果很难修改应用程序,也可以使用代理的方式来分离数据库读写请求。

其实,我们可以把主从复制引申为存储节点之间互相负责存储数据的技术,它可以实现数据的冗余,以达到备份和提升横向扩展能力的占用。在使用主从复制这个技术是,需要考虑如下问题:

  • 主从的一致性和写入性能的权衡:
    • 如果你要保证所有从节点都写入成功,那么写入性能一定会受到影响;
    • 如果你只写入主节点就返回成功,那么从节点可能会出现数据同步失败而造成主从不一致
    • 当然,在互联网项目中,我们一般会优先考虑性能而不是数据的强一致性。
  • 主从的延迟问题。

如果你的方案种部署了多个从库,推荐用“HAProxy+Keepalived”来做这些从库的负载均衡和高可用,这个方案的好处是简单稳定而且足够灵活,不需要增加额外的服务器部署,便于维护并且不增加故障点。

我们采用的很多组件都会使用到这个技术,比如,Redis 也是通过主从复制实现读写分离;Elasticsearch 中存储的索引分片也可以被复制到多个节点中;写入到 HDFS 中文件也会被复制到多个 DataNode 中。只是不同的组件对于复制的一致性、延迟要求不同,采用的方案也不同。但是这种设计的思想是通用的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值