Redis - 1 ( 11000 字 Redis 入门级教程 )
一:服务端高并发分布式结构演进之路
1.1 常见概念
概念 | 定义 | 生活例子类比 |
---|---|---|
应用(Application)/ 系统(System) | 为完成一整套服务的程序或一组相互配合的程序群。 | 为完成一项任务而组成的由一个人或一群相互配合的人构成的团队。 |
模块(Module)/ 组件(Component) | 在复杂应用中,为分离职责,将具有清晰职责和强内聚性的部分抽象为独立概念,以便理解和管理。 | 军队中为攻克某据点,将人员分为突击小组、爆破小组、掩护小组、通信小组等。 |
分布式(Distributed) | 系统的多个模块部署在不同服务器上,模块间通过网络通信协作完成任务。 | 为满足实际需求,一个原本在同一办公场地工作的团队被分散到多个城市,通过远程协作完成目标。 |
集群(Cluster) | 部署在多台服务器上、为实现特定目标而组成的一组组件。 | 为攻克坚固的目标城市,指挥部集中大批炮兵部队形成一个炮兵打击集群。 |
分布式 vs 集群 | 分布式注重物理形态,强调模块运行在不同服务器上;集群注重逻辑形态,强调多个组件协作完成特定服务目标。 | 无需严格区分,分布式是物理分布,集群是逻辑协作。 |
主(Master)/ 从(Slave) | 集群中承担更多职责的称为主,其他承担附属职责的称为从。例如 MySQL 集群中,主库负责写入操作,从库负责从主库同步数据。 | 主库类似于军队指挥官,掌握更多职责;从库类似于协助执行任务的团队成员。 |
中间件(Middleware) | 提供不同应用程序之间通信的软件,充当技术、工具和数据库之间的桥梁。 | 一家饭店从市场采购食材业务量变大后,成立采购部作为厨房和市场之间的桥梁。 |
1.2 评价指标
概念 | 定义 | 说明 |
---|---|---|
可用性(Availability) | 指在单位时间段内,系统能够正常提供服务的概率或期望。计算公式为系统正常提供服务时长 / 一年总时长。例如,4个9表示99.99%的可用性,5个9表示99.999%的可用性。 | 常用“高可用”(High Availability,HA)作为非量化目标,表达对系统可用性的追求。 |
响应时长(Response Time,RT) | 指用户从完成输入到系统返回响应的时间间隔。例如外卖业务的响应时长 = 拿到外卖的时间 - 完成点单的时间。通常衡量最小响应时长、平均响应时长和中位数响应时长。 | 响应时长越小越好,但实际中需结合实现限制和业务需求具体判断。 |
吞吐(Throughput) | 指单位时间内系统成功处理的请求数量。例如,高速公路每分钟通过20辆车,则吞吐量为20。 | 衡量系统处理请求的能力,通常以极短时间内的吞吐量为近似值。 |
并发(Concurrent) | 指系统同一时刻支持的最大请求数量。例如,高速公路的并发为同时行驶的车辆数量,如同一时刻有2辆车,则并发量为2。 | 并发量常用“高并发”(High Concurrent)作为非量化目标,表达对系统并发能力的追求。 |
吞吐 vs 并发 | 吞吐量关注的是单位时间内处理的请求总量,而并发量关注的是同一时间点同时处理的请求数量。 | 两者通常一起衡量系统性能,实际中可通过极短时间的吞吐量近似代替并发量。 |
1.3 架构演进
1.3.1 单机架构
在初期阶段,我们可以依靠精干的技术团队快速将业务系统投入市场以进行验证,并灵活应对变化需求。由于前期用户访问量较小,对系统性能和安全性要求不高,且架构简单,无需专业运维团队,因此选择单机架构是一个合理的决定。当用户在浏览器中输入 www.bit.com 时,首先通过 DNS 服务将域名解析为 IP 地址 10.102.41.1,随后浏览器会访问该 IP 对应的应用服务。
1.3.2 应用数据分离架构
随着系统上线,我们如预期般取得了成功,吸引了一批忠实用户,系统访问量逐步上升,逐渐逼近硬件资源的极限。同时,团队在此过程中积累了丰富的业务流程经验。面对当前的性能压力,我们需要未雨绸缪,通过系统重构和架构优化来提升系统的承载能力。由于预算有限,我们选择以最小代价提升性能的方法,将应用和数据分离。与之前的架构相比,主要区别在于将数据库服务独立部署在同一数据中心的其他服务器上,应用服务通过网络访问数据库,从而提高了系统的承载能力。
1.3.3 应用服务集群架构
随着系统的广受欢迎,爆款产品的出现使单台应用服务器无法满足需求。单机架构的应用服务器首先遇到了性能瓶颈,技术团队面临两种解决方案,并围绕各方案的优劣展开了热烈讨论。
扩展方式 | 定义 | 优势 | 劣势 |
---|---|---|---|
垂直扩展 / 纵向扩展 | 通过购买性能更高、价格更昂贵的应用服务器来处理更多流量,提升单台服务器的性能。 | 无需对系统软件进行任何调整,实施简单。 | 硬件性能与价格增长关系非线性,性能翻倍可能需要超过4倍的成本;硬件性能提升有明显上限。 |
水平扩展 / 横向扩展 | 调整软件架构,增加应用层服务器,将用户流量分担到多台服务器上,提升系统整体承载能力。 | 成本相对较低,扩展空间大,性能提升上限高。 | 增加系统复杂性,对技术团队的经验要求更高。 |
经过团队的学习、调研和讨论,我们最终选择了水平扩展方案来解决问题,但这需要引入一个新的组件——负载均衡。负载均衡的作用是解决用户流量如何分发到不同应用服务器的问题,它是一个专门用于流量分发的系统组件。在实际应用中,负载均衡不仅工作在应用层,也可能涉及网络层。同时,流量调度的算法种类繁多,这里简单介绍几种常见的算法。
算法 | 定义 | 特点 |
---|---|---|
Round-Robin 轮询算法 | 将请求依次公平地分发到不同的应用服务器上。 | 简单公平,适用于服务器性能一致的场景。 |
Weight-Round-Robin 权重轮询算法 | 为不同服务器设置权重,根据权重大小分配请求,使性能更强的服务器处理更多请求。 | 能者多劳,适用于服务器性能不同的场景。 |
一致哈希散列算法 | 根据用户的特征值(如 IP 地址)计算哈希值,并根据哈希结果分发请求,确保来自相同用户的请求总是分配到同一台服务器。 | 保证请求一致性,适用于需要为特定用户提供固定服务的场景,例如专项客户经理服务。 |
1.3.4 读写分离 / 主从分离架构
在将用户请求通过负载均衡分发到不同的应用服务器后,系统可以并行处理请求,并通过动态扩展服务器数量来缓解压力。然而,在当前架构下,无论扩展多少台应用服务器,这些请求最终都需要从数据库进行读写操作。随着业务增长,数据库的压力逐渐成为系统承载能力的瓶颈。与应用服务器不同,数据库由于其特殊性无法简单地通过横向扩展来解决问题。原因在于,数据分散到多台服务器后,无法保证数据的一致性。数据的一致性是指在同一系统中,无论何时何地,用户看到的数据都应保持统一。例如,在银行的转账系统中,如果一笔转账的金额在一台数据库中被修改,而另一台数据库中未修改,用户看到的账户金额将是错误的。
为了解决这一问题,我们采用主从架构的方式。系统中保留一个主数据库处理写操作,而其他数据库作为从数据库,只负责从主库同步数据并处理读请求。从库通过数据同步机制维持与主库一致的状态。这样,所有写操作由主库承担,而读操作则分散到多个从库,从而分担数据库的压力。由于大多数系统中读写请求的比例极不均衡,例如100次读对应1次写,因此通过将读请求分散到各个从库,数据库的整体压力显著降低。当然,这种架构也有一定的代价,即主库到从库的数据同步存在时间成本,但这一问题暂时不在讨论范围内。
1.3.5 引入缓存 ⸺ 冷热分离架构
随着访问量的持续增加,我们发现业务中有一部分数据的读取频率远高于其他数据,这部分被称为热点数据,而其余为冷数据。针对热点数据,为了提升读取响应时间,我们引入本地缓存和分布式缓存机制。例如,缓存热门商品信息或热门商品的 HTML 页面。通过缓存,可以在数据库读写前拦截绝大多数请求,从而大幅降低数据库的压力。具体技术包括使用 Memcached 作为本地缓存,Redis 作为分布式缓存,同时需要解决缓存一致性、缓存穿透、缓存击穿、缓存雪崩以及热点数据集中失效等问题。
1.3.6 垂直分库
随着业务数据量的不断增加,将大量数据存储在同一个数据库中已显得力不从心,因此可以根据业务需求对数据进行分库分表。例如,对于评论数据,可以按照商品ID进行哈希路由到对应的表中存储;对于支付记录,可以按小时创建表,并进一步拆分为小表,通过用户ID或记录编号来路由数据。这样,只要实时操作的表数据量足够小,并且请求能够均匀分发到多台服务器上的小表,就能通过水平扩展的方式提升数据库性能。其中,像 Mycat 这样的工具可以支持在大表拆分为小表情况下的访问控制。然而,这种方法显著增加了数据库运维的难度,对DBA提出了更高的要求。
当数据库设计发展到这种结构时,可以称为分布式数据库。尽管它在逻辑上是一个整体,但其不同部分由独立的组件实现。例如,分库分表的管理和请求分发由 Mycat 实现,SQL 的解析依赖单机数据库,读写分离可能由网关和消息队列完成,查询结果的汇总则由数据库接口层处理。这种架构是一类大规模并行处理(MPP)架构的实现方式,能够在高并发和大数据量的场景中发挥显著优势。
1.3.7 业务拆分 ⸺ 微服务
随着人员的增加和业务的发展,我们将业务拆分给不同的开发团队维护。每个团队独立实现自己的微服务,并通过隔离数据的直接访问来降低耦合性。团队之间的服务调用可以通过 Gateway、消息总线等技术实现关联。此外,一些通用功能,如用户管理、安全管理、数据采集等业务,也可以提取为公共服务,供各团队共享使用,从而提高整体开发效率和系统灵活性。
二: Redis
2.1 Redis 简介
Redis 是一种基于键值对(key-value)的 NoSQL 数据库,与其他键值对数据库不同,Redis 的值可以是多种数据结构和算法的组合,如字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)、位图(Bitmaps)、HyperLogLog 和地理信息定位(GEO)等,因此能够满足多种应用场景。Redis 将所有数据存储在内存中,具备极高的读写性能,同时通过快照和日志的方式将数据持久化到硬盘上,保证在断电或机器故障等情况下数据不会丢失。此外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等功能,灵活强大。在合适的场景中,Redis 如同一把功能丰富的瑞士军刀,发挥出强大的作用。
2.2 Redis 的特性
2.2.1 速度快
正常情况下 Redis 执行命令的速度非常快,根据官方数据,读写性能可以达到每秒 10 万次。当然,这也与机器性能有关,但这里暂不讨论硬件差异,仅从以下四个方面分析 Redis 高速性能的原因:
原因 | 描述 |
---|---|
数据存储在内存中 | Redis 的所有数据都存储在内存中,根据 Google 2009 年给出的各层级硬件执行速度表明,内存的高访问速度是 Redis 快速性能的主要原因。 |
使用 C 语言实现 | Redis 使用 C 语言编写,C 语言与操作系统的交互更紧密,程序执行速度相对更快。 |
单线程架构 | Redis 使用单线程架构,避免了多线程可能导致的竞争问题,从而提高了执行效率。6.0 版本引入了多线程机制,但仅用于处理网络和 IO,不涉及命令执行,命令仍采用单线程模式。 |
精细优化的源代码 | Redis 源代码经过精心打磨,既追求性能又兼顾优雅,被评价为少有的性能与设计俱佳的开源代码。 |
2.2.2 基于键值对的数据结构服务器
几乎所有编程语言都提供类似字典的数据结构,例如 C++ 的 map、Java 的 map、Python 的 dict 等,这种以键值对方式组织数据的方式在开发中非常常见。而 Redis 不同于普通的键值对数据库,除了支持字符串作为值,还支持多种复杂的数据结构,这不仅方便应对多种应用场景,还能显著提高开发效率。Redis 的全称是 Remote Dictionary Server,主要提供五种数据结构:字符串(string)、哈希(hash)、列表(list)、集合(set)和有序集合(ordered set/zset)。此外,在字符串基础上,衍生出了位图(Bitmaps)和 HyperLogLog 等特殊数据结构,并在 Redis 3.2 版本中增加了 GEO(地理信息定位)功能,以支持 LBS(基于位置服务)的开发。在这些强大数据结构的帮助下,开发者可以构建出更多“有趣”和实用的应用。
2.2.3 丰富的功能
除了 5 种数据结构,Redis 还提供了许多额外的功能:
功能 | 描述 |
---|---|
键过期功能 | 提供键的过期机制,可用于实现缓存功能。 |
发布订阅功能 | 支持发布订阅机制,可以用来构建消息系统。 |
Lua 脚本支持 | 支持 Lua 脚本功能,可以利用 Lua 创造出新的 Redis 命令,增强灵活性。 |
简单事务支持 | 提供简单的事务功能,可以在一定程度上保证事务特性,支持多命令的原子性操作。 |
流水线(Pipeline)功能 | 客户端可以将一批命令一次性发送到 Redis,减少网络开销,提高执行效率。 |
2.2.4 简单稳定
Redis 的简单性主要体现在以下三个方面:首先,Redis 的源码非常精简,早期版本只有约 2 万行代码,3.0 版本后由于增加了集群功能,代码量也仅增至约 5 万行,相较于许多 NoSQL 数据库,代码量要少得多,这使得开发和运维人员完全可以深入理解其源码。其次,Redis 使用单线程模型,这不仅让服务端的处理模型更简单,同时也简化了客户端开发。最后,Redis 不依赖操作系统的外部类库(例如 Memcached 依赖 libevent),而是自行实现了事件处理的相关功能。尽管 Redis 设计简单,但其稳定性极高,在大量使用场景中,因 Redis 自身 BUG 导致宕机的情况非常少见。
2.2.5 客户端语言多
Redis 提供了简单的 TCP 通信协议,使得许多编程语言可以轻松接入。同时,Redis 因受到社区和各大公司的广泛认可,支持它的客户端语言也非常丰富,几乎涵盖了所有主流编程语言,如 C、C++、Java、PHP、Python、NodeJS 等。后续将对 Redis 的客户端使用进行详细说明。
2.2.6 持久化
通常情况下,将数据存放在内存中存在一定风险,一旦断电或机器故障,重要数据可能会丢失。为了解决这一问题,Redis 提供了两种持久化方式:RDB 和 AOF。这两种策略可以将内存中的数据保存到硬盘,从而保障数据的持久性。后续将对 Redis 的持久化机制进行详细说明。
2.2.7 主从复制
Redis 提供了复制功能,可以创建多个数据完全相同的 Redis 副本(Replica),这也是实现分布式 Redis 的基础。后续将对 Redis 的复制功能进行详细演示。
2.2.8 高可用和分布式
Redis 提供了高可用的实现方式,如 Redis 哨兵(Redis Sentinel),用于故障检测和自动故障转移。同时,Redis 还支持 Redis 集群(Redis Cluster),实现真正的分布式架构,具备高可用性、读写扩展性和容量扩展能力。
2.3 Redis 的应用场景
应用场景 | 描述 |
---|---|
缓存(Cache) | 缓存机制广泛应用于大型网站,可加速数据访问速度并降低后端数据源压力。Redis 提供键值过期时间设置、灵活的内存控制和淘汰策略,为网站稳定性保驾护航。 |
排行榜系统 | Redis 提供列表和有序集合结构,支持按热度、发布时间或复杂维度构建排行榜系统,是开发各种排行榜功能的理想选择。 |
计数器应用 | 计数器在网站中至关重要,如视频播放数或电商浏览数。Redis 天然支持计数功能,性能卓越,可轻松应对高并发场景,是计数器系统的重要选择。 |
社交网络 | Redis 支持社交网站的关键功能,如赞/踩、粉丝、共同好友/喜好、推送和下拉刷新。其灵活的数据结构可轻松实现这些功能,并高效处理大规模访问量。 |
消息队列系统 | 消息队列是大型网站的基础组件,具有业务解耦和削峰特性。Redis 提供发布订阅和阻塞队列功能,虽然不如专业消息队列强大,但足以满足一般消息队列需求。 |
2.4 Redis 重要文件及作用
Redis 的安装过程就跳过了,我们直接讲 Redis 中重要文件的作用
程序/工具 | 描述 |
---|---|
redis-server | Redis 服务器程序,是 Redis 的核心运行程序。 |
redis-check-aof | 修复 AOF 文件的工具,是 redis-server 的软链接。 |
redis-check-rdb | 修复 RDB 文件的工具,是 redis-server 的软链接。 |
redis-sentinel | Redis 哨兵程序,用于监控 Redis 集群的高可用性,是 redis-server 的软链接。 |
redis-cli | Redis 命令行客户端程序,常用于学习和测试 Redis 操作。 |
redis-benchmark | 用于对 Redis 性能进行基准测试的工具。 |
redis-shutdown | 专用于停止 Redis 的脚本程序。 |
文件/目录 | 描述 |
---|---|
/etc/redis.conf | Redis 服务器的配置文件,用于定义 Redis 的运行参数和行为。 |
/etc/redis-sentinel.conf | Redis Sentinel 的配置文件,用于配置 Redis 哨兵程序的运行参数和行为。 |
/var/lib/redis/ | Redis 持久化文件(RDB 和 AOF)的默认存储目录,持久化时会在该目录下生成相关文件。 |
/var/log/redis/ | Redis 日志文件的默认存储目录。运行期间生成的日志按天分割,过期日志会以 gzip 格式压缩保存,方便查看运行情况。 |
2.5 Redis 命令行客户端
现在我们已经启动了 Redis 服务,接下来介绍如何使用 redis-cli 来连接和操作 Redis 服务。redis-cli 提供了两种方式连接 Redis 服务器,具体如下。
连接方式 | 描述 |
---|---|
交互式方式 | 使用 redis-cli -h {host} -p {port} 连接到 Redis 服务,连接后可以在交互式环境中执行所有操作,无需重复输入 redis-cli。 |
命令方式 | 使用 redis-cli -h {host} -p {port} {command} 直接执行命令并获取返回结果,无需进入交互式环境。 |
- 交互方式:
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set key hello
OK
127.0.0.1:6379> get key
"hello"
- 命令方式:
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379 ping
PONG
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379 set key hello
OK
[root@host ~]# redis-cli -h 127.0.0.1 -p 6379 get key
"hello"
三: 预备知识
3.1 KEYS pattern
KEYS pattern 返回匹配指定 pattern 的所有键。
通配样式 | 描述 | 解释 |
---|---|---|
h?llo | 匹配 hello、hallo 和 hxllo | ? 表示匹配任意一个字符 |
h*llo | 匹配 hllo 和 heeeello | * 表示匹配零个或多个任意字符 |
h[ae]llo | 匹配 hello 和 hallo,但不匹配 hillo | [ae] 表示匹配 a 或 e |
h[^e]llo | 匹配 hallo、hbllo 等,但不匹配 hello | [^e] 表示匹配除 e 之外的任意一个字符 |
h[a-b]llo | 匹配 hallo 和 hbllo | [a-b] 表示匹配 a 到 b 范围内的任意一个字符 |
MSET firstname Jack lastname Stuntman age 35
"OK"KEYS *name*
1) "firstname"
2) "lastname"KEYS a??
1) "age"KEYS *
1) "age"
2) "firstname"
3) "lastname"
3.2 EXISTS
EXISTS 用于判断一个或多个 key 是否存在,返回存在的 key 的数量。
redis> SET key1 "Hello"
"OK"redis> EXISTS key1
(integer) 1redis> EXISTS nosuchkey
(integer) 0redis> SET key2 "World"
"OK"redis> EXISTS key1 key2 nosuchkey
(integer) 2
3.3 DEL
DEL 命令用于删除一个或多个 key,返回值为成功删除的 key 的数量。
redis> SET key1 "Hello"
"OK"redis> SET key2 "World"
"OK"redis> DEL key1 key2 key3
(integer) 2
3.4 EXPIRE
EXPIRE 命令为指定的 key 添加秒级的过期时间。如果设置成功返回值为 1;如果设置失败返回值为 0,因为 key 可能不存在,所以导致设置失败。
redis> SET mykey "Hello"
"OK"redis> EXPIRE mykey 10
(integer) 1redis> TTL mykey
(integer) 10
3.5 TTL
TTL 用于获取指定 key 以秒为单位的剩余过期时间。返回值为剩余的过期时间;如果返回 -1 表示该 key 没有设置过期时间,返回 -2 表示该 key 不存在。
redis> SET mykey "Hello"
"OK"redis> EXPIRE mykey 10
(integer) 1redis> TTL mykey
(integer) 10
3.6 TYPE
TYPE 命令用于返回指定 key 的数据类型,可能的返回值包括:none(key 不存在)、string(字符串)、list(列表)、set(集合)、zset(有序集合)、hash(哈希)和 stream(流)。
redis> SET key1 "value"
"OK"redis> LPUSH key2 "value"
(integer) 1redis> SADD key3 "value"
(integer) 1redis> TYPE key1
"string"redis> TYPE key2
"list"redis> TYPE key3
"set"
3.7 Redis 数据结构和内部编码
Redis 提供多种数据结构,包括 string(字符串)、list(列表)、hash(哈希)、set(集合)和 zset(有序集合),这些是对外暴露的基本数据类型。实际上,Redis 针对每种数据结构都有多种底层内部编码实现,并会根据具体场景自动选择最适合的内部编码,以优化性能和存储效率。
数据结构 | 内部编码 |
---|---|
string | raw, int, embstr |
hash | hashtable, ziplist |
list | linkedlist, ziplist |
set | hashtable, intset |
zset | skiplist, ziplist |
内部编码 | 作用 |
---|---|
raw | 用于存储较大的字符串,直接以原始格式保存,适合处理大数据量的字符串值。 |
int | 用于存储整型数据,将字符串转换为整数存储,减少内存消耗,提高操作效率。 |
embstr | 用于存储小的、不可变的字符串,提供高效的内存分配和释放,适合短字符串的快速存取操作。 |
hashtable | 用于存储哈希表,适用于包含较多键值对或键值对较大的情况,支持快速查找、插入和删除操作。 |
ziplist | 用于存储紧凑型的数据,适用于元素数量较少且每个元素较小的情况,通过连续内存存储节省空间,但在元素较多时性能会下降。 |
linkedlist | 适用于列表元素数量较多或每个元素较大的情况,通过指针连接元素,支持快速插入和删除,适合处理大规模数据。 |
intset | 用于存储小范围的整数集合,元素较少时内存占用低,适合集合元素为整数且数量较少的场景。 |
skiplist | 用于存储有序集合的数据,支持快速范围查找和排序操作,适合处理大范围的有序数据,如排名或分值范围查询。 |
quicklist | 是 ziplist 和 linkedlist 的结合体,既保留了 ziplist 的内存紧凑性,又支持 linkedlist 的快速插入删除操作,适合复杂列表场景。 |
可以看到每种数据结构通常都有两种或以上的内部编码实现,例如 list 数据结构包含 linkedlist 和 ziplist 两种编码。同时,一些内部编码(如 ziplist)可以被多种数据结构共用作为其内部实现。具体的内部编码可以通过执行 object encoding 命令进行查询。
127.0.0.1:6379> set hello world
OK127.0.0.1:6379> lpush mylist a b c
(integer) 3127.0.0.1:6379> object encoding hello
"embstr"127.0.0.1:6379> object encoding mylist
"quicklist"
可以看出,键 hello 的值采用了 embstr 编码,而键 mylist 的值则使用了 ziplist 编码。Redis 的这种设计带来了两个重要优势:
优势 | 描述 |
---|---|
可改进内部编码 | 内部编码的优化不会影响外部数据结构和命令。比如 Redis 3.2 引入了 quicklist,将 ziplist 和 linkedlist 的优点结合,为列表类型提供了更优的内部编码实现,用户几乎无感知。 |
场景化优化 | 不同的内部编码在不同场景下发挥优势。比如 ziplist 节省内存,但在列表元素较多时性能下降,此时 Redis 会根据配置自动将编码切换为 linkedlist,用户无需干预,完全无感知。 |
3.8 单线程架构
Redis 采用单线程架构来实现高性能的内存数据库服务。下面通过多个客户端命令调用的示例,说明 Redis 单线程的命令处理机制;接着分析其单线程模型为何能够实现如此高的性能;最后解释为什么理解单线程模型是使用和运维 Redis 的关键。首先我们开启三个 redis-cli 客户端同时执行命令
- 客⼾端 1 设置⼀个字符串键值对:
127.0.0.1:6379> set hello world
- 客⼾端 2 对 counter 做⾃增操作:
127.0.0.1:6379> incr counter
- 客⼾端 2 对 counter 做⾃增操作:
127.0.0.1:6379> incr counter
Redis 客户端发送的命令经历了发送命令、执行命令、返回结果三个阶段,其中重点在于命令的执行过程。Redis 的单线程模型指的是:尽管从宏观上看,多个客户端似乎同时向 Redis 发送命令,但从微观角度来看,这些命令是以线性方式逐条执行的。虽然命令的执行顺序可能不确定,但一定不会有两条命令同时被执行。可以将 Redis 想象成只有一个服务窗口,多个客户端按照到达的先后顺序排队接受服务。例如,两条 incr 命令无论执行顺序如何,结果一定是正确的,不会发生并发问题,这就是 Redis 单线程执行模型的核心特点。
通常情况下,单线程的处理能力往往不如多线程。例如,运输 10,000 公斤货物,如果每辆车一次只能运载 200 公斤,需要 50 次才能完成;但如果有 50 辆车合理分工,只需一次即可完成任务。然而,Redis 使用单线程模型仍然能够实现每秒万级别的处理能力,这主要归结于以下三点原因。
原因 | 描述 |
---|---|
纯内存访问 | Redis 将所有数据存储在内存中,内存的响应时长大约为 100 纳秒,这是 Redis 达到每秒万级别访问的重要基础。 |
非阻塞 IO | Redis 使用 epoll 作为 I/O 多路复用技术,并通过自身的事件处理模型将连接、读写、关闭等操作转换为事件,避免在网络 I/O 上浪费时间。 |
单线程避免线程切换和竞态 | 单线程简化了数据结构和算法的实现,使程序模型更加简单;同时避免了多线程环境下因线程竞争共享数据而导致的切换和等待消耗。 |
虽然单线程为 Redis 带来了许多优势,但也存在一个致命的问题:对单个命令的执行时间有严格要求。如果某个命令执行时间过长,其他命令将被阻塞在等待队列中,无法及时响应,从而导致客户端阻塞。这对 Redis 这样的高性能服务来说是非常严重的。因此,Redis 更适用于需要快速执行的场景。