【Redis 入门系列】为什么需要 Redis?一文串起缓存、分布式、读写分离、分库分表与微服务
本文专栏Redis作者主页努力努力再努力wz今日博客励志语录最难的不是做出决定的那一瞬间而是在此后无数个想要放弃的深夜里依然能够在心里再一次‘选择’坚持。思维导图引入从这篇博客开始我们就正式进入 Redis 的学习。在具体认识 Redis 之前我们可以先对 Redis 建立一个最基本的印象Redis 是一种用来存储数据的技术。在实际开发中Redis 经常被用作缓存用来保存一些访问频率较高的热点数据从而提高系统的访问速度。但是这里马上就会产生一个问题在此前的学习中我们已经认识了 MySQL。MySQL 本身也是用来存储、组织和管理数据的数据库服务程序。那么既然 MySQL 已经能够存储数据为什么还需要再引入 Redis 这样一个组件呢换句话说Redis 的存在并不是为了简单重复 MySQL 的功能而是为了解决 MySQL 在某些访问场景下的性能压力。本文的开篇就先围绕这个问题展开Redis 为什么会存在在此前的 MySQL 学习中我们已经知道MySQL 的核心作用是对数据进行持久化存储、组织和管理。在很多实际业务场景中比如用户注册信息、订单信息、商品信息等这些数据的规模可能非常大内存无法完整承载同时这类数据又不能因为服务器断电或程序退出而丢失。因此这类数据通常需要存储到磁盘等外部存储设备中而 MySQL 就是运行在操作系统之上的一个数据库服务程序用来管理这些需要持久化保存的数据。但是对于服务器程序来说例如一个 HTTP 服务器它在运行过程中会不断接收客户端发送来的请求报文。有些请求只需要服务器自身处理即可而有些请求则需要访问数据库中的数据。此时服务器就需要向 MySQL 发起请求让 MySQL 执行对应的 SQL 语句然后再根据 MySQL 返回的结果继续完成业务处理。这里就会引出一个重要问题磁盘 I/O 的速度远慢于内存访问速度。CPU 要处理数据前提是数据必须先被加载到内存中。如果目标数据只存在于磁盘中那么就需要先发生磁盘 I/O将磁盘中的数据读取到内存之后CPU 才能继续处理。由于内存和磁盘的访问速度不在同一个数量级上因此频繁的磁盘 I/O 会严重影响服务器整体的响应速度。当然MySQL 本身也不是每次访问数据都直接读取磁盘。以 InnoDB 存储引擎为例MySQL 启动之后会向操作系统申请一片内存空间作为 Buffer Pool。Buffer Pool 的作用就是缓存 InnoDB 访问过的数据页和索引页从而减少后续访问时的磁盘 I/O 次数。也就是说如果某个数据页已经被加载到了 Buffer Pool 中那么后续再次访问这个页时就可以直接从内存中读取而不需要再次访问磁盘。但是这里需要注意Buffer Pool 缓存的是 InnoDB 内部的“页”而不是直接缓存某一条业务记录本身。对于 InnoDB 来说数据通常是按照 B 树的逻辑结构组织起来的。B 树中的节点本质上就是一个个页其中内部节点页主要用于索引导航叶子节点页才真正保存数据记录。如果是聚簇索引那么叶子页中保存的是完整的行记录如果是二级索引那么叶子页中保存的是二级索引列以及对应的主键值。因此Buffer Pool 中缓存的内容并不只是保存业务数据的叶子页也包括 B 树中的内部节点页以及 InnoDB 运行过程中需要使用的其他页。也就是说MySQL 即使命中了 Buffer Pool也并不意味着可以直接拿到目标记录而是仍然需要按照 B 树的结构进行定位根节点页 - 内部节点页 - 叶子节点页 - 页内定位具体记录只不过在这个过程中如果相关的页都已经存在于 Buffer Pool 中那么就可以避免从磁盘中重新读取这些页如果某一层需要访问的页不在 Buffer Pool 中那么仍然需要从磁盘中读取对应的页并加载到 Buffer Pool 中。所以Buffer Pool 解决的主要问题是减少磁盘 I/O。但是它并没有改变 MySQL 以“页”和“B 树”组织数据的基本方式也没有消除 SQL 执行过程本身带来的开销。对于一次完整的 MySQL 访问来说服务器并不是直接从 MySQL 中拿到某条数据而是需要构造 SQL 语句并将请求发送给 MySQL 服务端。MySQL 服务端收到请求之后还需要进行 SQL 解析、语法检查、权限校验、索引定位、事务并发控制等一系列工作。尤其在多连接并发访问的场景下不同线程可能会访问同一个数据页或者同一条记录。此时InnoDB 内部需要通过 page latch 来保护内存中页结构的一致性如果涉及事务并发修改同一行记录还会通过 row lock 来控制并发冲突。对于普通的 SELECT 语句InnoDB 通常会通过 MVCC 机制实现一致性读从而减少读写之间的阻塞。因此访问 MySQL 的成本不能只从“有没有发生磁盘 I/O”这一个角度看。即使 Buffer Pool 命中了MySQL 依然要走一整套数据库访问链路。这个时候我们就可以引入 Redis 了。Redis 的一个重要作用就是作为缓存。它通常会把业务中访问频率较高的热点数据存储在内存中并且以 key-value 的形式组织数据。所谓 key-value可以先简单理解为应用程序通过一个 key 作为访问入口找到对应的 value。比如在一个网站中用户登录之后服务器经常需要获取该用户的基本信息例如用户名、头像、用户等级等。此时服务器可能会构造一条 SQL 语句SELECT...FROMuserWHEREid1001;然后服务器会将这条 SQL 语句发送给 MySQL 服务端执行。MySQL 服务端收到请求之后首先需要解析客户端发送过来的请求报文从中得到完整的 SQL 语句。接着MySQL 还需要对这条 SQL 语句进行语法解析、权限校验、索引选择以及执行等一系列操作。对于 InnoDB 来说如果这条 SQL 语句通过索引定位数据那么它需要按照 B 树的结构从根节点页开始向下查找逐步定位到保存目标记录的叶子页。在这个查找过程中每访问一个页都需要先判断这个页是否已经存在于 Buffer Pool 中。如果该页已经在 Buffer Pool 中就可以直接访问内存中的页如果该页不在 Buffer Pool 中则需要从磁盘中读取对应页并加载到 Buffer Pool 中。最终定位到叶子页之后MySQL 还需要在页内继续查找目标记录。同时即使相关页已经命中了 Buffer PoolMySQL 也不是直接无成本返回结果。因为在访问这些页和记录的过程中InnoDB 还需要维护页结构的安全并根据事务隔离级别判断当前事务能不能看到这条记录。也就是说这里仍然可能涉及 page latch、MVCC 等机制。而 Redis 的思路就更直接一些。服务器可以提前把用户1001的基本信息整理好并以 key-value 的形式存入 Redis 中例如使用user:1001作为 key对应的 value 就是该用户的基本信息。后续服务器再次需要查询这个用户信息时就可以先去 Redis 中根据user:1001查询。如果 Redis 中存在这份数据就可以直接拿到结果而不必每次都让 MySQL 执行完整的 SQL 查询流程。所以Redis 在这里节省的并不只是磁盘 I/O更重要的是减少了服务器频繁访问 MySQL 时所需要经过的完整数据库执行链路。相比 MySQLRedis 的访问路径更加直接。MySQL 中的数据需要经过 SQL 执行流程、B 树定位、页内查找、事务并发控制等过程而 Redis 中缓存的数据通常已经是业务层整理好的热点数据服务器可以根据 key 直接获取对应的 value。因此在缓存命中的情况下Redis 往往能够比 MySQL 更快地返回结果。所以Redis 的存在并不是因为 MySQL 完全没有缓存能力。事实上MySQL 的 Buffer Pool 本身就是非常重要的缓存机制它能够显著减少磁盘 I/O。但是Buffer Pool 是 MySQL 内部的缓存它缓存的是 InnoDB 使用的数据页和索引页目的是让 MySQL 在访问数据时尽量少读磁盘。而 Redis 则不同。Redis 是服务器程序额外使用的一个缓存服务。服务器可以把一些经常被访问的数据提前存到 Redis 中。后续客户端再次请求这些数据时服务器可以先去 Redis 中查询。如果 Redis 中已经有结果就可以直接使用 Redis 返回的数据而不必每次都让 MySQL 执行完整的 SQL 查询流程。所以使用 Redis 的核心原因可以概括为一个字快。Redis 之所以快首先是因为它主要把数据存储在内存中。相比磁盘内存的访问速度更快因此 Redis 在读取热点数据时可以避免频繁的磁盘 I/O。其次Redis 采用 key-value 的方式组织数据。对于服务器来说访问 Redis 时通常是根据一个 key 直接找到对应的 value。而访问 MySQL 时即使 Buffer Pool 已经命中也仍然需要经过 SQL 执行流程、B 树定位、页内查找、事务并发控制等一系列过程。因此Redis 的快并不只是“内存快”还在于它减少了服务器获取热点数据时需要走的查询流程。所谓查询流程变短指的是服务器不需要每次都让 MySQL 执行完整的 SQL 查询而是可以通过 Redis 的 key-value 方式更直接地拿到数据。MySQL 服务器构造 SQL - 发送给 MySQL - MySQL 解析 SQL - 权限校验 - 选择索引 - 从 B 树根节点页开始定位 - 找到叶子页 - 页内查找记录 - 判断事务可见性 - 返回结果 Redis 根据 key user:1001 查询 Redis - 找到对应 value - 返回结果服务器可以把一些访问频率高、短时间内变化不频繁的数据提前放到 Redis 中后续请求到来时先访问 Redis。如果 Redis 命中就可以直接拿到结果而不必每次都让 MySQL 执行完整的查询流程。换句话说Redis 快一方面快在内存访问另一方面快在 key-value 访问方式让热点数据的获取路径更短。这也是 Redis 经常被用作缓存的重要原因它不是替代 MySQL 持久化保存所有数据而是把高频访问的热点数据提前放到更容易、更快访问的位置从而减少服务器对 MySQL 的直接访问压力。Redis 背后的系统架构认知缓存、分布式与服务拆分Redis 与 MySQL 的协同关系缓存热点数据降低数据库访问压力根据上文我们已经对 Redis 建立了一个基本认知。从功能上看Redis 和 MySQL 似乎都可以用来存储数据。但是需要注意的是它们之间并不是互相独立、互相替代的关系而是一种协同关系。MySQL 的核心作用是对数据进行持久化存储它负责保存完整、可靠的数据而 Redis 的一个重要作用是作为缓存用来保存访问频率较高的热点数据。也就是说Redis 并不是为了取代 MySQL而是为了帮助服务器更快地访问某些高频数据从而减少服务器对 MySQL 的直接访问压力。之所以 Redis 访问数据的速度通常比 MySQL 更快原因在前文已经分析过。一方面Redis 主要把数据存储在内存中内存访问速度本身就比磁盘快另一方面Redis 采用 key-value 的方式组织数据服务器可以根据 key 更直接地获取对应的 value。相比之下访问 MySQL 时即使 Buffer Pool 已经命中也仍然需要经过 SQL 解析、索引定位、B 树查找、页内定位、事务可见性判断等一系列流程。但是这并不意味着我们可以只使用 Redis而不需要 MySQL。因为 Redis 主要把数据存储在内存中而内存容量相对有限无法完整承载大规模业务数据。同时很多业务数据还需要长期可靠地保存不能因为程序退出或服务器断电就丢失。因此完整的数据仍然需要保存在 MySQL 中而 Redis 通常只保存其中访问频率较高的一部分热点数据。这里还可以引入一个常见的经验规律二八原则。所谓二八原则就是在很多实际场景中大约 20% 的热点数据可能承载了 80% 的访问量。这个结论并不是一个严格的数学定理而是对实际访问规律的一种经验总结和程序中的局部性原理有一定相似之处。也就是说系统中的数据虽然很多但真正被频繁访问的往往只是其中一小部分。因此如果我们把这部分高频访问的数据提前存放到 Redis 中那么后续请求到来时服务器就可以优先从 Redis 中查询。只要 Redis 命中服务器就可以直接拿到结果而不必每次都让 MySQL 执行完整的查询流程。这样不仅可以减少 MySQL 的查询压力也可以减少后续可能发生的磁盘 I/O从而提高服务器整体的响应速度。在引入 Redis 之前我们对服务器访问数据的认知模型大致是这样的客户端请求 - 服务器 - MySQL - 返回结果也就是说服务器在处理业务请求时如果需要访问数据就直接向 MySQL 发起查询。而在引入 Redis 之后这个模型可以进一步优化。对于适合缓存的热点数据服务器不会一上来就直接访问 MySQL而是会先访问 Redis客户端请求 - 服务器 - Redis | 命中直接返回结果 | 未命中服务器再访问 MySQL - 查询到结果 - 将结果写入 Redis - 返回结果这里需要特别注意Redis 未命中后并不是 Redis 主动去访问 MySQL。Redis 本身只是一个缓存服务它只负责保存和返回 key-value 数据。真正控制访问流程的是服务器程序。也就是说当服务器访问 Redis 发现目标数据不存在时服务器会继续访问 MySQL从 MySQL 中查询完整数据。查询到结果之后服务器可以将这份数据写入 Redis方便后续请求再次访问时直接从 Redis 命中。最后服务器再将查询结果返回给客户端。因此Redis 和 MySQL 的关系可以这样理解MySQL 负责保存完整、可靠的数据Redis 负责缓存高频访问的热点数据。或者说MySQL 解决的是数据长期存储的问题而 Redis 解决的是热点数据快速访问的问题。这就是 Redis 和 MySQL 在实际系统中的协同关系。Redis 并不是替代 MySQL而是在 MySQL 之前增加了一层缓存让服务器在访问热点数据时可以优先走 Redis从而减少对 MySQL 的访问次数提高系统整体性能。从单机部署到分布式系统为什么需要横向扩展根据上文我们已经知道Redis 通常会和 MySQL 配合使用MySQL 负责保存完整、可靠的数据Redis 负责缓存访问频率较高的热点数据从而提高服务器获取热点数据的效率。而在刚才的讨论中我们默认采用的是一种比较简单的部署模型HTTP 服务器、MySQL 服务和 Redis 服务都运行在同一台主机上。这样的模型可以称为单机部署模型。所谓单机部署模型就是多个服务程序实例都运行在同一台机器上。比如一台主机上既运行着 HTTP 服务器实例也运行着 MySQL 服务器实例同时还运行着 Redis 服务器实例。由于这些服务都部署在同一台主机上所以它们会共享这台机器底层的硬件资源例如 CPU、内存、磁盘和网络带宽。在访问量不大的情况下这种部署方式理解起来比较简单维护成本也相对较低。但是在高并发场景下单机部署模型就很容易遇到资源瓶颈。比如对于 HTTP 服务器来说它需要和大量客户端建立连接。每一个客户端连接都需要对应的套接字以及文件描述符服务器还需要为连接维护相应的管理对象。如果服务器内部使用线程池处理请求那么线程本身也会消耗一定的内存资源和 CPU 调度资源。并且在多线程并发处理数据时还可能带来锁竞争、CPU缓存失效、上下文切换等额外开销。对于 MySQL 来说它不仅需要管理大量持久化数据还需要维护 Buffer Pool 等内存结构用来缓存数据页和索引页从而减少磁盘 I/O。对于 Redis 来说它本身主要将数据存储在内存中对于 Redis 来说它把数据存在内存里缓存的数据越多吃的内存就越多。也就是说如果所有服务都集中运行在一台主机上那么这台机器就需要同时承担 HTTP 请求处理、MySQL 数据管理、Redis 缓存管理等多方面压力。当访问连接数越来越多甚至达到非常高的规模时单台机器的 CPU、内存、磁盘 I/O、网络带宽等资源都可能成为瓶颈。面对单机资源瓶颈最直接的思路就是升级硬件配置。比如增加内存容量升级更多核心的 CPU或者使用性能更好的磁盘和网卡。这种方式通常被称为纵向扩展也就是通过增强单台机器的硬件能力来提升系统性能。但是纵向扩展存在明显的上限。比如主板上的内存插槽数量是有限的CPU 核心数也不可能无限增加单台机器的磁盘和网络能力也都有物理限制。并且硬件规格越高成本通常也会迅速上升。因此单纯依靠升级单台机器的硬件配置无法长期匹配用户规模和访问量的持续增长。所以在实际开发中如果一个系统需要支撑很大的访问量就不能只依赖单机部署模型也不能单纯依靠升级单台机器的硬件配置来解决问题。此时更常见的思路是引入分布式系统通过增加多个节点来共同承担请求压力也就是从单机的纵向扩展逐渐转向多节点的横向扩展。所谓分布式系统并没有想象中那么抽象。简单来说就是系统不再只依赖一个节点处理所有请求而是引入多个节点让不同节点共同承担请求处理压力。在最朴素的理解中可以先把一个节点理解为一台独立的主机。这台主机上运行着对应的服务程序例如 HTTP 服务器实例、Redis 实例、MySQL 实例等。这样一来原本所有客户端请求都集中打到一台机器上现在就可以被分发到多个节点上处理从而减轻单个节点的资源压力。当然这只是对分布式系统的一个初始理解。在实际项目中节点不一定直接等价于一台物理机器也可能是虚拟机、容器或者某个独立的服务实例。而且实际架构中也不一定是每个节点都运行一整套 HTTP、Redis 和 MySQL 服务。更常见的方式是多个应用服务器节点负责处理请求而 Redis 和 MySQL 作为独立的服务或集群存在。这里我们先从最容易理解的版本入手后续再逐步深入。当系统引入多个节点之后就会出现一个新的问题客户端请求应该发送到哪个节点因为客户端不应该自己去关心后端到底有多少个服务器节点所以通常会在客户端和后端服务器之间增加一个统一入口这个入口就是负载均衡器。负载均衡器的作用就是接收客户端请求然后按照一定的规则将请求分发到不同的服务器节点上。这样对客户端来说整个后端系统仍然像是一个整体而对后端来说请求压力已经被分散到了多个节点上。一种最简单的负载均衡算法是轮询算法。假设后端有 10 个服务器节点那么第 1 个请求分发给第 1 个节点第 2 个请求分发给第 2 个节点以此类推。当第 10 个请求分发给第 10 个节点之后第 11 个请求再重新分发给第 1 个节点。不过轮询算法只是最简单的分发方式。在实际场景中我们有时还希望同一个用户、同一个会话或者同一个连接上的请求尽量被分发到同一个节点处理。这样做的好处是节点本地可能已经保存了和该用户相关的一些临时状态或缓存数据继续由同一个节点处理这些数据可以继续被命中避免在新节点上重新加载一遍。因此实际的负载均衡策略往往会比简单轮询更加复杂。引入分布式系统之后系统整体的处理能力确实可以得到提升。因为原本由一台机器承担的压力现在可以被多个节点共同承担。但是这里一定要注意分布式系统不是没有代价的。很多读者一看到分布式系统可以提升性能就容易无脑推崇分布式架构这是不对的。凡是架构升级都会带来新的复杂度。虽然我们把请求分发到了不同节点上但是这些节点并不是完全没有关系的。对于客户端来说整个分布式系统仍然应该表现得像一个统一的服务。也就是说不管客户端的请求最终被分发到哪个节点上它都应该得到正确、一致的结果。这就会引出很多新的问题。比如多个节点之间的数据如何同步如果某个节点宕机了请求应该如何转移这些问题都属于分布式系统中非常重要的内容。因此我们需要对分布式系统祛魅。分布式系统确实可以提高系统的整体承载能力但它同时也会带来更高的硬件成本、运维成本和系统复杂度。一般来说只有当系统访问量达到一定规模单机部署已经难以支撑时才有必要进一步引入复杂的分布式架构。也正因为有这一层成本分布式系统通常是中大厂——尤其是大厂——才会上的方案。原因有两条大厂用得起维护多台物理主机是真金白银的硬件成本大厂用得到只有它们的业务访问量比如抖音真的撑爆了单机才有非用不可的理由。小公司不上分布式多数时候不是用不起是没那个量。只有当业务访问量足够大单机部署已经无法承载请求压力时分布式系统的价值才会真正体现出来。所以分布式系统不是为了显得高级而是为了解决单机已经无法承载的问题。所以在当前阶段我们只需要先建立这样一个基本认知单机部署模型的核心问题是所有服务共享同一台机器的硬件资源容易在高并发场景下遇到资源瓶颈而分布式系统的核心思路就是引入多个节点让多个节点共同承担请求处理压力。后续我们再进一步学习 Redis 时也会继续围绕这个思路展开Redis 不仅可以在单机模型中作为缓存使用也可以在更复杂的分布式系统中承担热点数据缓存、会话数据存储、计数统计等工作从而帮助系统提高整体访问效率。分布式部署模型的进一步演进按服务职责拆分部署根据上文我们已经对分布式系统建立了一个基本认知。所谓分布式系统简单来说就是系统不再只依赖一个节点处理所有请求而是引入多个节点再通过负载均衡器将客户端请求分发到不同节点上从而让多个节点共同承担请求压力。在前面的初步模型中我们可以先把分布式系统中的一个节点理解为一台物理机器。在这台机器上运行着一整套服务程序比如 HTTP 服务器实例、Redis 服务器实例和 MySQL 服务器实例。这样理解的好处是比较直观原本所有请求都集中在一台机器上处理现在可以分发到多个节点上由多个节点共同处理请求从而分摊单个节点的资源压力。但是这只是一个最初级的理解。接下来我们需要对这个认知模型进行进一步迭代。在真实项目中HTTP 服务器、Redis 服务器和 MySQL 服务器通常不会简单地全部运行在同一台机器上而是会根据它们的职责进行拆分部署。也就是说HTTP 服务器负责处理客户端请求和业务逻辑Redis 负责缓存热点数据MySQL 负责持久化保存完整数据。不同服务各自承担不同职责而不是全部挤在同一台主机上共享同一套硬件资源。之所以要这样设计一个重要原因就是为了进行资源隔离。如果 HTTP 服务器、Redis 和 MySQL 都部署在同一台主机上那么它们会共同消耗这台机器的 CPU、内存、磁盘 I/O 和网络带宽等硬件资源。可是这些资源本身是有限的。对于 HTTP 服务器来说在高并发、高访问量场景下它需要和大量客户端建立连接。每一个连接都需要对应的套接字和文件描述符服务器还需要为这些连接维护相应的管理对象。如果服务器使用线程池处理请求那么线程本身也会消耗内存资源和 CPU 调度资源。并且在多线程并发处理数据时还可能带来锁竞争、缓存失效、上下文切换等额外开销。对于 MySQL 来说它不仅需要管理大规模的持久化数据还需要维护 Buffer Pool、连接线程、锁结构等资源并且还可能产生磁盘 I/O 压力。对于 Redis 来说它主要将热点数据存储在内存中因此缓存的数据越多占用的内存资源也就越多。所以如果这几个服务全部部署在同一台机器上就容易出现互相争抢资源的问题。比如 Redis 占用了大量内存MySQL 的 Buffer Pool 可用空间就可能受到影响MySQL 磁盘 I/O 压力过大也可能影响整台机器的整体性能HTTP 服务器连接数过多也会持续消耗文件描述符、内存和 CPU 资源。因此将 HTTP 服务器、Redis 和 MySQL 拆分部署可以让不同类型的服务使用相对独立的硬件资源减少彼此之间的资源竞争。这样一来HTTP 服务器可以更专注于处理请求Redis 可以更充分地利用内存缓存热点数据MySQL 也可以更稳定地进行数据存储和管理。除了资源隔离之外将 Redis 和 MySQL 从应用服务器中拆分出来还有另一个非常重要的原因统一数据服务避免数据同步复杂化。在前面的朴素模型中我们可能会认为每一个节点都配备一套独立的 Redis 和 MySQL节点 1HTTP 服务器 Redis MySQL 节点 2HTTP 服务器 Redis MySQL 节点 3HTTP 服务器 Redis MySQL这样看起来每个节点都很完整HTTP 服务器只需要访问自己所在节点上的 Redis 和 MySQL 即可。但是这个模型会带来一个严重问题不同节点之间的数据如何保持一致举个具体例子。用户在节点 1 上修改了个人信息那么节点 2 和节点 3 的 MySQL 里这条记录什么时候更新节点 1 的 Redis 缓存已经更新了节点 2 的 Redis 缓存还是旧数据下次负载均衡器把这个用户的请求分到节点 2是不是会读到旧数据也就是说如果每个节点都维护一套独立的 Redis 和 MySQL那么多个节点之间的数据同步会变得非常复杂。因此在实际项目中更常见的方式并不是让每个 HTTP 节点都各自维护一套独立的数据服务而是让多个 HTTP 服务器节点共同访问后端统一的 Redis 服务和 MySQL 服务客户端 ↓ 负载均衡器 ↓ 多个 HTTP 服务器节点 ↓ ↓ Redis 服务 MySQL 服务在这个模型中多个 HTTP 服务器节点主要负责接收请求、执行业务逻辑Redis 作为独立的缓存服务用来保存热点数据MySQL 作为独立的数据库服务用来保存完整、可靠的数据。这样做的好处是两层的HTTP 服务器不再和 Redis、MySQL 挤在同一台机器上抢资源多个 HTTP 节点也不再各自维护一份独立的数据副本而是共同访问同一份后端数据——从源头上就绕开了多份数据怎么同步这个噩梦。所以在这里我们只需要先建立一个新的认知前面“一个节点运行一整套服务”的模型只是帮助我们理解分布式系统的入门模型而在真实项目中更常见的方式是按服务职责拆分部署HTTP 服务器负责处理请求Redis 负责缓存热点数据MySQL 负责持久化保存完整数据。Redis 和 MySQL 之所以要从应用服务器中拆分出来本质上有两个重要原因第一进行资源隔离避免 HTTP 服务器、Redis、MySQL 互相争抢同一台机器的硬件资源。第二统一数据服务避免每个应用节点都维护一套独立数据而带来复杂的数据同步问题。这个模型就比前面“每个节点一整套服务”的理解更加接近真实项目中的部署方式。MySQL 读写分离从单实例瓶颈到主从架构至此我们对于分布式系统部署模型的认知已经逐渐清晰。在真实项目中系统通常不会把 HTTP 服务器、Redis 服务器和 MySQL 服务器全部部署在同一台机器上而是会按照服务职责进行拆分部署。HTTP 服务器负责处理客户端请求和业务逻辑Redis 负责缓存热点数据MySQL 负责持久化保存完整数据。不同服务可以部署在不同的物理主机、虚拟机或者容器中从而避免它们共同瓜分同一台机器上的硬件资源。对于 MySQL 服务来说它也可以被单独部署出来。这样做的好处在前文已经分析过MySQL 不需要再和 HTTP 服务器、Redis 等服务共同争抢同一台机器上的 CPU、内存、磁盘 I/O 和网络带宽而是可以更加专注于完成数据库本身的数据管理工作。不过随着访问量继续增大仅仅把 MySQL 从应用服务器中拆分出来还不一定够。因为此时所有数据库读写请求仍然可能集中在同一个 MySQL 实例上。对于数据库来说常见的访问操作大致可以分为两类读操作和写操作。读操作就是查询数据例如SELECT写操作就是修改数据例如INSERT、UPDATE、DELETE。在很多业务场景中读操作通常比写操作更加频繁。比如用户浏览商品、查看文章、刷新个人信息、查看评论列表这些大多都是读请求而真正修改数据的写请求通常相对少一些。如果所有读请求和写请求都由同一个 MySQL 实例处理那么这个 MySQL 实例就需要同时承担所有查询和修改压力。大量读请求到来时MySQL 服务端首先需要维护更多客户端连接。每一个连接在服务端内部都需要对应的连接对象或者会话上下文用来保存当前连接的状态信息。同时MySQL 还需要创建或者分配工作线程来处理这些连接上的 SQL 请求。因此即使这些请求只是普通查询也依然会消耗线程、CPU、内存、Buffer Pool 等资源并带来索引查找和页内定位等开销。随着请求量不断增大单个 MySQL 实例很容易成为系统瓶颈。这里需要注意InnoDB 并不是简单地让所有读操作和写操作完全互斥。为了提高并发能力InnoDB 引入了 MVCC 机制使普通SELECT可以通过快照读读取数据而不是每次都对记录加锁读取。这样可以减少读写之间的阻塞。但是MVCC 并不意味着读操作完全没有成本。普通查询仍然需要经过 SQL 解析、索引定位、B 树查找、页内记录定位、事务可见性判断等流程。如果大量读请求全部集中到同一个 MySQL 实例上即使它们不一定和写操作直接互斥也依然会消耗这个实例的 CPU、内存、连接数和 Buffer Pool 等资源。因此当一个 MySQL 实例既要处理大量读请求又要处理写请求时整体性能仍然可能受到影响。这时就可以进一步引出一种常见的数据库架构优化方式读写分离。读写分离的基本思想是不再让一个 MySQL 实例同时承担所有读写请求而是把写请求交给主库处理把读请求分发给从库处理。这样可以让大量读请求从主库中分离出去从而减轻主库压力提高数据库整体的访问能力。所以读写分离不是因为 MySQL 完全不能并发读写而是因为在高访问量场景下单个 MySQL 实例同时承担大量读请求和写请求整体压力太大。此时通过主库负责写、从库负责读的方式可以把读请求压力拆分出去让数据库系统具备更好的整体承载能力。其次在引入读写分离之后我们就会接触到两个新的概念主库和从库。根据前面的分析我们已经知道在很多业务场景中读请求往往远多于写请求。如果所有读请求和写请求都集中到同一个 MySQL 实例上那么这个实例就需要同时承担大量查询压力和写入压力随着访问量增大很容易成为系统瓶颈。因此读写分离的基本思路就是让主库主要负责处理写请求让从库主要负责处理读请求。也就是说当服务器需要执行INSERT、UPDATE、DELETE这类修改数据的操作时就将请求发送给主库而当服务器需要执行普通查询操作时就可以将请求发送给从库。由于读请求数量通常更多因此从库往往不止一个而是可以部署多个从而让多个从库共同承担查询压力。在部署上主库和从库通常会运行在不同的物理主机、虚拟机或者容器中。这样做的好处是每个数据库实例都可以使用相对独立的 CPU、内存、磁盘 I/O 和网络资源避免所有读写请求都挤在同一个 MySQL 实例上处理。不过这样设计也会带来新的问题主库和从库之间的数据必须保持同步。因为写请求是发送到主库执行的所以主库中的数据会先发生变化。而从库主要负责查询如果主库修改后的数据还没来得及同步到从库那么服务器后续从从库读取数据时就可能读到旧数据。因此读写分离并不是简单地多部署几个 MySQL 实例就结束了。只要系统中存在主库和从库就必须引入主从同步机制让主库中的数据变化能够同步到从库中从而尽量保证主库和从库之间的数据状态一致。所以这里可以先建立一个基本认知读写分离的核心目的是让主库承担写请求让多个从库承担大量读请求从而分摊单个 MySQL 实例的压力而主从同步机制的存在则是为了让主库和从库之间的数据状态尽量保持一致。这样一来MySQL 服务本身也从单个实例进一步演变成了由主库和从库组成的数据库服务体系。其次这里的“从库”这个名字容易让读者产生一个误区以为多个从库是在保存不同的逻辑数据库。比如从库 1 保存 database1从库 2 保存 database2从库 3 保存 database3。但这并不是读写分离中的从库含义。在 MySQL 中database 是逻辑上的数据库可以理解为一组表的容器。而在读写分离模型中从库不是用来保存某一个单独的逻辑数据库的而是保存主库数据状态的一份完整副本。也就是说如果主库中有 database1、database2、database3那么每一个从库中也会保存这些逻辑数据库对应的一份副本。也就是说这里的模型不是从库 1保存 user_database 从库 2保存 order_database 从库 3保存 product_database也不是从库 1保存 user 表 从库 2保存 order 表 从库 3保存 product 表而是正确理解 主库保存 database1、database2、database3 从库 1保存 database1、database2、database3 的完整副本 从库 2保存 database1、database2、database3 的完整副本 从库 3保存 database1、database2、database3 的完整副本后面这种“把不同数据拆开存储”的思路更接近后续要讨论的分库分表不是当前读写分离要表达的重点。当前阶段我们只需要先记住从库不是用来存储数据库里的某一部分数据。**也正是因为从库保存了完整的数据副本所以普通查询请求才可以被分发到不同从库上执行。比如同样是查询用户信息请求可以发给从库 1也可以发给从库 2还可以发给从库 3。这样一来原本集中在一个 MySQL 实例上的大量读请求就可以被多个从库共同承担。不过既然从库可以有多个那么这里又会出现一个新的问题大量读请求应该发送给哪个从库这个问题和前面分布式系统中的请求分发问题很像。多个 HTTP 服务器节点前面需要负载均衡器来分发客户端请求同样地多个从库之间也需要一种读请求分发机制用来决定某一次查询请求应该交给哪个从库处理。因此在读写分离模型中我们可以先把从库前面的这层分发机制理解为一个统一的读请求入口。服务器的读请求会先到达这个入口然后再按照一定规则被分发到不同从库上执行。整体模型可以理解为写请求 - 主库 读请求 - 读请求入口 / 分发器 | |---- 从库 1 |---- 从库 2 |---- 从库 3这里需要注意这个“读请求入口”不一定非要是一个独立的负载均衡器。实际项目中它可以由专门的数据库代理或负载均衡组件完成也可以由服务器程序内部自己选择要访问哪个从库。当前阶段我们不需要展开这些实现细节只需要先建立一个基本认知读写分离中的从库本质上是保存完整数据副本的 MySQL 实例多个从库共同承担读请求因此读请求也需要通过某种分发机制被分配到不同从库上执行。MySQL 分库分表从数据规模拆分到分片路由认识了主库和从库之后我们已经知道读写分离可以通过多个从库来分摊大量读请求的压力。但是读写分离并不能解决所有问题。因为在读写分离模型中无论是主库还是从库本质上保存的都是完整数据的一份副本。也就是说如果主库中的某张表数据量非常大那么从库中也会保存同样规模的数据。随着业务规模不断增长数据库中存储的数据量也会越来越大。此时MySQL 面临的压力就不只是“读请求太多”这一件事了。单个数据库或者单张表中的数据规模过大也会带来很多问题。比如表中的记录数量越来越多索引结构也会越来越庞大查询时需要定位的数据范围变大写入时维护索引的成本也会提高。同时单个 MySQL 实例需要管理的数据越多它在存储、查询、更新和维护方面的压力也会越大。虽然我们可以通过读写分离创建多个从库来分摊读请求但是这些从库仍然保存的是完整数据副本。换句话说读写分离只是增加了更多“完整副本”来承担读压力并没有真正把单个数据库或单张表的数据规模缩小。因此当数据规模继续扩大单库单表已经难以承载时就需要进一步引入另一种思路分库分表。所谓分表就是把原来集中在一张表中的数据按照一定规则拆分到多张表中所谓分库就是把原来集中在一个 MySQL 实例中的数据按照一定规则拆分到多个 MySQL 实例中。它们的核心目的都是缩小单个表或者单个 MySQL 实例需要管理的数据规模从而降低单点压力。比如原来所有用户数据都存放在一张user表中user 表保存所有用户数据当用户数量越来越多时这张表会变得越来越大。此时可以按照一定规则将它拆成多张表例如分表前 MySQL 实例 1 └── user_db └── user 表保存所有用户数据 分表后 MySQL 实例 1 └── user_db ├── user_0 表保存一部分用户数据 ├── user_1 表保存另一部分用户数据 └── user_2 表保存另一部分用户数据这就是分表。如果进一步把数据拆到不同的数据库实例中比如分库前 MySQL 实例 1 └── user_db └── user 表保存所有用户数据 分库后 MySQL 实例 1 └── user_db └── user 表保存一部分用户数据 MySQL 实例 2 └── user_db └── user 表保存另一部分用户数据 MySQL 实例 3 └── user_db └── user 表保存另一部分用户数据这就是分库。所以分库分表和读写分离的区别非常关键读写分离中的从库保存的是主库完整数据的一份副本用来分摅读请求而分库分表是把数据按照规则拆开让不同数据库或不同表保存不同部分的数据。也就是说读写分离解决的是“读请求压力集中”的问题而分库分表解决的是“单库单表数据规模过大”的问题。在认识了分库分表之后我们还需要继续思考一个问题既然数据已经被拆分到了不同的 MySQL 实例或者不同的数据表中那么服务器在访问数据时怎么知道这次请求应该访问哪个 MySQL 实例、哪张表呢也就是说分库分表之后系统中仍然存在一个“请求分发”的过程。不过这里需要注意这里的“分发”和前面负载均衡器分发客户端请求不太一样。前面负载均衡器解决的问题是这个客户端请求应该交给哪一个 HTTP 服务器节点处理而分库分表中的分发解决的问题是这次数据库访问请求应该落到哪个 MySQL 实例、哪张表这个过程通常可以理解为分片路由。所谓分片路由就是服务器在访问数据库之前先根据请求中的某个关键字段结合提前设计好的拆分规则计算出目标数据所在的位置然后再把 SQL 请求发送到对应的 MySQL 实例和数据表中。这里的关键字段通常可以称为分片键。比如用户数据可以按照user_id进行拆分那么user_id就可以作为分片键订单数据可以按照order_id进行拆分那么order_id就可以作为分片键。有了分片键之后还需要有对应的分片规则。比如最简单的一种规则就是取模user_id % 分片数量假设我们把用户表拆成 4 张表user_0 user_1 user_2 user_3那么当服务器需要查询user_id 1001的用户数据时就可以先进行计算1001 % 4 1于是服务器就知道这个用户的数据应该去user_1表中查询。因此分库分表之后的访问流程大致可以理解为服务器收到请求 - 提取分片键例如 user_id - 根据分片规则计算目标位置 - 确定目标 MySQL 实例和目标数据表 - 向对应位置发送 SQL 请求 - 获取并返回结果所以分库分表并不是简单地把数据拆开就结束了。数据一旦被拆分访问数据时就必须有一套规则来判断目标数据在哪里。否则服务器并不知道应该访问哪个 MySQL 实例、哪张表。这里可以把它和前面的负载均衡进行对比理解负载均衡 把客户端请求分发到不同 HTTP 服务器节点 分片路由 把数据库访问请求分发到对应 MySQL 实例和数据表至于这个分片路由逻辑具体由谁来完成可以有不同实现方式。它可以写在服务器程序内部由业务代码自己根据分片键计算目标位置也可以由专门的数据库代理或者中间件来完成。当前阶段我们不需要展开这些具体实现只需要先建立一个基本认知分库分表之后数据被拆到了不同位置因此服务器访问数据前需要先通过分片键和分片规则判断目标数据所在的位置。这个判断和转发过程就可以理解为分片路由。也就是说分库分表解决的是单库单表数据规模过大的问题而分片路由解决的是数据拆开之后“该去哪里查、该往哪里写”的问题。Redis 的部署扩展从单机实例到集群模式其次注意的是Redis 本身也不是只能单机部署。当缓存数据规模变大或者 Redis 的访问压力继续增大时Redis 也可以通过主从复制、集群等方式进行扩展。比如可以让多个 Redis 节点共同承担缓存访问压力甚至让不同 Redis 节点保存不同范围的 key。不过这些内容属于 Redis 后续的高阶部署模型当前阶段我们只需要先知道Redis 和 MySQL 一样也可以从单机实例进一步扩展为多个实例共同工作的集群模式。微服务架构按业务职责拆分 HTTP 服务器在前面的内容中我们已经对分布式系统、Redis 缓存、MySQL 读写分离以及分库分表建立了一个初步认知。从整体架构上看系统已经不再是一个简单的单机模型而是逐渐演变成了多个服务组件协同工作的模型HTTP 服务器负责接收客户端请求和处理业务逻辑Redis 负责缓存热点数据MySQL 负责持久化保存完整数据当 MySQL 压力继续增大时还可以通过读写分离分摊读请求通过分库分表缩小单库单表的数据规模。但是这里还可以继续对 HTTP 服务器这一层进行进一步拆分。在最开始的模型中我们通常会把 HTTP 服务器理解成一个整体。也就是说所有业务逻辑都写在同一个服务器程序中。比如一个电商系统中用户登录、商品展示、订单创建、支付处理、库存扣减、评论管理等功能可能都由同一个 HTTP 服务器程序负责处理。这种模型在业务规模较小时比较容易理解也方便开发和部署。但是随着业务越来越复杂如果所有业务逻辑都堆在同一个服务器程序中这个程序就会变得越来越庞大模块之间的耦合也会越来越严重。比如用户模块出现问题可能会影响整个服务器程序订单模块访问量突然增大也可能拖慢其他业务模块如果我们只想扩容订单处理能力也不得不把整个 HTTP 服务器程序一起扩容。这样一来系统的维护、部署和扩展都会变得越来越困难。因此在更复杂的业务场景中我们可以进一步按照业务职责对 HTTP 服务器进行拆分。也就是说不再让一个服务器程序负责所有业务而是将不同业务拆分成不同的服务程序。比如用户服务负责登录、注册、用户信息管理 商品服务负责商品列表、商品详情 订单服务负责创建订单、查询订单 库存服务负责库存查询、库存扣减 支付服务负责支付相关流程 评论服务负责评论发布、评论查询这种按照业务职责拆分出来的小型服务就可以理解为微服务。所以所谓微服务并不是一个特别神秘的概念。它的核心思想就是把一个庞大的服务器程序按照业务职责拆分成多个更小、更独立的服务。在拆分之后原本的模型可能是客户端 | 负载均衡器 | HTTP 服务器 | 处理所有业务用户、商品、订单、库存、支付、评论而引入微服务之后模型就会变成客户端 | 统一入口 | |---- 用户服务 |---- 商品服务 |---- 订单服务 |---- 库存服务 |---- 支付服务 |---- 评论服务这样拆分之后每个服务只负责自己对应的业务范围职责更加清晰。用户服务只关心用户相关逻辑订单服务只关心订单相关逻辑库存服务只关心库存相关逻辑。相比一个庞大的服务器程序单个服务的代码规模更小维护起来也更清楚。微服务带来的另一个好处是可以按业务进行独立扩展。比如在某个促销活动中订单服务和库存服务的访问压力可能会明显增加但用户服务、评论服务的压力不一定同步增加。此时如果所有业务都写在同一个 HTTP 服务器程序中我们只能整体扩容这个服务器程序。而在微服务模型中我们可以只增加订单服务和库存服务的实例数量从而更有针对性地提升系统处理能力。也就是说微服务让系统的扩展粒度变得更细单体服务扩容整个 HTTP 服务器一起扩容 微服务扩容哪个业务压力大就扩容哪个服务除了独立扩展微服务还可以独立部署——更新订单服务的代码不需要把整个系统一起重启这在业务迭代频繁的场景下尤其重要。不过和前面讲分布式系统一样微服务也不是没有代价的。服务拆分之后原本在一个进程内部可以直接调用的业务逻辑现在可能变成了不同服务之间的网络调用。比如订单服务在创建订单时可能需要调用库存服务扣减库存也可能需要调用用户服务校验用户信息。这样一来系统中就会出现更多服务之间的通信过程。同时服务数量变多之后也会带来新的问题服务之间如何调用某个服务宕机之后怎么办这些都是微服务架构需要额外考虑的问题。所以微服务并不是越早引入越好也不是所有项目都必须使用微服务。它本质上是当业务规模变大、单个服务器程序内部的业务逻辑变得过于复杂之后为了降低单体服务复杂度、提高系统扩展能力而采用的一种拆分方式。因此我们可以先这样理解微服务分布式系统解决的是“多个节点共同承载请求压力”的问题而微服务进一步解决的是“一个庞大服务内部业务职责过于复杂”的问题。或者说微服务的核心思想就是按照业务职责拆分服务让每个服务只负责一类相对独立的业务并且可以根据不同业务的压力进行独立扩展。到这里我们对系统架构的认知又完成了一次升级一开始是单机模型后来引入 Redis 缓存提高热点数据访问效率再到分布式部署分摊请求压力接着通过 MySQL 读写分离和分库分表优化数据库层最后又可以通过微服务对应用层业务逻辑进行更细粒度的拆分。结语那么这就是本篇文章的全部内容带你认识Redis我会持续更新希望你能够多多关注如果本文有帮助到你的话还请三连加关注你的支持就是我创作的最大动力