Redis:概述与背景

Redis,即 Remote Dictionary Server(远程字典服务) ,是一个开源的、基于内存的数据结构存储系统,可用作数据库、缓存和消息队列代理。它支持多种数据类型,如字符串(string)、哈希表(hash)、列表(list)、集合(set)、有序集合(sorted set)等,这使得开发者可以根据不同的业务需求选择最合适的数据结构来存储和处理数据。Redis 的这些特性使其在当今的软件开发中扮演着至关重要的角色,尤其是在应对高并发和高性能需求的场景下。

Redis 最初是为了解决LLOOGG.com网站的负载问题而开发的。随着互联网应用的发展,数据量和并发访问量不断增加,传统的关系型数据库在处理高并发读写和复杂数据结构时逐渐显露出性能瓶颈。Redis 的出现填补了这一空白,它以其卓越的性能和丰富的数据结构,成为了众多互联网公司的首选解决方案。如今,Redis 已经被广泛应用于各个领域,包括社交媒体、电子商务、游戏、金融等。

Redis 高性能之谜

内存存储:速度的基石

Redis 之所以能够拥有卓越的性能,首要原因在于其将所有数据都存储在内存中。内存,作为计算机硬件中与 CPU 直接交互的存储介质,具有极快的读写速度。与传统的基于磁盘存储的数据库不同,Redis 在内存中进行数据操作时,无需经历磁盘 I/O 的漫长等待过程。磁盘 I/O 操作涉及到机械运动,如磁盘的旋转和磁头的寻道,这些物理动作使得磁盘访问速度相对缓慢。而内存则是通过电信号来读写数据,其速度远远超过磁盘。据统计,内存的访问速度通常在纳秒级,而磁盘的访问速度则在毫秒级,两者相差可达数十万倍 。这就好比在图书馆中查找书籍,内存就像是你手边的书架,你可以迅速地找到所需的书籍;而磁盘则像是位于遥远仓库中的藏书,需要花费大量的时间去检索和搬运。这种巨大的速度差异使得 Redis 在处理数据读写时能够快如闪电,大大提升了应用程序的响应速度。

单线程模型:简洁高效的力量

Redis 采用单线程模型来处理所有客户端请求,这一设计看似简单,却蕴含着巨大的优势。在传统的多线程编程中,线程之间的上下文切换和锁竞争是不可避免的问题。上下文切换需要保存和恢复线程的执行状态,这会消耗一定的 CPU 时间和资源;而锁竞争则可能导致线程等待,降低系统的并发性能。Redis 的单线程模型则完全避免了这些问题。由于所有操作都在一个线程中顺序执行,Redis 无需担心线程安全问题,也不需要进行复杂的锁管理。这使得 Redis 的代码实现更加简洁明了,开发和维护成本也大大降低。Redis 官方文档中也强调了单线程设计的优势,它使得 Redis 的性能更加稳定和可预测。在实际应用中,许多高并发场景下的项目,如微博、抖音等,都大量使用了 Redis,其单线程模型在处理海量请求时依然能够保持高效稳定的运行,为这些平台的高性能提供了有力保障。

非阻塞 I/O 与多路复用:并发处理的核心

Redis 使用多路复用 I/O 技术,如 epoll(Linux 系统)、kqueue(FreeBSD 系统)等,来实现高效的并发处理。在传统的 I/O 模型中,一个线程在处理一个 I/O 操作时,如果该操作未完成,线程就会被阻塞,无法处理其他任务。而多路复用技术则允许一个线程同时监听多个文件描述符(即网络连接),当其中任何一个文件描述符就绪(有数据可读或可写)时,线程就会被唤醒,去处理相应的事件。这样,Redis 就能够在单线程的情况下,高效地处理大量的并发连接,大大提高了系统的并发性能。以 epoll 为例,它采用了事件驱动的方式,当有事件发生时,内核会将这些事件通知给应用程序,而不是像 select 和 poll 那样需要不断地轮询所有的文件描述符。这种方式大大减少了 CPU 的开销,提高了 I/O 操作的效率。通过测试对比,在高并发场景下,使用 epoll 的 Redis 服务器能够处理的并发连接数是传统 I/O 模型的数倍甚至数十倍,性能提升显著。

