文章目录
- 第一章 认识 Redis
- 1. Redis 是什么
- 2. 为什么 Redis 要把数据放在内存中
- 3. 既然变量也存在内存里,为什么还需要 Redis
- 4. Redis 解决了不同进程之间的数据共享问题
- 5. Redis 和 MySQL 应该怎么选择
- 6. 二八原则与热点数据
- 7. Redis + MySQL 也会带来新的问题
- 8. 没有银弹
- 第二章 分布式系统
- 1. 为什么需要分布式系统
- 2. 从最简单的单机架构开始
- 3. 第一次拆分:应用服务与数据库分离
- 4. 应用服务器不够用了:引入应用集群
- 4.1 负载均衡如何分配请求
- 5. 应用服务器解决了,数据库又成为瓶颈
- 6. 主从架构与读写分离
- 7. 数据库还是太忙:引入缓存
- 8. 数据越来越多:分库分表
- 9. 应用越来越大:业务拆分与`微服务`
- 10. 分布式系统中的常见概念
- 10.1 应用 Application / 系统 System
- 10.2 模块 Module / 组件 Component
- 10.3 分布式 Distributed
- 10.4 集群 Cluster
- 10.5 主 Master / 从 Slave
- 10.6 中间件 Middleware
- 11. 衡量一个系统的几个重要指标
- 11.1 可用性 Availability
- 11.2 响应时长 Response Time
- 11.3 吞吐量 Throughput 与并发 Concurrent
- 12. 分布式架构的整体演进
第一章 认识 Redis
1. Redis 是什么
Redis 官方对自己的描述是:
The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker.
简单来说,Redis 是一个开源的内存数据存储系统,可以作为数据库、缓存、流式数据处理引擎以及消息中间件使用。
这里最重要的关键词是in-memory(内存存储)。Redis 的数据主要保存在内存中,因此数据读写速度非常快,这也是 Redis 高性能的重要原因。
Redis 最初一个很典型的用途就是作为消息中间件。在分布式系统中,经常存在生产者和消费者:生产者产生数据,消费者处理数据,而 Redis 可以作为两者之间传递消息的中间层。
生产者 → Redis → 消费者不过 Redis 并不是专门为消息队列设计的产品。对于更加复杂、更加专业的消息场景,通常会使用专门的消息中间件。Redis 可以承担消息传递工作,但这只是它众多能力中的一种。
2. 为什么 Redis 要把数据放在内存中
传统数据库,例如 MySQL,数据最终主要存储在磁盘中。磁盘容量大、成本低,但和内存相比,访问速度更慢。
Redis 则把数据主要放在内存中:
MySQL:磁盘存储为主 Redis:内存存储为主因此在很多简单的数据读写场景中,Redis 的速度非常快。
但内存也存在明显的缺点:容量有限,而且成本高。所以虽然 Redis 也可以直接作为数据库使用,但如果需要保存非常大量的数据,把所有数据都放在内存中往往并不划算。
也就是说,Redis 的特点非常鲜明:
速度快,但存储空间相对昂贵、有限。
3. 既然变量也存在内存里,为什么还需要 Redis
普通程序中的变量,本身同样存储在内存中。
例如:
intcount=10;count这个变量就在当前程序的内存中。从这个角度看,似乎直接使用变量就可以了,为什么还需要 Redis?
关键区别并不只是“是不是使用内存”,而是这些数据能不能被其他进程使用。
如果只是一个单机程序,并且数据只需要当前程序自己使用,那么直接使用程序中的变量往往更加简单,没有必要专门引入 Redis。
Redis 真正能够发挥威力的地方,是分布式系统。
4. Redis 解决了不同进程之间的数据共享问题
不同进程之间具有隔离性。
例如有两个程序:
进程 A 进程 B 变量 data = 10 无法直接访问 A 的变量进程 A 中定义的变量属于进程 A 自己的内存空间,进程 B 不能直接拿来使用。
而分布式系统中,一个系统往往由多个进程组成,甚至这些进程还运行在不同的主机上。因此就会遇到一个非常重要的问题:
不同进程之间如何共享数据?
进程之间进行通信,一种最常见的方式就是通过网络。
Redis 本身就是一个独立运行、可以通过网络访问的服务。一个进程可以把数据存入 Redis,其他进程再通过网络访问 Redis,从而获得这份数据。
进程 A │ │ 写入数据 ▼ Redis ▲ │ 读取数据 │ 进程 B甚至进程 A 和进程 B 不在同一台主机上,只要它们都能够连接 Redis,就可以共享 Redis 中的数据。
因此,可以把 Redis 简单理解成:
Redis 把原本只能属于某一个进程的内存数据,变成了可以通过网络被多个进程共同访问的数据。
这也是 Redis 在分布式系统中非常重要的原因。
5. Redis 和 MySQL 应该怎么选择
Redis 快,但是内存空间有限;MySQL 相比之下速度没有 Redis 这么快,但更加适合保存大量数据。
因此实际开发中,并不一定要在 Redis 和 MySQL 之间二选一,更典型的方案是:
Redis 和 MySQL 配合使用。
MySQL 保存完整的数据,Redis 保存那些经常被访问的数据。
热点数据 │ ▼ Redis 完整数据 │ ▼ MySQL这样既可以利用 Redis 的速度,也可以利用 MySQL 更大的存储能力。
6. 二八原则与热点数据
很多系统中的数据访问并不是平均的,而是少部分数据会被反复访问。
这可以用常见的二八原则来理解:
大约 20% 的热点数据,可能满足大约 80% 的访问需求。
这里的20%和80%并不是固定不变的数字,重点在于表达:
少量热点数据承担了大量访问请求。
既然如此,就没有必要把数据库中的所有数据全部放进 Redis。
可以只把经常访问的热点数据放进 Redis:
大量完整数据 → MySQL 少量热点数据 → Redis访问热点数据时直接读取 Redis,就可以利用 Redis 的高性能;其他数据仍然保存在 MySQL 中。这也是 Redis 最典型的使用方式之一。
7. Redis + MySQL 也会带来新的问题
Redis 和 MySQL 配合使用虽然可以提高访问速度,但系统也会变得更加复杂。
原本只有一份数据:
程序 → MySQL现在变成了两份:
Redis / 程序 \ MySQL这时就会产生一个新的问题:
如果数据发生修改,Redis 和 MySQL 中的数据如何保持同步?
例如 MySQL 中的数据已经修改,但是 Redis 中仍然保存旧数据,那么程序从 Redis 中读取到的就是错误的数据。
因此,引入 Redis 之后,虽然提高了性能,却同时引入了 Redis 与 MySQL 之间的数据同步问题。
这说明一个很重要的道理:
架构越复杂,能够解决的问题越多,但同时也会产生新的问题。
Redis 并不是用了之后系统就一定更好。如果业务本身非常简单,为了追求性能而盲目增加 Redis,反而可能让系统变得更加复杂。
8. 没有银弹
软件开发中有一句非常经典的话:
没有银弹。
“银弹”在传说中是用来击败狼人的特殊武器,后来被用来形容一种能够轻松解决所有问题的万能方案。
但软件开发中并不存在真正的万能方案。
Redis 很快,但它需要内存;Redis 可以提高系统性能,但它也会增加系统复杂度;Redis 和 MySQL 配合能够减轻数据库压力,但同时又会产生数据同步问题。
所以学习 Redis,不能简单理解成:
Redis 快,所以哪里都应该用 Redis而应该理解成:
Redis 快 ↓ 适合解决特定的性能问题 ↓ 但也会带来新的复杂度 ↓ 需要根据实际场景选择而 Redis 真正能够体现价值的场景,通常都离不开一个重要背景——分布式系统。
第二章 分布式系统
1. 为什么需要分布式系统
一个系统刚开始时,通常并不需要复杂的分布式架构。用户量少、访问压力小,一台服务器往往就能够同时承担应用程序和数据库的工作。
但一台服务器的硬件资源始终是有限的,例如:
CPU 内存 磁盘 网络服务器每处理一个请求,都需要消耗这些硬件资源。当用户越来越多、请求越来越密集时,单台服务器最终一定会接近自己的性能极限。
面对这个问题,最直接的办法有两个。
第一种是升级硬件,例如更换更强的 CPU、更大的内存和更快的磁盘,这种方式称为Scale Up(纵向扩展 / 垂直扩展)。它的优点是简单,原来的程序通常不需要进行太大的修改;缺点也很明显:硬件性能存在上限,而且越高端的硬件价格通常越昂贵。
第二种是增加服务器数量,让多台机器共同承担原本一台机器的工作,这种方式称为Scale Out(横向扩展 / 水平扩展)。它能够获得更大的扩展空间,但随着服务器越来越多,不同机器之间需要通信和协调,系统也会变得更加复杂。
因此,可以先建立一个非常重要的认识:
所谓分布式,本质上就是当一台机器无法满足需求时,引入更多机器共同完成工作。
2. 从最简单的单机架构开始
假设现在要开发一个简单的电商网站,其中包含用户、商品、交易三个主要业务。
在系统刚刚上线的时候,用户量很少,没有必要一开始就搭建复杂的架构,因此可以把应用程序和数据库全部部署在同一台服务器上。
用户在浏览器中输入网站地址后,首先通过 DNS 将域名解析成服务器的 IP 地址,然后浏览器向这台服务器发送 HTTP 请求。应用程序负责处理用户、商品和交易等业务逻辑,需要访问数据时再调用 MySQL 等数据库。
这种架构最大的优点就是简单。开发简单、部署简单、维护也简单,在业务量还很小的时候完全够用,所以很多系统都是从单机架构开始的。
但随着用户越来越多,应用程序和数据库都在争抢同一台服务器的 CPU、内存、磁盘和网络资源,单机最终还是会达到性能极限。
3. 第一次拆分:应用服务与数据库分离
当单机开始出现压力时,不一定需要立刻增加大量服务器,一个很自然的做法就是先把应用服务和数据库服务拆开。
原本:
应用程序 + 数据库 ↓ 一台服务器拆分之后,应用程序放到应用服务器,数据库放到独立的存储服务器,两台服务器通过网络通信。
这样做有两个明显好处。
第一,应用程序和数据库不再争抢同一台机器的硬件资源。应用程序主要负责业务逻辑,通常更加依赖 CPU 和内存;数据库需要保存大量数据,更加关注存储容量和磁盘读写能力,因此两种服务器可以根据自己的特点配置不同的硬件。
第二,系统已经可以使用两台机器的硬件资源,而不是所有工作都挤在一台机器中。
此时应用程序访问数据库的方式也发生了变化。原来它们在同一台服务器中,现在应用服务器需要通过网络访问数据库服务器。
这其实已经具有了分布式系统最基本的特点:
原本位于同一台机器中的不同部分,被部署到不同机器,并通过网络共同完成工作。
4. 应用服务器不够用了:引入应用集群
系统继续发展以后,数据库虽然已经独立出去,但应用服务器仍然只有一台。当用户请求越来越多时,单台应用服务器同样会达到极限。
这时可以继续进行水平扩展:
多台应用服务器共同处理业务,就形成了应用服务器集群。
但是服务器增加以后,马上就会出现一个新的问题:
用户的请求到底应该交给哪一台应用服务器?
因此,需要在用户和应用服务器之间增加一个新的组件——负载均衡(Load Balance)。
┌──► 应用服务器 A 用户 ──► 负载均衡 ──┤ └──► 应用服务器 B
负载均衡本质上就是一个“任务分配器”。用户的请求先到负载均衡,再由它决定把请求发送给哪一台应用服务器。这样,多台服务器的计算能力才能真正被利用起来。
4.1 负载均衡如何分配请求
最简单的方式是Round-Robin(轮询)。例如存在 A、B、C 三台服务器,第一个请求交给 A,第二个交给 B,第三个交给 C,第四个再重新交给 A,如此循环。
如果不同服务器的性能不同,还可以使用Weight Round-Robin(加权轮询)。性能更强的服务器设置更高权重,让它处理更多请求;性能较弱的服务器处理较少请求,也就是“能者多劳”。
还有一种方式是根据用户特征计算哈希值,例如根据用户 IP 地址进行计算,再根据结果选择服务器。这样可以让同一个用户的请求尽量稳定地进入同一台服务器。
常见的负载均衡软件包括 Nginx、HAProxy、LVS、F5 等。
这里真正需要理解的是:
应
用服务器变多以后,必须有人负责把请求分发给这些服务器,这就是负载均衡存在的原因。
5. 应用服务器解决了,数据库又成为瓶颈
应用服务器可以从一台扩展到两台、四台甚至更多,因此应用层能够同时处理越来越多的请求。
但无论有多少应用服务器,它们最终都需要访问数据库。
于是系统会出现新的问题:
应用服务器越来越多 ↓ 数据库请求越来越多 ↓ 数据库成为新的瓶颈数据库和应用服务器不同,不能简单地复制几台以后随便使用,因为数据库中保存的是系统的数据。
假设用户账户中有 1000 元,完成一次支付后,数据库 A 已经变成 900 元,但数据库 B 仍然保存 1000 元,那么用户访问不同数据库就可能看到完全不同的结果。
这就是数据库非常重要的一个问题——数据一致性。
简单来说:
对于同一份数据,不应该因为访问了不同数据库,就得到不同的结果。
6. 主从架构与读写分离
为了解决数据库扩展问题,可以引入主从架构。
多个数据库节点中,保留一个主要数据库作为主库(Master),其他数据库作为从库(Slave)主库主要负责数据写入,当主库中的数据发生变化之后,再把变化同步到从库,让从库尽量保持和主库相同的数据。
有了主库和从库以后,就可以进一步进行读写分离:
写请求 → 主数据库 读请求 → 从数据库为什么这样可以提高性能?
因为很多系统中的读取请求远远多于写入请求。例如可能存在 100 次读取,却只有 1 次写入。如果所有请求全部交给主库,那么主库需要承担全部压力;现在把大量读取请求分散给多个从库,主库的压力自然就会明显降低。
当然,主库的数据同步到从库需要一定时间,因此主从之间可能短暂出现数据不同步。这里暂时只需要理解读写分离最核心的思想:
主库负责写,从库帮助主库承担大量读取请求。
实际项目中还可以使用 MyCat、TDDL、Amoeba、Cobar 等数据库中间件帮助完成读写请求的分离。
7. 数据库还是太忙:引入缓存
即使已经进行了读写分离,如果访问量继续增长,数据库仍然可能需要处理大量读取请求。
这时继续观察业务会发现一个非常重要的特点:
并不是所有数据都会被同样频繁地访问。
例如热门商品、热门页面可能一天被访问几十万次,而一些冷门商品很长时间才会被访问一次。
因此,可以把访问频率很高的数据称为热点数据,访问频率较低的数据称为冷数据。
对于热点数据,没有必要每一次请求都重新查询数据库,可以把它们提前保存到缓存中。
应用程序读取数据时,首先访问缓存。如果缓存中已经存在结果,就直接返回,不再访问数据库;只有缓存中没有需要的数据时,才继续查询数据库。
这样,大量针对热点数据的请求就会在真正到达数据库之前被缓存拦截,从而明显降低数据库压力。
这正是 Redis 在分布式系统中非常重要的角色:
Redis 作为分布式缓存,保存大量访问频繁的热点数据。
这也和前面提到的“二八原则”联系起来:少量热点数据往往承担了大量访问请求,因此只需要把这些数据缓存起来,就能获得非常明显的效果。
不过,缓存也不是没有代价。
例如数据库中的商品价格已经从100修改成80,而 Redis 中仍然保存100,那么用户读取 Redis 时得到的就是旧数据。
于是又产生了一个新的问题:
Redis 中的数据如何和数据库保持一致?
除此之外,还会继续遇到缓存穿透、缓存击穿、缓存雪崩、热点数据集中失效等问题,这些内容会在后面的 Redis 学习中继续展开。
8. 数据越来越多:分库分表
缓存解决的主要是访问压力,但如果业务中的数据本身越来越多,又会出现另一个问题:
一个数据库已经放不下这么多数据了怎么办?
例如最开始所有数据都放在同一个数据库中:
数据库 ├── 用户表 ├── 商品表 └── 交易表当数据量越来越大时,就可以按照业务继续拆分,例如:
用户数据 → 用户库 商品数据 → 商品库 交易数据 → 交易库这种按照业务职责拆分数据库的方式,可以理解为垂直分库。
拆分之后,不同数据库还可以部署到不同的服务器中,于是原来只有一台数据库服务器的系统,开始逐渐形成多个独立的存储集群。
如果某一张表本身仍然非常大,还可以继续拆表。
例如评论数据可以根据商品 ID 进行 Hash,然后根据 Hash 结果决定数据应该进入哪一张表;支付记录也可以按照时间划分,再根据用户 ID 或记录编号继续拆分。
不管具体采用什么规则,它背后的思想都非常简单:
一张表太大,就拆成多张表;一台数据库服务器压力太大,就把数据分散到更多服务器。
这样数据库也可以通过增加服务器数量继续扩展。
但分库分表以后,数据库管理、数据路由、查询和运维都会变得更加复杂,因此它同样不是免费的性能提升。
9. 应用越来越大:业务拆分与微服务
前面的拆分主要解决服务器性能和数据库压力,但随着系统继续发展,还有一个问题会越来越明显——业务本身也会越来越复杂。
最开始的一个应用程序中,可能同时包含:
用户 商品 交易业务少的时候这样没有问题,但随着功能越来越多、开发人员越来越多,一个巨大的应用会越来越难开发和维护。
于是可以继续按照业务进行拆分:
用户服务 商品服务 交易服务不同业务交给不同的开发团队负责,每个团队维护自己的服务。不同服务之间不再随意访问对方的数据,而是通过 Gateway、消息总线等方式进行通信。
进一步发展以后,每一个子系统内部甚至都可以拥有自己的应用集群、缓存和存储集群。例如用户系统拥有自己的应用服务器、Redis 和数据库,商品系统同样拥有自己的应用服务器、Redis 和数据库。
一些多个业务都会使用的功能,例如安全管理、监控预警、数据采集等,也可以继续抽取成公共服务。
这就是微服务逐渐形成的过程。
微服务解决的不只是“服务器性能不够”,还解决另外一个非常现实的问题:
当项目越来越大、开发人员越来越多时,如何让不同团队更加独立地开发和维护自己的业务。
所以系统最终如何拆分,本质上还是取决于业务本身。
10. 分布式系统中的常见概念
经过前面的架构演进,再来看分布式系统中的一些常见概念就会简单很多。
10.1 应用 Application / 系统 System
应用或者系统,可以理解为:
为了完成一整套业务而存在的一个程序,或者一组相互配合的程序。
例如一个完整的电商系统,就是一个应用或系统,它共同完成用户、商品、交易等业务。
10.2 模块 Module / 组件 Component
一个应用内部通常还会继续划分不同功能。
例如:
电商系统 ├── 用户模块 ├── 商品模块 └── 交易模块这些职责清晰、功能相对独立的部分,就可以称为模块或者组件。
简单来说:
应用表示一个完整系统,模块表示系统内部的某一部分功能。
10.3 分布式 Distributed
如果一个系统的不同部分被部署在不同的服务器上,并通过网络共同完成工作,就可以认为它具有分布式的特点。
例如:
服务器 A:应用服务 服务器 B:数据库服务两台服务器通过网络通信,共同完成整个系统的业务。
所以分布式可以简单理解为:
把系统的不同工作分散到不同物理机器上,再通过网络协同完成。
10.4 集群 Cluster
集群强调的是:
多台服务器为了完成同一个目标共同工作。
“集群”强调的是多个节点协同工作,不要求每个节点必须执行完全相同的任务。对于应用服务器集群通常是同一任务的多个副本;对于存储和计算集群,节点可能负责不同的数据或不同的子任务。
例如多台应用服务器共同提供 Web 服务,可以称为应用服务器集群;多台数据库服务器共同提供数据库服务,可以称为数据库集群。
因此可以简单区分:
分布式 → 更强调物理上分散到不同服务器 集群 → 更强调多个节点共同完成一个目标实际学习中没有必要过度纠结两者细微的边界。
10.5 主 Master / 从 Slave
主从是一种常见的集群结构。
其中一个节点承担主要职责,称为主节点;其他节点承担辅助职责,称为从节点。
例如前面的数据库主从架构:
主数据库 │ │ 数据同步 ▼ 从数据库主库负责主要的数据修改,从库的数据来自主库同步,并帮助主库承担部分查询压力。
10.6 中间件 Middleware
中间件可以简单理解成:
夹在不同程序之间,为它们提供通用能力的软件。
例如前面出现过的数据库中间件、缓存、消息队列等,都不是具体的用户、商品、交易业务,却可以为业务系统提供数据访问、缓存、通信等公共能力。
11. 衡量一个系统的几个重要指标
11.1 可用性 Availability
可用性表示系统在一段时间内能够正常提供服务的程度。
可以简单理解为:
可用性 = 系统正常运行时间 ÷ 总时间平时常说的“4 个 9”,表示系统可用性约为99.99%;“5 个 9”则表示约99.999%。
可用性越高,意味着系统一年中允许出现故障的时间越短。
所谓高可用(High Availability,HA),本质上就是:
希望系统尽可能长时间保持正常服务。
11.2 响应时长 Response Time
响应时长简称RT(Response Time),表示从用户发出请求,到系统返回结果所经历的时间。
例如一次请求耗时20ms,那么这次请求的响应时间就是20ms。
一般来说:
响应时间越短,用户感觉系统越快。
实际系统中通常还会关注平均响应时间、中位数响应时间以及较慢请求的响应时间。
11.3 吞吐量 Throughput 与并发 Concurrent
并发表示同一个时刻,系统能够同时处理多少请求。
吞吐量表示一段时间内,系统总共能够处理多少请求。
例如一条道路同一时刻最多容纳 2 辆车,但一分钟可以通过 20 辆车,那么可以简单理解为:
并发:2 吞吐量:20 辆 / 分钟服务器也是一样。
并发关注的是“同时有多少请求正在处理”,吞吐量关注的是“一段时间总共处理了多少请求”。
实际系统中,并发量往往不容易直接统计,因此经常使用很短时间内的吞吐量,例如每秒能够处理多少请求,来衡量服务器的处理能力。
12. 分布式架构的整体演进
回头看整个过程,系统并不是突然变成一个庞大的分布式系统,而是随着业务不断增长,一步一步演进出来的:
这并不是一套必须严格执行的固定流程。
实际项目首先遇到什么问题,就应该优先解决什么问题。有的系统用户量非常大,所以首先需要解决高并发;有的系统并发并不高,但业务功能特别复杂,那么首先要解决的可能是业务拆分。
因此:
架构的演进应该由实际业务和实际瓶颈推动,而不是为了使用某种技术而增加系统复杂度。
而在整个演进过程中,Redis 的位置已经非常清楚:Redis 通常位于应用程序和数据库之间,作为分布式缓存保存热点数据,通过内存的高速访问能力拦截大量数据库查询。
这也是学习 Redis 之前必须先理解分布式系统的原因。