Redis - 1 ( 7000 字 Redis 入门级教程 )
一: Redis
1.1 Redis 简介
Redis 是一种基于键值对(key-value)的 NoSQL 数据库,与其他键值对数据库不同,Redis 的值可以是多种数据结构和算法的组合,如字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)、位图(Bitmaps)、HyperLogLog 和地理信息定位(GEO)等,因此能够满足多种应用场景。Redis 将所有数据存储在内存中,具备极高的读写性能,同时通过快照和日志的方式将数据持久化到硬盘上,保证在断电或机器故障等情况下数据不会丢失。此外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等功能,灵活强大。在合适的场景中,Redis 如同一把功能丰富的瑞士军刀,发挥出强大的作用。
1.2 Redis 的特性
1.2.1 速度快
正常情况下 Redis 执行命令的速度非常快,根据官方数据,读写性能可以达到每秒 10 万次。当然,这也与机器性能有关,但这里暂不讨论硬件差异,仅从以下四个方面分析 Redis 高速性能的原因:
原因 | 描述 |
---|---|
数据存储在内存中 | Redis 的所有数据都存储在内存中,根据 Google 2009 年给出的各层级硬件执行速度表明,内存的高访问速度是 Redis 快速性能的主要原因。 |
使用 C 语言实现 | Redis 使用 C 语言编写,C 语言与操作系统的交互更紧密,程序执行速度相对更快。 |
单线程架构 | Redis 使用单线程架构,避免了多线程可能导致的竞争问题,从而提高了执行效率。6.0 版本引入了多线程机制,但仅用于处理网络和 IO,不涉及命令执行,命令仍采用单线程模式。 |
精细优化的源代码 | Redis 源代码经过精心打磨,既追求性能又兼顾优雅,被评价为少有的性能与设计俱佳的开源代码。 |
1.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(基于位置服务)的开发。在这些强大数据结构的帮助下,开发者可以构建出更多“有趣”和实用的应用。
1.2.3 丰富的功能
除了 5 种数据结构,Redis 还提供了许多额外的功能:
功能 | 描述 |
---|---|
键过期功能 | 提供键的过期机制,可用于实现缓存功能。 |
发布订阅功能 | 支持发布订阅机制,可以用来构建消息系统。 |
Lua 脚本支持 | 支持 Lua 脚本功能,可以利用 Lua 创造出新的 Redis 命令,增强灵活性。 |
简单事务支持 | 提供简单的事务功能,可以在一定程度上保证事务特性,支持多命令的原子性操作。 |
流水线(Pipeline)功能 | 客户端可以将一批命令一次性发送到 Redis,减少网络开销,提高执行效率。 |
1.2.4 简单稳定
Redis 的简单性主要体现在以下三个方面:首先,Redis 的源码非常精简,早期版本只有约 2 万行代码,3.0 版本后由于增加了集群功能,代码量也仅增至约 5 万行,相较于许多 NoSQL 数据库,代码量要少得多,这使得开发和运维人员完全可以深入理解其源码。其次,Redis 使用单线程模型,这不仅让服务端的处理模型更简单,同时也简化了客户端开发。最后,Redis 不依赖操作系统的外部类库(例如 Memcached 依赖 libevent),而是自行实现了事件处理的相关功能。尽管 Redis 设计简单,但其稳定性极高,在大量使用场景中,因 Redis 自身 BUG 导致宕机的情况非常少见。
1.2.5 客户端语言多
Redis 提供了简单的 TCP 通信协议,使得许多编程语言可以轻松接入。同时,Redis 因受到社区和各大公司的广泛认可,支持它的客户端语言也非常丰富,几乎涵盖了所有主流编程语言,如 C、C++、Java、PHP、Python、NodeJS 等。后续将对 Redis 的客户端使用进行详细说明。
1.2.6 持久化
通常情况下,将数据存放在内存中存在一定风险,一旦断电或机器故障,重要数据可能会丢失。为了解决这一问题,Redis 提供了两种持久化方式:RDB 和 AOF。这两种策略可以将内存中的数据保存到硬盘,从而保障数据的持久性。后续将对 Redis 的持久化机制进行详细说明。
1.2.7 主从复制
Redis 提供了复制功能,可以创建多个数据完全相同的 Redis 副本(Replica),这也是实现分布式 Redis 的基础。后续将对 Redis 的复制功能进行详细演示。
1.2.8 高可用和分布式
Redis 提供了高可用的实现方式,如 Redis 哨兵(Redis Sentinel),用于故障检测和自动故障转移。同时,Redis 还支持 Redis 集群(Redis Cluster),实现真正的分布式架构,具备高可用性、读写扩展性和容量扩展能力。
1.3 Redis 的应用场景
应用场景 | 描述 |
---|---|
缓存(Cache) | 缓存机制广泛应用于大型网站,可加速数据访问速度并降低后端数据源压力。Redis 提供键值过期时间设置、灵活的内存控制和淘汰策略,为网站稳定性保驾护航。 |
排行榜系统 | Redis 提供列表和有序集合结构,支持按热度、发布时间或复杂维度构建排行榜系统,是开发各种排行榜功能的理想选择。 |
计数器应用 | 计数器在网站中至关重要,如视频播放数或电商浏览数。Redis 天然支持计数功能,性能卓越,可轻松应对高并发场景,是计数器系统的重要选择。 |
社交网络 | Redis 支持社交网站的关键功能,如赞/踩、粉丝、共同好友/喜好、推送和下拉刷新。其灵活的数据结构可轻松实现这些功能,并高效处理大规模访问量。 |
消息队列系统 | 消息队列是大型网站的基础组件,具有业务解耦和削峰特性。Redis 提供发布订阅和阻塞队列功能,虽然不如专业消息队列强大,但足以满足一般消息队列需求。 |
1.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 格式压缩保存,方便查看运行情况。 |
1.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"
二: 预备知识
2.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"
2.2 EXISTS
EXISTS 用于判断一个或多个 key 是否存在,返回存在的 key 的数量。
redis> SET key1 "Hello"
"OK"
redis> EXISTS key1
(integer) 1
redis> EXISTS nosuchkey
(integer) 0
redis> SET key2 "World"
"OK"
redis> EXISTS key1 key2 nosuchkey
(integer) 2
2.3 DEL
DEL 命令用于删除一个或多个 key,返回值为成功删除的 key 的数量。
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> DEL key1 key2 key3
(integer) 2
2.4 EXPIRE
EXPIRE 命令为指定的 key 添加秒级的过期时间。如果设置成功返回值为 1;如果设置失败返回值为 0,因为 key 可能不存在,所以导致设置失败。
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
2.5 TTL
TTL 用于获取指定 key 以秒为单位的剩余过期时间。返回值为剩余的过期时间;如果返回 -1 表示该 key 没有设置过期时间,返回 -2 表示该 key 不存在。
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
2.6 TYPE
TYPE 命令用于返回指定 key 的数据类型,可能的返回值包括:none(key 不存在)、string(字符串)、list(列表)、set(集合)、zset(有序集合)、hash(哈希)和 stream(流)。
redis> SET key1 "value"
"OK"
redis> LPUSH key2 "value"
(integer) 1
redis> SADD key3 "value"
(integer) 1
redis> TYPE key1
"string"
redis> TYPE key2
"list"
redis> TYPE key3
"set"
2.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
OK
127.0.0.1:6379> lpush mylist a b c
(integer) 3
127.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,用户无需干预,完全无感知。 |
2.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 更适用于需要快速执行的场景。