优化的数据结构:量身定制的存储方案

Redis 针对不同的应用场景,设计了一系列优化的数据结构,这些数据结构在提高操作效率方面发挥了关键作用。以简单动态字符串(SDS)为例,它是 Redis 用于表示字符串的数据结构。与 C 语言中的传统字符串相比,SDS 在获取字符串长度时,时间复杂度为 O (1),而 C 字符串则需要遍历整个字符串,时间复杂度为 O (n)。这是因为 SDS 在结构中记录了字符串的长度信息,无需额外的遍历操作。SDS 还具有二进制安全的特性,能够存储任意二进制数据,而不会受到字符串结束符 '\0' 的影响。这使得 Redis 在处理各种类型的数据时更加灵活和高效。再如哈希表,Redis 的哈希表采用了链地址法来解决哈希冲突,并且在哈希表的扩容和缩容过程中,采用了渐进式的方式,避免了一次性操作带来的性能抖动。跳表则是 Redis 用于实现有序集合(sorted set)的数据结构,它通过多层索引的方式,使得在集合中插入、删除和查找元素的时间复杂度都能够控制在 O (log n),大大提高了有序集合的操作效率。

Redis 数据结构深度剖析

字符串(String)

Redis 的字符串类型是最基础的数据结构,它基于简单动态字符串(SDS)实现。SDS 的结构包含了已使用的长度(len)、未使用的长度(free)以及字符数组(buf) 。这种结构设计使得 SDS 在操作字符串时具有显著的优势。在获取字符串长度时,由于 SDS 直接记录了长度信息,时间复杂度为 O (1),而 C 语言传统的以 '\0' 结尾的字符串获取长度的时间复杂度为 O (n),需要遍历整个字符串。在进行字符串拼接时,SDS 会先检查剩余空间是否足够,如果不足则进行空间扩展,并且采用空间预分配策略,当修改后的字符串长度小于 1MB 时,会额外分配与已使用空间相同大小的未使用空间;当修改后的字符串长度大于 1MB 时,会额外分配 1MB 的未使用空间。这样可以减少频繁的内存分配操作,提高性能。SDS 还采用惰性释放策略,当字符串缩短时,不会立即释放多余的空间,而是将其记录在 free 字段中,等待后续使用,这在一定程度上减少了内存分配和释放的开销。SDS 是二进制安全的,它可以存储任意二进制数据,不会因为数据中包含 '\0' 而导致截断,这使得 Redis 可以存储各种类型的数据,如图片、音频等二进制文件的内容。

在实际应用中,String 类型被广泛用于缓存数据。在一个电商系统中,商品的详细信息,如名称、描述、价格等,可以以 JSON 格式存储在 String 类型的键值对中。当用户请求商品信息时,首先从 Redis 中获取缓存数据,如果缓存中没有,则从数据库中查询并将结果存入 Redis 缓存。这样可以大大减少数据库的负载,提高系统的响应速度。String 类型还常用于实现计数器功能,如统计文章的阅读量、点赞数等。通过 INCR 和 DECR 命令,可以对存储在 String 类型中的数值进行原子性的增减操作,保证在高并发场景下计数的准确性。在分布式系统中,String 类型还可以用于实现分布式锁,通过 SETNX 命令尝试设置一个键值对,如果设置成功则表示获取到锁,否则表示锁已被其他进程占用,从而实现对共享资源的互斥访问。

列表(List)

