目录
Redis提供了五种数据结构,每一种都有其独特的特点和优势。深刻理解这些特点对于Redis的开发与运维至关重要。同时,我们需要熟练掌握每种数据结构的常用命令,以便后续可以使在使用Redis时游刃有余。
我们的大致学习内容如下:
• 预备知识:在深入研究Redis的数据结构之前,需要了解几个全局(generic)命令,理解数据结构的内部编码方式,以及掌握Redis单线程模式的机制分析。
• 5种数据结构的特点、命令使用和应用场景示例:Redis提供了字符串(String)、列表(List)、哈希(Hash)、集合(Set)和有序集合(Sorted Set)等五种数据结构。每种数据结构都有其独特的特点和优势,在不同的应用场景中发挥着重要作用。掌握它们的常见命令和应用场景,可以帮助开发者更加灵活地运用Redis。
• 键遍历、数据库管理:除了了解数据结构,我们还需要掌握如何进行键遍历以及数据库管理。通过键遍历可以快速了解Redis中存在的键以及它们的类型,而数据库管理则包括数据库切换、数据库清空和数据库信息统计等操作。这些操作能够帮助开发者更好地管理和维护Redis数据库。
预备知识
在正式介绍Redis的五种数据结构之前,我们需要了解一些全局命令、数据结构和内部编码、以及单线程命令处理机制是非常必要的。这些知识点对于我们后续内容的学习打下了良好的基础。
这主要体现在以下两个方面:
-
通用性的命令理解:Redis拥有大量的命令,如果仅仅依靠死记硬背,对于我们来说可能会感到困难。然而,通过理解Redis的一些机制,我们可以发现这些命令具有很强的通用性。深入了解Redis的内部机制和数据结构,可以帮助我们更好地理解命令的设计和用途,从而更加灵活地运用这些命令。
-
命令和数据结构的场景使用:Redis并非万金油,不同的数据结构和命令适用于不同的场景。一旦在不恰当的场景下错误使用,可能会对Redis本身或应用系统造成严重影响甚至致命伤害。因此,了解每种数据结构和命令的最佳实践以及其适用的场景是至关重要的。只有在正确理解了Redis的特性和机制后,我们才能够避免不当使用命令和数据结构带来的潜在风险,并且能够更好地优化和设计应用系统。
综上所述,对于初学者和有经验的开发者来说,深入理解Redis的全局命令、数据结构和内部机制,以及命令和数据结构的最佳实践都是非常必要的,这将有助于我们更加高效地使用Redis,并确保系统的稳定性和性能优化。
接下来我们就通过redis-cli客户端和redis服务器的交互,来学习一些常用的redis命令。
注意,redis中的命令不区分大小写。
首先我们要学会使用redis文档:ACL CAT | Redis
GET和SET
在Redis中,最核心的两个命令之一是GET,用于根据键来获取对应的值。另一个是SET,用于将指定的键与相应的值存储在Redis中。这两个命令是Redis中最基本、最常用的命令之一。
GET命令
GET命令的语法如下:
GET key
它用于获b取存储在指定键中的值。如果键存在,则返回键对应的值;如果键不存在,则返回nil。
"Nil" 和 "null" 的区别
"Nil" 和 "null" 都是用于表示空值或者缺失值的术语,但它们的使用和含义在不同的编程语言中可能会有所不同。
-
Nil:
- 在一些编程语言中,如Objective-C、Swift等,"nil" 用于表示一个空对象或者指针。
- 在Objective-C中,"nil"是一个空指针,用于表示没有指向任何对象的指针。
- 在Swift中,"nil"用于表示可选类型(Optional),表示该类型可以是一个值,也可以是空(没有值)。
- 在Lisp编程语言中,"nil"是一个特殊的符号,用于表示空列表或者空值。
- 在Ruby中,"nil"表示空值或者未初始化的对象。
- 在Redis中,nil 表示的是一个特殊的值,用于表示键不存在或者某个键的值为空。
-
Null:
- 在很多其他的编程语言中,如Java、C#、Python等,使用 "null" 表示空值或者缺失值。
- 在Java中,"null" 表示一个不引用任何对象的引用变量。
- 在C#中,"null" 用于表示一个不指向任何对象的引用。
- 在Python中,"None" 用于表示空值或者缺失值,而不是 "null"。
总的来说,"nil" 和 "null" 都用于表示空值或者缺失值,但其具体含义和在编程语言中的使用方式会因语言而异。
SET命令
SET命令的语法如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
它用于将指定的键与相应的值存储在Redis中。其中,key是要设置的键名,value是要设置的值,它们都是字符串(不需要加引号,加了也不会报错)。可选的参数EX和PX用于设置键的过期时间,分别表示过期时间的秒数和毫秒数。NX和XX是可选参数,用于设置条件,NX表示只有在键不存在时才设置值,XX表示只有在键已存在时才设置值。
这两个命令是Redis中最基础、最重要的命令之一,几乎所有的Redis操作都离不开它们。GET用于从Redis中获取数据,而SET用于向Redis中存储数据。通过这两个命令,可以实现各种复杂的数据操作和应用场景,如缓存、会话管理、计数器、队列等。因此,熟练掌握和理解这两个命令对于使用Redis进行数据存储和管理至关重要。
基本全局命令
Redis提供了五种主要的数据结构,但它们都是存储在键值对中的值。也就是说,Redis键值对结构中的key固定就是字符串,但是value实际上会有多种类型,以后我们学习的命令大多都会根据类型不同而不同。
"基本全局命令"其实就是指,在Redis中具有通用性且能够应用于多种数据结构的一组命令。这些命令可以被用于操作Redis中的任何数据类型,例如字符串、列表、集合、哈希等。基本全局命令通常用于执行一般性的操作,如设置值、获取值、删除键等。
一些常见的基本全局命令及其作用:
- SET: 用于设置指定键的值。
- GET: 用于获取指定键的值。
- DEL: 用于删除一个或多个键。
- EXISTS: 用于检查指定键是否存在。
- TYPE: 用于获取指定键的数据类型。
- EXPIRE: 用于为指定键设置过期时间。
- TTL: 用于获取指定键的剩余过期时间。
- KEYS: 用于匹配满足指定模式的键。
- FLUSHDB: 用于清空当前数据库的所有键。
- FLUSHALL: 用于清空所有数据库的所有键。
这些基本全局命令提供了对Redis数据的基本操作,是使用Redis时最常用的命令之一。通过这些命令,我们可以进行数据的增删改查以及管理Redis数据库。
我们详细介绍其中最常用的几个:
KEYS命令
KEYS命令是Redis中用于返回所有满足指定模式的键的命令。它支持一些通配符样式,使用户可以根据特定的模式来匹配键。
该命令允许使用通配符来匹配键名,其中通配符有以下含义:
- *:匹配零个或多个字符
- ?:匹配一个字符
- []:匹配括号内的任意字符,如[abc]匹配a、b或c
- [^]:匹配除了括号内的字符之外的任意字符,如[^abc]匹配除了a、b、c之外的任意字符
- [-]:匹配指定范围内的字符,如[a-z]匹配a到z之间的任意字符
KEYS命令的语法如下:
KEYS pattern
该命令自1.0.0版本起就已经存在,时间复杂度为O(N),其中N是匹配模式的键的数量。
该命令用于在Redis中匹配指定模式的键,并返回所有满足条件的键名。在使用KEYS pattern命令时需要注意,如果数据集非常大,匹配过程可能会导致性能问题,因为Redis会遍历所有键来进行匹配,时间复杂度为O(N)。因此,如果可能的话,我们应尽量避免在生产环境中频繁使用该命令,或者限制匹配范围以提高性能。
示例:KEYS h*llo
该命令将返回所有以h开头且以llo结尾的键,例如hello、hallo、hxllo等。
下面是一些关于该例子的常见的通配符样式:
- h?llo:匹配hello、hallo和hxllo等。
- h*llo:匹配hllo和heeeello等。
- h[ae]llo:匹配hello和hallo,但不匹配hillo。
- h[^e]llo:匹配hallo、hbllo等,但不匹配hello。
- h[a-b]llo:匹配hallo和hbllo。
使用KEYS命令需要注意,如果在大型数据库中使用过于通用的模式,可能会导致性能问题,因为Redis在执行KEYS命令时需要遍历整个键空间,而Redis是一个单线程的服务器,执行类似key * 这样的操作的时间非常长,就使得Redis服务器被阻塞了,无法再给其他客户端提供服务!这样的后果可能是灾难性的。Redis经常会用作缓存抵挡在MySQL前面替它“负重前行”,如果Redis被keys * 阻塞住了,此时其他的查询redis操作就会超时,然后这些请求就会直接查数据库,很容易就会把我们的数据库弄崩溃。因此,建议在生产环境中谨慎使用KEYS命令,特别是对于模式中使用通配符的情况。
EXISTS命令
EXISTS命令用于判断某个键是否存在于当前数据库中。它可以一次查询一个或多个键的存在性。
它的语法如下:
EXISTS key [key ...]
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,该命令的执行时间都是固定的。
但是你会发现官方文档里面是这么说的:“Time complexity : O(N) where N is the number of keys to check.”
在Redis中,EXISTS命令的时间复杂度确实是O(1),不用怀疑,它不受数据库中存在的键数量的影响。因此,无论数据库中有多少个键,检查单个键的存在与否的时间复杂度始终是固定的。所以在实际使用中,可以放心地将EXISTS命令的时间复杂度视为O(1),无论键的数量如何。而官方文档中 O(N) 的意思是,你检查几个key,这里的N就是几。
EXISTS命令接受一个或多个键作为参数,返回给定键存在的个数。如果键存在,则返回1;如果键不存在,则返回0。
示例:EXISTS mykey
该命令将判断mykey是否存在,如果存在则返回1,否则返回0。
在实际应用中,EXISTS命令通常用于检查给定的键是否存在,以便在需要时执行相应的操作。例如,在执行操作之前,我们可以使用EXISTS命令来检查某个键是否存在,以避免对不存在的键进行操作,从而提高程序的稳定性和健壮性。
需要注意的是,虽然EXISTS命令的时间复杂度为O(1),但在大型数据库中频繁地使用该命令可能会对性能产生影响。因此,在设计数据存储方案时,我们应合理使用EXISTS命令,避免不必要的性能开销。
你觉得上述两种方式有差别吗?
天大地大的差别。
redis是一个客户端服务器结构的程序,客户端和服务器之间通过网络进行通信。
-
单次检查:在第一种写法中,只有一次命令请求和一次响应,因此只需要进行一次网络通信,这使得执行速度更快。
-
多次检查:而在第二种写法中,需要进行多次命令请求和多次响应,因此会产生更多轮次的网络通信,这会增加延迟并且可能影响性能。
因此,尽管EXISTS命令的时间复杂度为O(1),但在实际应用中,应该尽量减少对该命令的频繁调用,以避免不必要的网络开销和性能损耗。
DEL命令
DEL命令是Redis中用于删除指定键的命令。它可以一次删除一个或多个键。
它的语法如下:
DEL key [key ...]
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,DEL命令的执行时间都是固定的。DEL命令接受一个或多个键作为参数,删除指定的键,并返回成功删除的键的数量。
示例:DEL mykey
该命令将删除名为mykey的键。如果成功删除了该键,则返回1;如果该键不存在,则返回0。
DEL命令在实际应用中非常常用,可以用于清除缓存、清理无用数据、执行一次性任务等场景。需要注意的是,由于DEL命令的时间复杂度为O(1),因此即使是大型数据库中也能够快速执行。
我们以前一直强调删除操作是很危险的,那么这里也一样吗?
不一定,还得分情况讨论。
1. Redis作为缓存:
如果将Redis用作缓存,而原始数据仍然存储在MySQL等持久性数据库中,那么误删几个缓存数据通常不会造成严重问题。然而,如果删除了大量缓存数据,将导致大量请求直接传递给MySQL,可能会给数据库带来压力,甚至导致性能下降或宕机。
2. Redis作为数据库:
如果将Redis作为数据库使用,特别是存储了业务重要的数据,那么删除操作就变得非常危险。在这种情况下,一定要小心谨慎地进行删除操作,避免误删造成的数据丢失或者业务异常。
3. Redis作为消息队列:
如果将Redis用作消息队列,误删数据可能会对系统产生不同程度的影响,取决于消息队列在系统中的作用和重要性。在某些情况下,删除操作可能会导致消息丢失或者系统处理逻辑混乱,因此需要仔细评估删除操作的影响,并谨慎执行。
EXPIRE命令
EXPIRE命令是Redis中用于为指定键设置秒级的过期时间(Time To Live TTL)的命令。通过EXPIRE命令,可以指定某个键在一定时间后自动过期,从而实现自动清理过期数据的功能。
它的语法如下:
EXPIRE key seconds
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,EXPIRE命令的执行时间都是固定的。EXPIRE命令接受两个参数,第一个参数是要设置过期时间的键名,第二个参数是过期时间的秒数。成功设置过期时间后,该键在指定秒数后将自动被删除。
Redis提供的 EXPIRE 命令在很多场景下都有广泛的应用,包括但不限于验证码、优惠券等场景,以及我们后续可能会涉及到的基于Redis实现的分布式锁。
- 在验证码和优惠券等场景中,通常需要对临时数据进行快速的存储和删除。Redis的快速读写能力和支持过期时间的特性使得它非常适合用来存储这些临时数据。通过设置适当的过期时间,可以在不需要的时候自动清理过期数据,从而减少对系统资源的占用。
- 基于Redis实现的分布式锁是在分布式系统中实现并发控制的重要方式之一。通过利用Redis的原子性操作和单线程执行特性,可以很容易地实现一个简单而有效的分布式锁。分布式锁可以用于保护共享资源,避免多个客户端同时修改某个共享资源,从而保证数据的一致性和完整性。
示例:EXPIRE mykey 60
该命令将为名为mykey的键设置60秒的过期时间。如果成功设置了过期时间,则返回1;如果键不存在或设置失败,则返回0。
EXPIRE命令常用于对缓存数据进行管理,可以有效地控制数据的生命周期,减少内存占用。需要注意的是,过期时间仅在设置之后开始计算,并且只对当前数据库中的键有效。
注意,此处的设定过期时间必须是针对已经存在的key。
你可能会感到奇怪——这里的这个单位为什么是秒?秒这个单位对于计算机来说太长了,一般都是毫秒啊?
我们还有一个及其类似的命令:
PEXPIRE key milliseconds
key:要设置过期时间的键名。
milliseconds:过期时间,以毫秒为单位。键在设定的毫秒数后将会自动过期并被删除。
PEXPIRE命令是Redis中用于为指定键设置毫秒级的过期时间的命令。与EXPIRE命令不同,PEXPIRE命令接受的过期时间单位是毫秒。
PEXPIRE命令通常用于需要更精确控制过期时间的场景,如限时任务、会话管理等。
TTL命令
TTL命令是Redis中用于获取指定键的剩余过期时间(Time To Live TTL)的命令。通过TTL命令,我们可以查看某个键距离过期还剩余多少秒,或者判断键是否已经过期。
它的语法如下:
TTL key
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,TTL命令的执行时间都是固定的。TTL命令接受一个参数,即要查询过期时间的键名。它返回的是键的剩余过期时间,以秒为单位。
示例:TTL mykey
该命令将返回名为mykey的键的剩余过期时间。如果键不存在或者键没有关联过期时间,则返回-1;如果键存在但没有关联过期时间,则返回-2。
TTL命令通常用于检查键的过期状态,以便根据需要执行相应的操作。例如,可以定期检查过期键并将其清除,或者根据剩余过期时间执行不同的逻辑。通过TTL命令,可以有效地管理Redis中键的生命周期,保持数据的有效性和一致性。
同时,还有一个 PTTL key 和 PEXPIRE key milliseconds 是相对应的。
TYPE命令
TYPE命令是Redis中用于返回指定键对应的数据类型的命令。通过TYPE命令,可以查看键存储的值的数据类型,以便进行相应的操作。
它的语法如下:
TYPE key
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,TYPE命令的执行时间都是固定的。TYPE命令接受一个参数,即要查询数据类型的键名。它返回的是键对应的数据类型,可能的返回值包括none、string、list、set、zset、hash和stream。
示例:TYPE mykey
该命令将返回名为mykey的键对应的数据类型。可能的返回值包括:
- none:键不存在。
- string:字符串。
- list:列表。
- set:集合。
- zset:有序集合。
- hash:哈希表。
- stream:流(Redis 5.0新增的数据类型,当Redis作为消息队列的时候使用)。
当然,还有很多其他类型:
通过TYPE命令,我们可以方便地了解指定键存储的数据类型,从而在进行操作时选择合适的命令和方法。这对于我们开发者来说是非常有用的,因为不同的数据类型有不同的操作方式和特性。
在Redis中,键过期机制是指可以为键设置生存时间(TTL),一旦设置了生存时间,键将在一段时间后自动过期并被删除。这种机制可以用于实现缓存、会话管理等场景,有效地控制数据的生命周期,减少内存占用。
键过期机制的原理
键过期机制的原理分为定期删除和惰性删除:
设置生存时间(TTL):通过使用EXPIRE命令,可以为指定的键设置生存时间,即键的过期时间。一旦设置了生存时间,Redis会自动计算键的剩余过期时间,并在此时间之后将键删除。
过期检查:Redis通过定时任务来检查键是否已经过期。在每次访问键时,Redis会先检查键是否已经过期,如果过期则立即删除。此外,Redis还会在后台周期性地扫描过期键,并删除已过期的键。
惰性删除:为了提高性能,Redis采用了惰性删除的策略。即当某个键过期后,不会立即删除,而是等到下次访问该键时才会删除。这样可以避免频繁地进行删除操作,提高了性能。
定期删除的流程如下:
-
定期任务启动:Redis服务器在后台启动了一个定时任务,用于执行定期删除操作。
-
定期检查过期键:定时任务会每隔一段时间(默认每秒执行10次,可通过配置参数hz调整)检查部分过期键,这样可以分摊过期键的检查和删除压力,避免一次性删除过多过期键导致性能问题。
-
检查过期键的时间计算:Redis会计算每个键的过期时间与当前时间的差值,如果该差值小于等于0,则说明该键已经过期。
-
删除过期键:一旦发现有过期键,定期任务会立即删除这些过期键,释放占用的内存空间。
-
定期删除频率调整:定期删除的频率可以通过配置文件中的参数hz来调整,默认值为10。可以根据实际情况调整该参数,以适应不同的系统负载和性能需求。
总的来说,定期删除通过定时任务的方式,定期检查并删除部分过期键,从而保证了过期键能够及时被删除,释放内存空间,提高了Redis的性能和可靠性
惰性删除是Redis中的一种过期键管理策略,其流程如下:
-
操作触发检查: 在对键进行读取或写入操作时,Redis会先检查该键是否设置了过期时间,并且是否已经过期。
-
过期检查: 如果键已经设置了过期时间,并且当前时间已经超过了键的过期时间,那么这个键就被认为是过期的。
-
删除操作: 一旦键被检测到已经过期,Redis会立即将该键删除,从而释放相应的内存空间。
-
无过期检查,直接返回值: 如果键没有设置过期时间,或者未过期,Redis会直接返回键对应的值,而不会进行删除操作。
这种惰性删除的策略保证了Redis在实际使用中的高性能和低延迟。由于只有在对键进行操作时才会进行过期检查和删除操作,因此不会对数据库的读写操作产生太大的性能影响。此外,由于只在键被访问时才进行过期检查,所以即使有大量过期键,也不会对Redis的性能造成明显的影响。
尽管Redis通过定期删除和惰性删除这两种策略来管理过期键,但仍然可能会有一些过期的键残留在内存中,从而导致内存资源的浪费。为了解决这个问题,Redis还提供了一系列内存淘汰机制,以便在内存达到设定的上限时,自动删除一些键来释放内存空间。
Redis的内存淘汰机制包括以下几种:
-
LRU(Least Recently Used): 最近最少使用策略,Redis会根据键的最近访问时间来选择要删除的键,即删除最近最少被访问的键。
-
LFU(Least Frequently Used): 最不经常使用策略,Redis会根据键被访问的频率来选择要删除的键,即删除访问频率最低的键。
-
TTL(Time To Live): 过期时间策略,Redis会优先删除已经过期的键。
-
Random(随机删除): 随机选择要删除的键。
如何配置内存淘汰策略:
可以通过Redis的配置文件或者动态命令来配置内存淘汰策略。常用的配置选项包括maxmemory(最大内存限制)和maxmemory-policy(内存淘汰策略)。通过配置这些选项,可以根据实际需求来选择合适的内存淘汰策略,以便在内存达到限制时,自动删除部分键来释放内存空间。
关于内存淘汰机制还大有文章,我们以后会详细学习,这里只做简单了解即可。
键过期机制的优势
-
节省内存空间:可以自动删除不再需要的键,释放内存空间,避免内存溢出。
-
提高性能:通过定时任务和惰性删除机制,有效地管理和维护过期键,减少了删除操作对系统性能的影响,提高了系统的整体性能。
-
实现缓存:通过设置键的生存时间,可以实现缓存功能,缓存过期后自动更新,保持数据的有效性。
-
实现会话管理:可以为会话信息等数据设置生存时间,实现会话过期自动清理,提高系统的安全性和稳定性。
综上所述,Redis的键过期机制能够有效地管理和维护键的生命周期,提高系统的性能和稳定性,是Redis中重要的特性之一。
当然,除了Redis已有的定期删除和惰性删除策略外,我们还可以采用基于优先级队列和定时器的方式来管理过期键。
具体流程如下:
1. 创建优先级队列 / 堆:
将所有设置了过期时间的键加入到一个优先级队列中,优先级规则是过期时间越早的键优先级越高,即越早过期的键越靠前。
2. 启动定时器线程:
启动一个单独的定时器线程,该线程负责检查队首元素的过期时间,并执行相应的任务。
3. 定时器线程主循环:
定时器线程在一个循环中执行以下操作:
a. 检查队首元素过期时间:
- 线程从优先级队列中获取队首元素,即过期时间最早的键。
- 获取当前时间,计算当前时间与队首元素过期时间的时间差。
b. 设置等待:
- 根据时间差设置等待,如果时间差较长,则将线程阻塞,等待一段时间后再次唤醒。
- 如果时间差较短,则立即执行下一步操作。
c. 执行任务:
- 如果队首元素的过期时间已到,即键已过期,线程执行相应的删除操作,并将该键从优先级队列中移除。
- 如果队首元素的过期时间未到,则继续等待或执行其他任务。
4. 新任务添加唤醒:
在队列中添加新的任务时,可以唤醒定时器线程,重新检查队首元素的过期时间,并根据当前时间调整阻塞时间。
5. 性能优化:
定时器线程不需要高频率地扫描队首元素,而是根据当前时间与队首元素的时间差来设置合适的等待时间,以节省CPU开销。
通过优先级队列的方式,定时器线程只需关注队首元素,无需遍历所有键,提高了效率。
通过以上流程,我们可以实现一种高效的过期键管理机制,即保证了过期键及时被删除,又避免了对数据库的频繁访问,从而提高了系统的性能和可靠性。
还有一种方式——基于时间轮实现的定时器。
基于时间轮实现的定时器是一种常见的时间管理机制,可以有效地处理定时任务。
时间轮的基本结构:
- 时间轮: 时间轮可以看作是一个环形的数据结构,被划分为多个格子(或槽)。
- 每个格子: 每个格子代表一个时间段,相当于定时器的一个时间单位。在每个格子中,维护一个链表,存放需要在该时间段内执行的任务。
流程:
-
初始化时间轮: 根据实际需求,将整个时间轮划分为多个格子,每个格子代表一段时间,例如100ms。
-
添加任务: 将待执行的任务添加到时间轮的相应位置,即计算任务的执行时间,然后将任务添加到对应的格子链表中。
-
时间轮滚动: 时间轮以固定的速度(如100ms)滚动,每次滚动一个格子。
-
执行任务: 当时间轮滚动到某个格子时,执行该格子上链表中的所有任务。
-
移除已执行任务: 执行完毕后,移除已执行的任务。
-
重复执行: 时间轮会不断滚动,并重复上述过程,直到程序结束或手动停止。
优势和适用场景:
- 高效性: 时间轮的执行速度固定,不受任务数量的影响,执行效率高。
- 灵活性: 可以根据实际需求灵活调整时间轮的划分粒度和格子数量。
- 适用于定时任务: 适用于需要定时执行的任务,例如定时器任务、超时处理等。
通过时间轮实现的定时器,能够有效地管理定时任务,并且具有一定的灵活性和高效性,因此在实际开发中得到了广泛应用。
此处大家一定要注意!!!
Redis 并没有采取上述两种定时器的方案!!!
虽然Redis并没有采用上述所述的事件循环或基于时间轮的方案,但理解这两种方案对于理解Redis内部工作原理和其他系统设计仍然是非常有帮助的。这两种方案都属于高效的定时器实现方式,具有一定的优势和适用场景。
Redis常见数据类型和内部编码
在Redis中,数据存储时会根据不同的数据结构来组织。这些数据结构也对应着不同的内部编码方式,这有助于提高Redis的性能和效率。
-
字符串(String):字符串是最简单的数据结构之一,在Redis中用于存储文本或二进制数据。字符串的内部编码可以是int、raw、或者embstr,这取决于字符串的长度以及是否符合整数的表示范围。
-
列表(List):列表是一系列按照插入顺序排序的元素的集合。Redis中的列表使用双向链表来实现。列表的内部编码可以是ziplist(压缩列表)或者linkedlist(双向链表),这取决于列表的长度和元素的大小。
-
哈希(Hash):哈希存储了键值对的集合,其中每个键对应一个值。在Redis中,哈希使用哈希表来实现。哈希的内部编码可以是ziplist(压缩列表)或者hashtable(哈希表),这取决于哈希的大小和键值对的数量。
-
集合(Set):集合是一组唯一的无序元素的集合。在Redis中,集合使用哈希表来实现。集合的内部编码可以是intset(整数集合)或者hashtable(哈希表),这取决于集合的大小和元素的类型。
-
有序集合(Sorted Set):有序集合与集合类似,但每个元素都关联着一个分数(权重),有序集合根据分数排序。在Redis中,有序集合使用跳跃表和哈希表来实现。有序集合的内部编码可以是ziplist(压缩列表)或者skiplist(跳跃表),这取决于集合的大小和元素的类型。
这些内部编码方式是Redis根据数据的特性和大小动态选择的,旨在提高性能和节省内存。通过使用不同的内部编码方式,Redis可以在不同情况下选择最优的数据表示方式。
type 命令实际返回的是当前键的数据结构类型,包括:string(字符串)、list(列表)、hash(哈希)、set(集合)、zset(有序集合)。
Redis 的 5 种常用数据类型
然而,这些只是 Redis 对外暴露的抽象数据结构,实际上 Redis 针对每种数据结构都有多种底层内部编码实现,会根据数据的特性和使用场景动态选择合适的内部编码方式。
具体一点,当Redis实现上述数据结构时,它会在源代码层面对这些结构进行特定的优化,以达到节省时间和空间的效果。具体的实现可能会因为不同的场景和需求而有所变化,但在保证操作的时间复杂度为O(1)的前提下。例如,如果我们使用了哈希表作为数据结构,那么我们通常会使用标准的哈希表实现。然而,实际上这个哈希表的底层实现可能会有所不同,但Redis会保证其操作的时间复杂度符合承诺。在特定的场景下,Redis可能会选择使用其他的数据结构来实现,或者根据实际情况进行一定的优化,以提高性能并保证操作的时间复杂度符合承诺。
Redis针对每种数据结构都有多种底层内部编码实现,这些实现在不同的场景下可以提供不同的性能和内存利用效率。根据数据的大小、类型、以及操作的频率等因素,Redis会动态地选择最合适的内部编码实现,以优化性能和节省内存。
举例来说,对于字符串(String)这种数据结构,Redis可能会选择不同的内部编码实现,比如int、raw、或者embstr。当字符串较短且只包含可打印字符时,可能会使用embstr(内部字符串),而当字符串较长或者包含不可打印字符时,可能会使用raw编码。这样可以在不同的场景下选择最合适的编码方式,以提高性能和节省内存。
对于其他数据结构,如列表(List)、哈希(Hash)、集合(Set)、有序集合(Sorted Set)等,也有类似的情况。Redis会根据数据的特性和使用情况,选择最适合的底层内部编码实现,以提高性能和节省内存。
这种动态选择内部编码实现的机制,使得Redis在处理不同类型和规模的数据时能够灵活、高效地适应各种场景。
Redis 数据结构和内部编码
Redis底层对于不同数据结构的优化和实现方式的详细介绍:
-
哈希表(Hash Table):
- 标准实现: Redis中的哈希表是一种常见的数据结构,用于实现字典和集合等数据类型。通常情况下,Redis的哈希表实现是一种标准的哈希表结构,具有常规的查找、插入和删除操作,时间复杂度为O(1)。
- 内部优化: Redis在实现哈希表时可能会针对特定场景进行优化,例如采用更快速的哈希函数、优化内存布局或缓存等方式,以提高哈希表的性能和效率。
- 内部编码: Redis的哈希表内部编码使用ziplist和hashtable,根据哈希表的大小和存储元素的特性动态选择。
-
字符串(String):
- 标准实现: Redis中的字符串是一种简单的数据结构,通常采用动态数组或缓冲区实现。标准实现下,字符串的操作包括插入、删除和修改等,时间复杂度为O(1)。
- 编码方式: Redis中的字符串可以采用不同的编码方式,包括int、embstr和raw等。根据字符串的长度和内容,Redis会动态选择合适的编码方式,以节省内存空间并提高效率。
-
列表(List):
- 标准实现: Redis中的列表是一种双向链表结构,支持在头部和尾部进行插入、删除和修改操作。标准实现下,列表的操作复杂度为O(1)。
- 内部优化: Redis可能会针对特定场景对列表进行优化,例如在频繁的头部插入操作时,采用快速的头部插入方式或者采用压缩列表等方式,以提高性能。
- 内部编码: Redis的列表内部编码使用ziplist和linkedlist,根据列表的大小和操作特性动态选择。
-
集合(Set):
- 标准实现: Redis中的集合是一种无序不重复元素的数据结构,通常采用哈希表实现。标准实现下,集合的操作复杂度为O(1)。
- 内部优化: Redis可能会在集合的实现中进行优化,例如采用压缩列表或整数集合等特定场景的优化方式,以减少内存占用和提高性能。
- 内部编码: Redis的集合内部编码使用intset和hashtable,根据集合的大小和存储元素的特性动态选择。
-
有序集合(Sorted Set):
- 标准实现: Redis中的有序集合是一种有序不重复元素的数据结构,通常采用跳跃表和哈希表相结合的方式实现。标准实现下,有序集合的操作复杂度为O(log(N))。
- 内部优化: Redis可能会针对有序集合进行特定的优化,例如在范围查询操作时采用二分查找或者优化跳跃表的实现方式,以提高有序集合的性能。
- 内部编码: Redis的有序集合内部编码使用ziplist和skiplist,根据有序集合的大小和存储元素的特性动态选择。
所以,Redis在底层实现不同数据结构时,会根据实际需求和特定场景进行优化,以提高性能并保证操作的时间复杂度符合承诺。这些优化可能涉及到不同的数据结构实现、编码方式选择、内存布局优化等方面,以达到节省时间和空间的效果。
每种内部编码方式的含义和区别:
-
raw(原始):
- 含义: raw编码表示以字节数组形式存储的原始数据。对于字符串等较大的数据,通常采用 raw编码。
- 特点: 原始数据存储在连续的内存块中,不做任何额外的优化或压缩。
-
int(整型):
- 含义: int编码表示存储整数类型的数据。当存储的数据是整数时,Redis会采用int编码。
- 特点: 整数以二进制形式直接存储,节省了存储空间,并且操作效率较高。
-
embstr(小字符串):
- 含义: embstr编码表示存储长度较小的字符串数据。当字符串长度不超过一定阈值时,Redis会采用embstr编码。
- 特点: 对于较短的字符串,直接存储在embstr结构中,不需要额外的内存分配和指针操作,节省了空间和时间。
-
hashtable(哈希表):
- 含义: hashtable编码也表示存储哈希表数据结构,用于表示字典、哈希集合等数据类型。
- 特点: 和hash编码类似,内部也采用哈希表实现,但可能会针对大型数据进行优化。
-
ziplist(压缩列表):
- 含义: ziplist编码表示存储压缩列表数据结构。常用于表示列表和有序集合等数据类型。
- 特点: 压缩列表是一种紧凑的、连续存储的数据结构,对于长度较短的列表或有序集合具有较高的存储效率。
-
linkedlist(链表):
- 含义: linkedlist编码表示存储双向链表数据结构。常用于表示列表等数据类型。
- 特点: 链表结构灵活,支持快速的插入和删除操作,但访问时间复杂度较高。
-
intset(整数集合):
- 含义: intset编码表示存储整数集合数据结构。用于表示整数集合。
- 特点: 内部采用有序数组实现,对于较小的整数集合具有较高的存储效率和快速的成员检查操作。
- skiplist(跳跃表):
- 含义: skiplist编码表示存储跳跃表数据结构。通常用于实现有序集合等数据类型。
- 特点: 跳跃表是一种有序链表的扩展,支持快速的范围查询操作,同时也保持较高的插入和删除效率。
同时,Redis 3.2 版本引入了一种新的对列表(list)数据结构的实现方式,称为 QuickList(快速列表)。QuickList 代替了之前版本中对列表的两种实现方式:linkedlist(链表)和 ziplist(压缩列表)。
QuickList 结合了链表和压缩列表的优点,在一定程度上提高了列表的性能和效率。它将列表分割成多个节点(node),每个节点可以包含多个元素,并且使用压缩列表来存储节点中的元素。这种设计允许 QuickList 在处理大型列表时保持较低的内存消耗,同时在处理小型列表时保持较高的性能。
QuickList 的主要优点包括:
-
灵活性: QuickList 可以根据列表的大小动态地调整节点的大小,以最大限度地减少内存消耗。
-
性能: QuickList 在处理大型列表时具有较低的内存消耗和较高的性能,而在处理小型列表时也能保持较高的效率。
-
压缩列表优化: QuickList 使用压缩列表作为节点的存储方式,这种紧凑的数据结构可以有效地节省内存空间,并提高数据访问的速度。
-
节点分割: QuickList 将列表分割成多个节点,每个节点都是一个压缩列表,这样可以降低在执行插入和删除操作时的复杂度,并提高整体的性能。
QuickList 是 Redis 在列表实现方面的一次重大改进,它使得 Redis 能够更好地处理大型列表,并在内存消耗和性能之间取得平衡。
我们还要简单了解一下跳跃表。
跳跃表(Skip List)是一种基于有序链表的数据结构,通过添加多层索引来加速查找操作。它是一种随机化数据结构,可以在平均情况下实现较快的搜索、插入和删除操作。跳跃表在 Redis 中被广泛应用于实现有序集合(Sorted Set)和其他数据结构的底层实现。
结构特点:
-
多层索引: 跳跃表通过维护多层索引来加速查找操作。每一层索引都是原始链表的一个子集,最底层索引包含所有元素,而上层索引则包含部分元素,每个元素在不同层级的索引中出现的概率是随机的。
-
升维结构: 每个节点包含多个指针,指向同一层中的下一个节点,以及可能指向其他层级中的节点。这种升维结构使得跳跃表的查找操作可以跳过多个元素,从而实现快速查找。
-
平衡性: 跳跃表的高度是对数级别的,并且每一层的节点数量都尽量保持平衡,使得在平均情况下查找操作的时间复杂度为 O(logN),其中 N 为跳跃表中的元素数量。
操作:
-
查找: 跳跃表的查找操作类似于二分查找,从顶层索引开始,逐层向下查找,直到找到目标元素或者到达原始链表的底层。
-
插入: 插入操作首先需要执行查找操作,找到插入位置的前一个节点,然后在相应的层级上插入新节点,并更新相应的指针。
-
删除: 删除操作也需要执行查找操作,找到待删除节点的前一个节点,然后在相应的层级上删除该节点,并更新相应的指针。
优点:
-
快速查找: 跳跃表的平均查找时间复杂度为 O(logN),比较适用于有序集合等需要频繁查找的场景。
-
简单高效: 跳跃表的实现相对简单,插入和删除操作的时间复杂度也是 O(logN),在实际应用中表现良好。
-
支持范围查询: 跳跃表可以方便地支持范围查询操作,例如查找某个范围内的元素或者统计某个范围内的元素数量。
缺点:
-
空间复杂度高: 跳跃表的多层索引会占用较多的额外空间,对于存储空间较为敏感的场景可能不太合适。
-
不支持动态扩容: 跳跃表通常需要预先确定最大层数,因此不支持动态扩容,需要根据实际情况预先分配足够的空间。
综上,跳跃表是一种高效的数据结构,适用于需要快速查找和范围查询的场景,尤其适用于有序集合等需要频繁操作的数据结构。
在刚刚上面那张表里,你可以看到,在Redis中,每种数据结构都有至少两种以上的内部编码实现,例如列表(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"
在这个示例中,我们首先使用SET命令将字符串world存储在名为hello的键中,然后使用LPUSH命令将元素a、b和c依次推入名为mylist的列表中。接着,分别使用OBJECT ENCODING命令查询键hello和mylist对应值的内部编码。结果显示,hello键对应的值使用的是embstr内部编码,而mylist键对应的值使用的是quicklist内部编码。
通过查询内部编码,我们可以更好地了解Redis是如何存储和管理数据的,为性能优化和数据结构选择提供了参考。
Redis这样设计有两个明显的好处:
-
灵活改进内部编码:Redis的设计允许改进内部编码,而对外的数据结构和命令没有任何影响。这意味着一旦开发出更优秀的内部编码,就无需修改外部数据结构和命令。例如,Redis 3.2提供了quicklist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现。对于用户来说,这种改进是基本无感知的,他们可以继续使用相同的命令和数据结构,而无需了解内部编码的变化。
-
适应不同场景的需求:多种内部编码实现可以在不同的场景下发挥各自的优势。例如,ziplist相比较linkedlist来说,节省内存空间,但在列表元素较多的情况下,性能可能会下降。在这种情况下,Redis会根据配置选项将列表类型的内部实现转换为linkedlist,从而提高性能。这个过程对于用户来说同样是无感知的,他们可以在不知情的情况下享受到更好的性能。
综上所述,Redis的设计使得内部编码的改进和优化成为可能,同时保持了用户对外部数据结构和命令的稳定性和一致性。这种设计使得Redis能够灵活适应不同的场景需求,提供更好的性能和用户体验。
单线程架构
Redis采用单线程架构来实现高性能的内存数据库服务。一会儿我们首先会通过多个客户端命令调用的例子来说明Redis的单线程命令处理机制。然后,我们将分析为什么Redis的单线程模型能够实现如此高性能。最后,我们将说明为什么理解单线程模型对于使用和运维Redis至关重要。
引出单线程模型
Redis 在处理命令请求时通常采用单线程模型,即使用一个主线程来处理所有的命令请求。这使得 Redis 在处理命令时具有简单、高效的特性。然而,尽管 Redis 主要的命令处理是单线程的,但在底层实现中,确实会涉及到多线程的操作,尤其是在处理网络 I/O 时。
具体来说,虽然 Redis 主线程负责接收客户端的命令请求、解析命令、执行命令等操作,但在进行网络 I/O 时,会使用多个线程来处理。Redis 服务器会维护一个事件循环(Event Loop),通过事件驱动的方式来处理网络请求。事件驱动模型允许 Redis 在单线程的情况下同时处理多个客户端的网络请求。
在事件循环中,Redis 服务器会监听多个套接字(Socket),当有客户端连接或发送请求时,会触发相应的事件。这些事件可以是客户端连接事件、读取数据事件、写入数据事件等。为了处理这些事件,Redis 服务器会使用多个线程来进行网络 I/O 操作,例如读取客户端发送的数据、向客户端发送响应等。
因此,虽然 Redis 主要的命令处理是单线程的,但在处理网络 I/O 时确实会涉及到多线程的操作,这使得 Redis 能够更高效地处理并发的网络请求,提高了服务器的并发能力。
Redis 之所以能够有效地利用单线程模型进行工作,主要有以下几个原因:
-
短平快的命令处理: Redis 的核心业务逻辑是基于内存的快速数据存储和处理,大多数命令操作都非常简单且耗时短暂。例如,GET、SET、INCR 等命令通常只涉及简单的内存读写操作,不会消耗过多的 CPU 资源。
-
非阻塞 I/O 操作: Redis 使用了非阻塞的 I/O 模型,通过事件驱动的方式处理网络请求。这意味着当 Redis 在等待网络 I/O 时,主线程可以继续处理其他请求,而不会因为等待而阻塞,从而最大程度地利用了 CPU 资源。
-
单线程的简单性: 单线程模型相对于多线程或多进程模型来说,实现起来更加简单,减少了线程间的同步和通信的复杂性。这使得 Redis 的代码更易于维护和调试。
-
避免了多线程的竞态条件和锁等问题: 在多线程环境下,需要考虑线程安全性和并发控制,例如竞态条件、死锁、资源争用等问题,而单线程模型可以避免这些问题的出现。
现在我们开启了三个 redis-cli 客户端同时执行命令。
客户端 1 设置了一个字符串键值对:
127.0.0.1:6379> set hello world
客户端 2 对 counter 执行了自增操作:
127.0.0.1:6379> incr counter
客户端 3 也对 counter 执行了自增操作:
127.0.0.1:6379> incr counter
我们已经知道从客户端发送的命令经历了三个阶段:发送命令、执行命令、返回结果。在这些阶段中,我们重点关注第二步。
我们所谓的 Redis 采用单线程模型执行命令的意思是:尽管三个客户端看起来同时请求 Redis 执行命令,但实际上在微观层面,这些命令是以线性方式执行的。只是在原则上,命令执行的顺序是不确定的。但是一定不会有两条命令被同步执行。可以想象,Redis 内部就像是一个服务窗口,多个客户端按照它们到达的顺序排队在窗口前,依次接受 Redis 的服务。
因此,无论两条 incr 命令的执行顺序如何,结果都是 2,不会出现并发问题。这就是 Redis 的单线程执行模型。
总结一下就是,在Redis的单线程执行模型下,即使有多个客户端同时发送命令请求,这些命令仍然是按照它们达到的先后顺序被排队在服务窗口前依次接受Redis的服务。虽然在逻辑上看起来是同时要求Redis去执行命令,但在微观层面,这些命令仍然是采用线性方式去执行的。Redis内部只有一个服务窗口,多个客户端按照它们到达的先后顺序排队等待服务,依次接受Redis的处理。因此,虽然两条incr命令的执行顺序不确定,但它们的执行结果一定是2,不会发生并发问题。
这种单线程执行模型保证了命令的顺序性和一致性,避免了多线程并发执行时可能出现的竞争和同步问题,确保了系统的稳定性和可靠性。同时,单线程模型也简化了系统的设计和维护,减少了开发和运维的复杂性。
宏观上同时要求服务的客户端
微观上客户端发送命令的时间有先后次序的
Redis 的单线程模型
为什么单线程还能这么快?
通常来说,单线程处理能力通常要比多线程差。举个例子来说,假设有 10,000 公斤的货物需要运输,每辆车每次能够运载 200 公斤,那么需要进行 50 次运输才能完成任务。然而,如果有 50 辆车,只要合理安排,就可以依次完成任务。
为什么 Redis 使用单线程模型却能够达到每秒万级别的处理能力呢?这可以归结为以下几点原因:
a. 纯内存访问:Redis 将所有数据放置在内存中,而内存的响应时间约为 100 纳秒,这是 Redis 实现每秒万级别访问的重要基础。相较于磁盘存储,内存访问速度极快,使得 Redis 能够快速地读写数据,而无需考虑磁盘 I/O 的延迟。这种纯内存访问的优势,使得 Redis 能够在高效地处理大量请求的同时保持快速响应。
b. 非阻塞 I/O:Redis 使用 epoll 作为 I/O 多路复用技术的实现。通过将连接、读写、关闭等操作转换为事件,Redis 的事件处理模型能够有效地管理大量客户端连接,并避免在网络 I/O 上浪费过多时间。这种非阻塞 I/O 的机制使得 Redis 能够更好地利用系统资源,提高了系统的并发处理能力。通过精细的事件处理机制,Redis 能够高效地响应客户端请求,并实现高并发的数据处理。
c. 单线程避免了线程切换和竞态产生的消耗:Redis 的单线程模型避免了多线程中线程切换和竞态条件带来的性能损耗。在单线程模式下,Redis 能够简化数据结构和算法的实现,使得程序模型更加简单和高效。此外,单线程模型还避免了在多线程环境中因竞争同一份共享数据而带来的线程切换和等待的开销。因此,Redis 能够在高并发情况下保持出色的性能,并具备良好的可扩展性和稳定性。
d. 比数据库核心功能更简单:Redis专注于提供对数据的高效存储、读取和处理,其核心功能相对数据库来说更为简单。相比于传统数据库,Redis不需要支持复杂的查询语言、事务处理或者数据持久化机制,这些功能的缺失使得Redis的内部实现更加轻量级和高效。由于Redis不需要处理诸如SQL解析、查询优化等复杂逻辑,其单线程模型更容易管理和维护,减少了系统的复杂度和资源消耗。因此,Redis能够专注于提供高性能的内存数据存储和处理服务,使得其在每秒万级别的处理能力下依然保持简单而高效的特性。
关于epoll作为I/O多路复用技术这个说法,我们还需要进一步了解。
Linux上提供了三种IO多路复用的API,包括select、poll和epoll。这些API都是操作系统提供给程序员的机制,用于实现在一个线程内同时监听多个IO事件,以提高IO操作的效率和性能。
-
select:是最古老的IO多路复用机制之一,它允许程序员指定一组文件描述符,并通过调用select函数来阻塞等待其中任何一个文件描述符上的IO事件发生。select的效率不高,主要原因是它采用线性扫描的方式查找就绪文件描述符,导致随着文件描述符数量的增加,性能下降明显。
-
poll:是对select的改进,它使用了链表数据结构来管理文件描述符,相比于select,poll在性能上有所提升,但仍然存在性能瓶颈,特别是在处理大量文件描述符时,性能下降明显。
-
epoll:是Linux内核提供的高效IO多路复用机制,它是目前性能最优的一种IO复用方法。epoll采用了事件驱动的方式,能够以O(1)的时间复杂度来处理大量的文件描述符,极大地提高了IO操作的效率和性能。epoll使用了红黑树和双向链表等数据结构,以及边缘触发和水平触发两种工作模式,使得程序员能够更加灵活地处理IO事件。
这三种IO多路复用机制都是操作系统提供给程序员的API,通过调用这些API,程序员可以在一个线程内同时监听多个IO事件,并在IO事件发生时及时响应。而epoll由于其高效的实现方式和优秀的性能表现,在实际应用中被广泛使用。
在Linux系统中,epoll是一种高效的I/O多路复用机制,它通过操作系统内核提供的epoll API实现。
epoll的实现原理和具体流程:
-
事件注册:
- 程序通过调用epoll_create函数创建一个epoll句柄,用于管理事件。
- 使用epoll_ctl函数将需要监听的文件描述符注册到epoll句柄上,并指定所关注的事件类型,例如可读、可写等。
-
事件等待:
- 调用epoll_wait函数等待事件的发生。epoll_wait会阻塞当前线程,直到有注册的文件描述符上的事件发生或者超时。
- 当有事件发生时,epoll_wait会返回已经就绪的文件描述符列表。
-
事件处理:
- 程序通过遍历epoll_wait返回的就绪文件描述符列表,获取每个文件描述符上发生的事件类型。
- 根据事件类型执行相应的操作,例如读取数据、写入数据或者关闭连接等。
-
事件移除:
对于一次性事件(例如EPOLLONESHOT),处理完事件后需要重新注册该文件描述符,以便下次继续监听。 -
工作原理:
- epoll的高效性主要体现在其数据结构和事件通知机制上。
- epoll内部维护了一个事件表(event table),通过红黑树和双向链表等数据结构来存储注册的文件描述符和事件。
- 当调用epoll_ctl注册文件描述符时,内核会根据文件描述符的事件类型和状态将其加入到事件表中。
- 调用epoll_wait等待事件时,内核会遍历事件表,检查每个文件描述符的状态,并将就绪的文件描述符添加到就绪队列中。
- 在多核CPU下,epoll利用了多线程和多核心的优势,能够并发地处理事件,提高了系统的响应速度和处理能力。
epoll通过高效的数据结构和事件通知机制,能够快速地监听大量的文件描述符,并在事件发生时及时通知应用程序,从而实现高性能的I/O多路复用。
在Redis中,使用epoll作为I/O多路复用技术的实现,是为了高效地管理和处理大量的客户端连接。
-
前因:
- Redis是一个服务器端的应用程序,需要处理来自多个客户端的连接请求和数据传输。
- 传统的I/O模型中,每个连接通常由一个线程来处理,当连接数量很大时,会导致大量线程的创建和上下文切换,降低了系统的性能和效率。
- 为了提高系统的并发处理能力和资源利用率,需要采用一种高效的I/O模型来管理和处理多个客户端连接。
-
epoll的特性:
- epoll是Linux内核提供的一种高效的I/O多路复用技术,适用于管理大量的文件描述符(包括套接字、管道等)。
- 与传统的select和poll相比,epoll具有更高的性能和可扩展性,能够处理成千上万个文件描述符,而不会随着文件描述符数量的增加而性能下降。
- epoll通过将文件描述符的状态变化转换为事件,将事件通知给应用程序,从而使得应用程序可以有效地处理多个连接的I/O操作,而无需阻塞等待或者轮询查询。
-
Redis使用epoll的后果:
- 通过使用epoll,Redis能够在单个线程中高效地管理大量客户端连接,而无需创建多个线程。
- epoll将连接、读写、关闭等操作转换为事件,并通过事件通知的方式告知Redis需要进行的操作,使得Redis的事件处理模型更加高效和灵活。
- 这种非阻塞的I/O机制使得Redis能够更好地利用系统资源,提高了系统的并发处理能力和性能表现。
- Redis的单线程模型与epoll的结合,使得Redis能够在高并发情况下保持出色的性能,并具备良好的可扩展性和稳定性。
总的来说,Redis使用epoll作为I/O多路复用技术的实现,能够更高效地管理和处理大量的客户端连接,提高了系统的并发处理能力和性能表现,同时降低了系统的复杂度和资源消耗。
虽然单线程模型给Redis带来了许多好处,但也存在一个致命的问题:对于单个命令的执行时间是有限制的。如果某个命令的执行时间过长,会导致其他命令都处于等待队列中,无法得到及时响应,从而造成客户端的阻塞。对于Redis这样高性能的服务来说,这种情况是非常严重的,因为它会降低系统的响应速度和并发能力。
因此,Redis更适合于处理快速执行的场景,即执行时间较短的命令。例如,对于读取和写入内存中的数据、执行简单的计算或者查询等操作,Redis表现出色。而对于需要长时间计算或者复杂逻辑的操作,可能不适合在Redis中执行,因为这会影响到其他客户端的请求响应速度,降低了系统的吞吐量和性能表现。
因此,在使用Redis时,我们需要合理设计命令的执行逻辑,避免长时间阻塞操作,保证系统的稳定性和性能。同时,可以通过分布式部署、优化命令设计、使用合适的数据结构等方式来减轻单线程模型带来的局限性,提高系统的并发能力和响应速度。
String 字符串
字符串类型在Redis中是最基础的数据类型,具有以下几点需要特别注意:
-
所有键的类型都是字符串类型:在Redis中,所有的键(Key)都是字符串类型,这意味着无论是用于存储简单的键值对还是复杂的数据结构,键的类型都是字符串。其他几种数据结构,如列表(List)和集合(Set)的元素类型也都是字符串类型。因此,对字符串类型的理解是掌握其他数据结构的基础。
-
值的多样性:字符串类型的值并不限于传统意义上的字符串,它可以包含不同类型的数据,例如:
- 字符串:可以是普通的文本字符串,如"user:123";
- 数字:可以是整数或者浮点数,例如"age:30";
- 二进制流数据:可以是图像、音频、视频等二进制数据,这使得Redis在一定程度上可以充当缓存服务器,存储一些非文本类型的数据。
- 特殊格式的字符串:例如JSON、XML等格式的字符串,Redis并不对其进行解析或验证,而是将其视为普通的字符串进行存储。
-
值的大小限制:Redis内部存储字符串完全是按照二进制流的形式保存的,二进制数据是指由0和1组成的数据序列,是计算机中最基本的数据表示方式。在计算机中,所有的数据最终都会被转换成二进制形式进行存储和处理。二进制数据可以表示各种类型的信息,包括数字、文本、图像、音频等。尽管字符串类型的值可以是各种形式的数据,但是Redis对单个字符串的大小有限制,其最大值不能超过512MB。这是为了保证Redis的性能和稳定性,防止存储过大的字符串导致内存溢出或性能下降。所以其实Redis主要用于存储文本数据,对体积较大的音频视频的存储频率不高。
此外,需要特别注意的是,由于Redis内部存储字符串完全是按照二进制流的形式保存的,因此Redis不处理字符集编码问题,也就很少出现乱码问题。这意味着Redis存储的字符串数据是原样存储的,不会对字符集编码进行转换或处理。因此,客户端传入的命令中使用的是什么字符集编码,Redis就会存储什么字符集编码的数据。这一点在处理多语言环境或特定字符集编码要求的场景下需要特别注意。
常见命令
SET
SET命令是用于将string类型的value设置到指定的key中。如果key之前已经存在,则新的value会覆盖旧的value,无论原来的数据类型是什么,同时之前设置的过期时间(TTL)也会失效。
语法:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
命令有效版本:1.0.0之后
时间复杂度:O(1)
选项:
- EX seconds:使用秒作为单位设置key的过期时间。
- PX milliseconds:使用毫秒作为单位设置key的过期时间。
- NX:只在key不存在时才进行设置,即如果key之前已经存在,设置不执行,返回 nil 。
- XX:只在key存在时才进行设置,相当于更新key的value。如果key之前不存在,设置不执行,返回 nil 。
-
|:表示命令选项之间的分隔符。通常用于指定命令的不同选项,如 SET 命令中设置过期时间时使用的 EX 和 PX 选项就是通过 | 分隔的。例如,SET key value EX 10 表示设置键 key 的过期时间为 10 秒。
[ ]:表示可选项的起始和结束。在 Redis 命令中,方括号用于标识一些可选项,这些选项在命令中可以存在,也可以省略。例如,SET 命令中的 NX 和 XX 就是可选项,它们分别表示只在键不存在时执行设置操作或者只在键已经存在时执行设置操作。例如,SET key value NX 表示只有当键 key 不存在时才执行 SET 操作。
然而,如果只需要设置键值对而不需要额外的选项,或者只需要设置过期时间而不需要条件选项,可以使用其他专门的命令代替,这样可以使得命令更加简洁和直观。
具体的替代命令如下:
SETNX:用于设置键值对,但是只有在键不存在时才会设置成功,如果键已经存在,则不进行任何操作。这样可以保证只有在键不存在时才进行设置操作,避免了使用 SET 命令时需要添加 NX 选项的繁琐。示例:SETNX key value。
SETEX:用于设置键值对并同时设置过期时间,指定的过期时间是以秒为单位的。这样可以一次性完成设置键值对和设置过期时间的操作,避免了使用 SET 命令时需要添加 EX 选项的繁琐。示例:SETEX key seconds value。
PSETEX:与 SETEX 类似,但是指定的过期时间是以毫秒为单位的。这样可以更精确地控制过期时间,适用于需要高精度过期时间的场景。示例:PSETEX key milliseconds value。
注意:由于带选项的SET命令可以被SETNX、SETEX、PSETEX等命令代替,所以在之后的版本中,Redis可能会进行合并。
返回值:
- 如果设置成功,返回"OK"。
- 如果由于SET指定了NX或者XX但条件不满足,SET不会执行,并返回(nil)。
示例:
SET mykey "Hello"
以上示例将字符串"Hello"设置为键名为"mykey"的值。
SET mykey "Hello" EX 3600 NX
以上示例设置了键名为"mykey"的值为"Hello",并且设置了过期时间为3600秒,仅当该键名之前不存在时才会执行设置操作。
GET
GET命令用于获取指定key对应的value。如果key不存在,则返回nil。如果value的数据类型不是string,会报错。
语法:
GET key
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回key对应的value,如果key不存在则返回nil。
示例:
GET mykey
以上示例将返回键名为"mykey"的值。
MGET
MGET 命令用于一次性获取多个键的值。如果指定的键不存在,或者对应的数据类型不是字符串类型,那么返回的结果集中对应位置的值将为 nil。
语法:
MGET key [key ...]
命令有效版本:1.0.0之后
时间复杂度:O(N),其中N是要获取的key的数量。
返回值:返回一个列表,列表中包含了对应key的值。如果某个key不存在或者对应的数据类型不是string,则在对应位置返回nil。
示例:
MGET key1 key2 key3
以上示例将返回key1、key2和key3对应的值,如果某个key不存在或者对应的数据类型不是string,则在对应位置返回nil。
MSET
MSET命令用于一次性设置多个key的值。
语法:
MSET key value [key value ...]
命令有效版本:1.0.1之后
时间复杂度:O(N),其中N是要设置的key的数量。
返回值:永远是"OK"。
示例:
MSET key1 value1 key2 value2 key3 value3
以上示例将同时设置key1、key2和key3的值为value1、value2和value3。
多次 get vs 单次 mget:
使用MGET和MSET命令能够有效地减少网络通信时间,因此在性能上相较于单个GET操作或SET操作更为优越。举例来说,假设网络通信耗时为1毫秒,而命令执行时间耗时为0.1毫秒,则在以下表格中展示了不同操作的执行时间:
1000 次 get 和 1 次 mget 对比:
学会使用批量操作能够有效提高业务处理效率。然而,需要注意的是,我们每次批量操作所发送的键的数量也不是无限制的。如果一次批量操作发送的键数量过多,可能会导致单个命令执行时间过长,从而导致Redis阻塞。因此,在使用批量操作时,需要根据实际情况合理设置批量操作的范围,以充分发挥其优势,同时避免潜在的性能问题。
SETNX
SETNX命令用于设置key-value,但只允许在key之前不存在的情况下设置。
语法:
SETNX key value
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:如果设置成功,则返回1,表示设置成功;如果key已经存在,设置不会执行,返回0表示没有设置。
示例:
SETNX mykey "Hello"
以上示例尝试将键名为"mykey"的值设置为"Hello",如果之前"mykey"不存在,则设置成功并返回1,否则设置不执行并返回0。
SET、SET NX、SET XX 执行流程:
计数命令
INCR
INCR命令用于将key对应的字符串表示的数字加一。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。
语法:
INCR key
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回integer类型的加一后的数值。
示例:
INCR mykey
以上示例会将键名为"mykey"对应的值加一。如果"mykey"不存在,则将其视为0处理,执行INCR后返回的值为1。
另外,需要再次强调,由于 Redis 使用单线程模型处理命令,多个客户端同时对同一个 key 进行 INCR 操作不会引发线程安全问题。在 Redis 中,所有命令都是按顺序执行的,即使有多个客户端同时对同一个 key 发起 INCR 命令,Redis 也会依次处理这些命令,不会发生并发访问的情况。
Redis 的单线程模型保证了命令的原子性,即每个命令都是原子操作,不会被中断或交错执行。因此,多个客户端对同一个 key 进行 INCR 操作时,Redis 会按照顺序逐个执行这些命令,每次执行都是原子的,不会出现并发冲突导致的线程安全问题。这种单线程模型的优势在于简化了并发控制的复杂性,避免了锁机制的引入,提高了系统的性能和可靠性。
INCRBY
INCRBY命令用于将key对应的字符串表示的数字加上对应的值。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。
语法:
INCRBY key decrement
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回integer类型的加上对应值后的数值。
示例:
INCRBY mykey 5
以上示例会将键名为"mykey"对应的值加上5。如果"mykey"不存在,则将其视为0处理,执行INCRBY后返回的值为5。
也可以将数字设置为负数:
DECR
DECR命令用于将key对应的字符串表示的数字减一。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。
语法:
DECR key
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回integer类型的减一后的数值。
示例:
DECR mykey
以上示例会将键名为"mykey"对应的值减一。如果"mykey"不存在,则将其视为0处理,执行DECR后返回的值为-1。
DECRBY
DECRBY命令用于将key对应的字符串表示的数字减去对应的值。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。
语法:
DECRBY key decrement
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回integer类型的减去对应值后的数值。
示例:
DECRBY mykey 5
以上示例会将键名为"mykey"对应的值减去5。如果"mykey"不存在,则将其视为0处理,执行DECRBY后返回的值为-5。
INCRBYFLOAT
INCRBYFLOAT命令用于将key对应的字符串表示的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果key不存在,则将其视为0进行操作。如果key对应的不是字符串,或者不是一个浮点数,则会报错。允许采用科学计数法表示浮点数。
语法:
INCRBYFLOAT key increment
命令有效版本:2.6.0之后
时间复杂度:O(1)
返回值:加/减完后的数值。
示例:
INCRBYFLOAT mykey 3.5
以上示例会将键名为"mykey"对应的浮点数值增加3.5。如果"mykey"不存在,则将其视为0处理,执行INCRBYFLOAT后返回的值为3.5。
在许多存储系统和编程语言中,实现计数功能通常涉及使用CAS(Compare-and-Swap)机制,这可能会带来一定的CPU开销。然而,在Redis中不存在这个问题,因为Redis采用单线程架构。这意味着无论何时,任何命令到达Redis服务器,都会被顺序执行。单线程架构确保了对共享资源的串行访问,从而避免了由于并发访问而产生的竞态条件。因此,Redis能够提供高效的计数功能,而无需考虑CAS带来的CPU开销。
但是没有相对应的所谓“decrbyfloat”,只能使用incrbyfloat加上负数。
其他命令
APPEND
APPEND命令用于将值追加到已有键的字符串之后。如果键已存在且其对应的值是字符串类型,则该值将会被追加。如果键不存在,则效果与执行SET命令相同。
语法:
APPEND KEY VALUE
命令有效版本: 此命令在Redis版本2.0.0及以后的版本中有效。
时间复杂度: APPEND命令的时间复杂度为O(1)。由于通常追加的字符串较短,因此可以将其视为常数时间复杂度。
返回值: 返回追加完成后字符串的长度。
示例:
APPEND mykey "world"
这将会将字符串"world"追加到键"mykey"对应的字符串值之后,并返回追加完成后字符串的长度。
在 Redis 中,append 命令用于向已有字符串值的末尾追加指定的值,并返回追加后的字符串长度(单位为字节)。Redis 对字符串的处理是基于字节而非字符的,因此它并不会对字符进行任何特殊处理,只会简单地处理字节序列。
在使用 xshell 终端时,默认的字符编码是 UTF-8。UTF-8 是一种针对 Unicode 的可变长度字符编码,对于大部分常用字符而言,它通常使用一个字节来表示 ASCII 字符,而对于其他字符,如汉字,通常使用多个字节来表示。
一个汉字在 UTF-8 编码中通常占据 3 个字节。因此,当我们在 xshell 终端中输入汉字时,它会被按照 UTF-8 编码转换成对应的字节序列,然后发送给 Redis。
也就是说,对于 Redis 来说,它只关心字符串值所包含的字节序列,不会对字符进行解析或者考虑字符编码。因此,无论我们在 xshell 终端中输入的是什么字符,Redis 都会将其视作一系列字节序列进行处理。
当使用 Redis 的 GET 命令获取键值对应的字符串时,如果该字符串中包含多字节字符,Redis 会直接返回其字节表示形式,而不会对字符进行解析或考虑字符编码。这意味着,如果在 Redis 中存储的字符串是以 UTF-8 编码的,那么 GET 命令返回的将是该字符串的 UTF-8 字节序列,而不是字符本身。
举例来说,我们在 Redis 中存储了一个字符串“你好”,这个字符串在 UTF-8 编码下占据了6个字节(3个汉字 * 3个字节/汉字)。当我们使用 GET 命令获取这个键对应的值时,Redis 会以字节为单位返回这个字符串,即返回的内容将是“\xE4\xBD\xA0\xE5\xA5\xBD”的字节序列,而不是字符串“你好”。
因此,需要注意的是,Redis 对于存储的字符串并不关心字符的含义或编码方式,它只是简单地将字符串存储为一系列字节,并以字节形式返回。因此,当从 Redis 中检索字符串时,需要根据应用程序的需要,自行解析这些字节以得到正确的字符表示。
如果希望 Redis 客户端能够以原始形式显示字符串,包括汉字在内的任何字符,可以在启动客户端时使用 --raw 选项。这个选项告诉 Redis 客户端以原始形式处理字符串,而不进行任何解析或转换。
通过添加 --raw 选项,Redis 客户端将会以原始的二进制形式显示字符串,包括汉字在内的所有字符,而不会将其视作特定字符编码的文本。这样做可以确保 Redis 客户端直接显示存储在 Redis 中的字节序列,而无需进行字符编码转换或解析。
我们现在先按下Ctrl+D退出客户端,注意不要按到Ctrl+S,这会冻结当前画面,可以用Ctrl+Q解冻。
GETRANGE
GETRANGE命令用于返回与指定键相关联的字符串的子串。子串的起始位置和结束位置由参数start和end确定,包括start和end指定的字符(左闭右闭)。如果需要,可以使用负数来表示相对于字符串末尾的偏移量。例如,-1表示倒数第一个字符,-2表示倒数第二个字符,以此类推。如果指定的偏移量超出了字符串的范围,系统将根据字符串的长度自动调整偏移量,确保它们落在正确的范围内。
也就是说,Redis 支持负数索引。在 Redis 中,当使用负数索引时,它们表示从字符串的末尾开始的位置。这种负数索引的支持使得在处理字符串时更加方便,特别是在需要访问字符串末尾的情况下。因此,可以通过正数索引和负数索引来访问字符串中的字符,使得操作更加灵活。
语法:
GETRANGE key start end
命令有效版本:该命令在Redis版本2.4.0及以后的版本中有效。
时间复杂度:
GETRANGE命令的时间复杂度为O(N),其中N为子串[start, end]区间的长度。然而,由于通常处理的字符串较短,因此可以将时间复杂度视为O(1)。
返回值:返回一个字符串,即与指定键关联的子串。
示例:
GETRANGE mykey 0 3
这将返回键"mykey"关联的字符串的从第一个字符到第四个字符(包括第四个字符)的子串。
如果字符串中保持的是汉字,此时进行字串切分,很可能不是完整的汉字。
比如我们强行切出中间四个字节:
上述问题,在C++中同样存在。因为C++中字符串的基本单位是字节。
但是在Java中就没事,因为Java中字符串的基本单位是字符(2个字节),相当于String已经帮我们把汉字的编码转化处理好了。
SETRANGE
SETRANGE命令用于覆盖字符串中的一部分,从指定的偏移位置开始。SETANGE 命令会将指定偏移量开始的连续字节替换为给定的值。如果替换的内容超出了原始字符串的范围,Redis 会自动扩展字符串长度并用空字节填充。如果键不存在,则会创建一个新的字符串并执行替换操作。
需要注意的是,替换的字节数取决于要替换的值的字节数。因此,如果替换值的字节数与原始值的字节数不同,字符串的长度将相应地增加或减少。
语法:
SETRANGE key offset value
命令有效版本: 此命令在Redis版本2.2.0及以后的版本中有效。
时间复杂度: SETRANGE命令的时间复杂度为O(N),其中N为value的长度。由于通常提供的value比较短,因此通常将其视为O(1)。
返回值: 返回替换后字符串的长度。
示例:
SETRANGE mykey 6 "Redis"
这将从键"mykey"对应的字符串中的第7个字符(偏移量为6)开始,将其后的部分替换为"Redis",并返回替换后字符串的长度。
注意,在使用 SETRANGE 命令时,如果指定的偏移量超出了现有字符串的范围(即该偏移量之前的内容不存在),Redis 会自动扩展字符串长度,并将扩展部分填充为零字节(\x00),然后再执行替换操作。
因此,上面我们指定偏移量为1,但是键对应的字符串不存在,Redis 会在偏移量1之前填充一个零字节,然后将新值追加到这个零字节后面。这样,即使原始字符串不存在,也可以通过 SETRANGE 命令来创建新的字符串并执行替换操作。
STRLEN
STRLEN 命令会返回指定键对应的字符串值的长度,如果该键不存在或者键对应的值不是字符串类型,则返回 0。需要注意的是,这里返回的长度是以字节为单位的,即字符串中包含的字节数。
语法:
STRLEN key
命令有效版本: 此命令在Redis版本2.2.0及以后的版本中有效。
时间复杂度: STRLEN命令的时间复杂度为O(1)。
返回值: 返回字符串的长度。如果键不存在或者键对应的值不是字符串类型,则返回0。
示例:
STRLEN mykey
这将返回键"mykey"对应的字符串的长度。
字符串类型命令
命令 | 执行效果 | 时间复杂度 |
---|---|---|
set key value [key value...] | 设置 key 的值为 value | O(k),其中 k 是键个数 |
get key | 获取 key 的值 | O(1) |
del key [key ...] | 删除指定的 key | O(k),其中 k 是键个数 |
mset key value [key value ...] | 批量设置指定的 key 和 value | O(k),其中 k 是键个数 |
mget key [key ...] | 批量获取 key 的值 | O(k),其中 k 是键个数 |
incr key | 指定的 key 的值加1 | O(1) |
decr key | 指定的 key 的值减1 | O(1) |
incrby key n | 指定的 key 的值加 n | O(1) |
decrby key n | 指定的 key 的值减 n | O(1) |
incrbyfloat key n | 指定的 key 的值加 n (浮点数) | O(1) |
append key value | 指定的 key 的值追加 value | O(1) |
strlen key | 获取指定 key 的值的长度 | O(1) |
setrange key offset value | 覆盖指定 key 从 offset 开始的部分值 | O(n),其中 n 是字符串长度,通常视为 O(1) |
getrange key start end | 获取指定 key 从 start 到 end 的部分值 | O(n),其中 n 是字符串长度,通常视为 O(1) |
内部编码
Redis中的字符串类型有三种内部编码方式:
-
int:这种编码方式用于存储长整型数据。在内存中,长整型数据占用8个字节的空间。当存储的值可以表示为长整型时,Redis会使用这种编码方式。
-
embstr:这是一种优化的编码方式,用于存储长度小于等于39个字节的字符串。在内存中,该编码方式只会消耗刚好所需的内存空间,避免了额外的内存开销。当存储的字符串较短时,Redis会选择这种编码方式。
-
raw:当存储的字符串长度超过39个字节时,Redis会使用这种编码方式。它可以处理长度大于39个字节的字符串,但会占用更多的内存空间。
Redis会根据当前值的类型和长度动态决定使用哪种内部编码实现,以便在性能和内存消耗之间达到平衡。
整型类型的示例如下:
127.0.0.1:6379> set key 6379
OK
127.0.0.1:6379> object encoding key
"int"
短字符串类型的示例如下:
# 小于等于 39 个字节的字符串
127.0.0.1:6379> set key "hello"
OK
127.0.0.1:6379> object encoding key
"embstr"
长字符串类型的示例如下:
# 大于 39 个字节的字符串
127.0.0.1:6379> set key "one string greater than 39 bytes ........"
OK
127.0.0.1:6379> object encoding key
"raw"
在 Redis 中,整数是使用 int 类型来存储的,但是小数则是以字符串的形式存储的。这意味着当进行算术运算时,对于小数类型的数据,Redis 需要将其先转换为浮点数(double),进行运算后再将结果转换回字符串形式进行存储。
这种处理方式在进行算术运算时会带来一些额外的开销,因为涉及到了字符串到浮点数的转换和运算结果的再次转换。相比整数类型,这会导致小数类型的运算性能稍低。因此,在设计 Redis 数据结构时,如果需要进行频繁的算术运算,并且需要保持高性能,我们还是建议尽量使用整数类型来存储数据,避免使用字符串来表示小数。
典型使用场景
缓存(Cache)功能
图 2-10 展示了一个典型的缓存使用场景,其中 Redis 充当缓存层,而 MySQL 则是存储层。在这种架构中,大部分请求的数据都是从 Redis 中获取的。Redis具有支持高并发的特性,因此缓存通常能够加速读写操作,并降低后端服务器的压力。
在这个架构中,Redis作为缓存存储系统,主要提供以下功能:
-
快速数据访问:Redis将热门数据缓存在内存中,因此能够以极快的速度响应客户端的读取请求,大大提高了数据的访问速度。
-
减轻后端压力:通过缓存数据,Redis可以减少对后端存储系统(如MySQL)的请求量,从而减轻了后端服务器的压力,提高了整个系统的性能和吞吐量。
-
提高系统可扩展性:使用Redis作为缓存层可以提高系统的可扩展性。由于Redis能够有效地处理高并发请求,可以轻松地通过增加Redis节点来扩展系统的容量和性能,而不会对系统的稳定性产生负面影响。
综上所述,Redis作为缓存层在分布式系统中扮演着至关重要的角色,能够显著提升系统的性能、可扩展性和稳定性。
Redis + MySQL 组成的缓存存储架构:
// 根据用户 uid 获取用户信息
UserInfo getUserInfo(long uid) {
// 构造 Redis 的键
String key = "user:info:" + uid;
// 尝试从 Redis 中获取对应的值
String value = Redis 执行命令:get key;
// 如果缓存命中
if (value != null) {
// 假设用户信息按照 JSON 格式存储
UserInfo userInfo = JSON 反序列化(value);
return userInfo;
}
// 如果缓存未命中
// 从数据库中,根据 uid 获取用户信息
UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>;
// 如果数据库中不存在对应 uid 的用户信息
if (userInfo == null) {
响应 404;
return null;
}
// 将用户信息序列化成 JSON 格式
String jsonValue = JSON 序列化(userInfo);
// 写入缓存,设置过期时间为 1 小时
Redis 执行命令:set key jsonValue ex 3600;
// 返回用户信息
return userInfo;
}
以上伪代码模拟了一个基于 Redis 缓存的业务数据访问过程:
- 首先定义了一个函数 getUserInfo,用于根据用户的 uid 获取用户信息。
- 在从 Redis 中获取用户信息的部分,先构造了 Redis 的键,假设用户信息保存在 "user:info:<uid>" 对应的键中。然后尝试从 Redis 中获取对应的值,如果命中缓存(value 不为 null),则将获取的值进行 JSON 反序列化,并返回用户信息。
- 如果 Redis 中未命中缓存,则继续从 MySQL 中获取用户信息。在此步骤中,首先执行 SQL 查询语句从数据库中获取用户信息。如果数据库中不存在对应 uid 的用户信息,则返回 404。如果获取到了用户信息,则将其序列化为 JSON 格式,并写入 Redis 缓存中,并设置过期时间为 1 小时。最后返回获取到的用户信息。
通过增加缓存功能,可以极大地提升查询效率,因为在一小时内,对于相同的 uid 只会有一次 MySQL 查询,而其他请求都可以从 Redis 缓存中获取数据,从而降低了对 MySQL 数据库的访问次数,减轻了数据库的压力。
与关系型数据库如 MySQL 不同,Redis 没有表、字段这种命名空间的概念,并且对于键名也没有强制的规定,除了不能使用一些特殊字符。然而,设计合理的键名对于防止键冲突以及提高项目的可维护性至关重要。比较推荐的做法是使用 "业务名:对象名:唯一标识:属性" 的格式作为键名。
举例来说,假设 MySQL 中的数据库名为 vs,用户表名为 user_info,那么对应的键名可以采用 "vs:user_info:6379"、"vs:user_info:6379:name" 等形式来表示。如果当前 Redis 实例只会被一个业务使用,那么可以省略业务名部分,简化为 ":user_info:6379"、":user_info:6379:name" 等。
在键名过长或者需要简化的情况下,可以使用团队内部认同的缩写来替代完整的键名。例如,将 "user:6379:friends:messages:5217" 简化为 "u:6379:fr:m:5217"。因为过长的键名会导致 Redis 的性能下降,因此简化键名对于提高 Redis 的性能和可维护性都是有益的。
另外,评判数据热点程度的标准因业务场景而异,但通常可以基于以下几个方面来评估:
-
访问频率:数据的访问频率是评判数据热点的一个重要指标。如果某个数据被频繁地读取或写入,那么它很可能是热点数据。可以通过监控系统的请求量或者访问日志来统计数据的访问频率。
-
访问比例:除了访问频率外,数据在整体数据集中所占比例也是一个重要考量因素。如果某个数据占据了整体数据集的相对较大比例,那么它可能是热点数据。可以通过统计数据的访问量或者数据的大小来评估数据的访问比例。
-
数据重要性:数据的重要性是评判数据热点的另一个重要指标。某些数据可能对业务的核心功能或者用户体验有重要影响,因此被认为是热点数据。可以通过业务需求和业务价值来评估数据的重要性。
-
数据流量:数据的流量是评判数据热点的另一个关键因素。某些数据可能会导致大量的数据流量,例如广告点击数据、搜索数据等。可以通过监控数据流量和网络流量来评估数据的热点程度。
-
数据的时效性:某些数据可能具有较高的时效性,需要在短时间内被快速处理。这样的数据也可能被认为是热点数据。可以通过评估数据的时效性需求来判断数据的热点程度
上述策略存在一个明显的问题:
随着时间的推移,随着业务数据的增长,确实会有越来越多的键在 Redis 中访问不到,从而导致需要从 MySQL 数据库中读取并写入 Redis。如果没有合适的措施,这样会导致 Redis 中的数据越来越多,最终可能导致内存耗尽的问题。
解决这个问题的常见方法是为每个键设置一个合适的过期时间。通过设置适当的过期时间,可以确保 Redis 中的数据不会无限增长,而是在一定时间后自动过期并被淘汰,从而释放内存空间。这样可以有效地控制 Redis 中数据的大小,防止内存溢出问题的发生。
此外,当 Redis 内存不足时,Redis 提供了多种淘汰策略来释放内存空间,例如LRU(最近最少使用)、LFU(最少使用频率)等。这些淘汰策略可以根据不同的业务需求和性能要求进行配置,以确保 Redis 总是能够保持在可接受的内存使用范围内,并尽可能地保留重要的数据。
计数(Counter)功能
许多应用都会将 Redis 作为计数的基础工具,利用其快速计数和缓存查询的功能。同时,Redis 也支持数据的异步处理或落地到其他数据源。例如,在视频网站中,可以使用 Redis 来实现视频播放次数的计数功能:
每当用户播放一个视频时,相应视频的播放次数会在 Redis 中自增 1。这样,通过 Redis 的快速计数功能,可以高效地记录和统计视频的播放次数。同时,由于 Redis 的缓存特性,可以在需要时快速查询和展示视频的播放次数,提升了网站的性能和用户体验。
除了视频播放次数,Redis 还可以用于计数许多其他类型的数据,如文章的阅读次数、商品的点击次数等。这些计数功能可以帮助网站实时监控和分析用户行为,为业务决策提供有价值的数据支持。
记录视频播放次数:
// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
// 构造键名
String key = "video:" + vid;
// 执行增加操作,并获取计数器的当前值
long count = Redis 执行命令:incr key;
// 返回计数器的当前值
return count;
}
这段代码用于统计某视频的播放次数。首先构造了视频对应的键名,然后通过 Redis 的 incr 命令对键对应的值进行增加操作,并获取增加后的计数器当前值。最后返回该值,即为视频的播放次数。
❗实际中要开发一个成熟、稳定的真实计数系统,要⾯临的挑战远不止如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。
对其中几个主要挑战进行简要讨论:
-
防作弊:在计数系统中,防止作弊是一个重要的考量因素。可以采取多种手段来防止作弊,例如限制用户操作频率、使用验证码或者令牌验证等方式来确保每次计数都是合法有效的。此外,也可以通过监控和分析异常行为来及时发现和应对作弊行为。
-
按照不同维度计数:有时候需要根据不同的维度来进行计数,例如按照时间、地域、用户等进行计数。为了实现这样的需求,可以使用 Redis 的 Sorted Set 或 Hash 等数据结构来存储和统计不同维度的计数数据,并设计合适的数据模型和查询方式来支持按照不同维度进行计数。
-
避免单点问题:为了保证计数系统的稳定性和可靠性,需要避免单点故障。可以通过使用 Redis 的主从复制或者集群模式来实现数据的备份和故障转移,以及通过负载均衡等方式来分散请求,避免单点问题对系统的影响。
-
数据持久化到底层数据源:为了保证数据的持久性和可靠性,计数系统通常需要将数据持久化到底层数据源,如数据库或者日志文件中。可以使用 Redis 的持久化机制(如RDB和AOF)将数据定期或实时地持久化到磁盘中,并根据业务需求设计合适的数据同步策略和数据备份方案,以确保数据的安全性和可靠性。
综上所述,开发成熟、稳定的计数系统需要综合考虑防作弊、按照不同维度计数、避免单点问题、数据持久化等多方面的挑战,并采取相应的技术和措施来应对这些挑战,从而保证系统的性能、可靠性和安全性。
共享会话(Session)
在分布式 Web 服务中,通常会将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中。然而,由于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上。通常情况下,无法保证用户每次请求都会被均衡到同一台服务器上。这样的设计会带来一个严重的问题:当用户刷新页面时,可能会发现需要重新登录,这种体验是用户无法容忍的。
这个问题的产生是因为用户的 Session 信息被保存在单个服务器上,并且负载均衡机制导致用户的请求可能会被分发到不同的服务器上。因此,当用户在一个服务器上登录后,刷新页面时可能会被重新定向到另一个服务器,导致用户的 Session 信息无法被正确地保持和共享。
为了解决这个问题,可以采取以下一些措施:
-
Session 共享:将用户的 Session 信息存储在一个可供所有服务器访问的共享存储中,例如 Redis 或数据库。这样无论用户的请求被分发到哪个服务器,都可以保证能够访问到相同的 Session 信息。
-
Sticky Session:在负载均衡器中配置 Sticky Session(也称为持久性会话或粘性会话),确保用户的请求在一段时间内始终被分发到同一台服务器上。这样可以保证用户在会话期间的一致性体验,但可能会导致负载不均衡的问题。
-
JWT Token:使用基于 JSON Web Token(JWT)的身份验证和授权机制,将用户的身份信息以加密的方式存储在 Token 中,并在每次请求中发送给服务器。这样服务器无需保存用户的 Session 信息,也不受服务器间 Session 共享的影响。
所以,为了解决分布式 Web 服务中用户 Session 不一致的问题,需要采取适当的措施来确保用户在不同服务器间的一致性体验。这样可以提高用户的满意度和系统的稳定性。
我们主要谈谈第一种解决办法。
Session 分散存储:
为了解决这个问题,可以利用 Redis 将用户的 Session 信息进行集中管理。如图 2-13 所示,在这种模式下,只要保证 Redis 是高可用和可扩展的,无论用户被负载均衡到哪台 Web 服务器上,都可以集中从 Redis 中查询、更新 Session 信息。
具体而言,可以将用户的 Session 数据存储在 Redis 中,并为每个用户生成一个唯一的 Session ID。当用户进行登录或者访问时,服务器会根据用户请求中的 Session ID 到 Redis 中查询对应的 Session 信息。这样无论用户的请求被分发到哪个服务器,都可以通过统一的 Session ID 获取到相同的 Session 数据,从而保证用户的一致性体验。
在这种模式下,需要保证 Redis 是高可用和可扩展的。可以通过使用 Redis 的主从复制和集群模式来实现数据的备份和故障转移,以及通过负载均衡和故障检测来保证系统的可用性和稳定性。此外,还可以通过设置合适的数据持久化机制和监控系统来确保 Redis 的数据安全和性能稳定。
Redis 集中管理 Session
手机验证码
为了增强应用的安全性,很多应用会在每次用户登录时采取手机短信验证的方式。通常的流程是用户在登录页面输入手机号,然后应用会向该手机号发送验证码。用户需要再次在应用中输入收到的验证码进行验证,以确保登录操作是由用户本人发起的。为了防止滥用短信接口和保护用户隐私,通常会限制用户每分钟获取验证码的频率,例如一分钟内不能超过 5 次。
这种短信验证机制可以有效地提高用户账号的安全性,因为即使用户的密码泄露,攻击者也需要获取用户手机并输入正确的验证码才能完成登录操作。同时,限制获取验证码的频率可以有效防止恶意用户利用短信接口进行攻击或滥用,保护短信服务商的资源和用户的体验。
在实现这种短信验证机制时,需要考虑以下几个方面:
-
验证码生成和发送:应用需要生成随机的验证码,并将验证码发送到用户的手机上。可以使用第三方的短信服务提供商来实现验证码的发送,也可以自建短信发送平台。
-
验证码验证:用户收到验证码后,需要在应用中输入收到的验证码进行验证。应用需要对用户输入的验证码进行验证,确保其正确性,并且确保验证码的有效期。
-
频率限制:为了防止滥用短信接口,通常会限制用户每分钟获取验证码的频率。可以在应用中设置一个计数器来记录用户获取验证码的次数,并在达到限制后暂时禁止用户再次获取验证码。
-
安全性考虑:在验证码的传输和存储过程中需要注意安全性,确保验证码不被窃取或篡改。可以通过加密传输、设置有效期等方式增强安全性。
综上,手机短信验证是一种常见的增强应用安全性的方式,通过合理设置验证码获取频率限制和有效期,可以有效防止恶意攻击和滥用行为,提高用户账号的安全性和用户体验。
短信验证码:
伪代码如下:
String 发送验证码(String phoneNumber) {
String key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使用 NX,只在不存在 key 时才能设置成功
boolean r = Redis 执行命令:set key 1 ex 60 nx;
if (r == false) {
// 说明之前设置过该手机的验证码了
long c = Redis 执行命令:incr key;
if (c > 5) {
// 说明超过了一分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
String validationCode = 生成随机的 6 位数的验证码();
String validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执行命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过手机短信发送给用户
return validationCode;
}
// 验证用户输入的验证码是否正确
bool 验证验证码(String phoneNumber, String validationCode) {
String validationKey = "validation:" + phoneNumber;
String value = Redis 执行命令:get validationKey;
if (value == null) {
// 说明没有这个手机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
以上介绍的是 Redis 字符串数据类型的一些常见应用场景,但是 Redis 的字符串类型的应用远不止于此。
比如,除了上述介绍的场景外,还有许多其他应用场景可以利用 Redis 的字符串类型来实现,例如:
-
分布式锁:利用 Redis 的原子性操作和过期特性,实现分布式锁机制,保证多个客户端对共享资源的互斥访问。
-
消息队列:利用 Redis 的列表数据类型,实现简单的消息队列,用于解耦系统中的异步任务处理。
-
分布式 ID 生成器:利用 Redis 的自增操作,生成全局唯一的递增 ID,用于分布式系统中的数据唯一标识。
-
数据统计:将业务数据存储在 Redis 中,利用 Redis 的丰富的数据结构和操作命令,实现数据统计和分析功能。
总之,Redis 的字符串数据类型在实际应用中具有极大的灵活性和可扩展性,开发人员可以根据自己的需求和创造力,结合 Redis 提供的功能和特性,设计出更多创新的应用场景,提升系统性能和用户体验。
Hash 哈希
几乎所有的主流编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组或映射。在 Redis 中,哈希类型是指值本身是一个键值对结构,形如 key = "key",value = { { field1, value1 }, ..., {fieldN, valueN } }。在 Redis 中,键值对和哈希类型之间存在一种类似包含关系的联系,可以用下图来表现:
字符串和哈希类型对比:
❗哈希类型在 Redis 中是一种非常常用的数据结构,用于存储一组 field-value 映射关系。在 Redis 中,这种映射关系通常被称为 field-value,用于区分整体键值对(key-value)的概念。需要注意的是,在哈希类型中,这里的 value 指的是 field 对应的值,而不是键(key)对应的值,这一点在不同上下文中的作用可能会有所不同,需要特别注意。
举例来说,假设我们有一个 Redis 的哈希类型数据结构如下:
key = "user:1001"
value = { { "name", "John" }, { "age", "30" }, { "city", "New York" } }
在这个例子中,key 是 "user:1001",而其对应的 value 是一个包含了三组 field-value 映射关系的集合。其中,"name"、"age" 和 "city" 分别是 field,而对应的值 "John"、"30" 和 "New York" 则是每个 field 对应的 value。这种 field-value 映射关系可以方便地存储和访问相关联的数据,如用户信息中的姓名、年龄和所在城市等。
需要强调的是,在 Redis 中,哈希类型的 field-value 映射关系在不同场景下可能会有不同的作用。例如,在用户信息的示例中,field 可以代表用户的不同属性,而对应的 value 则是属性的具体取值。然而,在其他场景下,field-value 映射关系可能具有完全不同的含义和用途。因此,在使用哈希类型时,我们需要根据具体的业务需求和上下文来理解和使用其中的 field-value 映射关系,以确保数据的正确存储和访问。
哈希类型在 Redis 中是一种非常重要的数据结构,它具有以下特点和优势:
-
灵活性:哈希类型可以存储键值对,每个键值对都是一个独立的字段和值,使得存储和检索数据更加灵活。
-
高效性:Redis 使用哈希表来实现哈希类型,哈希表具有高效的查找、插入和删除操作,使得对哈希类型的操作都能在常数时间内完成。
-
结构化存储:哈希类型的键值对结构使得数据可以更加结构化地存储和管理,便于组织和维护复杂的数据结构。
-
支持嵌套:Redis 的哈希类型支持嵌套结构,即一个哈希类型的值本身可以是一个键值对结构,从而可以实现更复杂的数据模型和存储需求。
-
适用性广泛:哈希类型可以用于存储和管理各种类型的数据,如用户信息、商品信息、配置信息等,适用于各种场景和应用需求。
总之,哈希类型是 Redis 中一种非常重要且灵活的数据结构,它在数据存储和管理方面具有很大的优势和应用潜力。开发人员可以充分利用哈希类型的特性,设计出更加高效、灵活和结构化的数据存储方案,从而提升系统的性能和可维护性。
命令
HSET
HSET命令用于在哈希类型数据中为特定字段(field)设置值(value)。这个命令是在Redis数据库中使用的一种关键命令,它允许用户为已存在的哈希表设置字段与相应的值,或者在哈希表不存在时创建一个新的哈希表,并设置其字段与值。这种命令的使用使得在Redis中存储和检索结构化数据变得更加方便和高效。
它的语法如下:
HSET key field value [field value ...]
- key: 哈希数据的键。
- field: 要设置值的字段名。
- value: 要设置的值。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 插入一组field的时间复杂度为O(1),插入N组field的时间复杂度为O(N)。
返回值: 返回成功添加的字段的个数。
示例:
HSET user:1001 name "John"
这个示例命令将在名为"user:1001"的哈希类型数据中设置字段"name"的值为"John"。
HGET
HGET命令是Redis中用于获取哈希类型数据中指定字段(field)对应的值的命令。通过该命令,用户可以从存储在Redis中的哈希表中检索特定字段的值。这种功能对于检索和读取结构化数据非常有用,尤其在需要获取特定字段值而不需要整个哈希表内容的情况下。 HGET命令的使用简单而高效,使得对哈希类型数据进行快速读取成为可能,从而满足了各种数据检索需求。
它的语法如下:
HGET key field
- key: 哈希数据的键。
- field: 要获取值的字段名。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: O(1)。因为哈希表的键值对是通过哈希函数直接查找的,所以获取指定字段的值的时间复杂度是常量级别的。
返回值: 如果指定字段存在,则返回该字段对应的值;如果指定字段不存在,则返回nil。
示例:
HGET user:1001 name
这个示例命令将返回名为"user:1001"的哈希类型数据中字段"name"的值。
HEXISTS
HEXISTS命令是Redis中用于判断哈希类型数据中是否存在指定字段(field)的命令。通过该命令,用户可以轻松地检查哈希表中是否存在特定的字段。当需要确认某个字段是否存在时,HEXISTS命令可以提供快速而有效的解决方案。这种功能对于编程中的条件逻辑判断和数据操作十分有用,使得开发者可以根据字段的存在与否来进行相应的处理。
它的语法如下:
HEXISTS key field
- key: 哈希数据的键。
- field: 要判断是否存在的字段名。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: O(1)。因为哈希表的键值对是通过哈希函数直接查找的,所以判断指定字段是否存在的时间复杂度是常量级别的。
返回值: 如果指定字段存在,则返回1;如果指定字段不存在,则返回0。
示例:
HEXISTS user:1001 name
这个示例命令将判断名为"user:1001"的哈希类型数据中是否存在字段"name"。如果存在,则返回1;如果不存在,则返回0。
HDEL
HDEL命令是Redis中用于删除哈希类型数据中指定的一个或多个字段(field)的命令。通过该命令,用户可以轻松地从哈希表中移除一个或多个指定的字段及其对应的值。这个功能对于数据的清理和管理非常重要,特别是当需要从数据结构中删除特定字段时。HDEL命令的灵活性和效率使得对哈希类型数据的精确控制变得简单而可靠,有助于确保数据的一致性和准确性。
它的语法如下:
HDEL key field [field ...]
- key: 哈希数据的键。
- field: 要删除的字段名,可以指定一个或多个字段。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 删除一个元素的时间复杂度为O(1),删除N个元素的时间复杂度为O(N)。删除多个元素的情况下,时间复杂度取决于要删除的元素的个数。
返回值: 本次操作删除的字段个数。
示例:
HDEL user:1001 name age
这个示例命令将删除名为"user:1001"的哈希类型数据中的"name"和"age"两个字段。如果删除成功,则返回删除的字段个数。
注意区分,HDEL删除的是filed,不是key;DEL删除的是key。
HKEYS
HKEYS命令是Redis中用于获取哈希类型数据中所有字段名(field)的命令。通过该命令,用户可以一次性地获取哈希表中所有的字段名,而不需要获取字段对应的值。这种功能对于需要遍历哈希表中所有字段名的场景非常有用,例如在需要对哈希表进行全面检查或者进行批量操作时。HKEYS命令的使用简单而高效,使得对哈希类型数据的字段名进行管理和处理变得更加方便。
它的语法如下:
HKEYS key
- key: 哈希数据的键。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有字段的时间复杂度为O(N),其中N为字段的个数(哈希的元素个数)。
HKEYS命令用于获取哈希类型数据中的所有字段名(field)。其执行过程包括以下步骤:
- 根据给定的键(key),首先在Redis中查找对应的哈希类型数据,这个查找操作的时间复杂度是O(1),即常数时间复杂度。这是因为Redis的数据结构中使用了哈希表来存储键值对,通过哈希函数可以直接定位到对应的哈希表,而不需要遍历整个数据库。
- 找到哈希类型数据后,HKEYS命令会遍历这个哈希表,获取其中所有的字段名(field)。遍历哈希表的时间复杂度取决于哈希表中存储的键值对数量,通常情况下为O(N),其中N为哈希表中的元素数量。遍历哈希表获取字段名的过程是一个线性操作。
返回值: 返回一个包含所有字段名的列表。
示例:
HKEYS user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中的所有字段名列表。
HVALS
HVALS命令是Redis中用于获取哈希类型数据中所有值(value)的命令。通过该命令,用户可以一次性地获取哈希表中所有字段对应的值,而不需要获取字段名。这种功能对于需要获取哈希表中所有值的场景非常有用,例如在需要对哈希表中的所有值进行批量处理或者分析时。HVALS命令的使用简单而高效,使得对哈希类型数据的值进行管理和处理变得更加方便。
它的语法如下:
HVALS key
- key: 哈希数据的键。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有值的时间复杂度为O(N),其中N为字段的个数(哈希的元素个数)。
返回值: 返回一个包含所有值的列表。
示例:
HVALS user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中的所有值列表。
HGETALL
HGETALL命令是Redis中用于获取哈希类型数据中所有字段以及对应的值的命令。通过该命令,用户可以一次性地获取哈希表中所有字段和它们对应的值,以键值对的形式返回。这种功能对于需要获取哈希表中所有数据的场景非常有用,例如在需要对哈希表中的所有字段和值进行全面分析或者迁移时。HGETALL命令的使用简单而高效,使得对哈希类型数据的整体检索和操作变得更加方便。
它的语法如下:
HGETALL key
- key: 哈希数据的键。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有字段和对应值的时间复杂度为O(N),其中N为字段的个数。
返回值: 返回一个包含所有字段和对应值的列表。列表中的元素依次为字段名和对应的值交替出现。
示例:
HGETALL user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中的所有字段以及对应的值的列表。列表中的元素依次为字段名和对应的值。
在使用HGETALL命令时,如果哈希元素个数较多,可能会导致Redis出现阻塞的情况。这是因为HGETALL会一次性返回哈希表中所有的字段和对应的值,如果哈希表非常庞大,数据传输和处理的压力会加大。
如果开发者只需要获取部分字段的值,可以使用HMGET命令。HMGET允许指定需要获取的字段,从而减少数据传输量和处理压力。
另外,如果一定需要获取全部字段的值,并且哈希表非常大,开发者可以尝试使用HSCAN命令。HSCAN命令采用渐进式遍历哈希类型数据,它会分步获取哈希表中的元素,从而减少一次性获取全部数据所带来的压力。具体的HSCAN命令会在后续章节中进行介绍。
HMGET
HMGET命令是Redis中用于一次性获取哈希类型数据中多个字段的值的命令。通过指定哈希键(key)和多个字段名(field),HMGET命令可以同时获取这些字段对应的值。这种命令的使用简单高效,适用于需要一次性获取多个字段值的场景。
它的语法如下:
HMGET key field [field ...]
- key: 哈希数据的键。
- field: 要获取值的字段名,可以指定一个或多个字段。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 查询单个元素的时间复杂度为O(1),查询多个元素的时间复杂度为O(N),其中N为查询的元素个数。
返回值: 返回一个包含指定字段对应的值的列表。如果指定字段不存在,则对应的值为nil。
示例:
HMGET user:1001 name age city
这个示例命令将返回名为"user:1001"的哈希类型数据中"name"、"age"和"city"三个字段对应的值的列表。如果某个字段不存在,则对应的值为nil。
注意,filed和value之间有顺序上的对应关系。
上述 HKEYS,HVALS,HGETALL都存在一定的风险,如果hash的元素太多就会导致执行的耗时比较长,从而阻塞redis。这个时候我们引入一个新的命令——HSCAN,同样也是一个遍历redis的hash,但是它属于渐进式遍历。这种渐进式遍历的思想符合“化整为零”的原则,时间是可控的,每敲一次命令会遍历一小部分,连续执行多次就可以遍历完成整个过程。
它能够将大型数据集的处理过程拆分成多个小步骤,从而降低了单次操作的复杂度和风险。因此,在处理大型哈希表时,推荐使用HSCAN命令进行遍历操作,以确保Redis服务器的稳定性和性能。
HSCAN
HSCAN命令用于迭代哈希表中的所有字段,并以游标方式逐步获取结果。相比于一次性获取所有字段名或值的命令,如HKEYS或HVALS,HSCAN提供了一种无阻塞的方式来遍历哈希表,适用于大型哈希表的遍历操作。
使用HSCAN命令进行哈希表的迭代遍历时,需要指定一个起始游标(cursor)值,以及一些可选的参数,如匹配模式(MATCH)和返回数量限制(COUNT)。命令会返回一个游标值,用于指示下一次迭代的起始位置,以及一组哈希表中的字段及其对应的值。通过多次调用HSCAN命令,并不断更新游标值,直到游标返回0,表示遍历完成。
由于HSCAN命令以游标方式进行遍历,因此适用于大型哈希表的遍历操作,能够在不阻塞Redis主线程的情况下逐步获取数据,避免了一次性获取所有数据可能导致的内存占用过高和网络传输延迟等问题。
它的语法如下:
HSCAN key cursor [MATCH pattern] [COUNT count]
- key: 哈希表的键。
- cursor: 游标,用于迭代器定位。
- MATCH pattern: (可选)用于匹配字段的模式。
- COUNT count: (可选)指定每次迭代返回的元素个数。
命令有效版本: Redis 2.8.0之后可用。
时间复杂度: 每次迭代的时间复杂度为O(1),整个迭代过程的时间复杂度取决于哈希表的大小。
返回值: 返回一个包含游标和哈希表中匹配字段的列表。列表的第一个元素是下一个迭代的游标,后续元素是匹配的字段及其对应的值。
示例:
HSCAN myhash 0 MATCH field* COUNT 5
这个示例命令将从名为"myhash"的哈希表中以游标0开始迭代,匹配所有以"field"开头的字段,并每次返回最多5个匹配项。
HLEN
HLEN命令是Redis中用于获取哈希类型数据中所有字段的个数的命令。通过该命令,用户可以获取哈希表中字段的数量,即哈希表中键值对的数量。这个功能对于了解哈希表的大小和结构非常有用,特别是在需要进行哈希表大小估算或者性能优化时。HLEN命令的使用简单而高效,使得对哈希类型数据的大小进行检测变得更加方便。
它的语法如下:
HLEN key
- key: 哈希数据的键。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有字段的个数的时间复杂度为O(1)。因为Redis内部使用哈希表来存储哈希类型数据,直接获取哈希表的大小即可得到字段的个数。
返回值: 返回哈希类型数据中所有字段的个数。
示例:
HLEN user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中所有字段的个数。
HSETNX
HSETNX命令是Redis中用于在哈希类型数据中设置字段和对应的值的命令,但仅当字段不存在时才执行设置操作。如果字段已经存在,则该命令不执行任何操作,保持原有值不变。这个功能对于确保哈希表中特定字段的唯一性非常有用,特别是在需要避免重复设置字段值的情况下。HSETNX命令的使用使得对哈希类型数据的更新操作更加可靠和安全。
它的语法如下:
HSETNX key field value
- key: 哈希数据的键。
- field: 要设置的字段名。
- value: 要设置的值。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 该命令的时间复杂度为O(1),因为它只需要执行一次哈希表的插入操作。
返回值: 如果字段不存在并成功设置,则返回1;如果字段已经存在,则不执行任何操作并返回0。
示例:
HSETNX user:1001 name "Alice"
这个示例命令将在名为"user:1001"的哈希类型数据中,如果字段"name"不存在,则设置其值为"Alice"。如果字段"name"已经存在,则不执行任何操作。如果设置成功,则返回1;如果字段已经存在,则返回0。
HINCRBY
HINCRBY命令是Redis中的哈希类型数据操作命令,用于将哈希类型数据中指定字段对应的数值增加指定的值。如果指定字段不存在,则会创建该字段并将其值初始化为0,然后再执行增加操作。这个命令对于需要对哈希表中的数值字段进行增加操作非常有用,尤其是在需要原子性操作和简单逻辑的场景下。HINCRBY命令的使用简单方便,使得对哈希类型数据的数值字段进行增加操作变得更加灵活和可控。
它的语法如下:
HINCRBY key field increment
- key: 哈希数据的键。
- field: 要增加数值的字段名。
- increment: 要增加的值,可以为负数表示减少值。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 该命令的时间复杂度为O(1),因为它只需要执行一次哈希表的查找和更新操作。
返回值: 返回执行增加操作后,字段对应的新值。
示例:
HINCRBY user:1001 views 10
这个示例命令将在名为"user:1001"的哈希类型数据中,将字段"views"对应的数值增加10。如果字段"views"不存在,则会创建该字段并将其值初始化为0,然后再增加10。最后返回执行增加操作后"views"字段对应的新值。
HINCRBYFLOAT
HINCRBYFLOAT命令是Redis中的哈希类型数据操作命令,是HINCRBY的浮点数版本。它用于将哈希类型数据中指定字段对应的数值以浮点数形式增加指定的值。如果指定字段不存在,则会创建该字段并将其值初始化为0,然后再执行增加操作。这个命令对于需要对哈希表中的数值字段进行精确的浮点数增加操作非常有用,尤其是在需要保留精度的情况下。HINCRBYFLOAT命令的使用简单方便,使得对哈希类型数据的数值字段进行增加操作变得更加灵活和可控。
它的语法如下:
HINCRBYFLOAT key field increment
- key: 哈希数据的键。
- field: 要增加数值的字段名。
- increment: 要增加的值,可以为负数表示减少值。
命令有效版本: Redis 2.6.0之后可用。
时间复杂度: 该命令的时间复杂度为O(1),因为它只需要执行一次哈希表的查找和更新操作。
返回值: 返回执行增加操作后,字段对应的新值。
示例:
HINCRBYFLOAT user:1001 balance 5.25
这个示例命令将在名为"user:1001"的哈希类型数据中,将字段"balance"对应的数值以浮点数形式增加5.25。如果字段"balance"不存在,则会创建该字段并将其值初始化为0,然后再增加5.25。最后返回执行增加操作后"balance"字段对应的新值。
哈希类型命令小结
命令 | 执行效果 | 时间复杂度 |
---|---|---|
hset key field value | 设置值 | O(1) |
hget key field | 获取值 | O(1) |
hdel key field [field ...] | 删除 field | O(k),k 是 field 个数 |
hlen key | 计算 field 个数 | O(1) |
hgetall key | 获取所有的 field-value | O(k),k 是 field 个数 |
hmget key field [field ...] | 批量获取 field-value | O(k),k 是 field 个数 |
hmset key field value [field value ...] | 批量设置 field-value | O(k),k 是 field 个数 |
hexists key field | 判断 field 是否存在 | O(1) |
hkeys key | 获取所有的 field | O(k),k 是 field 个数 |
hvals key | 获取所有的 value | O(k),k 是 field 个数 |
hsetnx key field value | 设置值,但必须在 field 不存在时才能设置成功 | O(1) |
hincrby key field n | 对应 field-value 加上 n | O(1) |
hincrbyfloat key field n | 对应 field-value 加上 n | O(1) |
hstrlen key field | 计算 value 的字符串长度 | O(1) |
内部编码
哈希类型数据在 Redis 中有两种内部编码方式:
-
ziplist(压缩列表): 当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)且所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 使用 ziplist 作为哈希的内部实现。Ziplist 使用更加紧凑的结构实现多个元素的连续存储,从而在节省内存方面优于 hashtable。
上面的配置项就是可以写进redis.conf文件里面的。 -
hashtable(哈希表): 当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现。在这种情况下,ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。因此,当哈希元素较多或者元素值较大时,Redis会选择使用 hashtable。
这两种内部编码方式根据哈希类型的大小和元素值的大小进行选择,以在不同情况下实现更高效的存储和访问。下⾯的示例演示了哈希类型的内部编码,以及响应的变化。
在 Redis 中,哈希类型数据的内部编码方式取决于以下条件:
当 field 个数比较少且没有大的 value 时,内部编码为 ziplist。例如,当使用 hmset 命令设置的 field-value 键值对较少时,Redis 内部会选择使用 ziplist 作为哈希的内部实现。可以通过 object encoding 命令查看内部编码类型。
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
OK
127.0.0.1:6379> object encoding hashkey
"ziplist"
当有 value 大于 64 字节时,内部编码会转换为 hashtable。如果哈希中存在一个或多个 value 的大小超过了 64 字节,Redis 会自动将内部编码方式转换为 hashtable。
127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... omitted ..."
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
当 field 个数超过 512 时,内部编码也会转换为 hashtable。如果哈希中的 field 个数超过了 512 个,Redis 同样会将内部编码方式转换为 hashtable。
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ... omitted ... f513 v513
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
这些转换规则确保了 Redis 在不同情况下选择最合适的内部编码方式,以优化内存使用和操作效率。
使用场景
图 1 展示了关系型数据库中存储的两条用户信息,其中每个用户的属性以表格的列形式呈现,而每条用户信息则以行的形式表示。如果要将这两个用户信息映射为关系表,则可以使用图 2 中所示的形式。
关系型数据表保存用户信息
映射关系表示用户信息
与使用 JSON 格式的字符串缓存用户信息相比,使用哈希类型更加直观,并且在更新操作上更灵活。我们可以将每个用户的 ID 定义为键的后缀,然后使用多个 field-value 对来表示每个用户的各个属性。
参考伪代码:
UserInfo getUserInfo(long uid) {
// 根据 uid 得到 Redis 的键
String key = "user:" + uid;
// 尝试从 Redis 中获取对应的值
Map<String, String> userInfoMap = Redis 执行命令:hgetall key;
// 如果缓存命中(hit)
if (userInfoMap != null && !userInfoMap.isEmpty()) {
// 将映射关系还原为对象形式
UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
return userInfo;
}
// 如果缓存未命中(miss)
// 从数据库中,根据 uid 获取用户信息
UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>;
// 如果表中没有 uid 对应的用户信息
if (userInfo == null) {
响应 404;
return null;
}
// 将用户信息以哈希类型保存到缓存
Redis 执行命令:hmset key name userInfo.name age userInfo.age city userInfo.city;
// 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
Redis 执行命令:expire key 3600;
// 返回用户信息
return userInfo;
}
这段伪代码描述了一个根据用户ID从缓存中获取用户信息的过程。首先,根据用户ID构建缓存键,然后尝试从Redis缓存中获取对应的用户信息。如果缓存中存在用户信息,则直接从缓存中读取并返回。如果缓存未命中,则从数据库中查询用户信息,并将查询到的信息写入到Redis缓存中,并设置缓存的过期时间。
注意,哈希类型和关系型数据库在某些方面存在显著的不同之处:
-
稀疏性: 哈希类型是稀疏的,即每个键可以拥有不同的字段(field),而关系型数据库是完全结构化的,如果要添加新的列,则所有行都必须设置值,即使为null。这意味着在Redis中,可以在不同的哈希键中存储不同的字段,而在关系型数据库中,必须确保所有行都具有相同的结构。
-
查询功能差异: 关系型数据库可以执行复杂的关系查询,如联表查询、聚合查询等,而Redis的哈希类型数据结构并不适合执行类似的复杂查询。虽然Redis提供了一些基本的数据操作命令,但是去模拟关系型数据库中的复杂查询通常是不切实际且维护成本高的。Redis更适用于简单、快速的数据存储和检索,而不是用于复杂的关系型数据处理。
关系型数据库稀疏性
缓存方式对比
截至目前为止,我们已经探讨了三种方法来缓存用户信息,并给出了它们的实现方法以及优缺点分析。
-
原生字符串类型: 使用字符串类型,每个属性对应一个键。
set user:1:name James set user:1:age 23 set user:1:city Beijing
- 优点:实现简单,针对个别属性变更也很灵活。
- 缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,因此这种方案基本没有实用性。
-
序列化字符串类型,例如 JSON 格式: 使用序列化后的字符串类型,例如 JSON 格式,存储整个用户对象的信息。
set user:1 {"name":"James","age":23,"city":"Beijing"}
- 优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
- 缺点:本身序列化和反序列化需要一定开销,同时如果总是操作个别属性则非常不灵活。
效率低:当需要获取或修改用户信息中的某个字段时,必须将整个JSON字符串读取到内存中,然后解析成对象。这样的操作效率较低,尤其是对于大型JSON字符串而言。
更新复杂:修改用户信息中的某个字段时,需要先解析整个JSON字符串,然后找到要修改的字段进行更新,最后重新序列化为JSON字符串。这一过程较为繁琐,尤其是当字段较多时。
-
哈希类型: 使用哈希类型存储用户信息。
hmset user:1 name James age 23 city Beijing
- 优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
- 缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。相比于JSON格式,哈希表不够灵活,无法直观地表示复杂的数据结构,如嵌套对象或数组。哈希表的字段是固定的,不够灵活,难以应对未来可能的数据结构变化或扩展。
综上所述,哈希类型是相对较为优秀的方案,因为它既简单又灵活,可以轻松地处理信息的局部变更或获取操作。虽然存在一些内存消耗的问题,但在大多数情况下,这种消耗是可以接受的,特别是与其他两种方案相比,哈希类型更加合理和实用。
List 列表
列表类型是一种用于存储多个有序的字符串的数据结构。如图中所示,a、b、c、d、e 这五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element)。一个列表最多可以存储 N 个元素。在 Redis 中,可以对列表进行两端的插入(push)和弹出(pop)操作,同时也可以获取指定范围的元素列表以及获取指定索引下标的元素等操作。除此之外,列表还具有许多其他操作,例如在指定元素前或后插入新元素,通过索引修改元素的值等。列表是一种非常灵活的数据结构,它可以充当栈和队列的角色,在实际开发中有着广泛的应用场景。例如,在缓存、消息队列、排行榜等方面都可以使用列表来实现各种功能。
列表两端插入和弹出操作:
约定最左侧元素下标是0;
redis的下标支持负数下标。
列表的获取、删除等操作:
Redis中的列表类型在内部实现上更接近于双端队列而不是简单的数组。这种双端队列的内部结构使得Redis在处理列表类型时能够高效地执行头部和尾部的插入、删除操作,而不会因为元素的移动而导致性能下降。
具体来说,Redis的列表内部结构是由一系列节点(node)组成的链表,每个节点包含一个元素值和指向前一个节点和后一个节点的指针。这种双向链表的结构使得在头部和尾部执行插入、删除操作时具有常数时间复杂度(O(1)),这使得Redis的列表类型非常适合用于实现队列和栈等数据结构。
另外,Redis还通过一些优化措施来提高列表类型的性能,例如使用ziplist(压缩列表)来存储较小的列表,以及对列表的一些操作进行特殊处理,从而减少内存消耗和提高执行效率。
列表类型具有以下特点:
第一、列表中的元素是有序的,这意味着我们可以通过索引下标获取某个元素或者某个范围的元素列表。这里的“有序”并非指“升序、降序”,而是指“顺序很关键”!例如,要获取上图的第 5 个元素,可以执行 lindex user:1:messages 4,或者获取倒数第 1 个元素,使用 lindex user:1:messages -1 就可以得到元素 e。此外,列表还支持通过索引范围来获取一段连续的元素列表,如 lrange user:1:messages 0 2 可以获取列表中的前三个元素。
第二、列表区分获取和删除的操作。例如,在上面的图中,使用 lrem 1 b 从列表中删除从左数遇到的前 1 个 b 元素。这个操作会导致列表的长度从 5 变成 4。但是,执行 lindex 4 只会获取元素,而不会改变列表的长度。除了 lrem 外,还可以使用 ltrim 命令截取列表的一部分元素,而不影响原始列表中的其他元素。
第三、列表中的元素允许重复出现。例如,在下图中的列表中包含了两个 a 元素。这意味着列表中的元素可以重复出现,每个元素都可以在列表中多次出现,不受限制。这种特性使得列表在某些应用场景下非常有用,例如记录用户操作历史、存储用户消息等。
列表中允许有重复元素:
命令
LPUSH
LPUSH命令用于将一个或多个元素从左侧插入(头部插入)到列表中。这个命令允许用户指定一个列表键(key)以及一个或多个要插入的元素,然后将这些元素按顺序从左侧插入到列表中。LPUSH命令会将指定的元素从列表的左侧插入,使得插入后的元素排列顺序与插入的顺序一致。如果key不存在,则会创建一个新的列表,并将元素插入其中,然后返回插入后列表的长度。如果key已经存在,并且key对应的value类型不是list,此时LPUSH命令就会报错。
它的语法如下:
LPUSH key element [element ...]
- key: 列表的键。
- element: 要插入的一个或多个元素。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。
示例:
LPUSH mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从左侧插入到名为"mylist"的列表中。如果列表不存在,则会创建一个新的列表。最后返回执行插入操作后"mylist"列表的长度。需要注意的是,依次头插,最后全部插入完毕,“orange”是在最前面。
LPUSHX
LPUSHX命令用于在指定的键存在时,将一个或多个元素从左侧插入(头部插入)到列表中。这个命令允许用户指定一个列表键(key)以及一个或多个要插入的元素,然后将这些元素按顺序从左侧插入到列表中。如果key存在且是一个列表类型,LPUSHX命令会将指定的元素从列表的左侧插入,使得插入后的元素排列顺序与插入的顺序一致,然后返回插入后列表的长度。如果key不存在,则不执行任何操作,直接返回0。
它的语法如下:
LPUSHX key element [element ...]
- key: 列表的键。
- element: 要插入的一个或多个元素。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。如果键不存在,则不执行任何操作并返回0。
示例:
LPUSHX mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从左侧插入到名为"mylist"的列表中,如果列表不存在,则不执行任何操作,直接返回0。如果列表存在,则将元素插入列表左侧,并返回执行插入操作后"mylist"列表的长度。
RPUSH
RPUSH命令用于将一个或多个元素从右侧插入(尾部插入)到列表中。这个命令允许用户指定一个列表键(key)以及一个或多个要插入的元素,然后将这些元素按顺序从右侧插入到列表中。这个命令会将指定的元素从列表的右侧插入,相当于尾插法,使得插入后的元素排列顺序与插入的顺序一致。如果key不存在,则会创建一个新的列表,并将元素插入其中,然后返回插入后列表的长度。
它的语法如下:
RPUSH key element [element ...]
- key: 列表的键。
- element: 要插入的一个或多个元素。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。
示例:
RPUSH mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从右侧插入到名为"mylist"的列表中。如果列表不存在,则会创建一个新的列表。最后返回执行插入操作后"mylist"列表的长度。
RPUSHX
RPUSHX命令用于在指定的键存在时,将一个或多个元素从右侧插入(尾部插入)到列表中。如果键不存在,则不执行任何操作,直接返回0。这个命令通常用于在确保列表存在的情况下向列表尾部插入元素,避免了因为键不存在而出现错误或者创建新列表的情况。如果key存在且是一个列表类型,RPUSHX命令会将指定的元素从列表的右侧插入,然后返回插入后列表的长度。如果key不存在或者不是一个列表类型,则不执行任何操作,直接返回0。
它的语法如下:
RPUSHX key element [element ...]
- key: 列表的键。
- element: 要插入的一个或多个元素。
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。如果键不存在,则不执行任何操作并返回0。
示例:
RPUSHX mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从右侧插入到名为"mylist"的列表中,如果列表不存在,则不执行任何操作,直接返回0。如果列表存在,则将元素插入列表右侧,并返回执行插入操作后"mylist"列表的长度。
LRANGE
LRANGE(LIST RANGE)命令用于获取列表中指定区间范围内的所有元素,左闭右闭,即包括起始位置和结束位置的元素。通过指定列表的键(key)以及起始位置和结束位置的索引,LRANGE命令可以返回列表中指定区间范围内的所有元素。这个命令通常用于按范围获取列表中的元素,例如获取列表的前N个元素或者某个区间内的元素。LRANGE命令会返回指定区间范围内的所有元素,包括起始位置和结束位置的元素。如果起始位置大于列表的末尾索引,或者结束位置小于列表的起始索引,LRANGE命令会返回一个空列表(非未定义行为,也不会抛出异常)。
它的语法如下:
LRANGE key start stop
- key: 列表的键。
- start: 起始位置的索引,0表示第一个元素,1表示第二个元素,依此类推。负数表示倒数第N个元素。
- stop: 结束位置的索引,0表示第一个元素,1表示第二个元素,依此类推。负数表示倒数第N个元素。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 获取指定区间内所有元素的时间复杂度为O(N),其中N为要获取的元素数量。
返回值: 返回指定区间的所有元素。
示例:
LRANGE mylist 0 2
这个示例命令将返回名为"mylist"的列表中从第一个元素到第三个元素(左闭右闭)的所有元素。如果列表中只有两个元素,则返回这两个元素。
注意,元素前面的序号和元素下标无关。
在处理"下标超出范围"的情况时,不同编程语言或工具的处理方式可能有所不同,每种方式都有自己的优缺点。以下是对比各种处理方式的优缺点:
-
C++(未定义行为):
- 优点:速度快:由于不进行下标合法性验证,直接访问数组元素,因此执行效率较高。
- 缺点:难以调试:由于未定义行为,当下标超出范围时,程序可能会出现不可预测的行为,导致难以调试和定位问题。同时,由于未定义行为,程序可能在不同平台或环境下表现不一致。
-
Java(抛出异常):
- 优点:出错及时发现:当下标超出范围时,会抛出数组越界异常,提供了及时发现问题的机制,有助于调试和修复问题。
- 缺点:速度慢:因为需要额外的下标合法性验证,导致执行速度相对较慢。此外,异常的捕获和处理也会增加代码的复杂度。
-
Redis(返回符合实际情况的元素):
- 优点:
- 容错能力强:Redis会返回符合实际情况的元素,而不会抛出异常或导致未定义行为,因此具有较强的容错能力和鲁棒性。
- 方便处理:开发者可以根据实际需要自行处理返回的元素,例如,可以根据实际情况进行异常处理或特定的业务逻辑处理。
- 缺点:可能会隐藏错误:当下标超出范围时,如果返回的元素符合实际情况但开发者未预料到这种情况,可能会导致潜在的错误隐藏起来,需要开发者自行进行处理和调试。
- 优点:
LPOP
LPOP命令用于从列表的左侧取出一个元素,即执行头部删除操作。这个命令会移除并返回列表中的第一个元素。与RPOP命令相似,LPOP也可以被用来实现队列的先进先出(FIFO)的操作,或者用于获取并删除列表中的第一个元素。
它的语法如下:
LPOP key
- key: 列表的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 头部删除操作的时间复杂度为O(1),即常数时间复杂度。
返回值: 返回被取出的元素。如果列表为空,则返回nil。
示例:
LPOP mylist
这个示例命令将从名为"mylist"的列表的左侧取出一个元素,并将其返回。如果列表为空,则返回nil。
RPOP
RPOP命令用于从列表的右侧取出一个元素,即执行尾部删除操作。这个命令会移除并返回列表中的最后一个元素。其作用类似于弹出栈中的顶部元素,从列表的尾部取出一个元素,同时将该元素从列表中移除。RPOP命令可以用于实现队列的后进先出(LIFO)的操作,或者用于获取并删除列表中的最后一个元素。
它的语法如下:
RPOP key [count]
- key: 列表的键。
- count: redis6.2版本升级之后可以选择加上count代表删除个数。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 尾部删除操作的时间复杂度为O(1),即常数时间复杂度。
返回值: 返回被取出的元素。如果列表为空,则返回nil。
示例:
RPOP mylist
这个示例命令将从名为"mylist"的列表的右侧取出一个元素,并将其返回。如果列表为空,则返回nil。
LINDEX
LINDEX命令用于获取列表中从左侧开始的第index位置的元素。索引从0开始,即0表示第一个元素,1表示第二个元素,以此类推。如果index为负数,则表示从右侧开始计数,-1表示倒数第一个元素,-2表示倒数第二个元素,依此类推。该命令主要用于访问列表中特定位置的元素,根据索引可以快速获取对应位置的元素值,无需遍历整个列表。如果下标非法,返回nil。
它的语法如下:
LINDEX key index
- key: 列表的键。
- index: 要获取的元素的索引位置。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 获取指定位置元素的时间复杂度为O(N),其中N为列表的长度。
返回值: 返回被取出的元素。如果指定索引超出了列表的范围,则返回nil。
示例:
LINDEX mylist 2
这个示例命令将返回名为"mylist"的列表中从左侧开始的第3个元素(索引为2)。如果列表中不包含这个位置的元素,则返回nil。
LINSERT
LINSERT命令允许在列表中的特定位置插入元素,可以选择在指定元素之前(BEFORE)或之后(AFTER)插入新元素。如果指定的pivot元素在列表中存在,则在其前后插入新元素;如果pivot元素不存在,则命令不执行任何操作。这个命令通常用于需要在列表中特定位置插入元素的场景,例如在某个元素之前或之后插入新的元素,以修改列表的结构。
它的语法如下:
LINSERT key <BEFORE | AFTER> pivot element
- key: 列表的键。
- BEFORE | AFTER: 指定插入的位置,是在指定元素之前还是之后。
- pivot: 指定的参考元素,即要在其前后插入新元素的元素。
- element: 要插入的新元素。
命令有效版本: Redis 2.2.0之后可用。
时间复杂度: 插入操作的时间复杂度为O(N),其中N为列表的长度。
返回值: 返回执行插入操作后列表的长度。
示例:
LINSERT mylist BEFORE "world" "hello"
这个示例命令将在名为"mylist"的列表中,找到第一个出现的"world"元素,并在其前面插入一个新元素"hello"。如果列表中不存在"world"元素,则不进行任何操作。
找基准值从左往右找,第一个符合基准值的位置即可。
LLEN
LLEN命令是Redis中的列表操作命令,用于获取列表的长度,即列表中包含的元素个数。通过该命令,用户可以快速地获取列表的大小,以便于对列表进行管理和操作。这个命令的使用非常简单,只需指定列表的键(key),就可以返回该列表中包含的元素个数。
它的语法如下:
LLEN key
- key: 列表的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 获取列表长度的时间复杂度为O(1),即常数时间复杂度。
返回值: 返回列表的长度,即其中包含的元素个数。
示例:
LLEN mylist
这个示例命令将返回名为"mylist"的列表中包含的元素个数,即列表的长度。
LREM
LREM命令用于从列表中删除与给定值相等的元素。可以指定删除元素的数量或方向(从左侧或右侧开始删除)。具体来说,LREM命令会在列表中从头到尾遍历,找到与给定值相等的元素,并将其删除。可以通过指定一个参数来表示要删除的元素的数量,该参数可以为正数、负数或零。当参数为正数时,表示删除从左侧开始匹配的元素数量;当参数为负数时,表示删除从右侧开始匹配的元素数量;当参数为零时,表示删除列表中所有与给定值相等的元素。
它的语法如下:
LREM key count value
- key: 列表的键。
- count: 要删除的元素数量。可以是以下三种情况之一:
- 如果为正数,则表示从左到右,删除值为value的元素,直到删除count个元素为止。
- 如果为负数,则表示从右到左,删除值为value的元素,直到删除count个元素为止。
- 如果为0,则表示删除所有值为value的元素。
- value: 要删除的元素的值。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 删除单个元素的时间复杂度为O(N),其中N是列表的长度。删除多个元素的时间复杂度取决于count参数。
返回值: 返回被删除的元素数量。
示例:
LREM mylist 2 "foo"
这个示例命令将从名为"mylist"的列表中删除最多2个值为"foo"的元素。
LTRIM
LTRIM命令用于修剪(截取)列表,使其仅包含指定范围内的元素。它会保留列表中从开始位置到结束位置之间的元素,而将其他元素删除。具体来说,LTRIM命令需要指定列表的起始位置和结束位置(即索引范围),它会保留列表中从起始位置到结束位置之间的元素,而删除其他元素。这个范围是一个闭区间,即包括起始位置和结束位置的元素都会被保留在列表中。
它的语法如下:
LTRIM key start stop
- key: 列表的键。
- start: 开始位置的索引(0表示第一个元素)。
- stop: 结束位置的索引(-1表示最后一个元素)。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是列表被修剪后的元素数量。
返回值: 命令执行成功时返回"OK"。
示例:
LTRIM mylist 0 99
这个示例命令将名为"mylist"的列表修剪为只包含前100个元素,其余元素将被删除。
LSET
LSET命令用于设置列表中指定索引位置的元素的值。它会覆盖指定索引位置上原有的值。具体来说,LSET命令需要指定列表的名称、要设置的索引位置以及新的元素值。它会将列表中指定索引位置上原有的值替换为新的元素值。
它的语法如下:
LSET key index value
- key: 列表的键。
- index: 要设置值的元素的索引。索引从0开始,0表示列表的第一个元素,-1表示列表的最后一个元素。
- value: 要设置的值。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是列表的长度。
返回值: 命令执行成功时返回"OK"。
示例:
LSET mylist 2 "new_value"
这个示例命令将名为"mylist"的列表中索引为2的元素的值设置为"new_value"。
我们以前学习多线程的时候了解过一个东西——“生产者-消费者”模型,它使用队列来作为中间的“交易场所(broker)”。
在这个模型中,我们期望队列具备两个主要特性:
线程安全:多个线程可以同时访问队列而不会出现数据混乱或丢失的情况。这意味着队列的操作需要是原子的,即在执行队列操作期间不会被其他线程中断或修改。
阻塞:在队列为空时,尝试从队列中取出元素的操作应该被阻塞,直到队列不为空为止;同样,在队列已满时,尝试向队列中添加元素的操作也应该被阻塞,直到队列有足够的空间为止。
Redis中的列表(List)可以被视为一种阻塞队列的实现,其线程安全性是由Redis的单线程模型来保证的,即Redis在处理客户端请求时是按顺序逐个执行的。因此,多个客户端对列表进行操作时不会出现数据混乱的情况。
然而,Redis的列表在阻塞方面只支持“队列为空”的情况,即当列表为空时,尝试从列表中弹出元素的操作会被阻塞,直到列表不为空为止。但是,Redis并不支持在列表已满时阻塞添加元素的操作,因为列表的大小是没有限制的,它会随着元素的添加而自动扩容,所以不会出现队列已满的情况。
阻塞版本命令
blpop和brpop是lpop和rpop的阻塞版本,它们的作用基本一致,都用于从列表的左侧(blpop)或右侧(brpop)取出一个元素,并在列表为空时进行阻塞。
特性和区别:
-
阻塞行为: 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但是,如果列表中没有元素,非阻塞版本会立即返回nil,而阻塞版本会根据设置的timeout参数进行阻塞一段时间。在此期间,Redis可以执行其他命令,但是执行该命令的客户端会处于阻塞状态。也就是说,此处的BLPOP和BRPOP看似耗时很久,但是并不会对Redis服务器产生负面影响。
-
遍历键: 如果命令中设置了多个键,blpop和brpop会从左向右依次遍历这些键。一旦有一个键对应的列表中有元素可弹出,则命令立即返回,而不会继续遍历后面的键。
-
多客户端竞争: 如果多个客户端同时对多个键执行pop操作,那么最先执行命令的客户端会得到弹出的元素。这意味着在并发环境下,多个客户端对多个列表执行blpop或brpop操作时,只有一个客户端会成功地弹出元素,其他客户端会被阻塞等待。
示例用法:
blpop key1 key2 key3 timeout
该命令将会从键key1、key2和key3对应的列表中,从左侧依次弹出一个元素。如果列表为空,则会根据设置的timeout参数进行阻塞,直到有元素可弹出或超时。
brpop key1 key2 key3 timeout
该命令将会从键key1、key2和key3对应的列表中,从右侧依次弹出一个元素。同样地,如果列表为空,则会根据设置的timeout参数进行阻塞,直到有元素可弹出或超时。
阻塞版本的 blpop 和 非阻塞版本 lpop 的区别
当列表不为空时:
- 使用 lpop user:1:messages 命令可以立即得到列表 user:1:messages 中的首个元素x。
- 使用 blpop user:1:messages 命令同样可以立即得到列表 user:1:messages 中的首个元素x。
因此,两者的行为是一致的。
当列表不为空时,且在5秒内没有新元素加入时:
- 使用 lpop user:1:messages 命令会立即返回nil,因为它是非阻塞的操作。
- 使用 blpop user:1:messages 5 命令则会阻塞执行,等待5秒钟。如果在这5秒内列表中没有新元素加入,它会在5秒后返回nil。
因此,两者的行为是不一致的。
当列表不为空时,且在5秒内有新元素加入时:
- 使用 lpop user:1:messages 命令会立即返回nil,因为它是非阻塞的操作。
- 使用 blpop user:1:messages 5 命令会阻塞执行,直到新元素加入列表,然后返回该新元素。
因此,两者的行为是不一致的。
BLPOP
BLPOP命令是LPOP命令的阻塞版本,用于从列表的左侧取出元素(即执行头部删除操作)。其功能与LPOP相似,但在列表为空时,BLPOP命令会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。命令会从左侧列表头部获取元素,如果列表为空,则会阻塞等待直到有元素可弹出或超时。BLPOP命令在功能上与LPOP相似,但不同之处在于阻塞特性。当列表为空时,LPOP会立即返回nil,而BLPOP会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。
这种阻塞特性使得BLPOP命令非常适合用于队列的消费者端。消费者可以通过BLPOP命令阻塞地等待队列中有新的元素可用,从而实现了实时地消费队列中的元素。通过阻塞等待,可以有效地减少系统的轮询频率,提高了系统的性能和效率。
它的语法如下:
BLPOP key [key ...] timeout
- key: 一个或多个列表的键。
- timeout: 超时时间,单位为秒(秒)。如果列表为空,连接将被阻塞直到超过此超时时间。
- Redis6开始,超时时间允许设置成小数。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 由于BLPOP是阻塞命令,如果列表为空,则它的时间复杂度取决于超时时间。
返回值: 返回被取出的元素,以及其所在的列表的键。如果列表为空且超时时间到达,则返回nil。
示例:
BLPOP mylist 10
这个示例命令将阻塞连接,直到名为"mylist"的列表中有元素可供取出,或者达到超时时间10秒为止。如果列表不为空,则会取出左侧的元素并返回;如果列表为空且超过了10秒,则返回nil。
阻塞等待:
再打开一个客户端,输入元素:
BRPOP
BRPOP命令是RPOP命令的阻塞版本,用于从列表的右侧取出元素(即执行尾部删除操作)。其功能与RPOP相似,但在列表为空时,BRPOP命令会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。命令会从右侧列表尾部获取元素,如果列表为空,则会阻塞等待直到有元素可弹出或超时。BRPOP命令在功能上与RPOP相似,但不同之处在于阻塞特性。当列表为空时,RPOP会立即返回nil,而BRPOP会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。
BRPOP命令的阻塞特性使其非常适合用于队列的消费者端。消费者可以通过BRPOP命令阻塞地等待队列中有新的元素可用,从而实现了实时地消费队列中的元素。这种阻塞机制可以避免轮询和忙等待,提高了系统的性能和效率。
它的语法如下:
BRPOP key [key ...] timeout
- key: 一个或多个列表的键。
- timeout: 超时时间,单位为秒(秒)。如果列表为空,连接将被阻塞直到超过此超时时间。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 由于BRPOP是阻塞命令,如果列表为空,则它的时间复杂度取决于超时时间。
返回值: 返回被取出的元素,以及其所在的列表的键。如果列表为空且超时时间到达,则返回nil。
示例:
BRPOP mylist 10
这个示例命令将阻塞连接,直到名为"mylist"的列表中有元素可供取出,或者达到超时时间10秒为止。如果列表不为空,则会取出右侧的元素并返回;如果列表为空且超过了10秒,则返回nil。
列表命令小结
操作类型 | 命令 | 时间复杂度 |
---|---|---|
添加 | RPUSH key value [value ...] | O(k),k 是元素个数 |
LPUSH key value [value ...] | O(k),k 是元素个数 | |
LINSERT key before|after pivot value | O(n),n 是 pivot 距离头尾的距离 | |
查找 | LRANGE key start end | O(s+n),s 是 start 偏移量,n 是 start 到 end 的范围 |
LINDEX key index | O(n),n 是索引的偏移量 | |
LLEN key | O(1) | |
删除 | LPOP key | O(1) |
RPOP key | O(1) | |
LREM key count value | O(k),k 是元素个数 | |
LTRIM key start end | O(k),k 是元素个数 | |
修改 | LSET key index value | O(n),n 是索引的偏移量 |
阻塞操作 | BLPOP key [key ...] timeout | O(1) |
内部编码
列表类型的内部编码有两种:
- ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的长度都小于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选用ziplist作为列表的内部编码实现来减少内存消耗。ziplist是一种紧凑且连续存储的数据结构,能够有效地节省内存空间。在ziplist中,多个列表项会被紧凑地存储在一起,并且每个列表项包含了一个字节的前缀信息用于表示该项的长度,所以它非常适合于小型列表的存储。
- linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。linkedlist是一种经典的链表数据结构,每个节点包含指向前一个和后一个节点的指针。相比ziplist,linkedlist在大型列表上的性能可能更好,因为它不会受到固定大小的限制,但它会消耗更多的内存空间。
选择使用哪种内部编码由Redis根据配置参数和列表的特征来决定,优先选择ziplist以节省内存,只有在无法满足ziplist的条件时才会使用linkedlist。
# 当元素个数较少且没有大元素时,内部编码为 ziplist
127.0.0.1:6379> rpush listkey e1 e2 e3
OK
127.0.0.1:6379> object encoding listkey
"ziplist"
# 当元素个数超过 512 时,内部编码为 linkedlist
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
# 当某个元素的长度超过 64 字节时,内部编码为 linkedlist
127.0.0.1:6379> rpush listkey "one string is bigger than 64 bytes ... 省略 ..."
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
- 当元素个数较少且没有大元素时,内部编码为ziplist。通过rpush命令向列表listkey中插入了几个元素,由于元素个数较少且没有大元素,Redis内部选择了ziplist作为内部编码,因此通过object encoding listkey命令可以看到列表的内部编码为ziplist。
- 当元素个数超过512时,内部编码为linkedlist。通过rpush命令向列表listkey中插入了超过512个元素,这超过了ziplist的元素个数限制,因此Redis内部自动切换到linkedlist作为内部编码。
- 当某个元素的长度超过64字节时,内部编码为linkedlist。通过rpush命令向列表listkey中插入了一个元素,其长度超过了64字节,这超过了ziplist的单个元素长度限制,因此Redis内部自动切换到linkedlist作为内部编码。
但是上述两种内部编码的方式都已经是老黄历了。在Redis 3 中,引入了一种名为QuickList(快速列表)的新编码方式,取代了之前使用的ziplist(压缩列表)作为列表数据结构的默认编码方式。QuickList提供了更加灵活和高效的方式来存储列表数据,尤其适用于存储较大的列表。这个变化使得Redis能够更有效地处理各种大小的列表数据,并提高了存储和操作的效率。
QuickList的特点包括:
- 链式结构:QuickList通过将多个压缩列表连接起来的链式结构来存储数据。这意味着列表不再受单个压缩列表大小的限制,而是可以动态地扩展以容纳更多的元素。
- 灵活性:每个压缩列表(ziplist)不再需要受到固定大小的限制,因此QuickList可以适应各种大小的列表,从较小的列表到非常大的列表。
- 节省空间:QuickList在空间利用方面更加高效,不仅因为它能够处理更大的列表,而且还因为它采用了更有效的内部存储结构。
- 操作效率:尽管QuickList可以处理更大的列表,但其操作效率仍然非常高。它在迭代和修改列表方面的性能表现良好,与之前的ziplist相比,操作效率并未明显下降。
- 配置变化:引入QuickList后,之前用于控制ziplist大小的配置参数,如list-max-ziplist-entries和list-max-ziplist-value已经失效,因为QuickList不再受到相同的限制。
通过引入QuickList,Redis在存储列表数据方面变得更加灵活和高效,能够更好地满足各种应用场景下的需求。
QuickList配置
使用场景
消息队列
Redis可以利用lpush和brpop命令组合实现经典的阻塞式生产者-消费者模型队列,如下图所示。在这个模型中,生产者客户端使用lpush命令从队列的左侧插入元素,而多个消费者客户端使用brpop命令阻塞式地从队列中争抢队首元素。通过多个客户端来保证消费的负载均衡和高可用性。
这种模型的工作原理是生产者不断向队列中推送新元素,而消费者则通过阻塞式的方式等待队列中出现新元素。当有新元素进入队列时,所有阻塞在brpop命令上的消费者会立即被唤醒,并争抢队首元素,谁先执行这个BRPOP操作,谁就可以抢到新来的元素,这样的设定就能构成“轮询”式的效果。这样可以确保队列中的元素被及时消费,并且消费者之间可以实现负载均衡,因为每个消费者都可以从队列中获取到元素。
假设消费者的执行顺序是1、2、3。当新的消息到达队列时,首先会唤醒消费者1,并将消息发送给它。消费者1处理完消息后,如果还想继续消费,需要重新执行BRPOP命令。接着,如果有新消息到达队列,会先唤醒消费者2,并将消息发送给它,依次类推。
这种轮询式的消息队列实现方式具有简单、高效的特点,并且能够很好地保证消息的有序性和公平性。但是它也存在一些缺点,比如可能会导致资源的浪费,因为消费者需要不断地轮询队列以获取新的消息,即使队列中并没有新消息到达。因此,在实际应用中需要根据具体场景选择合适的消息队列实现方式。
使用这种模型,可以轻松实现任务队列、消息队列等功能,而且由于Redis的高性能和高可用性,可以保证队列的稳定运行和高效处理大量任务。
Redis 阻塞消息队列模型
分频道的消息队列
Redis可以利用lpush和brpop命令实现分频道的消息队列,如下图所示。在这个模型中,Redis通过使用不同的键来模拟频道的概念,不同的消费者可以通过brpop不同的键值来订阅不同的频道。多频道可以达到解耦合的目的,当某些数据发生问题的时候不会对其他数据造成影响。
多频道(Multiple Channels)是一种在消息传递系统中常见的设计模式,可以有效实现解耦合。在消息传递系统中,不同的消息可能需要被不同的处理程序处理,而多频道机制可以将不同类型或不同用途的消息路由到不同的频道或主题中,从而使消息的处理程序可以根据需要选择订阅特定的频道,而不必处理所有消息。
通过多频道机制,系统可以实现以下优点:
-
解耦合: 将不同类型或不同用途的消息分发到不同的频道,可以降低系统各个组件之间的耦合度。当某些数据发生问题或需要变更时,只需修改特定频道的处理逻辑,而不会影响其他频道的处理流程,从而提高了系统的灵活性和可维护性。
-
灵活性: 多频道机制使得系统可以根据实际需求动态地调整消息的路由和处理方式,可以根据业务需求新增、修改或移除频道,而不会影响系统的其他部分。
-
可扩展性: 多频道机制为系统的扩展提供了良好的支持。通过添加新的频道,可以将新功能或新业务逻辑与现有系统进行解耦,从而实现系统的水平扩展和功能扩展。
-
故障隔离: 当系统的某一部分出现故障或异常时,多频道机制可以限制故障的影响范围,防止故障蔓延到系统的其他部分,提高了系统的容错性和可用性。
具体实现方法是,每个频道对应一个列表,生产者向指定频道的列表中插入消息,而消费者则通过brpop命令阻塞式地从指定频道的列表中获取消息。每个频道的消息队列是独立的,消费者之间不会相互影响,因为它们订阅的是不同的键。
这种分频道的消息队列模型可以应用于多种场景,比如实时通讯中的消息分发、事件驱动架构中的消息订阅等。通过这种模型,可以实现消息的有序处理和分发,同时确保不同频道之间的消息隔离,提高了系统的可扩展性和灵活性。
Redis 分频道阻塞消息队列模型
微博 Timeline
每个用户都有属于自己的Timeline(微博列表),现需要分页展示文章列表。此时我们可以考虑使用Redis的列表结构,因为列表不仅是有序的,同时支持按照索引范围获取元素。在这个场景中,用户的微博列表可以作为一个有序的列表,按照时间顺序存储用户发布的微博,通过列表结构可以方便地获取用户的最新微博,并且支持分页展示。
使用列表结构存储用户的微博列表具有以下优势:
- 有序存储:微博按照发布时间顺序存储在列表中,保证了用户的Timeline是按照时间顺序展示的。
- 索引范围获取:列表结构支持按照索引范围获取元素,这样可以方便地实现分页展示,用户可以快速浏览到自己Timeline中的不同页面内容。
-
每篇微博使用哈希结构存储,包括微博的属性:title、timestamp、content。例如:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx ... hmset mblog:n title xx timestamp 1476536196 content xxxxx
这里使用了哈希结构hmset来存储每篇微博的信息。
-
向用户Timeline添加微博,使用lpush命令将微博的键(例如mblog:1、mblog:3等)添加到用户的Timeline列表中。例如:
lpush user:1:mblogs mblog:1 mblog:3 ... lpush user:k:mblogs mblog:9
这里使用了列表lpush命令来将微博的键添加到用户的Timeline列表中。
-
分页获取用户的Timeline,例如获取用户1的前10篇微博。首先使用lrange命令获取用户的Timeline列表中指定范围内的微博键,然后遍历这些键并使用hgetall命令获取每篇微博的详细信息。例如:
keylist = lrange user:1:mblogs 0 9 for key in keylist: hgetall key
这段代码首先通过lrange命令获取用户1的前10篇微博的键,然后遍历这些键,并使用hgetall命令获取每篇微博的详细信息。
这种方案使用了Redis的哈希结构和列表结构,可以高效地存储和检索用户的微博列表,并且支持分页展示,为用户提供了良好的使用体验。
但是此方案在实际应用中,使用列表结构存储用户的微博列表可能会遇到两个问题:
- 1 + n 问题: 当每次分页获取的微博数量较多时,需要执行多次hgetall操作,这可能导致性能下降。为了解决这个问题,可以考虑使用pipeline(流水线)模式批量提交命令,或者将微博不采用哈希类型,而是使用序列化的字符串类型存储,然后使用mget命令一次性获取多篇微博的内容。这样可以减少与Redis服务器的通信次数,提高查询效率。
- 分裂获取文章时的性能问题: 使用lrange命令在列表两端表现较好,但在获取列表中间的元素时性能可能较差。为了解决这个问题,可以考虑对列表进行拆分,将较长的列表拆分成多个子列表,然后根据需要选择合适的子列表进行分页查询。这样可以避免在获取中间元素时性能下降的问题,提高查询效率。
💡在选择列表类型时,可以参考以下原则:
-
同侧存取(lpush + lpop 或者 rpush + rpop)为栈: 如果需要实现后进先出(LIFO)的数据结构,例如栈,可以使用同侧存取方式。在这种模式下,新的元素会被推入(push)到列表的同一侧,而弹出(pop)操作也会从该侧取出最新插入的元素。
-
异侧存取(lpush + rpop 或者 rpush + lpop)为队列: 如果需要实现先进先出(FIFO)的数据结构,例如队列,可以使用异侧存取方式。在这种模式下,新的元素会被推入(push)到列表的一侧,而弹出(pop)操作会从另一侧取出最早插入的元素。
根据具体的业务需求和数据访问模式,选择合适的列表类型能够更好地满足功能要求,并且保持数据的有序性。
Set 集合
集合类型在Redis中用于存储多个字符串类型的元素,与列表类型不同的是,集合具有以下特点:
-
无序性:集合中的元素是无序排列的,即它们没有特定的顺序。
-
元素唯一性:集合中不允许出现重复的元素,每个元素都是唯一的。
集合类型最多可以存储约 2^32 - 1 个元素。
Redis提供了丰富的集合操作,包括增删查改以及集合间的交集、并集和差集等操作。合理地利用集合类型可以很好地解决实际开发中的各种问题,例如去重、计数、标签关联等。
和 List 类似,集合(Set)中的每个元素都是字符串类型的,但是我们也可以利用字符串的存储特性来存储结构化数据,例如使用 JSON 格式将结构化数据存储为字符串。这样做的好处是可以在 Redis 中方便地存储和检索结构化数据,同时保持了数据的简洁性和灵活性。就可以方便地在 Redis 中存储和检索结构化数据了。当我们需要访问用户信息时,可以从集合中获取对应的 JSON 字符串,并解析成对象进行处理。
集合类型
普通命令
SADD
SADD命令用于将一个或多个元素添加到集合(set)中。需要注意的是,集合中不允许重复的元素存在,如果集合中已经存在要添加的元素,则SADD命令不会重复添加,确保了集合中不会出现重复的元素。
它的语法如下:
SADD key member [member ...]
- key: 集合的键。
- member: 要添加到集合中的元素,可以指定一个或多个。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(1)。
返回值: 返回成功添加到集合中的元素个数,不包括已经存在于集合中的元素。
示例:
SADD myset member1 member2 member3
这个示例命令将元素"member1"、"member2"和"member3"添加到名为"myset"的集合中,并返回成功添加的元素个数。
SMEMBERS
SMEMBERS命令用于获取集合(set)中的所有元素。该命令会返回集合中的所有元素,但需要注意的是,集合中的元素是无序的,因此返回的元素列表也是无序的。
它的语法如下:
SMEMBERS key
- key: 集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N为集合中的元素数量。
返回值: 返回包含集合中所有元素的列表。
示例:
SMEMBERS myset
这个示例命令将返回名为"myset"的集合中所有的元素列表。
SISMEMBER
SISMEMBER命令用于判断集合(set)中是否包含指定的元素。如果元素存在于集合中,则返回1;如果元素不存在于集合中,则返回0。
它的语法如下:
SISMEMBER key member
- key: 集合的键。
- member: 要检查的元素。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(1)。
返回值: 如果元素在集合中,则返回1;如果元素不在集合中或者集合不存在,则返回0。
示例:
SISMEMBER myset "member1"
这个示例命令将判断集合"myset"中是否包含元素"member1",如果存在则返回1,否则返回0。
SCARD
SCARD命令用于获取集合(set)中的基数,即集合中元素的个数。它的语法格式为SCARD key,其中key是要获取基数的集合的键名。
它的语法如下:
SCARD key
- key: 集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(1)。
返回值: 返回集合内元素的个数。
示例:
SCARD myset
这个示例命令将返回集合"myset"中元素的个数。
SPOP
SPOP命令用于从集合(set)中随机删除并返回一个或多个元素。由于集合中的元素是无序的,因此无法确定返回的是哪个元素,可视为随机行为。如果指定了要弹出的元素数量,命令将返回指定数量的随机元素;如果未指定数量,则返回一个随机元素。
它的语法如下:
SPOP key [count]
- key: 集合的键。
- count: (可选)要删除并返回的元素个数,默认为1。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是count的大小。
返回值: 返回被删除的元素。如果count未指定或为1,则返回单个元素;如果count大于1,则返回一个包含被删除元素的列表。
示例:
SPOP myset
这个示例命令将从集合"myset"中随机删除并返回一个元素。
SPOP myset 3
这个示例命令将从集合"myset"中随机删除并返回3个元素,并将这些元素组成一个列表返回。
官方文档也承诺过了,完全是随机的。
还有一个很类似的命令,SRANDMEMBER。
SRANDMEMBER 是 Redis 中的一个命令,用于从指定的集合中随机地获取成员。根据提供的参数 count,其行为略有不同:
- 当不提供 count 参数或者提供的 count 参数为正数时,命令返回一个或多个随机成员。
- 如果提供的 count 参数为负数,则返回指定数量的不重复随机成员。
- 如果集合为空,则返回 nil 值。
这个命令的语法非常简单,只需指定要获取随机成员的集合名称以及可选的 count 参数。
SRANDMEMBER 命令在 Redis 2.0.0 版本之后开始提供。
SRANDMEMBER 和 SPOP 在功能上还是有一些区别的:
-
返回方式:
- SRANDMEMBER 返回一个或多个随机成员,而不会从集合中移除这些成员。
- SPOP 从集合中随机地弹出一个成员并返回,即从集合中移除这个成员。
-
参数用法:
- SRANDMEMBER 的 count 参数用法不同于 SPOP。在 SRANDMEMBER 中,count 可以是正数、负数或不提供。当提供正数时,表示返回多个随机成员;当提供负数时,表示返回指定数量的不重复随机成员;不提供 count 参数时,表示返回一个随机成员。
- SPOP 的 count 参数只能是正整数,用于指定要弹出的成员数量。
-
对集合的影响:
- SRANDMEMBER 不会修改集合的内容,仅是从集合中随机获取成员。
- SPOP 会从集合中移除所弹出的成员。
综上所述,SRANDMEMBER 主要用于获取集合中的随机成员,而不会对集合做出任何修改,而 SPOP 则用于从集合中弹出一个或多个成员,并将其从集合中移除。
SMOVE
SMOVE命令用于从源集合(source)中移除指定的元素,并将其添加到目标集合(destination)中。如果源集合中存在该元素,则该元素会被移动到目标集合中;如果源集合中不存在该元素,则命令不执行任何操作。
它的语法如下:
SMOVE source destination member
- source: 源集合的键。
- destination: 目标集合的键。
- member: 要移动的元素。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(1)
返回值: 返回1表示移动成功,返回0表示失败。
示例:
SMOVE set1 set2 member1
这个示例命令将集合"set1"中的"member1"元素移动到集合"set2"中。
SREM
SREM命令用于从集合中删除指定的元素。如果元素存在于集合中,则将其删除;如果元素不存在,则命令不执行任何操作。
它的语法如下:
SREM key member [member ...]
- key: 集合的键。
- member: 要删除的元素,可以指定多个。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),N 是要删除的元素个数。
返回值: 返回本次操作删除的元素个数。
示例:
SREM myset member1 member2
这个示例命令将集合"myset"中的"member1"和"member2"元素从集合中删除。
集合间操作
交集(inter)、并集(union)、差集(diff)的概念如图所示:
集合求交集、并集、差集
SINTER
SINTER命令用于获取给定集合的交集中的元素。这个命令执行时,Redis会找出所有给定集合中共同存在的元素,并将它们作为结果返回。换句话说,SINTER命令返回的结果集合包含了所有给定集合中共同拥有的元素。
它的语法如下:
SINTER key [key ...]
- key: 一个或多个集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N * M),其中N是最小的集合元素个数,M是最大的集合元素个数。
返回值: 返回交集的元素。
示例:
SINTER set1 set2 set3
这个示例命令将返回集合"set1"、"set2"和"set3"之间的交集中的元素。
SINTERSTORE
SINTERSTORE命令用于获取指定多个集合的交集,即找出同时存在于所有给定集合中的元素,并将这些元素保存到指定的目标集合中。这样做的好处是可以在Redis中方便地找出共同满足某些条件的元素,并将其保存下来以供后续使用。在执行SINTERSTORE命令时,Redis会遍历每个给定集合中的元素,并将同时存在于所有集合中的元素添加到目标集合中。最终,目标集合中包含的元素就是所有给定集合的交集。
它的语法如下:
SINTERSTORE destination key [key ...]
- destination: 目标集合的键。
- key: 一个或多个集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N * M),其中N是最小的集合元素个数,M是最大的集合元素个数。
返回值: 返回交集的元素个数。
示例:
SINTERSTORE destination set1 set2 set3
这个示例命令将计算集合"set1"、"set2"和"set3"之间的交集,并将结果保存到目标集合"destination"中。
SUNION
SUNION命令用于获取指定集合的并集,即将所有给定集合中的元素汇总到一起,并去除重复元素,返回并集结果。这个命令执行时,Redis会遍历每个指定集合中的元素,并将它们汇总到一起。在汇总的过程中,重复的元素会被自动去除,确保返回的结果中不包含重复的元素。最终,命令返回的结果就是所有指定集合的并集。
它的语法如下:
SUNION key [key ...]
- key: 一个或多个集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是给定的所有集合的总的元素个数。
返回值: 返回并集的元素。
示例:
SUNION set1 set2 set3
这个示例命令将计算集合"set1"、"set2"和"set3"之间的并集,并返回结果。
SUNIONSTORE
SUNIONSTORE命令用于获取指定集合的并集,并将结果保存到目标集合中。具体而言,它会找出所有给定集合中的元素,去除重复元素后,将结果存储在目标集合中。这个命令执行时,Redis会遍历每个指定集合中的元素,并将它们汇总到一起。在汇总的过程中,重复的元素会被自动去除,确保存储到目标集合中的结果不包含重复的元素。最终,目标集合中存储的就是所有指定集合的并集。
它的语法如下:
SUNIONSTORE destination key [key ...]
- destination: 目标集合的键。
- key: 一个或多个集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是给定的所有集合的总的元素个数。
返回值: 返回并集的元素个数。
示例:
SUNIONSTORE destination set1 set2 set3
这个示例命令将计算集合"set1"、"set2"和"set3"之间的并集,并将结果保存到名为"destination"的目标集合中。
SDIFF
SDIFF命令用于计算一个或多个集合的差集,并返回结果集合中的所有元素。它会找出存在于第一个集合但不存在于其他集合中的元素,然后将这些元素作为结果返回。这个命令执行时,Redis会遍历第一个集合中的所有元素,并将其与其他指定集合进行比较。对于存在于第一个集合但不存在于其他集合的元素,都会被添加到结果集合中。最终,SDIFF命令返回的就是差集的结果集合,其中包含了所有不同于其他集合的元素。
它的语法如下:
SDIFF key [key ...]
- key: 一个或多个集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是给定的所有集合的总的元素个数。
返回值: 返回差集的元素。
示例:
SDIFF set1 set2 set3
这个示例命令将计算集合"set1"相对于"set2"和"set3"的差集,即包含在"set1"中但不包含在其他集合中的元素。
SDIFFSTORE
SDIFFSTORE命令用于计算一个或多个集合的差集,并将结果存储在目标集合中。该命令会找出所有存在于第一个集合但不存在于其他集合中的元素,并将这些元素保存到目标集合中。如果目标集合在命令执行前不存在,则会被创建。如果目标集合已经存在,则会被覆盖。
它的语法如下:
SDIFFSTORE destination key [key ...]
- destination: 目标集合的键。
- key: 一个或多个集合的键。
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是给定的所有集合的总的元素个数。
返回值: 返回差集的元素个数。
示例:
SDIFFSTORE diffSet set1 set2 set3
这个示例命令将计算集合"set1"相对于"set2"和"set3"的差集,并将结果保存到名为"diffSet"的目标集合中。
集合类型命令小结
命令 | 时间复杂度 |
---|---|
SADD | O(k),k 是元素个数 |
SREM | O(k),k 是元素个数 |
SCARD | O(1) |
SISMEMBER | O(1) |
SRANDMEMBER | O(n),n 是 count |
SPOP | O(n),n 是 count |
SMEMBERS | O(k),k 是元素个数 |
SINTER | O(m * k),k 是多个集合中元素最小的个数,m 是键个数 |
SINTERSTORE | O(m * k),k 是多个集合中元素最小的个数,m 是键个数 |
SUNION | O(k),k 是多个集合的元素个数总和 |
SUNIONSTORE | O(k),k 是多个集合的元素个数总和 |
SDIFF | O(k),k 是多个集合的元素个数总和 |
SDIFFSTORE | O(k),k 是多个集合的元素个数总和 |
内部编码
集合类型的内部编码有两种:
-
intset(整数集合):当集合中的元素都是整数,并且元素的个数少于 Redis 配置参数 set-max-intset-entries(默认为 512 个)时,Redis 会选择 intset 作为集合的内部实现。intset 是一种紧凑且高效的数据结构,可以有效地节省内存空间。它以有序的方式存储整数元素,并通过压缩和编码来减少内存占用。
-
hashtable(哈希表):如果集合中的元素不满足 intset 的条件,即包含非整数元素或元素个数超过了 set-max-intset-entries 的限制,Redis 就会使用 hashtable 作为集合的内部实现。hashtable 提供了更灵活的存储方式,适用于各种类型的元素以及大规模的集合数据。它采用哈希表的方式存储元素,通过哈希算法来实现快速的元素查找和插入,但相比于 intset,它会占用更多的内存空间。
1、intset(整数集合):当集合中的元素都是整数,并且元素的个数较少时,Redis 会选择 intset 作为集合的内部实现。intset 是一种紧凑且高效的数据结构,它以有序的方式存储整数元素,并通过压缩和编码来减少内存占用。
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding setkey
"intset"
2、hashtable(哈希表):当集合中的元素包含非整数元素,或者元素个数超过了 Redis 配置参数 set-max-intset-entries(默认为 512 个)时,Redis 会使用 hashtable 作为集合的内部实现。hashtable 提供了更灵活的存储方式,适用于各种类型的元素以及大规模的集合数据。
127.0.0.1:6379> sadd setkey 1 2 3 4 ... 513
(integer) 513
127.0.0.1:6379> object encoding setkey
"hashtable"
127.0.0.1:6379> sadd setkey a
(integer) 1
127.0.0.1:6379> object encoding setkey
"hashtable"
在第一个示例中,集合中的元素都是整数且不超过 512 个,因此 Redis 选择了 intset 作为内部编码。在第二个示例中,集合中的元素超过了 512 个,因此 Redis 使用了 hashtable 作为内部编码。在第三个示例中,集合中存在非整数元素,所以同样使用了 hashtable 作为内部编码。
使用场景
用户画像
集合类型在 Redis 中典型的使用场景之一是标签(tag)。举例来说,假设有两个用户 A 和 B,他们对不同领域的兴趣可以被抽象为标签。比如,用户 A 对娱乐和体育感兴趣,用户 B 对历史和新闻感兴趣。这些兴趣点可以被表示为标签,如 "娱乐"、"体育"、"历史"、"新闻" 等等。
利用集合类型,可以很方便地管理用户的兴趣标签。通过将用户的兴趣标签存储在集合中,可以轻松地查询具有相同兴趣的用户,或者分析用户之间的共同兴趣标签。这些数据对于增强用户体验、提供个性化推荐等方面都非常有帮助。举例来说,电子商务网站可以根据用户的兴趣标签,为用户推荐不同领域的产品,提升用户体验和用户黏性。
下面的演示通过集合类型来实现标签的若干功能:
1)给用户添加标签:使用 SADD 命令将标签添加到用户对应的集合中,每个集合表示一个用户的标签集合。
# 给用户添加标签
redis.sadd("user:1:tags", "tag1", "tag2", "tag5")
redis.sadd("user:2:tags", "tag2", "tag3", "tag5")
# ...
2)给标签添加用户:使用 SADD 命令将用户添加到对应标签的集合中,每个集合表示拥有相同标签的用户集合。
# 给标签添加用户
redis.sadd("tag1:users", "user:1", "user:3")
redis.sadd("tag2:users", "user:1", "user:2", "user:3")
# ...
3)删除用户下的标签:使用 SREM 命令从用户的标签集合中删除指定的标签。
# 删除用户下的标签
redis.srem("user:1:tags", "tag1", "tag5")
# ...
4)删除标签下的用户:使用 SREM 命令从标签的用户集合中删除指定的用户。
# 删除标签下的用户
redis.srem("tag1:users", "user:1")
redis.srem("tag5:users", "user:1")
# ...
5)计算用户的共同兴趣标签:使用 SINTER 命令计算多个用户的标签集合的交集,从而得到共同兴趣的标签。
# 计算用户的共同兴趣标签
common_tags = redis.sinter("user:1:tags", "user:2:tags")
print(common_tags)
通过这种方式,可以方便地管理用户的标签信息,并根据用户的兴趣进行个性化推荐、群体分析等操作,从而提升用户体验和服务质量。
综合一下,使用Set数据结构在Redis中具有多种适用场景,包括但不限于:
-
存储标签和用户画像:Set可以用来存储用户的标签信息,例如兴趣爱好、偏好等。每个用户的标签集合可以存储在一个Set中,方便快速地进行查询和更新。通过这种方式,可以实现用户画像的建立和维护,从而为个性化推荐、定向营销等提供支持。
-
求交集,如共同好友、共同兴趣标签:Set数据结构支持交集运算,可以方便地求取多个集合之间的交集。在社交网络应用中,可以将用户的好友关系存储在不同的Set中,通过求取两个用户好友集合的交集,快速地得到共同的好友列表。同样地,也可以将用户的兴趣标签存储在不同的Set中,通过求取多个用户兴趣标签集合的交集,找到具有相同兴趣爱好的用户。
-
统计UV(独立访客):在互联网产品中,用户规模通常通过PV(页面访问量)和UV(独立访客)等指标来衡量。Set数据结构可以用来记录独立访客的信息,每个独立访客可以用一个Set来表示。通过记录每个用户的访问行为,例如用户的登录状态、访问时间等,可以统计出独立访客的数量,从而评估产品的用户规模和受欢迎程度。
总之,Set数据结构在Redis中具有广泛的应用场景,特别适合用于存储集合类型的数据以及进行集合运算。通过合理地利用Set数据结构,可以快速地实现各种复杂的数据操作和统计分析,为产品的功能和性能提升提供有力支持。
Zset 有序集合
有序集合(Sorted Set)是 Redis 中独特且功能强大的一种数据结构,它在某种程度上结合了集合和有序列表的特性。与普通的集合不同,有序集合中的每个成员都关联着一个分数(score),而这个分数是浮点类型的,它赋予了每个成员一个排序权重。这种特性使得有序集合成为了一个非常适合存储排行榜、计分板等需要按照某种顺序展示数据的场景。
举例来说,在一个游戏应用中,我们可以使用有序集合来存储玩家的游戏得分,其中玩家的ID作为成员,而得分则作为每个成员的分数。这样,我们就能够快速地根据分数对玩家进行排名,而不需要每次都进行排序操作。
有序集合的成员是唯一的,但是分数可以重复。如果两个成员具有相同的分数,它们将按照成员的字典序(lexicographical order)来排序。这一点非常重要,因为它为我们提供了更多的灵活性。例如,如果两名玩家得分相同,我们可以按照其ID进行排序,以保持排序结果的稳定性。
需要注意的是,虽然有序集合中的成员是有序的,但是它不是通过像数组或列表那样的下标来实现的。相反,有序集合中的成员是通过分数来排序的,这意味着我们可以根据分数的范围快速地获取成员。这种特性使得有序集合非常适合处理范围查询,例如获取某个范围内的排名、得分等信息。
总的来说,有序集合在 Redis 中具有广泛的应用场景,它不仅能够提供高效的排名和排序功能,还能够满足各种需要有序性的场景需求,从而为开发者提供了丰富而强大的工具。
有序集合
有序集合(Sorted Set)在实际开发中具有广泛的应用场景,主要由于其提供了丰富的功能和灵活性。首先,有序集合允许我们按照成员的分数进行排序,这为我们处理排行榜、计分板等场景提供了便利。其次,有序集合还提供了一系列功能,如根据指定分数范围查找成员、获取成员的排名等,这些功能能够满足各种实际需求。
💡值得注意的是,有序集合中的成员是唯一的,即同一个成员只能出现一次。这意味着每个成员都对应着唯一的一个分数。但是,不同的成员可以拥有相同的分数,这类似于一次考试之后,每个人都会有一个唯一的分数,但是分数允许出现相同的情况。这种特性为我们提供了更多的灵活性,在处理具有相同分数的成员时更加方便。
综上所述,合理地利用有序集合能够帮助我们解决许多实际开发中的问题,特别是那些需要排序、排名和范围查找等功能的场景。有序集合不仅提供了高效的数据结构,还能够满足各种实际需求,为我们的开发工作提供了强大的支持。
列表、集合、有序集合三者的异同点
数据结构 | 是否允许重复元素 | 是否有序 | 有序依据 | 应用场景 |
---|---|---|---|---|
列表 | 是 | 是 | 索引下标 | 时间轴、消息队列 |
集合 | 否 | 否 | 无 | 标签、社交 |
有序集合 | 否 | 是 | 分数 | 排行榜系统、社交 |
在有序集合(Sorted Set)中,每个成员(member)都关联有一个分数(score),它们之间的关系可以被称为一个“pair”,类似于C++中的std::pair。但是,需要强调的是,这种关系与哈希表中的键值对(key-value pair)是不同的。在哈希表中,键和值有明确的角色区分,通过键可以找到对应的值,而在有序集合中,成员和分数的关系是一对一的,它们之间的关系更像是一个元组,而不是键值对。
另外,在有序集合中,我们既可以通过成员找到对应的分数,也可以通过分数找到匹配的成员。这种双向的关联关系使得有序集合非常灵活,并且可以支持多种基于成员或分数的操作,如按照分数范围查询成员,按照成员进行排名等。这种特性是有序集合在实际应用中非常有用的一部分。
普通命令
ZADD
ZADD命令用于向有序集合(Sorted Set,简称zset)中添加或更新指定的元素以及关联的分数。有序集合是Redis中一种特殊的数据结构,它类似于普通的集合,但是每个元素都会关联一个分数(score),这使得有序集合中的元素按照分数进行排序。
命令语义上包括两个主要操作:
- 如果指定的元素不存在于有序集合中,则将该元素插入有序集合,并将其关联的分数设置为指定的分数。
- 如果指定的元素已经存在于有序集合中,则更新该元素的分数为指定的分数。
- 如果排序元素对应的分数相同,则按照元素自身字符串的字典序进行排列。
分数应为double类型,并且可以使用特殊值+inf和-inf来表示正负无穷大。如果指定的分数值不是合法的double类型,则命令会返回错误。
前面我们提到,所谓有序集合的“有序”,不再代表“顺序有意义”,而是真实的“升序、降序”。但是实际上有序集合内部就是按照升序的方式来排列的!
ZADD命令支持以下选项:
- NX:仅用于添加新元素(member),不会更新已经存在的元素,会更新已经存在的元素对应的分数。
- XX:仅用于更新已经存在的元素,不会添加新元素。
- GT(greater than):仅将元素添加到分数(score)大于给定分数的集合中。
- LT(less than):仅将元素添加到分数(score)小于给定分数的集合中。
- CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定此选项后,还会包含本次更新的元素的个数。
- INCR:此选项使命令类似于 ZINCRBY,将元素的分数加上指定的分数。此时只能指定一个元素和分数。
语法:
ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]
- key: 有序集合的键名。
- NX/XX: 用于控制行为。
- GT/LT: 用于限定添加的范围。
- CH: 控制返回结果。
- INCR: 控制行为。
- score: 元素的分数。
- member: 元素的值。
命令有效版本: Redis 1.2.0之后可用。
时间复杂度: O(log(N)),其中N是有序集合中元素的数量。每添加一次都是O(log(N))。
返回值: 返回本次添加成功的元素个数。
示例:
ZADD myset NX 2.0 "two" CH
这个示例命令将在名称为“myset”的有序集合中添加元素"two",其分数为2.0,并且仅在元素不存在时才执行添加操作。如果成功添加了元素,则返回值包含1(表示添加的元素个数)。
此时返回0不代表失败,而是因为没有进行更新操作,只是修改:
ZCARD
ZCARD命令用于获取一个有序集合(zset)的基数(cardinality),即有序集合中的元素个数。在Redis中,有序集合是一种特殊的数据结构,它与普通集合类似,但是每个元素都会关联一个分数(score),这使得有序集合的元素是按照分数进行排序的。ZCARD命令返回指定有序集合中的元素数量,可以帮助用户了解集合的规模和大小。对于大规模的有序集合,ZCARD命令可以在常量时间内(O(1))返回元素的数量,这使得它非常高效。该命令的返回值为有序集合中元素的个数,如果有序集合不存在,则返回0。
语法:
ZCARD key
- key: 目标有序集合的键名。
命令有效版本: Redis 1.2.0之后可用。
时间复杂度: O(1),即常数时间复杂度。
返回值: 返回有序集合内的元素个数。
示例:
ZCARD myset
这个示例命令将返回名称为“myset”的有序集合中元素的个数。
ZCOUNT
ZCOUNT命令用于返回有序集合(Sorted Set,简称zset)中分数在给定范围内的元素个数。默认情况下,范围的边界min和max是包含的,即会计算范围内的所有元素。但是,如果希望排除范围的边界,可以使用圆括号。
命令语义上包括以下几点:
- 返回有序集合中分数在[min, max]范围内(包括min和max)的元素个数。
- 如果[min, max]范围内的边界值被排除,即使用圆括号表示,则不包括对应的边界值。
在有序集合中,Redis内部会维护每个元素的排序或次序。这使得在查询元素时,可以立即知道元素的排序,而不需要额外的计算。因此,要计算有序集合中元素的数量,只需获取最大元素的排序值(max)和最小元素的排序值(min),然后用最大排序值减去最小排序值,即可得到元素的数量。这个操作的时间复杂度是O(1),即常数时间复杂度。
语法:
ZCOUNT key min max
- key: 目标有序集合的键名。
- min: 最小分数。可以是浮点数。
- max: 最大分数。可以是浮点数。
- 在有序集合中可以使用+inf和-inf来代表正无穷和负无穷
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: O(log(N)),其中N是有序集合中元素的个数。
返回值: 返回满足条件的元素个数。
示例:
ZCOUNT myset (20 100
这个示例命令将返回名称为“myset”的有序集合中分数在20(不包含)和100之间(包含)的元素个数。
ZRANGE
ZRANGE命令用于返回有序集合(Sorted Set,简称zset)中指定区间内的元素列表,按照分数从低到高排序。
该命令的基本语义包括以下几点:
- 返回有序集合中分数从低到高排序的指定区间内的元素列表。
- 区间范围由参数start和stop决定,start和stop都是以0为基准的索引,其中0表示第一个元素,1表示第二个元素,以此类推。即[start, stop]表示的是从第start个元素到第stop个元素,包括start和stop在内。
- 可以选择是否返回元素对应的分数,如果希望返回分数,则需要设置参数withscores为true。
语法:
ZRANGE key start stop [WITHSCORES]
- key: 目标有序集合的键名。
- start: 开始索引。
- stop: 结束索引。
- WITHSCORES: 可选参数,是否返回元素的分数。如果指定了该参数,则返回元素和对应的分数。
命令有效版本: Redis 1.2.0之后可用。
时间复杂度: O(log(N)+M),其中N是有序集合中元素的个数,M是返回的元素个数。
返回值: 返回指定区间内的元素列表。
示例:
ZRANGE myset 0 2
这个示例命令将返回名称为“myset”的有序集合中索引在0到2之间的元素列表,按照分数从低到高排序。
ZRANGE myset 0 2 WITHSCORES
这个示例命令将返回名称为“myset”的有序集合中索引在0到2之间的元素列表,同时返回元素对应的分数。
ZREVRANGE
ZREVRANGE命令用于返回有序集合(Sorted Set,简称zset)中指定区间内的元素列表,按照分数从高到低排序。
该命令的基本语义包括以下几点:
- 返回有序集合中分数从高到低排序的指定区间内的元素列表。
- 区间范围由参数start和stop决定,start和stop都是以0为基准的索引,其中0表示第一个元素,1表示第二个元素,以此类推。即[start, stop]表示的是从第start个元素到第stop个元素,包括start和stop在内。
- 可以选择是否返回元素对应的分数,如果希望返回分数,则需要设置参数withscores为true。
语法:
ZREVRANGE key start stop [WITHSCORES]
- key: 目标有序集合的键名。
- start: 开始索引。
- stop: 结束索引。
- WITHSCORES: 可选参数,是否返回元素的分数。如果指定了该参数,则返回元素和对应的分数。
命令有效版本: Redis 1.2.0之后可用。
时间复杂度: O(log(N)+M),其中N是有序集合中元素的个数,M是返回的元素个数。
返回值: 返回指定区间内的元素列表。
示例:
ZREVRANGE myset 0 2
这个示例命令将返回名称为“myset”的有序集合中索引在0到2之间的元素列表,按照分数从高到低排序。
ZREVRANGE myset 0 2 WITHSCORES
这个示例命令将返回名称为“myset”的有序集合中索引在0到2之间的元素列表,同时返回元素对应的分数。
ZRANGEBYSCORE
ZRANGEBYSCORE命令用于返回有序集合(Sorted Set,简称zset)中分数在指定范围内的元素列表,默认情况下,范围包括最小值和最大值。
该命令的基本语义包括以下几点:
- 返回有序集合中分数在指定范围内的元素列表。
- 范围由参数min和max决定,min和max可以是实数,表示分数的范围。默认情况下,范围包括min和max在内,可以通过在min和max前面加上括号来排除min和max。例如,"(1.0"表示大于1.0的范围,"[1.0"表示大于等于1.0的范围。
- 可以选择是否返回元素对应的分数,如果希望返回分数,则需要设置参数withscores为true。
语法:
ZRANGEBYSCORE key min max [WITHSCORES]
- key: 目标有序集合的键名。
- min: 最小分数。
- max: 最大分数。
- WITHSCORES: 可选参数,是否返回元素的分数。如果指定了该参数,则返回元素和对应的分数。
命令有效版本: Redis 1.0.5之后可用。
时间复杂度: O(log(N)+M),其中N是有序集合中元素的个数,M是返回的元素个数。
返回值: 返回分数在指定范围内的元素列表。
示例:
ZRANGEBYSCORE myset 0 5
这个示例命令将返回名称为“myset”的有序集合中分数在0到5之间的元素列表。
ZRANGEBYSCORE myset (0 5
这个示例命令将返回名称为“myset”的有序集合中分数在0到5之间的元素列表,不包括分数为0和5的元素。
ZRANGEBYSCORE myset (0 5 WITHSCORES
这个示例命令将返回名称为“myset”的有序集合中分数在0到5之间的元素列表,同时返回元素对应的分数。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
ZPOPMAX
ZPOPMAX命令用于删除并返回有序集合(Sorted Set,简称zset)中分数最高的元素,可以指定要删除并返回的元素数量。
该命令的基本语义包括以下几点:
- 删除并返回有序集合中分数最高的元素。
- 可以指定要删除并返回的元素数量,如果不指定数量,则默认为1。
- 如果有序集合为空,则返回nil。
- 返回的元素以及对应的分数作为数组返回。
- 如果存在多个元素分数相同,仍然只删除其中字典序排序靠后的那个元素。
语法:
ZPOPMAX key [count]
- key: 目标有序集合的键名。
- count: 可选参数,要删除并返回的元素数量。默认为1,表示删除并返回分数最高的一个元素。如果指定了大于1的值,将删除并返回分数最高的多个元素。
命令有效版本: Redis 5.0.0之后可用。
时间复杂度: O(log(N) * M),其中N是有序集合中元素的个数,M是要删除并返回的元素数量。
返回值: 返回被删除的元素列表,包含分数和元素。
示例:
ZPOPMAX myset
这个示例命令将从名称为“myset”的有序集合中删除并返回分数最高的元素,即分数最高的一个元素。
ZPOPMAX myset 2
这个示例命令将从名称为“myset”的有序集合中删除并返回分数最高的2个元素。
有序集合在某种程度上可以被视为一个优先级队列,因为它们具有按照分数排序的特性。在实际应用中,有序集合常被用作优先级队列来处理需要按照优先级顺序处理的任务或消息。
为了满足在优先级队列中需要等待新元素的需求,Redis提供了阻塞式命令BZPOPMIN和BZPOPMAX,用于在有序集合的左侧或右侧阻塞式地弹出具有最低分数或最高分数的元素。
- BZPOPMIN:该命令会阻塞地等待并弹出具有最低分数的元素,直到有元素可用或超时。语法为:BZPOPMIN key [key ...] timeout。
- BZPOPMAX:该命令会阻塞地等待并弹出具有最高分数的元素,直到有元素可用或超时。语法为:BZPOPMAX key [key ...] timeout。
这些阻塞式命令允许客户端在有序集合为空时阻塞等待,直到有新的元素可用为止。这种功能对于实现优先级队列非常有用,因为它允许任务或消息按照优先级顺序被处理,而不需要客户端轮询检查有序集合是否为空。
BZPOPMAX
BZPOPMAX命令是ZPOPMAX命令的阻塞版本,用于在有序集合(Sorted Set,简称zset)中阻塞式地删除并返回分数最高的元素。
该命令的基本语义包括以下几点:
- 如果有序集合为空,则BZPOPMAX命令会阻塞客户端,直到有元素可用为止,或者达到指定的超时时间。
- 如果有序集合不为空,则会删除并返回分数最高的元素。
- 可以指定要阻塞并删除并返回的有序集合的数量,如果不指定数量,则默认为1。
- 返回的元素以及对应的分数作为数组返回。
语法:
BZPOPMAX key [key ...] timeout
- key: 一个或多个目标有序集合的键名。
- timeout: 阻塞超时时间,以秒为单位,但是同时又支持double。如果在指定的时间内没有元素可供删除并返回,则命令将阻塞等待,直到超时或者有元素可供处理。
命令有效版本: Redis 5.0.0之后可用。
时间复杂度: O(log(N)),其中N是有序集合中元素的个数。
如果是当前BZPOPMAX同时监听了M个key,此时的时间复杂度是O(log(N)*M)?
当然不是!*M代表在每个这样的key上都执行一次删除操作,而这里是从若干key中只删除一次。也就是说,M并不直接影响时间复杂度,只有N才是影响时间复杂度的因素。
返回值: 返回被删除的元素列表,包含分数和元素。如果命令超时,则返回空列表。
示例:
BZPOPMAX myset 10
这个示例命令将阻塞式地从名称为“myset”的有序集合中删除并返回分数最高的元素,如果在10秒内有元素可供处理,则立即返回;如果在10秒内没有元素可供处理,则在10秒后返回一个空列表。
ZPOPMIN
ZPOPMIN命令用于删除并返回有序集合(Sorted Set,简称zset)中分数最低的元素。
该命令的基本语义包括以下几点:
- 如果有序集合为空,则ZPOPMIN命令会返回空列表。
- 如果有序集合不为空,则会删除并返回分数最低的元素。
- 可以指定要删除并返回的元素数量,如果不指定数量,则默认为1。
- 返回的元素以及对应的分数作为数组返回。
语法:
ZPOPMIN key [count]
- key: 目标有序集合的键名。
- count: 可选参数,指定要删除并返回的元素数量。如果未指定,则默认为1。
命令有效版本: Redis 5.0.0之后可用。
时间复杂度: O(log(N) * M),其中N是有序集合中元素的个数,M是要删除并返回的元素数量。
返回值: 返回被删除的元素列表,包含分数和元素。
示例:
ZPOPMIN myset 3
这个示例命令将从名称为“myset”的有序集合中删除并返回分数最低的3个元素。
BZPOPMIN
描述:
BZPOPMIN命令是ZPOPMIN的阻塞版本,用于从一个或多个有序集合(Sorted Set,简称zset)中阻塞式地删除并返回分数最低的元素。
其主要特点如下:
- 当指定的有序集合不为空时,会立即删除并返回其中分数最低的元素。
- 如果指定的有序集合为空,则会阻塞客户端,直到有元素可用为止,或者达到指定的超时时间。
- 如果有多个有序集合被指定,BZPOPMIN会优先从排序最前面的有序集合中获取元素。
语法:
BZPOPMIN key [key ...] timeout
其中,
- key:一个或多个目标有序集合的键名。
- timeout:阻塞超时时间,单位为秒。如果在指定的时间内没有元素可供弹出,则命令会超时返回。
命令有效版本:Redis 5.0.0之后可用。
时间复杂度:O(log(N)),其中N是有序集合中元素的个数。
返回值:返回被删除的元素列表,包含分数和元素。如果超时则返回nil。
示例:
BZPOPMIN myset1 myset2 10
在这个示例中,执行BZPOPMIN命令从myset1
和myset2
两个有序集合中弹出并返回分数最低的元素,如果在10秒内有可弹出的元素,则立即返回,否则等待10秒后超时返回nil。
ZRANK
描述:
ZRANK命令用于返回有序集合(Sorted Set,简称zset)中指定元素的排名,排名按照元素分数的升序排列。
其主要特点如下:
- 返回的排名是从0开始的,表示指定元素在有序集合中按照分数升序排序时的位置。
- 排名越小,表示元素的分数越小,即元素越靠前。
- 如果指定元素在有序集合中不存在,则返回nil。
- 如果有多个元素具有相同的分数,ZRANK将返回分数相同元素中的第一个元素的排名。
语法:
ZRANK key member
其中,
- key:目标有序集合的键名。
- member:指定的元素。
命令有效版本:Redis 2.0.0之后可用。
时间复杂度:O(log(N)),其中N是有序集合中元素的个数。
返回值:返回指定元素的排名,如果元素不存在于有序集合中,则返回nil。
示例:
ZADD myset 1 "one" 2 "two" 3 "three" ZRANK myset "two"
在这个示例中,假设有序集合myset包含了三个元素,分别是"one","two"和"three",对应的分数分别是1,2和3。然后,执行ZRANK myset "two"命令,返回值为1,表示元素"two"在有序集合中的排名为1(排名从0开始)。
ZREVRANK
描述:
ZREVRANK命令用于返回有序集合(Sorted Set,简称zset)中指定元素的排名,排名按照元素分数的降序排列。
其主要特点如下:
- 返回的排名是从0开始的,表示指定元素在有序集合中按照分数降序排序时的位置。
- 排名越小,表示元素的分数越大,即元素越靠前。
- 如果指定元素在有序集合中不存在,则返回nil。
- 如果有多个元素具有相同的分数,ZREVRANK将返回分数相同元素中的第一个元素的排名。
语法:
ZREVRANK key member
其中,
- key:目标有序集合的键名。
- member:指定的元素。
命令有效版本:Redis 2.0.0之后可用。
时间复杂度:O(log(N)),其中N是有序集合中元素的个数。
返回值:返回指定元素的排名,如果元素不存在于有序集合中,则返回nil。
示例:
ZADD myset 1 "one" 2 "two" 3 "three" ZREVRANK myset "two"
在这个示例中,假设有序集合myset包含了三个元素,分别是"one","two"和"three",对应的分数分别是1,2和3。然后,执行ZREVRANK myset "two"命令,返回值为1,表示元素"two"在有序集合中的排名为1(排名从0开始)。
ZSCORE
描述:
ZSCORE命令用于返回有序集合(Sorted Set,简称zset)中指定元素的分数。
其主要特点如下:
- 如果指定元素存在于有序集合中,则返回该元素的分数。
- 如果指定元素不存在于有序集合中,则返回nil。
语法:
ZSCORE key member
其中,
- key:目标有序集合的键名。
- member:指定的元素。
命令有效版本:Redis 1.2.0之后可用。
时间复杂度:O(1)
ZSCORE命令的时间复杂度为O(1),这是因为在有序集合(Sorted Set)中,每个成员都与一个分数相关联,并且有序集合内部使用了一种数据结构(通常是跳跃表或红黑树等),使得在给定成员的情况下,可以直接获取其对应的分数,而不需要进行线性搜索或遍历。因此,无论有序集合中的元素数量有多少,获取指定成员的分数的时间复杂度都是常数时间O(1)。
这种实现方式使得ZSCORE命令的性能非常高效,并且不受有序集合中元素数量的影响。因此,无论有序集合中的数据规模有多大,执行ZSCORE命令都能够以常数时间的复杂度返回指定成员的分数,这也是Redis中Sorted Set数据结构的优势之一。
返回值:返回指定元素的分数。如果元素不存在于有序集合中,则返回nil。
示例:
ZADD myset 1 "one" 2 "two" 3 "three" ZSCORE myset "two"
在这个示例中,假设有序集合myset包含了三个元素,分别是"one","two"和"three",对应的分数分别是1,2和3。然后,执行ZSCORE myset "two"命令,返回值为2,表示元素"two"在有序集合中的分数为2。
ZREM
描述:
ZREM命令用于从有序集合(Sorted Set,简称zset)中删除指定的一个或多个元素。
其主要特点如下:
- 如果指定的元素存在于有序集合中,则会被删除。
- 如果指定的元素不存在于有序集合中,则不执行任何操作。
语法:
ZREM key member [member ...]
其中,
- key:目标有序集合的键名。
- member:要删除的元素。
命令有效版本:Redis 1.2.0之后可用。
时间复杂度:删除单个元素的复杂度为O(log(N)),删除多个元素的总时间复杂度为O(M*log(N)),其中M为要删除的元素个数,N为有序集合的基数。
返回值:返回本次操作删除的元素个数,不包括不存在于有序集合中的元素。
示例:
ZADD myset 1 "one" 2 "two" 3 "three"
ZREM myset "two" "three" "four"
在这个示例中,假设有序集合myset包含了三个元素,分别是"one","two"和"three",对应的分数分别是1,2和3。然后,执行ZREM myset "two" "three" "four"命令,删除了"two"和"three"这两个存在于集合中的元素,因此返回值为2,表示成功删除了2个元素。
ZREMRANGEBYRANK
描述:ZREMRANGEBYRANK命令用于按照分数排序,以升序的方式删除有序集合中指定范围的元素。范围是左闭右闭的,即包括指定的起始和结束位置的元素。ZREMRANGEBYRANK命令会删除有序集合中指定排名范围内的元素,如果指定范围超出了有序集合的实际排名范围,则命令不会产生任何效果。
语法:
ZREMRANGEBYRANK key start stop
其中,
- key:有序集合的键名。
- start:要删除的元素的起始排名。
- stop:要删除的元素的结束排名。
命令有效版本:2.0.0之后
时间复杂度:O(log(N)+M),其中N是有序集合中的元素数量,M是要删除的元素数量。
返回值:本次操作删除的元素个数。
示例:
ZADD myset 1 "one" 2 "two" 3 "three" 4 "four" 5 "five" ZREMRANGEBYRANK myset 1 3
在这个示例中,假设有序集合myset包含了五个元素,分别是"one","two","three","four"和"five",对应的分数分别是1,2,3,4和5。然后,执行ZREMRANGEBYRANK myset 1 3命令,删除了排名在1到3之间的元素,也就是"two","three"和"four",因此返回值为3,表示成功删除了3个元素。
ZREMRANGEBYSCORE
描述:ZREMRANGEBYSCORE命令用于按照分数范围删除有序集合(Sorted Set)中的元素。范围是左闭右闭的,即包括指定的最小和最大分数所对应的元素。ZREMRANGEBYSCORE命令会删除有序集合中分数在指定范围内的元素,如果指定范围超出了有序集合的实际分数范围,则命令不会产生任何效果。
语法:
ZREMRANGEBYSCORE key min max
其中,
- key:有序集合的键名。
- min:最小分数,删除范围内的元素分数不小于该分数。
- max:最大分数,删除范围内的元素分数不大于该分数。
命令有效版本:1.2.0之后
时间复杂度:O(log(N)+M),其中N是有序集合中的元素数量,M是要删除的元素数量。
返回值:本次操作删除的元素个数。
示例:
ZADD myset 1 "one" 2 "two" 3 "three" 4 "four" 5 "five" ZREMRANGEBYSCORE myset 2 4
在这个示例中,假设有序集合myset
包含了五个元素,分别是"one","two","three","four"和"five",对应的分数分别是1,2,3,4和5。然后,执行ZREMRANGEBYSCORE myset 2 4命令,删除了分数在2到4之间的元素,也就是"two","three"和"four",因此返回值为3,表示成功删除了3个元素。
ZINCRBY
描述:ZINCRBY命令用于为有序集合中指定元素的分数增加指定的分数值。ZINCRBY命令会将指定元素的分数增加(或减少)指定的分数值。如果该元素不存在于有序集合中,则会先将其添加到集合中,然后再增加分数。如果有序集合中不存在指定元素,则会新建该元素并将分数设定为增量值。如果指定元素存在于有序集合中,但它的分数不是double类型,则无法执行ZINCRBY命令。
语法:
ZINCRBY key increment member
其中,
- key:有序集合的键名。
- increment:要增加的分数值,可以为负数。
- member:指定的元素。
命令有效版本:1.2.0之后
时间复杂度:O(log(N)),其中N是有序集合中的元素数量。
返回值:增加后元素的分数。
示例:
ZADD myset 1 "one" ZINCRBY myset 2 "one"
在这个示例中,首先向有序集合myset中添加了一个元素"one",分数为1。然后,执行ZINCRBY myset 2 "one"命令,将元素"one"的分数增加了2,因此增加后元素"one"的分数为3。
集合间操作
有序集合的交集操作
ZINTERSTORE
描述:ZINTERSTORE命令用于求取给定多个有序集合的交集,并将结果保存到目标有序集合中。在进行交集计算时,它会按照元素为单位进行合并,即在所有给定有序集合中都存在的元素才会被包含在结果集中。ZINTERSTORE命令会计算给定多个有序集合的交集,并将结果保存到目标有序集合中。在交集计算过程中,对于存在于所有给定有序集合中的元素,会根据指定的权重和聚合方式计算出新的分数,并存储到目标有序集合中。如果目标有序集合已存在,则会覆盖原有数据。
语法:
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
其中,
- destination:目标有序集合的键名。
- numkeys:指定的输入有序集合的数量。
- key:输入有序集合的键名。
- WEIGHTS weight [weight ...]:指定输入有序集合的权重,默认情况下权重为1。权重用于计算交集中元素的分数,可以用于加权求和或加权最值。
- AGGREGATE <SUM | MIN | MAX>:指定多个输入有序集合的聚合方式,可选的聚合方式包括SUM(求和,默认)、MIN(最小值)和MAX(最大值)。
命令有效版本:2.0.0之后
时间复杂度:O(NK)+O(Mlog(M)),其中N是输入的有序集合中最小的有序集合的元素个数;K是输入了几个有序集合;M是最终结果的有序集合的元素个数。
返回值:目标集合中的元素个数。
示例:
ZADD set1 1 "one" 2 "two" ZADD set2 2 "two" 3 "three" ZINTERSTORE destination 2 set1 set2 WEIGHTS 2 3 AGGREGATE SUM
在这个示例中,有两个输入有序集合set1和set2,它们分别包含了元素"one"、"two"和"three",对应的分数分别为1、2和3。执行ZINTERSTORE destination 2 set1 set2 WEIGHTS 2 3 AGGREGATE SUM命令,求出了set1和set2的交集并保存到目标有序集合destination中。在合并过程中,"one"的分数为1 * 2 = 2,"two"的分数为2 * 2 + 2 * 3 = 10,"three"的分数为3 * 3 = 9。因此,目标集合destination中包含了元素"one"、"two"和"three",返回值为3。
在Redis官方文档中解释了为什么在执行交集(Intersection)、并集(Union)或差集(Difference)操作时需要指定输入键的数量(numkeys)。这是因为在Redis的命令语法中,需要在传递输入键和其他可选参数之前明确指定输入键的数量,以便能够正确地解析命令。如果没有提供numkeys参数,Redis将无法区分命令的配置项和实际的输入键。
因此,在执行这些操作时,必须在命令中明确指定输入键的数量,这样Redis才能正确地解析命令并执行相应的操作。这也是为了确保命令的清晰性和一致性,以避免歧义和错误。
有序集合的并集操作
ZUNIONSTORE
描述:ZUNIONSTORE命令用于求出给定多个有序集合的并集,并将结果保存到目标有序集合中。在合并过程中,以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。ZUNIONSTORE命令会计算给定多个有序集合的并集,并将结果保存到目标有序集合中。在并集计算过程中,对于每个元素,会根据指定的权重和聚合方式计算出新的分数,并存储到目标有序集合中。如果目标有序集合已存在,则会覆盖原有数据。
语法:
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
其中,
- destination:目标有序集合的键名。
- numkeys:指定的输入有序集合的数量。
- key:输入有序集合的键名。
- WEIGHTS weight [weight ...]:指定输入有序集合的权重,默认情况下权重为1。权重用于计算并集中元素的分数,可以用于加权求和或加权最值。
- AGGREGATE <SUM | MIN | MAX>:指定多个输入有序集合的聚合方式,可选的聚合方式包括SUM(求和,默认)、MIN(最小值)和MAX(最大值)。
AGGREGATE参数用于指定在执行并集操作时,当两个或多个有序集合中存在相同成员时,将对它们的分数进行聚合。
所谓的“相同成员”指的是在参与并集操作的多个有序集合中具有相同成员标识(member)的成员。也就是说,如果两个或多个有序集合中都包含同一个成员,那么这个成员就被认为是“相同的”。
举个例子,假设我们有两个有序集合A和B,它们分别包含以下成员:
A = {(member1, 10), (member2, 20), (member3, 30)}
B = {(member2, 15), (member3, 25), (member4, 35)}
在这种情况下,成员2和成员3在两个有序集合中都出现了。因此,成员2和成员3就被认为是“相同的成员”。
在执行ZUNIONSTORE命令时,如果遇到了相同的成员,根据AGGREGATE参数的设置,然后对它们的分数进行相应的聚合操作。
这个参数有三个选项:SUM、MIN和MAX,分别表示求和、取最小值和取最大值。
这个规则的存在是为了处理在执行并集操作时,如果有多个有序集合中存在相同成员的情况。在这种情况下,根据应用的需求,我们可能希望对相同成员的分数进行不同的聚合操作。
具体地说:
- SUM:对于相同成员,将它们的分数相加得到新的分数。
- MIN:对于相同成员,将它们的分数取最小值作为新的分数。
- MAX:对于相同成员,将它们的分数取最大值作为新的分数。
命令有效版本:2.0.0之后
时间复杂度:O(N)+O(M*log(M)),其中N是输入的有序集合总的元素个数;M是最终结果的有序集合的元素个数。
返回值:目标集合中的元素个数。
示例:
ZADD set1 1 "one" 2 "two" ZADD set2 2 "two" 3 "three" ZUNIONSTORE destination 2 set1 set2 WEIGHTS 2 3 AGGREGATE SUM
在这个示例中,有两个输入有序集合set1和set2,它们分别包含了元素"one"、"two"和"three",对应的分数分别为1、2和3。执行ZUNIONSTORE destination 2 set1 set2 WEIGHTS 2 3 AGGREGATE SUM命令,求出了set1和set2的并集并保存到目标有序集合destination中。在合并过程中,"one"的分数为1 * 2 = 2,"two"的分数为2 * 2 + 2 * 3 = 10,"three"的分数为3 * 3 = 9。因此,目标集合destination中包含了元素"one"、"two"和"three",返回值为3。
💡在Redis 6.2版本中,引入了ZINTER、ZUNION和ZDIFF命令,它们可以用于执行有序集合之间的交集、并集和差集操作。这些命令的引入为处理多个有序集合提供了更加灵活和高效的方式,使得在Redis中执行复杂的集合运算变得更加方便。
有序集合命令小结
命令 | 时间复杂度 |
---|---|
zadd key score member [score member ...] | O(k * log(n)),其中 k 是添加成员的个数,n 是当前有序集合的元素个数 |
zcard key | O(1) |
zscore key member | O(1) |
zrank key member | O(log(n)),其中 n 是当前有序集合的元素个数 |
zrevrank key member | O(log(n)),其中 n 是当前有序集合的元素个数 |
zrem key member [member ...] | O(k * log(n)),其中 k 是删除成员的个数,n 是当前有序集合的元素个数 |
zincrby key increment member | O(log(n)),其中 n 是当前有序集合的元素个数 |
zrange key start end [withscores] | O(k + log(n)),其中 k 是获取成员的个数,n 是当前有序集合的元素个数 |
zrevrange key start end [withscores] | O(k + log(n)),其中 k 是获取成员的个数,n 是当前有序集合的元素个数 |
zrangebyscore key min max [withscores] | O(k + log(n)),其中 k 是获取成员的个数,n 是当前有序集合的元素个数 |
zrevrangebyscore key max min [withscores] | O(k + log(n)),其中 k 是获取成员的个数,n 是当前有序集合的元素个数 |
zcount | O(log(n)),其中 n 是当前有序集合的元素个数 |
zremrangebyrank key start end | O(k + log(n)),其中 k 是获取成员的个数,n 是当前有序集合的元素个数 |
zremrangebyscore key min max | O(k + log(n)),其中 k 是获取成员的个数,n 是当前有序集合的元素个数 |
zinterstore destination numkeys key [key ...] | O(n * k) + O(m * log(m)),其中 n 是输入的集合最小的元素个数,k 是集合个数, m 是目标集合元素个数 |
zunionstore destination numkeys key [key ...] | O(n) + O(m * log(m)),其中 n 是输入集合总元素个数,m 是目标集合元素个数 |
内部编码
有序集合类型的内部编码有两种:ziplist(压缩列表)和skiplist(跳表)。
-
ziplist(压缩列表):在ziplist内部实现中,元素个数小于配置的 zset-max-ziplist-entries(默认 128 个)且每个元素的值都小于配置的 zset-max-ziplist-value(默认 64 字节)时,Redis会选择使用ziplist作为有序集合的内部实现。Ziplist采用紧凑的数据结构,通过将多个小的元素合并在一起以节省内存空间。它是一种节省内存的方法,但在大型集合上的性能可能会受到影响。
-
skiplist(跳表):当ziplist的条件不满足时,即元素个数超过了配置值或者元素的值超过了配置大小,有序集合会使用skiplist作为内部实现。Skiplist是一种随机化的数据结构,用于在有序集合中维护元素的顺序,并支持快速的插入、删除和查找操作。它通过多层链表结构实现,每层链表都是元素的子集,从而加快了查找速度。
跳表(Skip List)是一种基于链表的数据结构,它利用了一定的随机化策略,可以在有序链表的基础上快速地进行查找、插入和删除操作,其效率与平衡二叉树相当。跳表的特点是简单、易于实现,并且具有较好的平均性能。
跳表的核心思想是通过构建多层次的索引,使得在查找过程中可以跳跃式地定位到目标节点,从而减少查找的时间复杂度。每一层的节点都是原始链表的子集,并且每一层都比下一层稀疏。最底层的链表包含所有的元素,而每一层的节点都可能引用下一层的若干个节点,从而形成了一种多层次的结构。跳表中的每个节点都包含一个指向下一个节点的指针,以及一个指向同一层次下一个节点的指针(通常称为“跳跃指针”)。通过这些跳跃指针,可以在每一层中以一定的概率“跳过”一些节点,从而加速查找过程。
跳表的插入、删除和查找操作的时间复杂度都是O(log n),其中n是跳表中的元素数量。这使得跳表在实际应用中具有较高的效率和性能表现。
所以不得不说跳表是一种高效的动态数据结构,适用于需要高效查找、插入和删除操作的场景,比如在数据库索引、有序集合等数据结构的实现中广泛应用。
跳表相比于树形结构在某些场景下更适合范围查找。
在跳表中,由于每个节点都有多层次的索引,因此在执行范围查找时,可以利用这些索引来跳跃式地定位到目标区间的起始节点,然后顺着链表进行线性扫描,直到找到范围结束的节点为止。这种跳跃式的定位和线性扫描结合起来,使得范围查找在跳表中的效率相对较高。
相比之下,在树形结构(如平衡二叉搜索树)中执行范围查找通常需要进行递归遍历或者迭代遍历,这可能会导致一些不必要的比较操作,从而影响查找效率。
另外,跳表的实现相对简单,不需要像平衡树那样进行复杂的平衡操作,因此在某些情况下也更容易实现和维护。
因此,对于需要频繁执行范围查找操作的场景,跳表可能是一个更合适的选择,而在其他一些情况下,树形结构可能更适合。在实际应用中,需要根据具体的需求和场景来选择适合的数据结构。
在实际应用中,Redis会根据集合的大小和元素的值来选择合适的内部编码,以最大程度地提高性能和节省内存空间。
1)当元素个数较少且每个元素较小时,内部编码为 ziplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3
(integer) 3
127.0.0.1:6379> object encoding zsetkey
"ziplist"
2)当元素个数超过128个时,内部编码为 skiplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 ...省略... 82 e129
(integer) 129
127.0.0.1:6379> object encoding zsetkey
"skiplist"
3)当某个元素大于64字节时,内部编码为 skiplist:
127.0.0.1:6379> zadd zsetkey 50 "one string bigger than 64 bytes ...省略..."
(integer) 1
127.0.0.1:6379> object encoding zsetkey
"skiplist"
使用场景
有序集合在排行榜系统中有着广泛的应用。排行榜可以根据不同的维度来展示,例如按照时间、阅读量或点赞量等。这些维度可以映射到有序集合中的成员和分数。
举例来说,一个网站上的热榜信息可以通过有序集合来实现。每个成员可以是一篇文章或者一个帖子,而分数可以是文章的发布时间、阅读量或点赞量。通过有序集合,可以轻松地按照不同的维度对文章进行排序,并实时更新热门内容。
这种应用场景中,有序集合提供了高效的方式来存储和查询帖子的信息,同时能够根据不同的需求进行排序,使得排行榜系统能够实现快速、动态的展示。
我们使用点赞数这个维度,维护每天的热榜:
1)添加用户赞数:
假设用户 "james" 发布了一篇文章,并获得了3个赞,可以使用有序集合的 zadd 命令添加用户和赞数:
zadd user:ranking:2022-03-15 3 james
如果后续再获得赞,可以使用 zincrby 命令:
zincrby user:ranking:2022-03-15 1 james
2)取消用户赞数:
如果用户由于各种原因需要被删除,比如用户注销或者用户作弊,需要将用户从榜单中删除,可以使用 zrem 命令。例如,删除名为 "tom" 的用户:
zrem user:ranking:2022-03-15 tom
3)展示获取赞数最多的10个用户:
通过 zrevrange 命令可以实现这个功能,它会按照分数从高到低的顺序返回指定范围内的成员。例如:
zrevrange user:ranking:2022-03-15 0 9
4)展示用户信息以及用户分数:
用户信息可以保存在哈希类型中,使用用户名称作为键后缀。用户的分数和排名可以使用 zscore 和 zrank 命令来获取。例如:
hgetall user:info:tom
zscore user:ranking:2022-03-15 mike
zrank user:ranking:2022-03-15 mike
5)根据每个维度的分数及权重得到综合得分,从而通过结果集合得到热度排名:
ZUNIONSTORE combined_ranking 2 user:ranking:2022-03-15 other_ranking WEIGHTS 1 0.5 AGGREGATE SUM
ZREVRANGE combined_ranking 0 9 WITHSCORES
这些命令可以帮助我们维护每天的热榜,并且可以方便地获取用户的信息、赞数和排名。
接下来我们简单了解一些特定场景下运用到的类型:
Redis类型简介——stream
Redis Stream 是Redis 5.0引入的一种新数据类型,用于实现类似于消息队列的功能。它提供了一种按时间顺序存储、读取和处理数据的方式。Stream 数据结构由一个不断增长的日志结构组成,每个条目都是一个包含唯一 ID 的消息。消息可以具有关联的字段和值,这使得 Stream 适用于记录事件、实现发布/订阅系统和构建消息队列等应用场景。
以下是 Stream 数据类型的主要特点和用法:
-
按时间顺序存储:Redis Stream 中的消息是按照插入的时间顺序进行排序的。这种设计使得对于需要按照时间顺序处理数据的应用场景(比如日志系统、事件溯源等),Stream 提供了一种非常高效的存储和检索机制。开发者可以根据时间范围来检索消息,从而实现对历史数据和实时数据的有效管理和分析。
-
唯一的消息 ID:每个消息在 Stream 中都有一个唯一的 ID 标识。这个 ID 是递增的,确保了消息的唯一性。通过这个唯一的标识,开发者可以方便地对消息进行检索、更新或删除操作。这种机制使得消息的管理和处理更加灵活和高效。
-
字段和值:除了消息的 ID 外,每个消息还可以包含多个字段和对应的值。这些字段可以用于存储消息的元数据或其他相关信息,比如消息的来源、类型、优先级等。这种结构化存储使得消息的内容更加丰富和灵活,同时也方便了对消息的检索和过滤。
-
消费者组:Redis Stream 支持消费者组的概念,多个消费者可以共同消费同一个 Stream,并且每个消费者组内的消费者可以以并行的方式处理消息。这种机制能够提高消息处理的并发性和可扩展性,从而更好地适应高并发和大规模的消息处理场景。消费者组还可以实现消息的负载均衡和故障恢复,保证了系统的稳定性和可靠性。
-
消费者阻塞:消费者可以以阻塞或非阻塞的方式等待新消息到达,并且可以通过 XREAD 命令读取 Stream 中的消息。这种阻塞机制使得消费者可以实时地获取新消息,而不需要频繁地轮询 Stream。这样可以有效地利用系统资源,减少了不必要的资源消耗,提高了系统的性能和响应速度。
-
消费者确认:消费者可以确认已经处理过的消息,从而确保消息不会被重复处理。这种确认机制能够保证消息处理的可靠性和一致性,避免了消息重复处理带来的数据不一致性和系统错误。同时,消费者确认还可以用于实现消息的事务性处理,确保消息的处理过程是原子的和可靠的。
-
生产者:生产者可以向 Stream 中添加新的消息,并且可以在需要时指定消息的 ID。这种灵活的生产机制使得生产者能够根据业务需求和场景特点,灵活地控制消息的生成和顺序。同时,生产者还可以通过设置消息的过期时间和持久化选项,来管理消息的生命周期和持久化存储需求。
-
事件日志:Redis Stream 可以用作事件日志,记录系统中发生的事件,并且可以根据需要对事件进行检索和处理。通过将事件记录到 Stream 中,开发者可以实现对系统状态和行为的实时监控、分析和回溯。这种实时日志功能对于构建实时数据处理系统和事件驱动的应用程序非常有用,能够帮助开发者更好地理解和优化系统的运行情况。
综上所述,Redis Stream 提供了一种高性能、持久化、可扩展的消息传递机制,适用于构建实时数据处理、事件驱动的应用程序和分布式系统中的消息队列等场景。其丰富的特性和灵活的用法使得它成为了解决大规模数据处理和通信需求的重要工具之一。
当使用 Redis Stream 数据类型时,有一些关键命令,感兴趣的可以自己去官方文档学习了解:
-
XADD:用于向 Stream 中添加消息。它接受键值对参数,并可选地指定消息的ID。如果未提供ID,则Redis会自动生成唯一的ID。
-
XREAD:用于从一个或多个 Stream 中读取消息。可以按照消息的ID或其他条件进行过滤。
-
XLEN:获取 Stream 中消息的数量。
-
XGROUP:用于管理消费者组。可以创建、删除和管理消费者组。
-
XREADGROUP:从消费者组中读取消息。消费者组内的消费者可以以并行的方式处理消息。
-
XPENDING:获取消费者组中待处理消息的信息。
-
XCLAIM:消费者组内的消费者可以使用此命令来声明和处理待处理的消息。
-
XDEL:从 Stream 中删除消息。
Redis类型简介——geospatial
Redis中的geospatial类型是指用于地理空间数据存储和查询的数据结构和相关命令。它主要用于存储地理位置信息,并支持根据位置坐标进行范围查询和附近位置查找等操作。这在许多应用中是非常有用的,比如地理位置服务、社交网络、物流和配送等领域。
在Redis中,geospatial类型主要由两种结构组成:地理位置集合(Geospatial Set)和地理位置索引(Geospatial Index)。地理位置集合是一种有序集合,其中每个元素都有一个地理位置坐标和一个成员标识。地理位置索引则是一种空间索引结构,用于快速查询附近的地理位置。
Redis提供了一系列命令用于对地理位置数据进行管理和查询,其中包括:
-
GEOADD:用于将一个或多个地理位置成员添加到指定的地理位置集合中。每个成员都有一个关联的名称和对应的经度纬度坐标。这个命令允许添加多个成员,每个成员可以有不同的名称。添加的成员会被存储在指定的地理位置集合中。
-
GEORADIUS:根据指定的中心坐标和半径范围,在地理位置集合中查询符合条件的成员。这个命令返回一个或多个成员,这些成员位于指定中心坐标半径范围内。结果按照距离中心坐标的远近排序。
-
GEORADIUSBYMEMBER:与GEORADIUS类似,但是是根据指定的地理位置成员来执行查询。它会返回在指定成员附近的其他成员,距离该成员不超过指定半径范围。
-
GEOPOS:用于获取地理位置集合中指定成员的经度和纬度坐标。给定一个或多个成员名,该命令返回相应的坐标信息。
-
GEODIST:计算地理位置集合中两个成员之间的距离。默认情况下,距离以米为单位,但是可以选择其他单位。这个命令可以用于计算任意两个地理位置成员之间的距离。
-
GEORADIUSSTORE:与GEORADIUS类似,但是它将查询结果存储到另一个新的地理位置集合中。这个命令允许将查询结果持久化,以便后续的使用。
-
GEOHASH:获取地理位置集合中指定成员的geohash值。geohash是一种将地理位置坐标编码为字符串的方法,用于快速索引和查询地理位置数据。
-
GEOSEARCH:(在某些 Redis 版本中可能存在)根据指定的条件查询地理位置集合中的成员。这个命令允许根据距离、名称、半径等条件来搜索地理位置成员,以满足特定的查询需求。
这些命令为处理地理位置数据提供了丰富的功能,可以方便地进行地理位置成员的添加、查询、距离计算和存储等操作。
Redis类型简介——hyperloglog
Redis中的HyperLogLog(简称HLL)是一种基数估计算法,用于估计集合中不同元素的数量,即集合的基数。它通过使用固定的空间来估计非常大的数据集的基数,且具有很高的精度。
HyperLogLog 专门用于估计大数据集的基数(即不重复元素的数量),它可以以很小的内存消耗来估算非常大的基数。
在统计服务器的 UV(Unique Visitors,独立访客)时,UV 的数量可能非常庞大,而且对于实时统计,我们不需要准确地知道每个独立访客的详细信息,而只需要知道 UV 的近似值。这时,HyperLogLog 就可以发挥作用了。
HyperLogLog 使用的内存远远少于存储所有独立访客的详细信息所需的内存,因为它只存储一些用于估计基数的统计信息。使用 HyperLogLog,我们可以在接近常量的内存空间下(与要统计的 UV 数量无关),对非常大的数据集进行基数估计。
简单来说,HyperLogLog 通过对数据进行哈希处理,将数据映射到固定大小的位数组中,并利用位数组中的位信息进行基数估计。虽然 HyperLogLog 估算的结果可能存在一定的误差,但是通常情况下,这种误差是可以接受的,特别是在大数据集合的场景下。
因此,在统计服务器的 UV 时,HyperLogLog 是一种非常有效的选择。它可以在保证一定的统计准确性的前提下,极大地减少内存消耗,提高统计效率。
HyperLogLog的主要特点包括:
-
固定内存消耗: 不管集合中有多少个不同的元素,HyperLogLog只需要固定数量的内存来存储数据,这使得它能够处理非常大的数据集而不会消耗过多的内存。
-
近似计数: HyperLogLog提供了对基数的近似估计,而不是精确计数。虽然它的估计结果不是精确的,但通常可以在可接受的范围内提供准确的估计值。
-
并行计算: HyperLogLog可以进行并行计算,这意味着它可以在分布式系统中有效地处理大规模数据集。
Redis中的HyperLogLog通过一组命令来实现其功能,主要包括:
- PFADD: 将一个或多个元素添加到HyperLogLog数据结构中。
- PFCOUNT: 返回HyperLogLog数据结构的近似基数估计值。
- PFMERGE: 合并多个HyperLogLog数据结构,得到它们的并集,从而得到一个新的HyperLogLog数据结构。
HyperLogLog适用于各种需要快速估计大规模数据集基数的场景,比如统计网站独立访客数量、统计广告点击次数、统计社交网络中用户的唯一访问者等。通过使用HyperLogLog,Redis可以在占用较少内存的情况下快速估计大型数据集的基数,从而为数据分析和统计提供了有效的解决方案。
Redis类型简介——bitmaps
Redis中的Bitmap是一种特殊的数据结构,它实际上是一系列位(或者说是二进制位)组成的序列,可以看作是Set类型针对整数的特化版本。通常使用一个字符串来表示。Bitmap主要用于对大规模数据集进行位级别的操作和存储,特别适用于需要高效存储和处理大量布尔型数据的场景。
Bitmap的主要特点包括:
-
紧凑的存储方式: 由于Bitmap使用位来表示数据,因此它在存储方面非常紧凑,可以节省大量内存空间。
-
高效的位级别操作: Bitmap支持各种位级别的操作,如设置位、清除位、获取位的状态等,这些操作可以在常数时间内完成,因此非常高效。
-
适用于标记和计数: Bitmap常用于标记和计数各种事件或状态,比如用户的在线状态、用户的活跃时间段、用户的访问记录等。
-
支持位运算: 由于Bitmap存储的是二进制位,因此可以利用位运算进行高效的集合操作,比如并集、交集、差集等。
Redis提供了一系列的命令来操作Bitmap,主要包括:
- SETBIT: 设置指定偏移量处的位的值。
- GETBIT: 获取指定偏移量处的位的值。
- BITCOUNT: 统计指定范围内位为1的个数。
- BITOP: 对多个Bitmap执行位级别的操作,如并集、交集、差集等。
- BITPOS: 查找指定位值的第一个出现位置。
Bitmap常用于需要高效存储和处理大规模布尔型数据的场景,比如用户在线状态标记、数据流量监控、消息队列中消息的已读状态标记等。通过合理使用Bitmap,可以在Redis中高效地处理和存储这些布尔型数据,从而提高系统的性能和效率。
Redis类型简介——bitfields
Redis中的Bitfield(位域)是一种用于对字符串进行位级别操作的命令。Bitfield可以理解为一串二进制序列(字节数组),它允许用户以原子方式对字符串中的位进行读取、设置和修改操作。Bitfield命令提供了一种灵活的方式来处理位级别的数据,适用于各种需要对二进制位进行操作的场景。
Bitfield的主要特点包括:
-
位级别操作: Bitfield允许用户对字符串中的位进行精确的读取、设置和修改操作,可以操作单个位、连续位段或跨越多个字节的位。
-
原子操作: 所有Bitfield命令都是原子操作,保证了在多个客户端同时操作时的数据一致性。
-
灵活的指定偏移量: 用户可以通过指定偏移量来操作字符串中的位,可以精确地定位到所需的位位置。
-
支持各种位运算: Bitfield支持各种位运算操作,包括AND、OR、XOR等,可以进行高效的位级别集合操作。
-
支持有符号和无符号数值: Bitfield支持对有符号和无符号数值进行位操作,用户可以根据需求选择适合的数据类型。
当使用Redis中的Bitfield(位域)命令时,可以对存储在字符串(byte array)中的位进行原子级别的操作:
-
位域数据结构:位域可以理解为一串二进制序列,通常存储在Redis的字符串数据类型中。这个二进制序列可以是任意长度的,最多可以包含512MB的位。位域的每个位都有一个唯一的偏移量(offset),从0开始递增。
-
操作类型:Bitfield命令支持多种位级别操作,包括读取、设置和修改位。通过指定位域的偏移量和长度,可以对指定范围内的位进行操作。
-
原子性操作:Bitfield命令提供了原子级别的操作,这意味着在多个客户端同时对同一个位域进行操作时,不会出现竞态条件或数据不一致的情况。所有的操作都是原子的,保证了数据的一致性和可靠性。
-
位操作:Bitfield命令支持各种位操作,包括设置位、清除位、翻转位和获取位的值。这些操作可以通过指定位的偏移量和长度来进行。
-
类型转换:Bitfield命令还支持将位域中的位序列解释为有符号整数或无符号整数,并且可以进行类型转换。这样可以方便地对位域进行数值计算和处理。
-
位计数:Bitfield命令提供了对位域中位的计数功能,可以统计位域中值为1或0的位的数量。这对于某些应用场景(如统计用户在线时长)非常有用。
总的来说,Bitfield命令提供了一种灵活、高效的方式来处理位级别的数据。它适用于各种需要对二进制位进行操作的场景,如布隆过滤器、计数器、权限控制等。通过Bitfield,开发者可以方便地对位级别的数据进行读取、设置和修改,并且保证了数据操作的原子性和一致性。
Bitfield命令包括以下几种:
- BITFIELD: 用于对字符串中的位进行读取、设置和修改操作。
- GETBIT: 用于获取指定偏移量处的位的值。
- SETBIT: 用于设置指定偏移量处的位的值。
- BITCOUNT: 用于统计指定范围内位为1的个数。
- BITOP: 用于对多个字符串执行位级别的操作,如AND、OR、XOR等。
Bitfield常用于需要对字符串中的位进行精确操作的场景,比如图像处理、压缩算法、布隆过滤器等。通过Bitfield命令,用户可以高效地处理和操作字符串中的位级别数据,实现各种复杂的位操作需求。
渐进式遍历
Redis使用SCAN命令进行渐进式遍历键,以解决直接使用KEYS命令可能导致的阻塞问题。SCAN命令允许在不阻塞服务器的情况下逐步迭代整个数据集的元素。每次执行SCAN命令的时间复杂度是O(1),但要完整地遍历整个数据集,可能需要执行多次SCAN命令。
SCAN命令的工作原理是通过游标(cursor)来逐步遍历数据集。客户端发送一个SCAN命令,服务器返回一个包含部分元素的结果集和一个游标。客户端可以使用返回的游标来请求下一批元素。这样,即使在遍历大型数据集时,Redis也不会在单个命令中阻塞很长时间,因为每次迭代都是以分片的方式进行的。
通过逐步遍历,SCAN命令避免了在大型数据集上执行阻塞式操作所带来的性能问题,尤其是在主从复制环境中,防止了主节点在执行KEYS命令时造成的阻塞,从而影响客户端请求的处理速度。
尽管SCAN命令的时间复杂度是O(1),但是需要执行多次SCAN才能完成整个数据集的遍历。因此,对于非常庞大的数据集,需要注意遍历的次数,以及在每次迭代期间可能产生的延迟。
scan 命令渐进式遍历
- ⾸次 scan 从 0 开始
- 当 scan 返回的下次位置为 0 时, 遍历结束
SCAN
描述:
SCAN命令以渐进式的方式进行键的遍历,允许在不阻塞服务器的情况下逐步遍历整个数据集。它用于替代KEYS命令,因为KEYS命令会在处理大数据集时导致服务器阻塞。
SCAN命令的工作原理是将数据集分割为多个小的数据块,然后逐个返回这些数据块的键。每次调用SCAN命令时,会返回一个游标和一个数据块,客户端收到数据块后可以对其中的键进行处理,然后再根据返回的游标调用下一次SCAN命令以获取下一个数据块,直到遍历完整个数据集。
通过使用SCAN命令,可以避免由于使用KEYS命令而导致的服务器阻塞问题,同时能够以渐进式的方式高效地遍历大数据集。这使得在生产环境中更安全地进行数据遍历和处理成为可能。
语法:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
其中,
- cursor:当前游标,用于标识遍历的起点位置。
- MATCH pattern:可选参数,用于指定要匹配的键的模式。
- COUNT count:可选参数,指定每次迭代返回的元素数量,默认10。
- TYPE type:可选参数,用于指定要匹配的键的类型。
注意,客户端和程序员通常无法直接了解游标的具体值,因为游标是由Redis服务器生成和管理的。Redis将游标作为一个不透明的整数值返回给客户端,并且在每次迭代操作后,更新游标的值。因此,客户端无法解读游标的具体含义,只能将游标传递给Redis服务器以进行下一次迭代。
游标并没有固定的规律,它的值在不同的情况下可能会有所不同。游标的具体实现取决于Redis服务器的内部机制,通常情况下,它可能是一个递增的整数值,也可能是一个随机生成的唯一标识符。由于游标是由Redis服务器管理的,因此它的值可能会根据服务器的实现和配置而变化。
在Redis中,对于一些需要迭代遍历的操作(如SCAN、SSCAN、HSCAN、ZSCAN等),通常会提供一个 COUNT 参数,用于指定每次迭代返回的元素数量的建议值。
这里所说的 "count 只代表你给了Redis一个建议" 的含义是,COUNT 参数并不是一个严格的限制,而是一个建议值。Redis可能会根据内部机制和性能考虑,适当地调整返回的元素数量,以达到更好的性能和效率。
因此,虽然你可以提供一个 COUNT 参数来建议每次迭代返回的元素数量,但实际上返回的元素数量可能会略多或略少于你提供的建议值。这一点与MySQL中的 LIMIT 不同,MySQL中的 LIMIT 是一个严格的限制,返回的行数将严格按照指定的数量进行限制。
命令有效版本:2.8.0之后
时间复杂度:O(1)
返回值:返回下一次scan的游标(cursor)以及本次得到的键。
示例:
SCAN 0 MATCH "user:*" COUNT 10
在这个示例中,执行SCAN命令,从游标0开始遍历键,匹配以"user:"开头的键,每次返回最多10个键。
除了 SCAN 命令外,Redis 针对不同的数据结构类型提供了以下遍历命令:
- HSCAN:用于遍历哈希类型数据结构。
- SSCAN:用于遍历集合类型数据结构。
- ZSCAN:用于遍历有序集合类型数据结构。
这些命令与 SCAN 的使用方法基本相似,用于以渐进式的方式遍历数据集中的元素,允许在不阻塞服务器的情况下逐步遍历整个数据集。
感兴趣的可以自行查阅相关文档进行深入学习和了解。
❗ 渐进式遍历命令(如SCAN、HSCAN、SSCAN、ZSCAN)虽然可以解决阻塞的问题,但是在遍历过程中如果键发生了变化(增加、修改、删除),可能会导致遍历时键的重复遍历或者遗漏的情况发生。
因此,在实际开发中,务必要考虑到这一点,并采取相应的措施来处理可能的数据变化。可以通过以下方式来减少这种问题的影响:
- 在遍历期间尽量减少数据修改操作,如果有必要修改数据,可以考虑使用事务或者乐观锁来保证数据的一致性。
- 在遍历过程中记录已经遍历的键,避免重复遍历。可以通过在应用程序中维护一个已经遍历过的键的集合或者标记已经遍历过的键来实现。
- 在处理遍历结果时,对可能发生变化的情况进行处理,比如在迭代过程中发现键不存在或者键的值发生变化时进行相应的处理。
综上所述,开发者在使用渐进式遍历命令时需要注意监控数据的变化情况,并采取适当的措施来保证数据的一致性和遍历的准确性。
数据库管理
在Redis中,默认提供了16个数据库(DB),编号从0到15。这些数据库是Redis服务器默认创建的,并且在Redis服务器启动时自动加载。
用户不能通过Redis提供的命令来创建新的数据库,也无法直接删除已有的数据库。Redis并没有提供专门的命令来执行这些操作。因此,用户无法通过Redis的原生命令来增加或删除数据库,也无法改变默认情况下的16个数据库的数量。
每个数据库之间的数据是相互隔离的,即使在同一个Redis实例中,不同数据库中的数据也是完全独立的,不会相互影响。这意味着在一个数据库中进行的操作不会影响到其他数据库中的数据,确保了数据的隔离性和独立性。
Redis 提供了几个面向 Redis 数据库的操作,分别是 dbsize、select、flushdb、flushall 命令。
- DBSIZE:DBSIZE命令用于获取当前数据库中的键的数量。这个命令可以帮助你快速了解当前数据库的大小。
- SELECT:SELECT命令用于切换到指定的数据库。Redis数据库默认有16个数据库,编号从0到15。通过SELECT命令可以切换到指定编号的数据库进行操作。默认情况下,Redis客户端连接后会选择数据库0。
- FLUSHDB:FLUSHDB命令用于清空当前数据库中的所有数据。执行该命令后,当前数据库中的所有键和相关数据都会被删除,数据库会恢复到初始状态。
- FLUSHALL:FLUSHALL命令用于清空所有数据库中的数据。执行该命令后,所有数据库中的所有键和相关数据都会被删除,Redis服务器会恢复到初始状态。
这些命令提供了对Redis数据库进行管理和清理的功能,可以根据实际需求灵活使用。
切换数据库
select dbIndex
关系型数据库如MySQL通常支持在一个实例下存在多个数据库,每个数据库都有一个唯一的名称用于区分。在MySQL中,这些数据库名称通常使用字符串来表示,并且在一个MySQL实例中,各个数据库之间是相互独立的,它们的数据不会相互影响。
相比之下,Redis采用了一种不同的方式来实现多个数据库的管理。在Redis中,多个数据库是通过数字来区分的,而不是使用字符串作为名称。默认情况下,Redis配置了16个数据库,编号从0到15。通过SELECT命令可以切换到不同编号的数据库进行操作,比如SELECT 0切换到第一个数据库,SELECT 15切换到最后一个数据库。
这些数据库是相互独立的,即每个数据库维护着自己的键值对数据。例如,0号数据库和15号数据库中保存的数据是完全不冲突的,它们各自拥有各自的键值对。这种设计使得Redis能够以一种简单而高效的方式管理多个数据库,并且可以根据实际需求对数据进行合理的划分和管理。
Redis 管理的数据库
❗当考虑使用Redis的多数据库特性时,需要了解一些潜在的限制和考虑因素:
-
Redis并没有为多数据库提供太多特性支持:虽然Redis支持多个数据库,但它并没有为多数据库提供额外的特性或功能。每个数据库之间的隔离性主要是通过数据库编号实现的。因此,除了切换数据库外,每个数据库之间的行为和性能表现是相同的。
-
单线程模型:Redis采用单线程模型处理所有命令请求,这意味着无论使用多少个数据库,所有命令都将在单个线程上顺序执行。这样一来,在多数据库场景下,如果某个命令在一个数据库上执行时间过长,会影响其他数据库的响应速度,因为它们共享同一个线程。
-
复杂性增加:使用多个数据库会增加系统的复杂性,特别是在开发、调试和维护时。需要管理多个数据库的配置、监控、备份和恢复等操作,而这些操作可能需要更多的人力和资源投入。
-
更好的替代方案:相比于在单个Redis实例中使用多个数据库,更好的替代方案是维护多个独立的Redis实例。这样可以更好地利用硬件资源,提高系统的扩展性和容错性,并且可以根据实际需求灵活调整每个实例的配置和管理。
因此,尽管Redis支持多数据库特性,但在实际应用中,通常更推荐将所有数据存储在数据库0中,
清除数据库
FLUSHDB和FLUSHALL是Redis中用于清空数据库的两个命令,它们之间有一些关键区别:
- FLUSHDB:FLUSHDB命令用于清空当前选择的数据库。执行该命令后,当前数据库中的所有键值对都会被删除,但其他数据库中的数据不受影响。语法为:FLUSHDB。
- FLUSHALL:FLUSHALL命令用于清空所有数据库,包括当前选择的数据库以及其他数据库中的数据。执行该命令后,所有数据库中的所有键值对都会被删除,Redis会回到初始状态。语法为:FLUSHALL。
因此,如果只需要清空当前选择的数据库,可以使用FLUSHDB命令;如果需要清空所有数据库,包括当前选择的数据库以及其他数据库中的数据,则应使用FLUSHALL命令。在实际应用中,需要根据需求来选择合适的命令来清空数据库。
可选参数:
- ASYNC:异步执行清空操作。这意味着Redis服务器将在后台执行清空操作,而不会阻塞其他命令的执行。但是清空操作可能不会立即完成,因此在后续命令执行过程中,仍可能会返回旧的数据。这是默认的行为。
- SYNC:同步执行清空操作。这意味着Redis服务器将立即清空当前数据库中的所有数据,并在清空完成后才返回结果。这会导致阻塞其他命令的执行,直到清空操作完成。
❗在生产环境中执行清除数据的操作是非常危险的,可能会导致严重的数据丢失和系统故障。除非有充分的理由和必要性,否则绝对不应该在生产环境中执行清除数据的操作。
在实际操作中,如果需要清除数据,应该首先进行充分的备份,并且在执行清除操作之前进行仔细的计划和测试。另外,建议在执行清除操作之前通知相关的团队成员,并且在清除之后立即进行数据恢复测试,以确保数据的完整性和可用性。
总之,清除数据操作必须谨慎对待,必须确保在适当的时间和环境中进行,并且遵循严格的操作流程和安全规范,以避免可能造成的灾难性后果。