赞
踩
目录
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
在Redis中,最核心的两个命令之一是GET,用于根据键来获取对应的值。另一个是SET,用于将指定的键与相应的值存储在Redis中。这两个命令是Redis中最基本、最常用的命令之一。
GET命令的语法如下:
GET key
它用于获b取存储在指定键中的值。如果键存在,则返回键对应的值;如果键不存在,则返回nil。
"Nil" 和 "null" 的区别
"Nil" 和 "null" 都是用于表示空值或者缺失值的术语,但它们的使用和含义在不同的编程语言中可能会有所不同。
Nil:
Null:
总的来说,"nil" 和 "null" 都用于表示空值或者缺失值,但其具体含义和在编程语言中的使用方式会因语言而异。
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中的任何数据类型,例如字符串、列表、集合、哈希等。基本全局命令通常用于执行一般性的操作,如设置值、获取值、删除键等。
一些常见的基本全局命令及其作用:
这些基本全局命令提供了对Redis数据的基本操作,是使用Redis时最常用的命令之一。通过这些命令,我们可以进行数据的增删改查以及管理Redis数据库。
我们详细介绍其中最常用的几个:
KEYS命令是Redis中用于返回所有满足指定模式的键的命令。它支持一些通配符样式,使用户可以根据特定的模式来匹配键。
该命令允许使用通配符来匹配键名,其中通配符有以下含义:
KEYS命令的语法如下:
KEYS pattern
该命令自1.0.0版本起就已经存在,时间复杂度为O(N),其中N是匹配模式的键的数量。
该命令用于在Redis中匹配指定模式的键,并返回所有满足条件的键名。在使用KEYS pattern命令时需要注意,如果数据集非常大,匹配过程可能会导致性能问题,因为Redis会遍历所有键来进行匹配,时间复杂度为O(N)。因此,如果可能的话,我们应尽量避免在生产环境中频繁使用该命令,或者限制匹配范围以提高性能。
示例:KEYS h*llo
该命令将返回所有以h开头且以llo结尾的键,例如hello、hallo、hxllo等。
下面是一些关于该例子的常见的通配符样式:
使用KEYS命令需要注意,如果在大型数据库中使用过于通用的模式,可能会导致性能问题,因为Redis在执行KEYS命令时需要遍历整个键空间,而Redis是一个单线程的服务器,执行类似key * 这样的操作的时间非常长,就使得Redis服务器被阻塞了,无法再给其他客户端提供服务!这样的后果可能是灾难性的。Redis经常会用作缓存抵挡在MySQL前面替它“负重前行”,如果Redis被keys * 阻塞住了,此时其他的查询redis操作就会超时,然后这些请求就会直接查数据库,很容易就会把我们的数据库弄崩溃。因此,建议在生产环境中谨慎使用KEYS命令,特别是对于模式中使用通配符的情况。
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命令是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命令是Redis中用于为指定键设置秒级的过期时间(Time To Live TTL)的命令。通过EXPIRE命令,可以指定某个键在一定时间后自动过期,从而实现自动清理过期数据的功能。
它的语法如下:
EXPIRE key seconds
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,EXPIRE命令的执行时间都是固定的。EXPIRE命令接受两个参数,第一个参数是要设置过期时间的键名,第二个参数是过期时间的秒数。成功设置过期时间后,该键在指定秒数后将自动被删除。
Redis提供的 EXPIRE 命令在很多场景下都有广泛的应用,包括但不限于验证码、优惠券等场景,以及我们后续可能会涉及到的基于Redis实现的分布式锁。
示例:EXPIRE mykey 60
该命令将为名为mykey的键设置60秒的过期时间。如果成功设置了过期时间,则返回1;如果键不存在或设置失败,则返回0。
EXPIRE命令常用于对缓存数据进行管理,可以有效地控制数据的生命周期,减少内存占用。需要注意的是,过期时间仅在设置之后开始计算,并且只对当前数据库中的键有效。
注意,此处的设定过期时间必须是针对已经存在的key。
你可能会感到奇怪——这里的这个单位为什么是秒?秒这个单位对于计算机来说太长了,一般都是毫秒啊?
我们还有一个及其类似的命令:
PEXPIRE key milliseconds
key:要设置过期时间的键名。
milliseconds:过期时间,以毫秒为单位。键在设定的毫秒数后将会自动过期并被删除。
PEXPIRE命令是Redis中用于为指定键设置毫秒级的过期时间的命令。与EXPIRE命令不同,PEXPIRE命令接受的过期时间单位是毫秒。
PEXPIRE命令通常用于需要更精确控制过期时间的场景,如限时任务、会话管理等。
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命令是Redis中用于返回指定键对应的数据类型的命令。通过TYPE命令,可以查看键存储的值的数据类型,以便进行相应的操作。
它的语法如下:
TYPE key
该命令自1.0.0版本起就已经存在,时间复杂度为O(1),即无论数据库中存在多少键,TYPE命令的执行时间都是固定的。TYPE命令接受一个参数,即要查询数据类型的键名。它返回的是键对应的数据类型,可能的返回值包括none、string、list、set、zset、hash和stream。
示例:TYPE mykey
该命令将返回名为mykey的键对应的数据类型。可能的返回值包括:
当然,还有很多其他类型:
通过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的性能和效率。
字符串(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):
字符串(String):
列表(List):
集合(Set):
有序集合(Sorted Set):
所以,Redis在底层实现不同数据结构时,会根据实际需求和特定场景进行优化,以提高性能并保证操作的时间复杂度符合承诺。这些优化可能涉及到不同的数据结构实现、编码方式选择、内存布局优化等方面,以达到节省时间和空间的效果。
每种内部编码方式的含义和区别:
raw(原始):
int(整型):
embstr(小字符串):
hashtable(哈希表):
ziplist(压缩列表):
linkedlist(链表):
intset(整数集合):
同时,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的实现原理和具体流程:
事件注册:
事件等待:
事件处理:
事件移除:
对于一次性事件(例如EPOLLONESHOT),处理完事件后需要重新注册该文件描述符,以便下次继续监听。工作原理:
epoll通过高效的数据结构和事件通知机制,能够快速地监听大量的文件描述符,并在事件发生时及时通知应用程序,从而实现高性能的I/O多路复用。
在Redis中,使用epoll作为I/O多路复用技术的实现,是为了高效地管理和处理大量的客户端连接。
前因:
epoll的特性:
Redis使用epoll的后果:
总的来说,Redis使用epoll作为I/O多路复用技术的实现,能够更高效地管理和处理大量的客户端连接,提高了系统的并发处理能力和性能表现,同时降低了系统的复杂度和资源消耗。
虽然单线程模型给Redis带来了许多好处,但也存在一个致命的问题:对于单个命令的执行时间是有限制的。如果某个命令的执行时间过长,会导致其他命令都处于等待队列中,无法得到及时响应,从而造成客户端的阻塞。对于Redis这样高性能的服务来说,这种情况是非常严重的,因为它会降低系统的响应速度和并发能力。
因此,Redis更适合于处理快速执行的场景,即执行时间较短的命令。例如,对于读取和写入内存中的数据、执行简单的计算或者查询等操作,Redis表现出色。而对于需要长时间计算或者复杂逻辑的操作,可能不适合在Redis中执行,因为这会影响到其他客户端的请求响应速度,降低了系统的吞吐量和性能表现。
因此,在使用Redis时,我们需要合理设计命令的执行逻辑,避免长时间阻塞操作,保证系统的稳定性和性能。同时,可以通过分布式部署、优化命令设计、使用合适的数据结构等方式来减轻单线程模型带来的局限性,提高系统的并发能力和响应速度。
字符串类型在Redis中是最基础的数据类型,具有以下几点需要特别注意:
所有键的类型都是字符串类型:在Redis中,所有的键(Key)都是字符串类型,这意味着无论是用于存储简单的键值对还是复杂的数据结构,键的类型都是字符串。其他几种数据结构,如列表(List)和集合(Set)的元素类型也都是字符串类型。因此,对字符串类型的理解是掌握其他数据结构的基础。
值的多样性:字符串类型的值并不限于传统意义上的字符串,它可以包含不同类型的数据,例如:
值的大小限制:Redis内部存储字符串完全是按照二进制流的形式保存的,二进制数据是指由0和1组成的数据序列,是计算机中最基本的数据表示方式。在计算机中,所有的数据最终都会被转换成二进制形式进行存储和处理。二进制数据可以表示各种类型的信息,包括数字、文本、图像、音频等。尽管字符串类型的值可以是各种形式的数据,但是Redis对单个字符串的大小有限制,其最大值不能超过512MB。这是为了保证Redis的性能和稳定性,防止存储过大的字符串导致内存溢出或性能下降。所以其实Redis主要用于存储文本数据,对体积较大的音频视频的存储频率不高。
此外,需要特别注意的是,由于Redis内部存储字符串完全是按照二进制流的形式保存的,因此Redis不处理字符集编码问题,也就很少出现乱码问题。这意味着Redis存储的字符串数据是原样存储的,不会对字符集编码进行转换或处理。因此,客户端传入的命令中使用的是什么字符集编码,Redis就会存储什么字符集编码的数据。这一点在处理多语言环境或特定字符集编码要求的场景下需要特别注意。
SET命令是用于将string类型的value设置到指定的key中。如果key之前已经存在,则新的value会覆盖旧的value,无论原来的数据类型是什么,同时之前设置的过期时间(TTL)也会失效。
语法:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
命令有效版本:1.0.0之后
时间复杂度:O(1)
选项:
|:表示命令选项之间的分隔符。通常用于指定命令的不同选项,如 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可能会进行合并。
返回值:
示例:
SET mykey "Hello"
以上示例将字符串"Hello"设置为键名为"mykey"的值。
SET mykey "Hello" EX 3600 NX
以上示例设置了键名为"mykey"的值为"Hello",并且设置了过期时间为3600秒,仅当该键名之前不存在时才会执行设置操作。
GET命令用于获取指定key对应的value。如果key不存在,则返回nil。如果value的数据类型不是string,会报错。
语法:
GET key
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回key对应的value,如果key不存在则返回nil。
示例:
GET mykey
以上示例将返回键名为"mykey"的值。
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命令用于一次性设置多个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。
使用MGET和MSET命令能够有效地减少网络通信时间,因此在性能上相较于单个GET操作或SET操作更为优越。举例来说,假设网络通信耗时为1毫秒,而命令执行时间耗时为0.1毫秒,则在以下表格中展示了不同操作的执行时间:
1000 次 get 和 1 次 mget 对比:
学会使用批量操作能够有效提高业务处理效率。然而,需要注意的是,我们每次批量操作所发送的键的数量也不是无限制的。如果一次批量操作发送的键数量过多,可能会导致单个命令执行时间过长,从而导致Redis阻塞。因此,在使用批量操作时,需要根据实际情况合理设置批量操作的范围,以充分发挥其优势,同时避免潜在的性能问题。
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命令用于将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命令用于将key对应的字符串表示的数字加上对应的值。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。
语法:
INCRBY key decrement
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回integer类型的加上对应值后的数值。
示例:
INCRBY mykey 5
以上示例会将键名为"mykey"对应的值加上5。如果"mykey"不存在,则将其视为0处理,执行INCRBY后返回的值为5。
也可以将数字设置为负数:
DECR命令用于将key对应的字符串表示的数字减一。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。
语法:
DECR key
命令有效版本:1.0.0之后
时间复杂度:O(1)
返回值:返回integer类型的减一后的数值。
示例:
DECR mykey
以上示例会将键名为"mykey"对应的值减一。如果"mykey"不存在,则将其视为0处理,执行DECR后返回的值为-1。
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命令用于将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命令用于将值追加到已有键的字符串之后。如果键已存在且其对应的值是字符串类型,则该值将会被追加。如果键不存在,则效果与执行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命令用于返回与指定键相关联的字符串的子串。子串的起始位置和结束位置由参数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命令用于覆盖字符串中的一部分,从指定的偏移位置开始。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 命令会返回指定键对应的字符串值的长度,如果该键不存在或者键对应的值不是字符串类型,则返回 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 数据结构时,如果需要进行频繁的算术运算,并且需要保持高性能,我们还是建议尽量使用整数类型来存储数据,避免使用字符串来表示小数。
图 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 缓存的业务数据访问过程:
通过增加缓存功能,可以极大地提升查询效率,因为在一小时内,对于相同的 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 总是能够保持在可接受的内存使用范围内,并尽可能地保留重要的数据。
许多应用都会将 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)将数据定期或实时地持久化到磁盘中,并根据业务需求设计合适的数据同步策略和数据备份方案,以确保数据的安全性和可靠性。
综上所述,开发成熟、稳定的计数系统需要综合考虑防作弊、按照不同维度计数、避免单点问题、数据持久化等多方面的挑战,并采取相应的技术和措施来应对这些挑战,从而保证系统的性能、可靠性和安全性。
在分布式 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)类型,它们的叫法可能是哈希、字典、关联数组或映射。在 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命令用于在哈希类型数据中为特定字段(field)设置值(value)。这个命令是在Redis数据库中使用的一种关键命令,它允许用户为已存在的哈希表设置字段与相应的值,或者在哈希表不存在时创建一个新的哈希表,并设置其字段与值。这种命令的使用使得在Redis中存储和检索结构化数据变得更加方便和高效。
它的语法如下:
HSET key field value [field value ...]
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 插入一组field的时间复杂度为O(1),插入N组field的时间复杂度为O(N)。
返回值: 返回成功添加的字段的个数。
示例:
HSET user:1001 name "John"
这个示例命令将在名为"user:1001"的哈希类型数据中设置字段"name"的值为"John"。
HGET命令是Redis中用于获取哈希类型数据中指定字段(field)对应的值的命令。通过该命令,用户可以从存储在Redis中的哈希表中检索特定字段的值。这种功能对于检索和读取结构化数据非常有用,尤其在需要获取特定字段值而不需要整个哈希表内容的情况下。 HGET命令的使用简单而高效,使得对哈希类型数据进行快速读取成为可能,从而满足了各种数据检索需求。
它的语法如下:
HGET key field
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: O(1)。因为哈希表的键值对是通过哈希函数直接查找的,所以获取指定字段的值的时间复杂度是常量级别的。
返回值: 如果指定字段存在,则返回该字段对应的值;如果指定字段不存在,则返回nil。
示例:
HGET user:1001 name
这个示例命令将返回名为"user:1001"的哈希类型数据中字段"name"的值。
HEXISTS命令是Redis中用于判断哈希类型数据中是否存在指定字段(field)的命令。通过该命令,用户可以轻松地检查哈希表中是否存在特定的字段。当需要确认某个字段是否存在时,HEXISTS命令可以提供快速而有效的解决方案。这种功能对于编程中的条件逻辑判断和数据操作十分有用,使得开发者可以根据字段的存在与否来进行相应的处理。
它的语法如下:
HEXISTS key field
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: O(1)。因为哈希表的键值对是通过哈希函数直接查找的,所以判断指定字段是否存在的时间复杂度是常量级别的。
返回值: 如果指定字段存在,则返回1;如果指定字段不存在,则返回0。
示例:
HEXISTS user:1001 name
这个示例命令将判断名为"user:1001"的哈希类型数据中是否存在字段"name"。如果存在,则返回1;如果不存在,则返回0。
HDEL命令是Redis中用于删除哈希类型数据中指定的一个或多个字段(field)的命令。通过该命令,用户可以轻松地从哈希表中移除一个或多个指定的字段及其对应的值。这个功能对于数据的清理和管理非常重要,特别是当需要从数据结构中删除特定字段时。HDEL命令的灵活性和效率使得对哈希类型数据的精确控制变得简单而可靠,有助于确保数据的一致性和准确性。
它的语法如下:
HDEL key field [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命令是Redis中用于获取哈希类型数据中所有字段名(field)的命令。通过该命令,用户可以一次性地获取哈希表中所有的字段名,而不需要获取字段对应的值。这种功能对于需要遍历哈希表中所有字段名的场景非常有用,例如在需要对哈希表进行全面检查或者进行批量操作时。HKEYS命令的使用简单而高效,使得对哈希类型数据的字段名进行管理和处理变得更加方便。
它的语法如下:
HKEYS key
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有字段的时间复杂度为O(N),其中N为字段的个数(哈希的元素个数)。
HKEYS命令用于获取哈希类型数据中的所有字段名(field)。其执行过程包括以下步骤:
返回值: 返回一个包含所有字段名的列表。
示例:
HKEYS user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中的所有字段名列表。
HVALS命令是Redis中用于获取哈希类型数据中所有值(value)的命令。通过该命令,用户可以一次性地获取哈希表中所有字段对应的值,而不需要获取字段名。这种功能对于需要获取哈希表中所有值的场景非常有用,例如在需要对哈希表中的所有值进行批量处理或者分析时。HVALS命令的使用简单而高效,使得对哈希类型数据的值进行管理和处理变得更加方便。
它的语法如下:
HVALS key
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有值的时间复杂度为O(N),其中N为字段的个数(哈希的元素个数)。
返回值: 返回一个包含所有值的列表。
示例:
HVALS user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中的所有值列表。
HGETALL命令是Redis中用于获取哈希类型数据中所有字段以及对应的值的命令。通过该命令,用户可以一次性地获取哈希表中所有字段和它们对应的值,以键值对的形式返回。这种功能对于需要获取哈希表中所有数据的场景非常有用,例如在需要对哈希表中的所有字段和值进行全面分析或者迁移时。HGETALL命令的使用简单而高效,使得对哈希类型数据的整体检索和操作变得更加方便。
它的语法如下:
HGETALL key
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有字段和对应值的时间复杂度为O(N),其中N为字段的个数。
返回值: 返回一个包含所有字段和对应值的列表。列表中的元素依次为字段名和对应的值交替出现。
示例:
HGETALL user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中的所有字段以及对应的值的列表。列表中的元素依次为字段名和对应的值。
在使用HGETALL命令时,如果哈希元素个数较多,可能会导致Redis出现阻塞的情况。这是因为HGETALL会一次性返回哈希表中所有的字段和对应的值,如果哈希表非常庞大,数据传输和处理的压力会加大。
如果开发者只需要获取部分字段的值,可以使用HMGET命令。HMGET允许指定需要获取的字段,从而减少数据传输量和处理压力。
另外,如果一定需要获取全部字段的值,并且哈希表非常大,开发者可以尝试使用HSCAN命令。HSCAN命令采用渐进式遍历哈希类型数据,它会分步获取哈希表中的元素,从而减少一次性获取全部数据所带来的压力。具体的HSCAN命令会在后续章节中进行介绍。
HMGET命令是Redis中用于一次性获取哈希类型数据中多个字段的值的命令。通过指定哈希键(key)和多个字段名(field),HMGET命令可以同时获取这些字段对应的值。这种命令的使用简单高效,适用于需要一次性获取多个字段值的场景。
它的语法如下:
HMGET key field [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命令用于迭代哈希表中的所有字段,并以游标方式逐步获取结果。相比于一次性获取所有字段名或值的命令,如HKEYS或HVALS,HSCAN提供了一种无阻塞的方式来遍历哈希表,适用于大型哈希表的遍历操作。
使用HSCAN命令进行哈希表的迭代遍历时,需要指定一个起始游标(cursor)值,以及一些可选的参数,如匹配模式(MATCH)和返回数量限制(COUNT)。命令会返回一个游标值,用于指示下一次迭代的起始位置,以及一组哈希表中的字段及其对应的值。通过多次调用HSCAN命令,并不断更新游标值,直到游标返回0,表示遍历完成。
由于HSCAN命令以游标方式进行遍历,因此适用于大型哈希表的遍历操作,能够在不阻塞Redis主线程的情况下逐步获取数据,避免了一次性获取所有数据可能导致的内存占用过高和网络传输延迟等问题。
它的语法如下:
HSCAN key cursor [MATCH pattern] [COUNT count]
命令有效版本: Redis 2.8.0之后可用。
时间复杂度: 每次迭代的时间复杂度为O(1),整个迭代过程的时间复杂度取决于哈希表的大小。
返回值: 返回一个包含游标和哈希表中匹配字段的列表。列表的第一个元素是下一个迭代的游标,后续元素是匹配的字段及其对应的值。
示例:
HSCAN myhash 0 MATCH field* COUNT 5
这个示例命令将从名为"myhash"的哈希表中以游标0开始迭代,匹配所有以"field"开头的字段,并每次返回最多5个匹配项。
HLEN命令是Redis中用于获取哈希类型数据中所有字段的个数的命令。通过该命令,用户可以获取哈希表中字段的数量,即哈希表中键值对的数量。这个功能对于了解哈希表的大小和结构非常有用,特别是在需要进行哈希表大小估算或者性能优化时。HLEN命令的使用简单而高效,使得对哈希类型数据的大小进行检测变得更加方便。
它的语法如下:
HLEN key
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 获取所有字段的个数的时间复杂度为O(1)。因为Redis内部使用哈希表来存储哈希类型数据,直接获取哈希表的大小即可得到字段的个数。
返回值: 返回哈希类型数据中所有字段的个数。
示例:
HLEN user:1001
这个示例命令将返回名为"user:1001"的哈希类型数据中所有字段的个数。
HSETNX命令是Redis中用于在哈希类型数据中设置字段和对应的值的命令,但仅当字段不存在时才执行设置操作。如果字段已经存在,则该命令不执行任何操作,保持原有值不变。这个功能对于确保哈希表中特定字段的唯一性非常有用,特别是在需要避免重复设置字段值的情况下。HSETNX命令的使用使得对哈希类型数据的更新操作更加可靠和安全。
它的语法如下:
HSETNX key field value
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 该命令的时间复杂度为O(1),因为它只需要执行一次哈希表的插入操作。
返回值: 如果字段不存在并成功设置,则返回1;如果字段已经存在,则不执行任何操作并返回0。
示例:
HSETNX user:1001 name "Alice"
这个示例命令将在名为"user:1001"的哈希类型数据中,如果字段"name"不存在,则设置其值为"Alice"。如果字段"name"已经存在,则不执行任何操作。如果设置成功,则返回1;如果字段已经存在,则返回0。
HINCRBY命令是Redis中的哈希类型数据操作命令,用于将哈希类型数据中指定字段对应的数值增加指定的值。如果指定字段不存在,则会创建该字段并将其值初始化为0,然后再执行增加操作。这个命令对于需要对哈希表中的数值字段进行增加操作非常有用,尤其是在需要原子性操作和简单逻辑的场景下。HINCRBY命令的使用简单方便,使得对哈希类型数据的数值字段进行增加操作变得更加灵活和可控。
它的语法如下:
HINCRBY key field increment
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 该命令的时间复杂度为O(1),因为它只需要执行一次哈希表的查找和更新操作。
返回值: 返回执行增加操作后,字段对应的新值。
示例:
HINCRBY user:1001 views 10
这个示例命令将在名为"user:1001"的哈希类型数据中,将字段"views"对应的数值增加10。如果字段"views"不存在,则会创建该字段并将其值初始化为0,然后再增加10。最后返回执行增加操作后"views"字段对应的新值。
HINCRBYFLOAT命令是Redis中的哈希类型数据操作命令,是HINCRBY的浮点数版本。它用于将哈希类型数据中指定字段对应的数值以浮点数形式增加指定的值。如果指定字段不存在,则会创建该字段并将其值初始化为0,然后再执行增加操作。这个命令对于需要对哈希表中的数值字段进行精确的浮点数增加操作非常有用,尤其是在需要保留精度的情况下。HINCRBYFLOAT命令的使用简单方便,使得对哈希类型数据的数值字段进行增加操作变得更加灵活和可控。
它的语法如下:
HINCRBYFLOAT 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
序列化字符串类型,例如 JSON 格式: 使用序列化后的字符串类型,例如 JSON 格式,存储整个用户对象的信息。
set user:1 {"name":"James","age":23,"city":"Beijing"}
哈希类型: 使用哈希类型存储用户信息。
hmset user:1 name James age 23 city Beijing
综上所述,哈希类型是相对较为优秀的方案,因为它既简单又灵活,可以轻松地处理信息的局部变更或获取操作。虽然存在一些内存消耗的问题,但在大多数情况下,这种消耗是可以接受的,特别是与其他两种方案相比,哈希类型更加合理和实用。
列表类型是一种用于存储多个有序的字符串的数据结构。如图中所示,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命令用于将一个或多个元素从左侧插入(头部插入)到列表中。这个命令允许用户指定一个列表键(key)以及一个或多个要插入的元素,然后将这些元素按顺序从左侧插入到列表中。LPUSH命令会将指定的元素从列表的左侧插入,使得插入后的元素排列顺序与插入的顺序一致。如果key不存在,则会创建一个新的列表,并将元素插入其中,然后返回插入后列表的长度。如果key已经存在,并且key对应的value类型不是list,此时LPUSH命令就会报错。
它的语法如下:
LPUSH key element [element ...]
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。
示例:
LPUSH mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从左侧插入到名为"mylist"的列表中。如果列表不存在,则会创建一个新的列表。最后返回执行插入操作后"mylist"列表的长度。需要注意的是,依次头插,最后全部插入完毕,“orange”是在最前面。
LPUSHX命令用于在指定的键存在时,将一个或多个元素从左侧插入(头部插入)到列表中。这个命令允许用户指定一个列表键(key)以及一个或多个要插入的元素,然后将这些元素按顺序从左侧插入到列表中。如果key存在且是一个列表类型,LPUSHX命令会将指定的元素从列表的左侧插入,使得插入后的元素排列顺序与插入的顺序一致,然后返回插入后列表的长度。如果key不存在,则不执行任何操作,直接返回0。
它的语法如下:
LPUSHX key element [element ...]
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。如果键不存在,则不执行任何操作并返回0。
示例:
LPUSHX mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从左侧插入到名为"mylist"的列表中,如果列表不存在,则不执行任何操作,直接返回0。如果列表存在,则将元素插入列表左侧,并返回执行插入操作后"mylist"列表的长度。
RPUSH命令用于将一个或多个元素从右侧插入(尾部插入)到列表中。这个命令允许用户指定一个列表键(key)以及一个或多个要插入的元素,然后将这些元素按顺序从右侧插入到列表中。这个命令会将指定的元素从列表的右侧插入,相当于尾插法,使得插入后的元素排列顺序与插入的顺序一致。如果key不存在,则会创建一个新的列表,并将元素插入其中,然后返回插入后列表的长度。
它的语法如下:
RPUSH key element [element ...]
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。
示例:
RPUSH mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从右侧插入到名为"mylist"的列表中。如果列表不存在,则会创建一个新的列表。最后返回执行插入操作后"mylist"列表的长度。
RPUSHX命令用于在指定的键存在时,将一个或多个元素从右侧插入(尾部插入)到列表中。如果键不存在,则不执行任何操作,直接返回0。这个命令通常用于在确保列表存在的情况下向列表尾部插入元素,避免了因为键不存在而出现错误或者创建新列表的情况。如果key存在且是一个列表类型,RPUSHX命令会将指定的元素从列表的右侧插入,然后返回插入后列表的长度。如果key不存在或者不是一个列表类型,则不执行任何操作,直接返回0。
它的语法如下:
RPUSHX key element [element ...]
命令有效版本: Redis 2.0.0之后可用。
时间复杂度: 插入一个元素的时间复杂度为O(1),插入多个元素的时间复杂度为O(N),其中N为插入元素的个数。
返回值: 返回执行插入操作后,列表的长度。如果键不存在,则不执行任何操作并返回0。
示例:
RPUSHX mylist "apple" "banana" "orange"
这个示例命令将元素"apple"、"banana"和"orange"依次从右侧插入到名为"mylist"的列表中,如果列表不存在,则不执行任何操作,直接返回0。如果列表存在,则将元素插入列表右侧,并返回执行插入操作后"mylist"列表的长度。
LRANGE(LIST RANGE)命令用于获取列表中指定区间范围内的所有元素,左闭右闭,即包括起始位置和结束位置的元素。通过指定列表的键(key)以及起始位置和结束位置的索引,LRANGE命令可以返回列表中指定区间范围内的所有元素。这个命令通常用于按范围获取列表中的元素,例如获取列表的前N个元素或者某个区间内的元素。LRANGE命令会返回指定区间范围内的所有元素,包括起始位置和结束位置的元素。如果起始位置大于列表的末尾索引,或者结束位置小于列表的起始索引,LRANGE命令会返回一个空列表(非未定义行为,也不会抛出异常)。
它的语法如下:
LRANGE key start stop
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 获取指定区间内所有元素的时间复杂度为O(N),其中N为要获取的元素数量。
返回值: 返回指定区间的所有元素。
示例:
LRANGE mylist 0 2
这个示例命令将返回名为"mylist"的列表中从第一个元素到第三个元素(左闭右闭)的所有元素。如果列表中只有两个元素,则返回这两个元素。
注意,元素前面的序号和元素下标无关。
在处理"下标超出范围"的情况时,不同编程语言或工具的处理方式可能有所不同,每种方式都有自己的优缺点。以下是对比各种处理方式的优缺点:
C++(未定义行为):
Java(抛出异常):
Redis(返回符合实际情况的元素):
LPOP命令用于从列表的左侧取出一个元素,即执行头部删除操作。这个命令会移除并返回列表中的第一个元素。与RPOP命令相似,LPOP也可以被用来实现队列的先进先出(FIFO)的操作,或者用于获取并删除列表中的第一个元素。
它的语法如下:
LPOP key
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 头部删除操作的时间复杂度为O(1),即常数时间复杂度。
返回值: 返回被取出的元素。如果列表为空,则返回nil。
示例:
LPOP mylist
这个示例命令将从名为"mylist"的列表的左侧取出一个元素,并将其返回。如果列表为空,则返回nil。
RPOP命令用于从列表的右侧取出一个元素,即执行尾部删除操作。这个命令会移除并返回列表中的最后一个元素。其作用类似于弹出栈中的顶部元素,从列表的尾部取出一个元素,同时将该元素从列表中移除。RPOP命令可以用于实现队列的后进先出(LIFO)的操作,或者用于获取并删除列表中的最后一个元素。
它的语法如下:
RPOP key [count]
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 尾部删除操作的时间复杂度为O(1),即常数时间复杂度。
返回值: 返回被取出的元素。如果列表为空,则返回nil。
示例:
RPOP mylist
这个示例命令将从名为"mylist"的列表的右侧取出一个元素,并将其返回。如果列表为空,则返回nil。
LINDEX命令用于获取列表中从左侧开始的第index位置的元素。索引从0开始,即0表示第一个元素,1表示第二个元素,以此类推。如果index为负数,则表示从右侧开始计数,-1表示倒数第一个元素,-2表示倒数第二个元素,依此类推。该命令主要用于访问列表中特定位置的元素,根据索引可以快速获取对应位置的元素值,无需遍历整个列表。如果下标非法,返回nil。
它的语法如下:
LINDEX key index
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 获取指定位置元素的时间复杂度为O(N),其中N为列表的长度。
返回值: 返回被取出的元素。如果指定索引超出了列表的范围,则返回nil。
示例:
LINDEX mylist 2
这个示例命令将返回名为"mylist"的列表中从左侧开始的第3个元素(索引为2)。如果列表中不包含这个位置的元素,则返回nil。
LINSERT命令允许在列表中的特定位置插入元素,可以选择在指定元素之前(BEFORE)或之后(AFTER)插入新元素。如果指定的pivot元素在列表中存在,则在其前后插入新元素;如果pivot元素不存在,则命令不执行任何操作。这个命令通常用于需要在列表中特定位置插入元素的场景,例如在某个元素之前或之后插入新的元素,以修改列表的结构。
它的语法如下:
LINSERT key <BEFORE | AFTER> pivot element
命令有效版本: Redis 2.2.0之后可用。
时间复杂度: 插入操作的时间复杂度为O(N),其中N为列表的长度。
返回值: 返回执行插入操作后列表的长度。
示例:
LINSERT mylist BEFORE "world" "hello"
这个示例命令将在名为"mylist"的列表中,找到第一个出现的"world"元素,并在其前面插入一个新元素"hello"。如果列表中不存在"world"元素,则不进行任何操作。
找基准值从左往右找,第一个符合基准值的位置即可。
LLEN命令是Redis中的列表操作命令,用于获取列表的长度,即列表中包含的元素个数。通过该命令,用户可以快速地获取列表的大小,以便于对列表进行管理和操作。这个命令的使用非常简单,只需指定列表的键(key),就可以返回该列表中包含的元素个数。
它的语法如下:
LLEN key
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 获取列表长度的时间复杂度为O(1),即常数时间复杂度。
返回值: 返回列表的长度,即其中包含的元素个数。
示例:
LLEN mylist
这个示例命令将返回名为"mylist"的列表中包含的元素个数,即列表的长度。
LREM命令用于从列表中删除与给定值相等的元素。可以指定删除元素的数量或方向(从左侧或右侧开始删除)。具体来说,LREM命令会在列表中从头到尾遍历,找到与给定值相等的元素,并将其删除。可以通过指定一个参数来表示要删除的元素的数量,该参数可以为正数、负数或零。当参数为正数时,表示删除从左侧开始匹配的元素数量;当参数为负数时,表示删除从右侧开始匹配的元素数量;当参数为零时,表示删除列表中所有与给定值相等的元素。
它的语法如下:
LREM key count value
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 删除单个元素的时间复杂度为O(N),其中N是列表的长度。删除多个元素的时间复杂度取决于count参数。
返回值: 返回被删除的元素数量。
示例:
LREM mylist 2 "foo"
这个示例命令将从名为"mylist"的列表中删除最多2个值为"foo"的元素。
LTRIM命令用于修剪(截取)列表,使其仅包含指定范围内的元素。它会保留列表中从开始位置到结束位置之间的元素,而将其他元素删除。具体来说,LTRIM命令需要指定列表的起始位置和结束位置(即索引范围),它会保留列表中从起始位置到结束位置之间的元素,而删除其他元素。这个范围是一个闭区间,即包括起始位置和结束位置的元素都会被保留在列表中。
它的语法如下:
LTRIM key start stop
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: O(N),其中N是列表被修剪后的元素数量。
返回值: 命令执行成功时返回"OK"。
示例:
LTRIM mylist 0 99
这个示例命令将名为"mylist"的列表修剪为只包含前100个元素,其余元素将被删除。
LSET命令用于设置列表中指定索引位置的元素的值。它会覆盖指定索引位置上原有的值。具体来说,LSET命令需要指定列表的名称、要设置的索引位置以及新的元素值。它会将列表中指定索引位置上原有的值替换为新的元素值。
它的语法如下:
LSET key index 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参数进行阻塞,直到有元素可弹出或超时。
当列表不为空时:
因此,两者的行为是一致的。
当列表不为空时,且在5秒内没有新元素加入时:
因此,两者的行为是不一致的。
当列表不为空时,且在5秒内有新元素加入时:
因此,两者的行为是不一致的。
BLPOP命令是LPOP命令的阻塞版本,用于从列表的左侧取出元素(即执行头部删除操作)。其功能与LPOP相似,但在列表为空时,BLPOP命令会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。命令会从左侧列表头部获取元素,如果列表为空,则会阻塞等待直到有元素可弹出或超时。BLPOP命令在功能上与LPOP相似,但不同之处在于阻塞特性。当列表为空时,LPOP会立即返回nil,而BLPOP会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。
这种阻塞特性使得BLPOP命令非常适合用于队列的消费者端。消费者可以通过BLPOP命令阻塞地等待队列中有新的元素可用,从而实现了实时地消费队列中的元素。通过阻塞等待,可以有效地减少系统的轮询频率,提高了系统的性能和效率。
它的语法如下:
BLPOP key [key ...] timeout
命令有效版本: Redis 1.0.0之后可用。
时间复杂度: 由于BLPOP是阻塞命令,如果列表为空,则它的时间复杂度取决于超时时间。
返回值: 返回被取出的元素,以及其所在的列表的键。如果列表为空且超时时间到达,则返回nil。
示例:
BLPOP mylist 10
这个示例命令将阻塞连接,直到名为"mylist"的列表中有元素可供取出,或者达到超时时间10秒为止。如果列表不为空,则会取出左侧的元素并返回;如果列表为空且超过了10秒,则返回nil。
阻塞等待:
再打开一个客户端,输入元素:
BRPOP命令是RPOP命令的阻塞版本,用于从列表的右侧取出元素(即执行尾部删除操作)。其功能与RPOP相似,但在列表为空时,BRPOP命令会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。命令会从右侧列表尾部获取元素,如果列表为空,则会阻塞等待直到有元素可弹出或超时。BRPOP命令在功能上与RPOP相似,但不同之处在于阻塞特性。当列表为空时,RPOP会立即返回nil,而BRPOP会阻塞连接,直到列表中有元素可供取出或达到指定的超时时间。
BRPOP命令的阻塞特性使其非常适合用于队列的消费者端。消费者可以通过BRPOP命令阻塞地等待队列中有新的元素可用,从而实现了实时地消费队列中的元素。这种阻塞机制可以避免轮询和忙等待,提高了系统的性能和效率。
它的语法如下:
BRPOP key [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) |
列表类型的内部编码有两种:
选择使用哪种内部编码由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"
但是上述两种内部编码的方式都已经是老黄历了。在Redis 3 中,引入了一种名为QuickList(快速列表)的新编码方式,取代了之前使用的ziplist(压缩列表)作为列表数据结构的默认编码方式。QuickList提供了更加灵活和高效的方式来存储列表数据,尤其适用于存储较大的列表。这个变化使得Redis能够更有效地处理各种大小的列表数据,并提高了存储和操作的效率。
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(微博列表),现需要分页展示文章列表。此时我们可以考虑使用Redis的列表结构,因为列表不仅是有序的,同时支持按照索引范围获取元素。在这个场景中,用户的微博列表可以作为一个有序的列表,按照时间顺序存储用户发布的微博,通过列表结构可以方便地获取用户的最新微博,并且支持分页展示。
使用列表结构存储用户的微博列表具有以下优势:
每篇微博使用哈希结构存储,包括微博的属性: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的哈希结构和列表结构,可以高效地存储和检索用户的微博列表,并且支持分页展示,为用户提供了良好的使用体验。
但是此方案在实际应用中,使用列表结构存储用户的微博列表可能会遇到两个问题:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。