Redis 的列表类型是一个双向链表结构,它的底层存储方式在不同情况下有所不同。在早期版本中,当列表元素较少时,会使用压缩列表(ziplist)存储,ziplist 是一种紧凑的、连续内存存储结构,它将所有元素紧挨着存储,通过记录前一个元素的长度和当前元素的编码方式来实现双向遍历 。这种结构在元素较少时可以节省内存空间,因为它避免了每个节点单独分配内存以及指针占用的空间。但当元素较多或者元素值较大时,ziplist 的性能会下降,因为插入和删除操作可能需要频繁地进行内存重新分配和数据移动。随着 Redis 版本的发展,引入了快速列表(quicklist),它结合了链表和压缩列表的优点,将链表按段切分,每一段使用 ziplist 存储,多个 ziplist 之间通过双向指针串接起来 。这样既可以在元素较少时利用 ziplist 的紧凑性节省内存,又可以在元素较多时通过链表结构保证操作的高效性。

List 类型在实际应用中常用于实现消息队列。在一个分布式系统中,生产者将消息通过 LPUSH 命令插入到 List 的一端,消费者通过 RPOP 命令从 List 的另一端取出消息进行处理,从而实现消息的异步传输和处理。在一个订单处理系统中,订单创建后,订单信息作为消息被发送到 Redis 的 List 中,订单处理服务从 List 中获取订单消息并进行后续的处理,如库存检查、支付处理等。List 类型还可以用于保存最新消息列表,在社交平台中,用户的动态可以通过 LPUSH 命令依次插入到 List 中,当需要获取用户的最新动态时,使用 LRANGE 命令从 List 的头部获取指定数量的元素,即可展示用户的最新消息。

哈希(Hash)

Redis 的哈希类型是一个键值对的集合,它可以看作是一个小型的字典,用于存储对象的属性。哈希类型的底层实现有两种方式,当哈希对象的键值对数量较少且键值对的键和值的字符串长度都小于 64 字节时,会使用压缩列表(ziplist)编码,在 ziplist 中,每个键值对依次紧凑存储 。当不满足上述条件时,会使用哈希表(hashtable)编码,哈希表采用 “数组 + 链表” 的结构,通过哈希函数计算键的哈希值来确定键值对在数组中的位置,如果发生哈希冲突,则通过链表将冲突的键值对串接起来 。哈希表的优点是操作效率高,插入、删除和查找操作的平均时间复杂度为 O (1),但在哈希冲突较多时,性能会下降。

哈希类型在实际应用中非常适合存储对象,在一个用户管理系统中,可以将用户的信息,如用户名、年龄、性别、邮箱等存储在一个哈希类型的键值对中,以用户 ID 作为键,用户的各个属性作为哈希表中的字段,属性值作为字段的值。这样可以方便地对用户的单个属性进行操作,如使用 HSET 命令修改用户的邮箱,使用 HGET 命令获取用户的年龄等。哈希类型还常用于存储配置信息,在一个应用程序中,各种配置参数,如数据库连接字符串、日志级别、系统参数等,可以存储在哈希类型中,通过键来管理和获取配置信息,便于配置的集中管理和修改。

集合(Set)

Redis 的集合类型是一个无序且元素唯一的数据结构,它的底层实现基于哈希表,集合中的每个元素作为哈希表的键,值为 NULL 。这种实现方式使得集合在进行元素的添加、删除和查找操作时具有较高的效率,时间复杂度为 O (1)。集合类型支持对多个集合进行交集、并集、差集运算,这些运算在实际应用中非常有用。

在实际应用中,集合类型常用于去重操作,在统计网站的独立访客数时,可以将每个访客的 ID 作为元素存储在集合中,由于集合元素的唯一性,重复的访客 ID 不会被重复添加,通过 SCARD 命令可以获取集合中元素的数量,即独立访客数。在社交网络中,集合类型可以用于实现社交关系的管理,如存储用户的好友列表,通过 SINTER 命令可以计算两个用户的共同好友,通过 SUNION 命令可以获取用户及其好友的所有关注列表,通过 SDIFF 命令可以获取用户与其他用户的差异关注列表,从而实现好友推荐等功能。

有序集合(Zset)

Redis 的有序集合类型在集合的基础上,为每个元素关联了一个分数(score),元素根据分数从小到大排序。有序集合的底层实现采用了跳跃表(skiplist)和哈希表两种数据结构 。跳跃表是一种分层的链表结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在跳跃表中,最底层的链表包含了所有的元素,而上层的链表包含的元素逐渐减少,通过这种多层索引的方式,使得在集合中插入、删除和查找元素的时间复杂度都能够控制在 O (log n)。哈希表则用于存储元素到节点的映射关系,以便快速定位元素所在的节点,提高操作效率。

有序集合在实际应用中常用于实现排行榜功能,在游戏中,可以根据玩家的积分作为分数,将玩家 ID 作为元素存储在有序集合中,通过 ZRANGE 命令可以获取积分排名靠前的玩家列表,展示游戏排行榜。在电商系统中,商品可以根据销量、评价等因素计算出一个综合分数,将商品 ID 作为元素,综合分数作为分数存储在有序集合中,通过 ZRANGE 命令可以获取热门商品排行榜,方便用户查看和购买。有序集合还可以用于实现带权重的任务队列,将任务 ID 作为元素,任务的优先级作为分数,通过 ZPOPMIN 或 ZPOPMAX 命令可以按照优先级取出任务进行处理,保证高优先级的任务先被执行。

Redis 常用命令速览

键命令(Key)

在 Redis 中,键命令用于对键进行基本操作,这些命令是操作 Redis 数据的基础,掌握它们能够灵活地管理和操作 Redis 中的数据。

SET:用于设置键值对。语法为SET key value,例如SET mykey "Hello Redis",这将在 Redis 中创建一个键为mykey,值为"Hello Redis"的键值对。如果mykey已经存在,其值将被新的值覆盖。在一个简单的 Web 应用中,可以使用 SET 命令将用户的登录状态缓存起来,键为用户 ID,值为登录状态(如"logged_in""logged_out"),这样在用户频繁请求时,可以快速从 Redis 中获取用户的登录状态,而无需每次都查询数据库,大大提高了系统的响应速度。

GET:用于获取键对应的值。语法为GET key,如GET mykey,它会返回mykey对应的"Hello Redis"。当应用程序需要获取某个缓存的数据时,就可以使用 GET 命令。在一个新闻网站中,新闻内容可能被缓存到 Redis 中,通过新闻 ID 作为键,使用 GET 命令就可以快速获取新闻的详细内容,展示给用户。

DEL:用于删除一个或多个键。语法为DEL key [key ...],比如DEL mykey,将删除键mykey及其对应的值。在数据更新时,可能需要先删除旧的缓存数据,就可以使用 DEL 命令。当一篇新闻的内容被更新后,先使用 DEL 命令删除旧的新闻缓存,然后再重新缓存新的内容,以保证数据的一致性。

EXISTS:用于判断一个键是否存在。语法为EXISTS key,返回值为 1 表示键存在,0 表示键不存在。在进行数据操作前,可以先使用 EXISTS 命令判断键是否存在,避免不必要的错误。在一个电商系统中,当需要获取某个商品的库存时,先使用 EXISTS 命令判断商品库存键是否存在,如果不存在,说明该商品可能是新上架的,还未初始化库存信息。

EXPIRE:用于为键设置一个过期时间(以秒为单位)。语法为EXPIRE key seconds,例如EXPIRE mykey 10,将mykey的过期时间设置为 10 秒后。对于一些时效性较强的数据,如验证码,就可以使用 EXPIRE 命令设置过期时间,保证验证码在一定时间内有效,超过时间后自动失效,提高系统的安全性和资源利用率。

数据类型命令

Redis 不同的数据类型有各自常用的命令,这些命令针对不同的数据结构特点进行设计,满足了各种复杂的业务需求。

String

INCR:用于将键的值增加 1。如果键不存在,则会先将键的值设置为 0,然后再执行 INCR 操作。语法为INCR key,例如INCR counter,假设counter初始不存在,执行后counter的值变为 1,再次执行则变为 2。常用于统计网站访问量、文章阅读量等场景,每有一次访问,就使用 INCR 命令对相应的计数器进行自增操作。

DECR:与 INCR 相反,用于将键的值减少 1。如果键不存在,会被设置为 0 并返回 -1。语法为DECR key,比如DECR counter,若counter的值为 5,执行后变为 4。在一些场景中,如库存管理中,当商品被卖出时,可以使用 DECR 命令减少库存数量。

Hash

HSET:用于在哈希表中设置字段的值。语法为HSET key field value,例如HSET user:1000 name "Alice",这将在键为user:1000的哈希表中,设置字段name的值为"Alice"。在用户信息管理系统中,使用 HSET 命令可以方便地存储和更新用户的各种属性。

HGET:用于获取哈希表中字段的值。语法为HGET key field,如HGET user:1000 name,将返回user:1000哈希表中name字段的值"Alice"。当需要获取用户的某个属性时,就可以使用 HGET 命令。

HDEL:用于删除哈希表中的一个或多个字段。语法为HDEL key field [field ...],比如HDEL user:1000 name,将删除user:1000哈希表中的name字段。当用户的某个属性不再需要时,可以使用 HDEL 命令将其删除。

List

LPUSH:用于将一个或多个值插入到列表的左侧(头部)。语法为LPUSH key value [value ...],例如LPUSH mylist "one",将"one"插入到mylist列表的左侧。如果mylist不存在,Redis 会自动创建一个新的列表。常用于实现消息队列,将消息从队列头部插入。

RPOP:用于移除并获取列表的最后一个元素(右侧,尾部)。语法为RPOP key,如RPOP mylist,将移除并返回mylist列表的最后一个元素。在消息队列场景中,消费者可以使用 RPOP 命令从队列尾部获取消息进行处理。

LRANGE:用于获取列表指定范围内的元素。语法为LRANGE key start stop,其中start是起始索引,stop是结束索引(包含该索引的元素),索引从 0 开始。例如LRANGE mylist 0 -1-1表示最后一个元素,该命令将获取mylist列表中的所有元素。在展示用户的消息列表时,可以使用 LRANGE 命令获取指定范围的消息进行展示。

Set

SADD:用于将一个或多个成员添加到集合中。语法为SADD key member [member ...],例如SADD myset "member1",将"member1"添加到myset集合中。由于集合元素的唯一性,重复的元素无法添加成功。在统计网站的独立访客时,可以使用 SADD 命令将访客的 ID 添加到集合中,实现去重统计。

SMEMBERS:用于返回集合中的所有成员。语法为SMEMBERS key,如SMEMBERS myset,将返回myset集合中的所有成员。当需要获取集合中的所有元素时,就可以使用 SMEMBERS 命令,在获取用户的所有兴趣标签时,可使用该命令。

SCARD:用于获取集合的基数,即集合中的元素个数。语法为SCARD key,比如SCARD myset,将返回myset集合中的元素个数。在统计独立访客数时,通过 SCARD 命令可以获取集合中唯一访客 ID 的数量,即独立访客数。

Zset

ZADD:用于将一个或多个成员及其分数添加到有序集合中。语法为ZADD key [NX|XX] [CH] [INCR] score member [score member ...],例如ZADD mysortedset 1 "one",将"one"及其分数 1 添加到mysortedset有序集合中。在游戏排行榜中,可以使用 ZADD 命令将玩家的积分和玩家 ID 添加到有序集合中,实现排行榜的功能。

ZRANGE:用于返回有序集合中指定排名范围的成员。语法为ZRANGE key start stop [WITHSCORES],其中startstop是排名范围,WITHSCORES表示是否同时返回成员的分数。例如ZRANGE mysortedset 0 -1 WITHSCORES,将返回mysortedset有序集合中的所有成员及其分数,按照分数从小到大排序。在展示排行榜时,可使用该命令获取排名靠前的玩家信息。

ZREM:用于从有序集合中移除一个或多个成员。语法为ZREM key member [member ...],比如ZREM mysortedset "one",将从mysortedset有序集合中移除"one"。当玩家的排名发生变化,需要从排行榜中移除旧的记录时,可使用 ZREM 命令。

文章作者: Z
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 微博客
Redis
喜欢就支持一下吧