当前位置:   article > 正文

Redis(二)Redis实战操作:预备知识(GET和SET、基本全局命令、常见数据类型和内部编码)、String字符串、Hash哈希、List列表、 Set集合、Zset有序集合、渐进式遍历、数据库_redis操作

redis操作

接上次博客:Redis(一)Redis 的使用和原理:初识Resdis(盛赞Redis、Redis特性、使用场景、重大版本)、安装Redis与客户端介绍(在Ubuntu上安装和配置、Redis客户端介绍)-CSDN博客

目录

预备知识

GET和SET

GET命令

SET命令

基本全局命令

KEYS命令

EXISTS命令

DEL命令

EXPIRE命令

TTL命令

TYPE命令

键过期机制的原理

键过期机制的优势

时间轮的基本结构:

流程:

优势和适用场景:

Redis常见数据类型和内部编码

单线程架构

引出单线程模型

String 字符串

常见命令

SET

GET

MGET

MSET

多次 get vs 单次 mget:

SETNX

计数命令

INCR

INCRBY

DECR

DECRBY

INCRBYFLOAT

其他命令

APPEND

GETRANGE

SETRANGE

STRLEN

字符串类型命令

内部编码

典型使用场景

缓存(Cache)功能

计数(Counter)功能

共享会话(Session)

手机验证码

Hash 哈希

命令

HSET

HGET

HEXISTS

HDEL

HKEYS

HVALS

HGETALL

HMGET

HSCAN

HLEN

HSETNX

HINCRBY

HINCRBYFLOAT

哈希类型命令小结

内部编码

使用场景

映射关系表示用户信息

缓存方式对比

List 列表

命令

LPUSH

LPUSHX

RPUSH

RPUSHX

LRANGE

LPOP

RPOP

LINDEX

LINSERT

LLEN

LREM

LTRIM

LSET

阻塞版本命令

阻塞版本的 blpop 和 非阻塞版本 lpop 的区别

BLPOP

BRPOP

列表命令小结

内部编码

使用场景

消息队列

微博 Timeline

Set 集合

普通命令

SADD

SMEMBERS

SISMEMBER

SCARD

SPOP

SMOVE

SREM

集合间操作

SINTER

SINTERSTORE

SUNION

SUNIONSTORE

SDIFF

SDIFFSTORE

集合类型命令小结

内部编码

使用场景

Zset 有序集合

普通命令

ZADD

ZCARD

ZCOUNT

ZRANGE

ZREVRANGE

ZRANGEBYSCORE

ZPOPMAX

BZPOPMAX

ZPOPMIN

BZPOPMIN

ZRANK

ZREVRANK

ZSCORE

ZREM

ZREMRANGEBYRANK

ZREMRANGEBYSCORE

ZINCRBY

集合间操作

ZINTERSTORE

ZUNIONSTORE

有序集合命令小结

内部编码

使用场景

Redis类型简介——stream

Redis类型简介——geospatial

Redis类型简介——hyperloglog

Redis类型简介——bitmaps

Redis类型简介——bitfields

渐进式遍历

SCAN

数据库管理

切换数据库

清除数据库


Redis提供了五种数据结构,每一种都有其独特的特点和优势。深刻理解这些特点对于Redis的开发与运维至关重要。同时,我们需要熟练掌握每种数据结构的常用命令,以便后续可以使在使用Redis时游刃有余。

我们的大致学习内容如下:

• 预备知识:在深入研究Redis的数据结构之前,需要了解几个全局(generic)命令,理解数据结构的内部编码方式,以及掌握Redis单线程模式的机制分析。

• 5种数据结构的特点、命令使用和应用场景示例:Redis提供了字符串(String)、列表(List)、哈希(Hash)、集合(Set)和有序集合(Sorted Set)等五种数据结构。每种数据结构都有其独特的特点和优势,在不同的应用场景中发挥着重要作用。掌握它们的常见命令和应用场景,可以帮助开发者更加灵活地运用Redis。

• 键遍历、数据库管理:除了了解数据结构,我们还需要掌握如何进行键遍历以及数据库管理。通过键遍历可以快速了解Redis中存在的键以及它们的类型,而数据库管理则包括数据库切换、数据库清空和数据库信息统计等操作。这些操作能够帮助开发者更好地管理和维护Redis数据库。

预备知识

在正式介绍Redis的五种数据结构之前,我们需要了解一些全局命令、数据结构和内部编码、以及单线程命令处理机制是非常必要的。这些知识点对于我们后续内容的学习打下了良好的基础。

这主要体现在以下两个方面:

  1. 通用性的命令理解:Redis拥有大量的命令,如果仅仅依靠死记硬背,对于我们来说可能会感到困难。然而,通过理解Redis的一些机制,我们可以发现这些命令具有很强的通用性。深入了解Redis的内部机制和数据结构,可以帮助我们更好地理解命令的设计和用途,从而更加灵活地运用这些命令。

  2. 命令和数据结构的场景使用: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" 都是用于表示空值或者缺失值的术语,但它们的使用和含义在不同的编程语言中可能会有所不同。

  1. Nil:

    • 在一些编程语言中,如Objective-C、Swift等,"nil" 用于表示一个空对象或者指针。
    • 在Objective-C中,"nil"是一个空指针,用于表示没有指向任何对象的指针。
    • 在Swift中,"nil"用于表示可选类型(Optional),表示该类型可以是一个值,也可以是空(没有值)。
    • 在Lisp编程语言中,"nil"是一个特殊的符号,用于表示空列表或者空值。
    • 在Ruby中,"nil"表示空值或者未初始化的对象。
    • 在Redis中,nil 表示的是一个特殊的值,用于表示键不存在或者某个键的值为空。
  2. 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中的任何数据类型,例如字符串、列表、集合、哈希等。基本全局命令通常用于执行一般性的操作,如设置值、获取值、删除键等。

一些常见的基本全局命令及其作用:

  1. SET: 用于设置指定键的值。
  2. GET: 用于获取指定键的值。
  3. DEL: 用于删除一个或多个键。
  4. EXISTS: 用于检查指定键是否存在。
  5. TYPE: 用于获取指定键的数据类型。
  6. EXPIRE: 用于为指定键设置过期时间。
  7. TTL: 用于获取指定键的剩余过期时间。
  8. KEYS: 用于匹配满足指定模式的键。
  9. FLUSHDB: 用于清空当前数据库的所有键。
  10. FLUSHALL: 用于清空所有数据库的所有键。

这些基本全局命令提供了对Redis数据的基本操作,是使用Redis时最常用的命令之一。通过这些命令,我们可以进行数据的增删改查以及管理Redis数据库。

我们详细介绍其中最常用的几个:

KEYS命令

KEYS | Redis

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 | Redis

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是一个客户端服务器结构的程序,客户端和服务器之间通过网络进行通信。

  1. 单次检查:在第一种写法中,只有一次命令请求和一次响应,因此只需要进行一次网络通信,这使得执行速度更快。

  2. 多次检查:而在第二种写法中,需要进行多次命令请求和多次响应,因此会产生更多轮次的网络通信,这会增加延迟并且可能影响性能。

因此,尽管EXISTS命令的时间复杂度为O(1),但在实际应用中,应该尽量减少对该命令的频繁调用,以避免不必要的网络开销和性能损耗。

DEL命令

DEL | Redis

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

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

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命令是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),一旦设置了生存时间,键将在一段时间后自动过期并被删除。这种机制可以用于实现缓存、会话管理等场景,有效地控制数据的生命周期,减少内存占用。

键过期机制的原理

键过期机制的原理分为定期删除和惰性删除:

  1. 设置生存时间(TTL):通过使用EXPIRE命令,可以为指定的键设置生存时间,即键的过期时间。一旦设置了生存时间,Redis会自动计算键的剩余过期时间,并在此时间之后将键删除。

  2. 过期检查:Redis通过定时任务来检查键是否已经过期。在每次访问键时,Redis会先检查键是否已经过期,如果过期则立即删除。此外,Redis还会在后台周期性地扫描过期键,并删除已过期的键。

  3. 惰性删除:为了提高性能,Redis采用了惰性删除的策略。即当某个键过期后,不会立即删除,而是等到下次访问该键时才会删除。这样可以避免频繁地进行删除操作,提高了性能。

定期删除的流程如下:

  1. 定期任务启动:Redis服务器在后台启动了一个定时任务,用于执行定期删除操作。

  2. 定期检查过期键:定时任务会每隔一段时间(默认每秒执行10次,可通过配置参数hz调整)检查部分过期键,这样可以分摊过期键的检查和删除压力,避免一次性删除过多过期键导致性能问题。

  3. 检查过期键的时间计算:Redis会计算每个键的过期时间与当前时间的差值,如果该差值小于等于0,则说明该键已经过期。

  4. 删除过期键:一旦发现有过期键,定期任务会立即删除这些过期键,释放占用的内存空间。

  5. 定期删除频率调整:定期删除的频率可以通过配置文件中的参数hz来调整,默认值为10。可以根据实际情况调整该参数,以适应不同的系统负载和性能需求。

总的来说,定期删除通过定时任务的方式,定期检查并删除部分过期键,从而保证了过期键能够及时被删除,释放内存空间,提高了Redis的性能和可靠性

惰性删除是Redis中的一种过期键管理策略,其流程如下:

  1. 操作触发检查: 在对键进行读取或写入操作时,Redis会先检查该键是否设置了过期时间,并且是否已经过期。

  2. 过期检查: 如果键已经设置了过期时间,并且当前时间已经超过了键的过期时间,那么这个键就被认为是过期的。

  3. 删除操作: 一旦键被检测到已经过期,Redis会立即将该键删除,从而释放相应的内存空间。

  4. 无过期检查,直接返回值: 如果键没有设置过期时间,或者未过期,Redis会直接返回键对应的值,而不会进行删除操作。

这种惰性删除的策略保证了Redis在实际使用中的高性能和低延迟。由于只有在对键进行操作时才会进行过期检查和删除操作,因此不会对数据库的读写操作产生太大的性能影响。此外,由于只在键被访问时才进行过期检查,所以即使有大量过期键,也不会对Redis的性能造成明显的影响。

尽管Redis通过定期删除和惰性删除这两种策略来管理过期键,但仍然可能会有一些过期的键残留在内存中,从而导致内存资源的浪费。为了解决这个问题,Redis还提供了一系列内存淘汰机制,以便在内存达到设定的上限时,自动删除一些键来释放内存空间。

Redis的内存淘汰机制包括以下几种:

  1. LRU(Least Recently Used): 最近最少使用策略,Redis会根据键的最近访问时间来选择要删除的键,即删除最近最少被访问的键。

  2. LFU(Least Frequently Used): 最不经常使用策略,Redis会根据键被访问的频率来选择要删除的键,即删除访问频率最低的键。

  3. TTL(Time To Live): 过期时间策略,Redis会优先删除已经过期的键。

  4. Random(随机删除): 随机选择要删除的键。

如何配置内存淘汰策略:

可以通过Redis的配置文件或者动态命令来配置内存淘汰策略。常用的配置选项包括maxmemory(最大内存限制)和maxmemory-policy(内存淘汰策略)。通过配置这些选项,可以根据实际需求来选择合适的内存淘汰策略,以便在内存达到限制时,自动删除部分键来释放内存空间。

关于内存淘汰机制还大有文章,我们以后会详细学习,这里只做简单了解即可。

键过期机制的优势

  1. 节省内存空间:可以自动删除不再需要的键,释放内存空间,避免内存溢出。

  2. 提高性能:通过定时任务和惰性删除机制,有效地管理和维护过期键,减少了删除操作对系统性能的影响,提高了系统的整体性能。

  3. 实现缓存:通过设置键的生存时间,可以实现缓存功能,缓存过期后自动更新,保持数据的有效性。

  4. 实现会话管理:可以为会话信息等数据设置生存时间,实现会话过期自动清理,提高系统的安全性和稳定性。

综上所述,Redis的键过期机制能够有效地管理和维护键的生命周期,提高系统的性能和稳定性,是Redis中重要的特性之一。

当然,除了Redis已有的定期删除和惰性删除策略外,我们还可以采用基于优先级队列和定时器的方式来管理过期键。

具体流程如下:

1. 创建优先级队列 / 堆:
将所有设置了过期时间的键加入到一个优先级队列中,优先级规则是过期时间越早的键优先级越高,即越早过期的键越靠前。

2. 启动定时器线程:
启动一个单独的定时器线程,该线程负责检查队首元素的过期时间,并执行相应的任务。

3. 定时器线程主循环:
定时器线程在一个循环中执行以下操作:

a. 检查队首元素过期时间:

  • 线程从优先级队列中获取队首元素,即过期时间最早的键。
  • 获取当前时间,计算当前时间与队首元素过期时间的时间差。

b. 设置等待:

  • 根据时间差设置等待,如果时间差较长,则将线程阻塞,等待一段时间后再次唤醒。
  • 如果时间差较短,则立即执行下一步操作。

c. 执行任务:

  • 如果队首元素的过期时间已到,即键已过期,线程执行相应的删除操作,并将该键从优先级队列中移除。
  • 如果队首元素的过期时间未到,则继续等待或执行其他任务。

4. 新任务添加唤醒:
在队列中添加新的任务时,可以唤醒定时器线程,重新检查队首元素的过期时间,并根据当前时间调整阻塞时间。

5. 性能优化:
定时器线程不需要高频率地扫描队首元素,而是根据当前时间与队首元素的时间差来设置合适的等待时间,以节省CPU开销。

通过优先级队列的方式,定时器线程只需关注队首元素,无需遍历所有键,提高了效率。
通过以上流程,我们可以实现一种高效的过期键管理机制,即保证了过期键及时被删除,又避免了对数据库的频繁访问,从而提高了系统的性能和可靠性。

还有一种方式——基于时间轮实现的定时器。

基于时间轮实现的定时器是一种常见的时间管理机制,可以有效地处理定时任务。

时间轮的基本结构:

  1. 时间轮: 时间轮可以看作是一个环形的数据结构,被划分为多个格子(或槽)。
  2. 每个格子: 每个格子代表一个时间段,相当于定时器的一个时间单位。在每个格子中,维护一个链表,存放需要在该时间段内执行的任务。

流程:

  1. 初始化时间轮: 根据实际需求,将整个时间轮划分为多个格子,每个格子代表一段时间,例如100ms。

  2. 添加任务: 将待执行的任务添加到时间轮的相应位置,即计算任务的执行时间,然后将任务添加到对应的格子链表中。

  3. 时间轮滚动: 时间轮以固定的速度(如100ms)滚动,每次滚动一个格子。

  4. 执行任务: 当时间轮滚动到某个格子时,执行该格子上链表中的所有任务。

  5. 移除已执行任务: 执行完毕后,移除已执行的任务。

  6. 重复执行: 时间轮会不断滚动,并重复上述过程,直到程序结束或手动停止。

优势和适用场景:

  • 高效性: 时间轮的执行速度固定,不受任务数量的影响,执行效率高。
  • 灵活性: 可以根据实际需求灵活调整时间轮的划分粒度和格子数量。
  • 适用于定时任务: 适用于需要定时执行的任务,例如定时器任务、超时处理等。

通过时间轮实现的定时器,能够有效地管理定时任务,并且具有一定的灵活性和高效性,因此在实际开发中得到了广泛应用。

此处大家一定要注意!!!

Redis 并没有采取上述两种定时器的方案!!!

虽然Redis并没有采用上述所述的事件循环或基于时间轮的方案,但理解这两种方案对于理解Redis内部工作原理和其他系统设计仍然是非常有帮助的。这两种方案都属于高效的定时器实现方式,具有一定的优势和适用场景。

Redis常见数据类型和内部编码

在Redis中,数据存储时会根据不同的数据结构来组织。这些数据结构也对应着不同的内部编码方式,这有助于提高Redis的性能和效率。

  1. 字符串(String):字符串是最简单的数据结构之一,在Redis中用于存储文本或二进制数据。字符串的内部编码可以是int、raw、或者embstr,这取决于字符串的长度以及是否符合整数的表示范围。

  2. 列表(List):列表是一系列按照插入顺序排序的元素的集合。Redis中的列表使用双向链表来实现。列表的内部编码可以是ziplist(压缩列表)或者linkedlist(双向链表),这取决于列表的长度和元素的大小。

  3. 哈希(Hash):哈希存储了键值对的集合,其中每个键对应一个值。在Redis中,哈希使用哈希表来实现。哈希的内部编码可以是ziplist(压缩列表)或者hashtable(哈希表),这取决于哈希的大小和键值对的数量。

  4. 集合(Set):集合是一组唯一的无序元素的集合。在Redis中,集合使用哈希表来实现。集合的内部编码可以是intset(整数集合)或者hashtable(哈希表),这取决于集合的大小和元素的类型。

  5. 有序集合(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底层对于不同数据结构的优化和实现方式的详细介绍:

  1. 哈希表(Hash Table)

    • 标准实现: Redis中的哈希表是一种常见的数据结构,用于实现字典和集合等数据类型。通常情况下,Redis的哈希表实现是一种标准的哈希表结构,具有常规的查找、插入和删除操作,时间复杂度为O(1)。
    • 内部优化: Redis在实现哈希表时可能会针对特定场景进行优化,例如采用更快速的哈希函数、优化内存布局或缓存等方式,以提高哈希表的性能和效率。
    • 内部编码: Redis的哈希表内部编码使用ziplist和hashtable,根据哈希表的大小和存储元素的特性动态选择。
  2. 字符串(String)

    • 标准实现: Redis中的字符串是一种简单的数据结构,通常采用动态数组或缓冲区实现。标准实现下,字符串的操作包括插入、删除和修改等,时间复杂度为O(1)。
    • 编码方式: Redis中的字符串可以采用不同的编码方式,包括int、embstr和raw等。根据字符串的长度和内容,Redis会动态选择合适的编码方式,以节省内存空间并提高效率。
  3. 列表(List)

    • 标准实现: Redis中的列表是一种双向链表结构,支持在头部和尾部进行插入、删除和修改操作。标准实现下,列表的操作复杂度为O(1)。
    • 内部优化: Redis可能会针对特定场景对列表进行优化,例如在频繁的头部插入操作时,采用快速的头部插入方式或者采用压缩列表等方式,以提高性能。
    • 内部编码: Redis的列表内部编码使用ziplist和linkedlist,根据列表的大小和操作特性动态选择。
  4. 集合(Set)

    • 标准实现: Redis中的集合是一种无序不重复元素的数据结构,通常采用哈希表实现。标准实现下,集合的操作复杂度为O(1)。
    • 内部优化: Redis可能会在集合的实现中进行优化,例如采用压缩列表或整数集合等特定场景的优化方式,以减少内存占用和提高性能。
    • 内部编码: Redis的集合内部编码使用intset和hashtable,根据集合的大小和存储元素的特性动态选择。
  5. 有序集合(Sorted Set)

    • 标准实现: Redis中的有序集合是一种有序不重复元素的数据结构,通常采用跳跃表和哈希表相结合的方式实现。标准实现下,有序集合的操作复杂度为O(log(N))。
    • 内部优化: Redis可能会针对有序集合进行特定的优化,例如在范围查询操作时采用二分查找或者优化跳跃表的实现方式,以提高有序集合的性能。
    • 内部编码: Redis的有序集合内部编码使用ziplist和skiplist,根据有序集合的大小和存储元素的特性动态选择。

所以,Redis在底层实现不同数据结构时,会根据实际需求和特定场景进行优化,以提高性能并保证操作的时间复杂度符合承诺。这些优化可能涉及到不同的数据结构实现、编码方式选择、内存布局优化等方面,以达到节省时间和空间的效果。

每种内部编码方式的含义和区别:

  1. raw(原始):

    • 含义: raw编码表示以字节数组形式存储的原始数据。对于字符串等较大的数据,通常采用 raw编码。
    • 特点: 原始数据存储在连续的内存块中,不做任何额外的优化或压缩。
  2. int(整型):

    • 含义: int编码表示存储整数类型的数据。当存储的数据是整数时,Redis会采用int编码。
    • 特点: 整数以二进制形式直接存储,节省了存储空间,并且操作效率较高。
  3. embstr(小字符串):

    • 含义: embstr编码表示存储长度较小的字符串数据。当字符串长度不超过一定阈值时,Redis会采用embstr编码。
    • 特点: 对于较短的字符串,直接存储在embstr结构中,不需要额外的内存分配和指针操作,节省了空间和时间。 
  4. hashtable(哈希表):

    • 含义: hashtable编码也表示存储哈希表数据结构,用于表示字典、哈希集合等数据类型。
    • 特点: 和hash编码类似,内部也采用哈希表实现,但可能会针对大型数据进行优化。
  5. ziplist(压缩列表):

    • 含义: ziplist编码表示存储压缩列表数据结构。常用于表示列表和有序集合等数据类型。
    • 特点: 压缩列表是一种紧凑的、连续存储的数据结构,对于长度较短的列表或有序集合具有较高的存储效率。
  6. linkedlist(链表):

    • 含义: linkedlist编码表示存储双向链表数据结构。常用于表示列表等数据类型。
    • 特点: 链表结构灵活,支持快速的插入和删除操作,但访问时间复杂度较高。
  7. intset(整数集合):

    • 含义: intset编码表示存储整数集合数据结构。用于表示整数集合。
    • 特点: 内部采用有序数组实现,对于较小的整数集合具有较高的存储效率和快速的成员检查操作。
  8. skiplist(跳跃表):
  • 含义: skiplist编码表示存储跳跃表数据结构。通常用于实现有序集合等数据类型。
  • 特点: 跳跃表是一种有序链表的扩展,支持快速的范围查询操作,同时也保持较高的插入和删除效率。

同时,Redis 3.2 版本引入了一种新的对列表(list)数据结构的实现方式,称为 QuickList(快速列表)。QuickList 代替了之前版本中对列表的两种实现方式:linkedlist(链表)和 ziplist(压缩列表)。

QuickList 结合了链表和压缩列表的优点,在一定程度上提高了列表的性能和效率。它将列表分割成多个节点(node),每个节点可以包含多个元素,并且使用压缩列表来存储节点中的元素。这种设计允许 QuickList 在处理大型列表时保持较低的内存消耗,同时在处理小型列表时保持较高的性能。

QuickList 的主要优点包括:

  1. 灵活性: QuickList 可以根据列表的大小动态地调整节点的大小,以最大限度地减少内存消耗。

  2. 性能: QuickList 在处理大型列表时具有较低的内存消耗和较高的性能,而在处理小型列表时也能保持较高的效率。

  3. 压缩列表优化: QuickList 使用压缩列表作为节点的存储方式,这种紧凑的数据结构可以有效地节省内存空间,并提高数据访问的速度。

  4. 节点分割: QuickList 将列表分割成多个节点,每个节点都是一个压缩列表,这样可以降低在执行插入和删除操作时的复杂度,并提高整体的性能。

QuickList 是 Redis 在列表实现方面的一次重大改进,它使得 Redis 能够更好地处理大型列表,并在内存消耗和性能之间取得平衡。

我们还要简单了解一下跳跃表。

跳跃表(Skip List)是一种基于有序链表的数据结构,通过添加多层索引来加速查找操作。它是一种随机化数据结构,可以在平均情况下实现较快的搜索、插入和删除操作。跳跃表在 Redis 中被广泛应用于实现有序集合(Sorted Set)和其他数据结构的底层实现。

结构特点:

  1. 多层索引: 跳跃表通过维护多层索引来加速查找操作。每一层索引都是原始链表的一个子集,最底层索引包含所有元素,而上层索引则包含部分元素,每个元素在不同层级的索引中出现的概率是随机的。

  2. 升维结构: 每个节点包含多个指针,指向同一层中的下一个节点,以及可能指向其他层级中的节点。这种升维结构使得跳跃表的查找操作可以跳过多个元素,从而实现快速查找。

  3. 平衡性: 跳跃表的高度是对数级别的,并且每一层的节点数量都尽量保持平衡,使得在平均情况下查找操作的时间复杂度为 O(logN),其中 N 为跳跃表中的元素数量。

操作:

  1. 查找: 跳跃表的查找操作类似于二分查找,从顶层索引开始,逐层向下查找,直到找到目标元素或者到达原始链表的底层。

  2. 插入: 插入操作首先需要执行查找操作,找到插入位置的前一个节点,然后在相应的层级上插入新节点,并更新相应的指针。

  3. 删除: 删除操作也需要执行查找操作,找到待删除节点的前一个节点,然后在相应的层级上删除该节点,并更新相应的指针。

优点:

  1. 快速查找: 跳跃表的平均查找时间复杂度为 O(logN),比较适用于有序集合等需要频繁查找的场景。

  2. 简单高效: 跳跃表的实现相对简单,插入和删除操作的时间复杂度也是 O(logN),在实际应用中表现良好。

  3. 支持范围查询: 跳跃表可以方便地支持范围查询操作,例如查找某个范围内的元素或者统计某个范围内的元素数量。

缺点:

  1. 空间复杂度高: 跳跃表的多层索引会占用较多的额外空间,对于存储空间较为敏感的场景可能不太合适。

  2. 不支持动态扩容: 跳跃表通常需要预先确定最大层数,因此不支持动态扩容,需要根据实际情况预先分配足够的空间。

综上,跳跃表是一种高效的数据结构,适用于需要快速查找和范围查询的场景,尤其适用于有序集合等需要频繁操作的数据结构。

在刚刚上面那张表里,你可以看到,在Redis中,每种数据结构都有至少两种以上的内部编码实现,例如列表(list)数据结构包含了linkedlist和ziplist两种内部编码。同时,有些内部编码,例如ziplist,可以作为多种数据结构的内部实现。

我们可以通过OBJECT ENCODING命令来查询指定键对应值的内部编码:

  1. 127.0.0.1:6379> SET hello world
  2. OK
  3. 127.0.0.1:6379> LPUSH mylist a b c
  4. (integer) 3
  5. 127.0.0.1:6379> OBJECT ENCODING hello
  6. "embstr"
  7. 127.0.0.1:6379> OBJECT ENCODING mylist
  8. "quicklist"

在这个示例中,我们首先使用SET命令将字符串world存储在名为hello的键中,然后使用LPUSH命令将元素a、b和c依次推入名为mylist的列表中。接着,分别使用OBJECT ENCODING命令查询键hello和mylist对应值的内部编码。结果显示,hello键对应的值使用的是embstr内部编码,而mylist键对应的值使用的是quicklist内部编码。

通过查询内部编码,我们可以更好地了解Redis是如何存储和管理数据的,为性能优化和数据结构选择提供了参考。

Redis这样设计有两个明显的好处:

  1. 灵活改进内部编码:Redis的设计允许改进内部编码,而对外的数据结构和命令没有任何影响。这意味着一旦开发出更优秀的内部编码,就无需修改外部数据结构和命令。例如,Redis 3.2提供了quicklist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现。对于用户来说,这种改进是基本无感知的,他们可以继续使用相同的命令和数据结构,而无需了解内部编码的变化。

  2. 适应不同场景的需求:多种内部编码实现可以在不同的场景下发挥各自的优势。例如,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 之所以能够有效地利用单线程模型进行工作,主要有以下几个原因:

  1. 短平快的命令处理: Redis 的核心业务逻辑是基于内存的快速数据存储和处理,大多数命令操作都非常简单且耗时短暂。例如,GET、SET、INCR 等命令通常只涉及简单的内存读写操作,不会消耗过多的 CPU 资源。

  2. 非阻塞 I/O 操作: Redis 使用了非阻塞的 I/O 模型,通过事件驱动的方式处理网络请求。这意味着当 Redis 在等待网络 I/O 时,主线程可以继续处理其他请求,而不会因为等待而阻塞,从而最大程度地利用了 CPU 资源。

  3. 单线程的简单性: 单线程模型相对于多线程或多进程模型来说,实现起来更加简单,减少了线程间的同步和通信的复杂性。这使得 Redis 的代码更易于维护和调试。

  4. 避免了多线程的竞态条件和锁等问题: 在多线程环境下,需要考虑线程安全性和并发控制,例如竞态条件、死锁、资源争用等问题,而单线程模型可以避免这些问题的出现。

现在我们开启了三个 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操作的效率和性能。

  1. select:是最古老的IO多路复用机制之一,它允许程序员指定一组文件描述符,并通过调用select函数来阻塞等待其中任何一个文件描述符上的IO事件发生。select的效率不高,主要原因是它采用线性扫描的方式查找就绪文件描述符,导致随着文件描述符数量的增加,性能下降明显。

  2. poll:是对select的改进,它使用了链表数据结构来管理文件描述符,相比于select,poll在性能上有所提升,但仍然存在性能瓶颈,特别是在处理大量文件描述符时,性能下降明显。

  3. epoll:是Linux内核提供的高效IO多路复用机制,它是目前性能最优的一种IO复用方法。epoll采用了事件驱动的方式,能够以O(1)的时间复杂度来处理大量的文件描述符,极大地提高了IO操作的效率和性能。epoll使用了红黑树和双向链表等数据结构,以及边缘触发和水平触发两种工作模式,使得程序员能够更加灵活地处理IO事件。

这三种IO多路复用机制都是操作系统提供给程序员的API,通过调用这些API,程序员可以在一个线程内同时监听多个IO事件,并在IO事件发生时及时响应。而epoll由于其高效的实现方式和优秀的性能表现,在实际应用中被广泛使用。

在Linux系统中,epoll是一种高效的I/O多路复用机制,它通过操作系统内核提供的epoll API实现。

epoll的实现原理和具体流程:

  1. 事件注册

    • 程序通过调用epoll_create函数创建一个epoll句柄,用于管理事件。
    • 使用epoll_ctl函数将需要监听的文件描述符注册到epoll句柄上,并指定所关注的事件类型,例如可读、可写等。
  2. 事件等待

    • 调用epoll_wait函数等待事件的发生。epoll_wait会阻塞当前线程,直到有注册的文件描述符上的事件发生或者超时。
    • 当有事件发生时,epoll_wait会返回已经就绪的文件描述符列表。
  3. 事件处理

    • 程序通过遍历epoll_wait返回的就绪文件描述符列表,获取每个文件描述符上发生的事件类型。
    • 根据事件类型执行相应的操作,例如读取数据、写入数据或者关闭连接等。
  4. 事件移除

    对于一次性事件(例如EPOLLONESHOT),处理完事件后需要重新注册该文件描述符,以便下次继续监听。
  5. 工作原理

    • epoll的高效性主要体现在其数据结构和事件通知机制上。
    • epoll内部维护了一个事件表(event table),通过红黑树和双向链表等数据结构来存储注册的文件描述符和事件。
    • 当调用epoll_ctl注册文件描述符时,内核会根据文件描述符的事件类型和状态将其加入到事件表中。
    • 调用epoll_wait等待事件时,内核会遍历事件表,检查每个文件描述符的状态,并将就绪的文件描述符添加到就绪队列中。
    • 在多核CPU下,epoll利用了多线程和多核心的优势,能够并发地处理事件,提高了系统的响应速度和处理能力。

epoll通过高效的数据结构和事件通知机制,能够快速地监听大量的文件描述符,并在事件发生时及时通知应用程序,从而实现高性能的I/O多路复用。

在Redis中,使用epoll作为I/O多路复用技术的实现,是为了高效地管理和处理大量的客户端连接。

  1. 前因:

    • Redis是一个服务器端的应用程序,需要处理来自多个客户端的连接请求和数据传输。
    • 传统的I/O模型中,每个连接通常由一个线程来处理,当连接数量很大时,会导致大量线程的创建和上下文切换,降低了系统的性能和效率。
    • 为了提高系统的并发处理能力和资源利用率,需要采用一种高效的I/O模型来管理和处理多个客户端连接。
  2. epoll的特性:

    • epoll是Linux内核提供的一种高效的I/O多路复用技术,适用于管理大量的文件描述符(包括套接字、管道等)。
    • 与传统的select和poll相比,epoll具有更高的性能和可扩展性,能够处理成千上万个文件描述符,而不会随着文件描述符数量的增加而性能下降。
    • epoll通过将文件描述符的状态变化转换为事件,将事件通知给应用程序,从而使得应用程序可以有效地处理多个连接的I/O操作,而无需阻塞等待或者轮询查询。
  3. 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中是最基础的数据类型,具有以下几点需要特别注意:

  1. 所有键的类型都是字符串类型:在Redis中,所有的键(Key)都是字符串类型,这意味着无论是用于存储简单的键值对还是复杂的数据结构,键的类型都是字符串。其他几种数据结构,如列表(List)和集合(Set)的元素类型也都是字符串类型。因此,对字符串类型的理解是掌握其他数据结构的基础。

  2. 值的多样性:字符串类型的值并不限于传统意义上的字符串,它可以包含不同类型的数据,例如:

    • 字符串:可以是普通的文本字符串,如"user:123";
    • 数字:可以是整数或者浮点数,例如"age:30";
    • 二进制流数据:可以是图像、音频、视频等二进制数据,这使得Redis在一定程度上可以充当缓存服务器,存储一些非文本类型的数据。
    • 特殊格式的字符串:例如JSON、XML等格式的字符串,Redis并不对其进行解析或验证,而是将其视为普通的字符串进行存储。
  3. 值的大小限制:Redis内部存储字符串完全是按照二进制流的形式保存的,二进制数据是指由0和1组成的数据序列,是计算机中最基本的数据表示方式。在计算机中,所有的数据最终都会被转换成二进制形式进行存储和处理。二进制数据可以表示各种类型的信息,包括数字、文本、图像、音频等。尽管字符串类型的值可以是各种形式的数据,但是Redis对单个字符串的大小有限制,其最大值不能超过512MB。这是为了保证Redis的性能和稳定性,防止存储过大的字符串导致内存溢出或性能下降。所以其实Redis主要用于存储文本数据,对体积较大的音频视频的存储频率不高。

此外,需要特别注意的是,由于Redis内部存储字符串完全是按照二进制流的形式保存的,因此Redis不处理字符集编码问题,也就很少出现乱码问题。这意味着Redis存储的字符串数据是原样存储的,不会对字符集编码进行转换或处理。因此,客户端传入的命令中使用的是什么字符集编码,Redis就会存储什么字符集编码的数据。这一点在处理多语言环境或特定字符集编码要求的场景下需要特别注意。

常见命令

SET

SET | Redis

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 | Redis

GET命令用于获取指定key对应的value。如果key不存在,则返回nil。如果value的数据类型不是string,会报错。

语法

GET key

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:返回key对应的value,如果key不存在则返回nil。

示例

GET mykey

以上示例将返回键名为"mykey"的值。

MGET

MGET | Redis

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 | 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

INCR | Redis

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 | 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

DECR | Redis

DECR命令用于将key对应的字符串表示的数字减一。如果key不存在,则将其视为0进行操作。如果key对应的字符串不是一个整数或超出了64位有符号整数的范围,则会报错。

语法

DECR key

命令有效版本:1.0.0之后

时间复杂度:O(1)

返回值:返回integer类型的减一后的数值。

示例

DECR mykey

以上示例会将键名为"mykey"对应的值减一。如果"mykey"不存在,则将其视为0处理,执行DECR后返回的值为-1。

DECRBY

DECRBY | Redis

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 | Redis

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 | Redis

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 | Redis

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 | Redis

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 | Redis

STRLEN 命令会返回指定键对应的字符串值的长度,如果该键不存在或者键对应的值不是字符串类型,则返回 0。需要注意的是,这里返回的长度是以字节为单位的,即字符串中包含的字节数。

语法

STRLEN key

命令有效版本: 此命令在Redis版本2.2.0及以后的版本中有效。

时间复杂度: STRLEN命令的时间复杂度为O(1)。

返回值: 返回字符串的长度。如果键不存在或者键对应的值不是字符串类型,则返回0。

示例

STRLEN mykey

这将返回键"mykey"对应的字符串的长度。

字符串类型命令

命令执行效果时间复杂度
set key value [key value...]设置 key 的值为 valueO(k),其中 k 是键个数
get key获取 key 的值O(1)
del key [key ...]删除指定的 keyO(k),其中 k 是键个数
mset key value [key value ...]批量设置指定的 key 和 valueO(k),其中 k 是键个数
mget key [key ...]批量获取 key 的值O(k),其中 k 是键个数
incr key指定的 key 的值加1O(1)
decr key指定的 key 的值减1O(1)
incrby key n指定的 key 的值加 nO(1)
decrby key n指定的 key 的值减 nO(1)
incrbyfloat key n指定的 key 的值加 n (浮点数)O(1)
append key value指定的 key 的值追加 valueO(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会根据当前值的类型和长度动态决定使用哪种内部编码实现,以便在性能和内存消耗之间达到平衡。

整型类型的示例如下:

  1. 127.0.0.1:6379> set key 6379
  2. OK
  3. 127.0.0.1:6379> object encoding key
  4. "int"

短字符串类型的示例如下:

  1. # 小于等于 39 个字节的字符串
  2. 127.0.0.1:6379> set key "hello"
  3. OK
  4. 127.0.0.1:6379> object encoding key
  5. "embstr"

长字符串类型的示例如下:

  1. # 大于 39 个字节的字符串
  2. 127.0.0.1:6379> set key "one string greater than 39 bytes ........"
  3. OK
  4. 127.0.0.1:6379> object encoding key
  5. "raw"

在 Redis 中,整数是使用 int 类型来存储的,但是小数则是以字符串的形式存储的。这意味着当进行算术运算时,对于小数类型的数据,Redis 需要将其先转换为浮点数(double),进行运算后再将结果转换回字符串形式进行存储。

这种处理方式在进行算术运算时会带来一些额外的开销,因为涉及到了字符串到浮点数的转换和运算结果的再次转换。相比整数类型,这会导致小数类型的运算性能稍低。因此,在设计 Redis 数据结构时,如果需要进行频繁的算术运算,并且需要保持高性能,我们还是建议尽量使用整数类型来存储数据,避免使用字符串来表示小数。

典型使用场景

缓存(Cache)功能

图 2-10 展示了一个典型的缓存使用场景,其中 Redis 充当缓存层,而 MySQL 则是存储层。在这种架构中,大部分请求的数据都是从 Redis 中获取的。Redis具有支持高并发的特性,因此缓存通常能够加速读写操作,并降低后端服务器的压力。

在这个架构中,Redis作为缓存存储系统,主要提供以下功能:

  1. 快速数据访问:Redis将热门数据缓存在内存中,因此能够以极快的速度响应客户端的读取请求,大大提高了数据的访问速度。

  2. 减轻后端压力:通过缓存数据,Redis可以减少对后端存储系统(如MySQL)的请求量,从而减轻了后端服务器的压力,提高了整个系统的性能和吞吐量。

  3. 提高系统可扩展性:使用Redis作为缓存层可以提高系统的可扩展性。由于Redis能够有效地处理高并发请求,可以轻松地通过增加Redis节点来扩展系统的容量和性能,而不会对系统的稳定性产生负面影响。

综上所述,Redis作为缓存层在分布式系统中扮演着至关重要的角色,能够显著提升系统的性能、可扩展性和稳定性。

Redis + MySQL 组成的缓存存储架构:

  1. // 根据用户 uid 获取用户信息
  2. UserInfo getUserInfo(long uid) {
  3. // 构造 Redis 的键
  4. String key = "user:info:" + uid;
  5. // 尝试从 Redis 中获取对应的值
  6. String value = Redis 执行命令:get key;
  7. // 如果缓存命中
  8. if (value != null) {
  9. // 假设用户信息按照 JSON 格式存储
  10. UserInfo userInfo = JSON 反序列化(value);
  11. return userInfo;
  12. }
  13. // 如果缓存未命中
  14. // 从数据库中,根据 uid 获取用户信息
  15. UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>;
  16. // 如果数据库中不存在对应 uid 的用户信息
  17. if (userInfo == null) {
  18. 响应 404;
  19. return null;
  20. }
  21. // 将用户信息序列化成 JSON 格式
  22. String jsonValue = JSON 序列化(userInfo);
  23. // 写入缓存,设置过期时间为 1 小时
  24. Redis 执行命令:set key jsonValue ex 3600;
  25. // 返回用户信息
  26. return userInfo;
  27. }

以上伪代码模拟了一个基于 Redis 缓存的业务数据访问过程:

  1. 首先定义了一个函数 getUserInfo,用于根据用户的 uid 获取用户信息。
  2. 在从 Redis 中获取用户信息的部分,先构造了 Redis 的键,假设用户信息保存在 "user:info:<uid>" 对应的键中。然后尝试从 Redis 中获取对应的值,如果命中缓存(value 不为 null),则将获取的值进行 JSON 反序列化,并返回用户信息。
  3. 如果 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 的性能和可维护性都是有益的。

另外,评判数据热点程度的标准因业务场景而异,但通常可以基于以下几个方面来评估:

  1. 访问频率:数据的访问频率是评判数据热点的一个重要指标。如果某个数据被频繁地读取或写入,那么它很可能是热点数据。可以通过监控系统的请求量或者访问日志来统计数据的访问频率。

  2. 访问比例:除了访问频率外,数据在整体数据集中所占比例也是一个重要考量因素。如果某个数据占据了整体数据集的相对较大比例,那么它可能是热点数据。可以通过统计数据的访问量或者数据的大小来评估数据的访问比例。

  3. 数据重要性:数据的重要性是评判数据热点的另一个重要指标。某些数据可能对业务的核心功能或者用户体验有重要影响,因此被认为是热点数据。可以通过业务需求和业务价值来评估数据的重要性。

  4. 数据流量:数据的流量是评判数据热点的另一个关键因素。某些数据可能会导致大量的数据流量,例如广告点击数据、搜索数据等。可以通过监控数据流量和网络流量来评估数据的热点程度。

  5. 数据的时效性:某些数据可能具有较高的时效性,需要在短时间内被快速处理。这样的数据也可能被认为是热点数据。可以通过评估数据的时效性需求来判断数据的热点程度

上述策略存在一个明显的问题:

随着时间的推移,随着业务数据的增长,确实会有越来越多的键在 Redis 中访问不到,从而导致需要从 MySQL 数据库中读取并写入 Redis。如果没有合适的措施,这样会导致 Redis 中的数据越来越多,最终可能导致内存耗尽的问题。

解决这个问题的常见方法是为每个键设置一个合适的过期时间。通过设置适当的过期时间,可以确保 Redis 中的数据不会无限增长,而是在一定时间后自动过期并被淘汰,从而释放内存空间。这样可以有效地控制 Redis 中数据的大小,防止内存溢出问题的发生。

此外,当 Redis 内存不足时,Redis 提供了多种淘汰策略来释放内存空间,例如LRU(最近最少使用)、LFU(最少使用频率)等。这些淘汰策略可以根据不同的业务需求和性能要求进行配置,以确保 Redis 总是能够保持在可接受的内存使用范围内,并尽可能地保留重要的数据。

计数(Counter)功能

许多应用都会将 Redis 作为计数的基础工具,利用其快速计数和缓存查询的功能。同时,Redis 也支持数据的异步处理或落地到其他数据源。例如,在视频网站中,可以使用 Redis 来实现视频播放次数的计数功能:

每当用户播放一个视频时,相应视频的播放次数会在 Redis 中自增 1。这样,通过 Redis 的快速计数功能,可以高效地记录和统计视频的播放次数。同时,由于 Redis 的缓存特性,可以在需要时快速查询和展示视频的播放次数,提升了网站的性能和用户体验。

除了视频播放次数,Redis 还可以用于计数许多其他类型的数据,如文章的阅读次数、商品的点击次数等。这些计数功能可以帮助网站实时监控和分析用户行为,为业务决策提供有价值的数据支持。

记录视频播放次数:

  1. // 在 Redis 中统计某视频的播放次数
  2. long incrVideoCounter(long vid) {
  3. // 构造键名
  4. String key = "video:" + vid;
  5. // 执行增加操作,并获取计数器的当前值
  6. long count = Redis 执行命令:incr key;
  7. // 返回计数器的当前值
  8. return count;
  9. }

这段代码用于统计某视频的播放次数。首先构造了视频对应的键名,然后通过 Redis 的 incr 命令对键对应的值进行增加操作,并获取增加后的计数器当前值。最后返回该值,即为视频的播放次数。

❗实际中要开发一个成熟、稳定的真实计数系统,要⾯临的挑战远不止如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。

对其中几个主要挑战进行简要讨论:

  1. 防作弊:在计数系统中,防止作弊是一个重要的考量因素。可以采取多种手段来防止作弊,例如限制用户操作频率、使用验证码或者令牌验证等方式来确保每次计数都是合法有效的。此外,也可以通过监控和分析异常行为来及时发现和应对作弊行为。

  2. 按照不同维度计数:有时候需要根据不同的维度来进行计数,例如按照时间、地域、用户等进行计数。为了实现这样的需求,可以使用 Redis 的 Sorted Set 或 Hash 等数据结构来存储和统计不同维度的计数数据,并设计合适的数据模型和查询方式来支持按照不同维度进行计数。

  3. 避免单点问题:为了保证计数系统的稳定性和可靠性,需要避免单点故障。可以通过使用 Redis 的主从复制或者集群模式来实现数据的备份和故障转移,以及通过负载均衡等方式来分散请求,避免单点问题对系统的影响。

  4. 数据持久化到底层数据源:为了保证数据的持久性和可靠性,计数系统通常需要将数据持久化到底层数据源,如数据库或者日志文件中。可以使用 Redis 的持久化机制(如RDB和AOF)将数据定期或实时地持久化到磁盘中,并根据业务需求设计合适的数据同步策略和数据备份方案,以确保数据的安全性和可靠性。

综上所述,开发成熟、稳定的计数系统需要综合考虑防作弊、按照不同维度计数、避免单点问题、数据持久化等多方面的挑战,并采取相应的技术和措施来应对这些挑战,从而保证系统的性能、可靠性和安全性。

共享会话(Session)

在分布式 Web 服务中,通常会将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中。然而,由于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上。通常情况下,无法保证用户每次请求都会被均衡到同一台服务器上。这样的设计会带来一个严重的问题:当用户刷新页面时,可能会发现需要重新登录,这种体验是用户无法容忍的。

这个问题的产生是因为用户的 Session 信息被保存在单个服务器上,并且负载均衡机制导致用户的请求可能会被分发到不同的服务器上。因此,当用户在一个服务器上登录后,刷新页面时可能会被重新定向到另一个服务器,导致用户的 Session 信息无法被正确地保持和共享。

为了解决这个问题,可以采取以下一些措施:

  1. Session 共享:将用户的 Session 信息存储在一个可供所有服务器访问的共享存储中,例如 Redis 或数据库。这样无论用户的请求被分发到哪个服务器,都可以保证能够访问到相同的 Session 信息。

  2. Sticky Session:在负载均衡器中配置 Sticky Session(也称为持久性会话或粘性会话),确保用户的请求在一段时间内始终被分发到同一台服务器上。这样可以保证用户在会话期间的一致性体验,但可能会导致负载不均衡的问题。

  3. 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 次。

这种短信验证机制可以有效地提高用户账号的安全性,因为即使用户的密码泄露,攻击者也需要获取用户手机并输入正确的验证码才能完成登录操作。同时,限制获取验证码的频率可以有效防止恶意用户利用短信接口进行攻击或滥用,保护短信服务商的资源和用户的体验。

在实现这种短信验证机制时,需要考虑以下几个方面:

  1. 验证码生成和发送:应用需要生成随机的验证码,并将验证码发送到用户的手机上。可以使用第三方的短信服务提供商来实现验证码的发送,也可以自建短信发送平台。

  2. 验证码验证:用户收到验证码后,需要在应用中输入收到的验证码进行验证。应用需要对用户输入的验证码进行验证,确保其正确性,并且确保验证码的有效期。

  3. 频率限制:为了防止滥用短信接口,通常会限制用户每分钟获取验证码的频率。可以在应用中设置一个计数器来记录用户获取验证码的次数,并在达到限制后暂时禁止用户再次获取验证码。

  4. 安全性考虑:在验证码的传输和存储过程中需要注意安全性,确保验证码不被窃取或篡改。可以通过加密传输、设置有效期等方式增强安全性。

综上,手机短信验证是一种常见的增强应用安全性的方式,通过合理设置验证码获取频率限制和有效期,可以有效防止恶意攻击和滥用行为,提高用户账号的安全性和用户体验。

短信验证码:

伪代码如下:

  1. String 发送验证码(String phoneNumber) {
  2. String key = "shortMsg:limit:" + phoneNumber;
  3. // 设置过期时间为 1 分钟(60 秒)
  4. // 使用 NX,只在不存在 key 时才能设置成功
  5. boolean r = Redis 执行命令:set key 1 ex 60 nx;
  6. if (r == false) {
  7. // 说明之前设置过该手机的验证码了
  8. long c = Redis 执行命令:incr key;
  9. if (c > 5) {
  10. // 说明超过了一分钟 5 次的限制了
  11. // 限制发送
  12. return null;
  13. }
  14. }
  15. // 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
  16. String validationCode = 生成随机的 6 位数的验证码();
  17. String validationKey = "validation:" + phoneNumber;
  18. // 验证码 5 分钟(300 秒)内有效
  19. Redis 执行命令:set validationKey validationCode ex 300;
  20. // 返回验证码,随后通过手机短信发送给用户
  21. return validationCode;
  22. }
  23. // 验证用户输入的验证码是否正确
  24. bool 验证验证码(String phoneNumber, String validationCode) {
  25. String validationKey = "validation:" + phoneNumber;
  26. String value = Redis 执行命令:get validationKey;
  27. if (value == null) {
  28. // 说明没有这个手机的验证码记录,验证失败
  29. return false;
  30. }
  31. if (value == validationCode) {
  32. return true;
  33. } else {
  34. return false;
  35. }
  36. }

 以上介绍的是 Redis 字符串数据类型的一些常见应用场景,但是 Redis 的字符串类型的应用远不止于此。

比如,除了上述介绍的场景外,还有许多其他应用场景可以利用 Redis 的字符串类型来实现,例如:

  1. 分布式锁:利用 Redis 的原子性操作和过期特性,实现分布式锁机制,保证多个客户端对共享资源的互斥访问。

  2. 消息队列:利用 Redis 的列表数据类型,实现简单的消息队列,用于解耦系统中的异步任务处理。

  3. 分布式 ID 生成器:利用 Redis 的自增操作,生成全局唯一的递增 ID,用于分布式系统中的数据唯一标识。

  4. 数据统计:将业务数据存储在 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 的哈希类型数据结构如下:

  1. key = "user:1001"
  2. 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 中是一种非常重要的数据结构,它具有以下特点和优势:

  1. 灵活性:哈希类型可以存储键值对,每个键值对都是一个独立的字段和值,使得存储和检索数据更加灵活。

  2. 高效性:Redis 使用哈希表来实现哈希类型,哈希表具有高效的查找、插入和删除操作,使得对哈希类型的操作都能在常数时间内完成。

  3. 结构化存储:哈希类型的键值对结构使得数据可以更加结构化地存储和管理,便于组织和维护复杂的数据结构。

  4. 支持嵌套:Redis 的哈希类型支持嵌套结构,即一个哈希类型的值本身可以是一个键值对结构,从而可以实现更复杂的数据模型和存储需求。

  5. 适用性广泛:哈希类型可以用于存储和管理各种类型的数据,如用户信息、商品信息、配置信息等,适用于各种场景和应用需求。

总之,哈希类型是 Redis 中一种非常重要且灵活的数据结构,它在数据存储和管理方面具有很大的优势和应用潜力。开发人员可以充分利用哈希类型的特性,设计出更加高效、灵活和结构化的数据存储方案,从而提升系统的性能和可维护性。

命令

HSET

HSET | Redis

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

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

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

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

HKEYS命令是Redis中用于获取哈希类型数据中所有字段名(field)的命令。通过该命令,用户可以一次性地获取哈希表中所有的字段名,而不需要获取字段对应的值。这种功能对于需要遍历哈希表中所有字段名的场景非常有用,例如在需要对哈希表进行全面检查或者进行批量操作时。HKEYS命令的使用简单而高效,使得对哈希类型数据的字段名进行管理和处理变得更加方便。

它的语法如下:

HKEYS key
  • key: 哈希数据的键。

命令有效版本: Redis 2.0.0之后可用。

时间复杂度: 获取所有字段的时间复杂度为O(N),其中N为字段的个数(哈希的元素个数)。

HKEYS命令用于获取哈希类型数据中的所有字段名(field)。其执行过程包括以下步骤:

  1. 根据给定的键(key),首先在Redis中查找对应的哈希类型数据,这个查找操作的时间复杂度是O(1),即常数时间复杂度。这是因为Redis的数据结构中使用了哈希表来存储键值对,通过哈希函数可以直接定位到对应的哈希表,而不需要遍历整个数据库。
  2. 找到哈希类型数据后,HKEYS命令会遍历这个哈希表,获取其中所有的字段名(field)。遍历哈希表的时间复杂度取决于哈希表中存储的键值对数量,通常情况下为O(N),其中N为哈希表中的元素数量。遍历哈希表获取字段名的过程是一个线性操作。

返回值: 返回一个包含所有字段名的列表。

示例:

HKEYS user:1001

这个示例命令将返回名为"user:1001"的哈希类型数据中的所有字段名列表。

HVALS

HVALS | Redis

HVALS命令是Redis中用于获取哈希类型数据中所有值(value)的命令。通过该命令,用户可以一次性地获取哈希表中所有字段对应的值,而不需要获取字段名。这种功能对于需要获取哈希表中所有值的场景非常有用,例如在需要对哈希表中的所有值进行批量处理或者分析时。HVALS命令的使用简单而高效,使得对哈希类型数据的值进行管理和处理变得更加方便。

它的语法如下:

HVALS key
  • key: 哈希数据的键。

命令有效版本: Redis 2.0.0之后可用。

时间复杂度: 获取所有值的时间复杂度为O(N),其中N为字段的个数(哈希的元素个数)。

返回值: 返回一个包含所有值的列表。

示例:

HVALS user:1001

这个示例命令将返回名为"user:1001"的哈希类型数据中的所有值列表。

HGETALL

HGETALL | Redis

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

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 | Redis

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命令是Redis中用于获取哈希类型数据中所有字段的个数的命令。通过该命令,用户可以获取哈希表中字段的数量,即哈希表中键值对的数量。这个功能对于了解哈希表的大小和结构非常有用,特别是在需要进行哈希表大小估算或者性能优化时。HLEN命令的使用简单而高效,使得对哈希类型数据的大小进行检测变得更加方便。

它的语法如下:

HLEN key
  • key: 哈希数据的键。

命令有效版本: Redis 2.0.0之后可用。

时间复杂度: 获取所有字段的个数的时间复杂度为O(1)。因为Redis内部使用哈希表来存储哈希类型数据,直接获取哈希表的大小即可得到字段的个数。

返回值: 返回哈希类型数据中所有字段的个数。

示例:

HLEN user:1001

这个示例命令将返回名为"user:1001"的哈希类型数据中所有字段的个数。

HSETNX

HSETNX | Redis

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

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

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 ...]删除 fieldO(k),k 是 field 个数
hlen key计算 field 个数O(1)
hgetall key获取所有的 field-valueO(k),k 是 field 个数
hmget key field [field ...]批量获取 field-valueO(k),k 是 field 个数
hmset key field value [field value ...]批量设置 field-valueO(k),k 是 field 个数
hexists key field判断 field 是否存在O(1)
hkeys key获取所有的 fieldO(k),k 是 field 个数
hvals key获取所有的 valueO(k),k 是 field 个数
hsetnx key field value设置值,但必须在 field 不存在时才能设置成功O(1)
hincrby key field n对应 field-value 加上 nO(1)
hincrbyfloat key field n对应 field-value 加上 nO(1)
hstrlen key field计算 value 的字符串长度O(1)

内部编码

哈希类型数据在 Redis 中有两种内部编码方式:

  1. ziplist(压缩列表): 当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)且所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 使用 ziplist 作为哈希的内部实现。Ziplist 使用更加紧凑的结构实现多个元素的连续存储,从而在节省内存方面优于 hashtable。
    上面的配置项就是可以写进redis.conf文件里面的。

  2. hashtable(哈希表): 当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现。在这种情况下,ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。因此,当哈希元素较多或者元素值较大时,Redis会选择使用 hashtable。

这两种内部编码方式根据哈希类型的大小和元素值的大小进行选择,以在不同情况下实现更高效的存储和访问。下⾯的示例演示了哈希类型的内部编码,以及响应的变化。

在 Redis 中,哈希类型数据的内部编码方式取决于以下条件:

当 field 个数比较少且没有大的 value 时,内部编码为 ziplist。例如,当使用 hmset 命令设置的 field-value 键值对较少时,Redis 内部会选择使用 ziplist 作为哈希的内部实现。可以通过 object encoding 命令查看内部编码类型。

  1. 127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
  2. OK
  3. 127.0.0.1:6379> object encoding hashkey
  4. "ziplist"

当有 value 大于 64 字节时,内部编码会转换为 hashtable。如果哈希中存在一个或多个 value 的大小超过了 64 字节,Redis 会自动将内部编码方式转换为 hashtable。

  1. 127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... omitted ..."
  2. OK
  3. 127.0.0.1:6379> object encoding hashkey
  4. "hashtable"

当 field 个数超过 512 时,内部编码也会转换为 hashtable。如果哈希中的 field 个数超过了 512 个,Redis 同样会将内部编码方式转换为 hashtable。

  1. 127.0.0.1:6379> hmset hashkey f1 v1 f2 v2 f3 v3 ... omitted ... f513 v513
  2. OK
  3. 127.0.0.1:6379> object encoding hashkey
  4. "hashtable"

这些转换规则确保了 Redis 在不同情况下选择最合适的内部编码方式,以优化内存使用和操作效率。

使用场景

图 1 展示了关系型数据库中存储的两条用户信息,其中每个用户的属性以表格的列形式呈现,而每条用户信息则以行的形式表示。如果要将这两个用户信息映射为关系表,则可以使用图 2 中所示的形式。 

关系型数据表保存用户信息

映射关系表示用户信息

与使用 JSON 格式的字符串缓存用户信息相比,使用哈希类型更加直观,并且在更新操作上更灵活。我们可以将每个用户的 ID 定义为键的后缀,然后使用多个 field-value 对来表示每个用户的各个属性。

参考伪代码:

  1. UserInfo getUserInfo(long uid) {
  2. // 根据 uid 得到 Redis 的键
  3. String key = "user:" + uid;
  4. // 尝试从 Redis 中获取对应的值
  5. Map<String, String> userInfoMap = Redis 执行命令:hgetall key;
  6. // 如果缓存命中(hit)
  7. if (userInfoMap != null && !userInfoMap.isEmpty()) {
  8. // 将映射关系还原为对象形式
  9. UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
  10. return userInfo;
  11. }
  12. // 如果缓存未命中(miss)
  13. // 从数据库中,根据 uid 获取用户信息
  14. UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>;
  15. // 如果表中没有 uid 对应的用户信息
  16. if (userInfo == null) {
  17. 响应 404;
  18. return null;
  19. }
  20. // 将用户信息以哈希类型保存到缓存
  21. Redis 执行命令:hmset key name userInfo.name age userInfo.age city userInfo.city;
  22. // 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
  23. Redis 执行命令:expire key 3600;
  24. // 返回用户信息
  25. return userInfo;
  26. }

这段伪代码描述了一个根据用户ID从缓存中获取用户信息的过程。首先,根据用户ID构建缓存键,然后尝试从Redis缓存中获取对应的用户信息。如果缓存中存在用户信息,则直接从缓存中读取并返回。如果缓存未命中,则从数据库中查询用户信息,并将查询到的信息写入到Redis缓存中,并设置缓存的过期时间。

注意,哈希类型和关系型数据库在某些方面存在显著的不同之处:

  1. 稀疏性: 哈希类型是稀疏的,即每个键可以拥有不同的字段(field),而关系型数据库是完全结构化的,如果要添加新的列,则所有行都必须设置值,即使为null。这意味着在Redis中,可以在不同的哈希键中存储不同的字段,而在关系型数据库中,必须确保所有行都具有相同的结构。

  2. 查询功能差异: 关系型数据库可以执行复杂的关系查询,如联表查询、聚合查询等,而Redis的哈希类型数据结构并不适合执行类似的复杂查询。虽然Redis提供了一些基本的数据操作命令,但是去模拟关系型数据库中的复杂查询通常是不切实际且维护成本高的。Redis更适用于简单、快速的数据存储和检索,而不是用于复杂的关系型数据处理。

关系型数据库稀疏性

缓存方式对比

截至目前为止,我们已经探讨了三种方法来缓存用户信息,并给出了它们的实现方法以及优缺点分析。

  1. 原生字符串类型: 使用字符串类型,每个属性对应一个键。

    1. set user:1:name James
    2. set user:1:age 23
    3. set user:1:city Beijing
    • 优点:实现简单,针对个别属性变更也很灵活。
    • 缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,因此这种方案基本没有实用性。
  2. 序列化字符串类型,例如 JSON 格式: 使用序列化后的字符串类型,例如 JSON 格式,存储整个用户对象的信息。

    set user:1 {"name":"James","age":23,"city":"Beijing"}
    • 优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
    • 缺点:本身序列化和反序列化需要一定开销,同时如果总是操作个别属性则非常不灵活。
      效率低:当需要获取或修改用户信息中的某个字段时,必须将整个JSON字符串读取到内存中,然后解析成对象。这样的操作效率较低,尤其是对于大型JSON字符串而言。
      更新复杂:修改用户信息中的某个字段时,需要先解析整个JSON字符串,然后找到要修改的字段进行更新,最后重新序列化为JSON字符串。这一过程较为繁琐,尤其是当字段较多时。
  3. 哈希类型: 使用哈希类型存储用户信息。

    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 | Redis

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 | Redis

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 | Redis

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 | Redis

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 | Redis

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"的列表中从第一个元素到第三个元素(左闭右闭)的所有元素。如果列表中只有两个元素,则返回这两个元素。

注意,元素前面的序号和元素下标无关。 

在处理"下标超出范围"的情况时,不同编程语言或工具的处理方式可能有所不同,每种方式都有自己的优缺点。以下是对比各种处理方式的优缺点:

  1. C++(未定义行为)

    • 优点:速度快:由于不进行下标合法性验证,直接访问数组元素,因此执行效率较高。
    • 缺点:难以调试:由于未定义行为,当下标超出范围时,程序可能会出现不可预测的行为,导致难以调试和定位问题。同时,由于未定义行为,程序可能在不同平台或环境下表现不一致。
  2. Java(抛出异常)

    • 优点:出错及时发现:当下标超出范围时,会抛出数组越界异常,提供了及时发现问题的机制,有助于调试和修复问题。
    • 缺点:速度慢:因为需要额外的下标合法性验证,导致执行速度相对较慢。此外,异常的捕获和处理也会增加代码的复杂度。
  3. Redis(返回符合实际情况的元素)

    • 优点
      • 容错能力强:Redis会返回符合实际情况的元素,而不会抛出异常或导致未定义行为,因此具有较强的容错能力和鲁棒性。
      • 方便处理:开发者可以根据实际需要自行处理返回的元素,例如,可以根据实际情况进行异常处理或特定的业务逻辑处理。
    • 缺点:可能会隐藏错误:当下标超出范围时,如果返回的元素符合实际情况但开发者未预料到这种情况,可能会导致潜在的错误隐藏起来,需要开发者自行进行处理和调试。

LPOP

LPOP | Redis

LPOP命令用于从列表的左侧取出一个元素,即执行头部删除操作。这个命令会移除并返回列表中的第一个元素。与RPOP命令相似,LPOP也可以被用来实现队列的先进先出(FIFO)的操作,或者用于获取并删除列表中的第一个元素。

它的语法如下:

LPOP key
  • key: 列表的键。

命令有效版本: Redis 1.0.0之后可用。

时间复杂度: 头部删除操作的时间复杂度为O(1),即常数时间复杂度。

返回值: 返回被取出的元素。如果列表为空,则返回nil。

示例:

LPOP mylist

这个示例命令将从名为"mylist"的列表的左侧取出一个元素,并将其返回。如果列表为空,则返回nil。

RPOP

RPOP | Redis

RPOP命令用于从列表的右侧取出一个元素,即执行尾部删除操作。这个命令会移除并返回列表中的最后一个元素。其作用类似于弹出栈中的顶部元素,从列表的尾部取出一个元素,同时将该元素从列表中移除。RPOP命令可以用于实现队列的后进先出(LIFO)的操作,或者用于获取并删除列表中的最后一个元素。

它的语法如下:

RPOP key [count]
  • key: 列表的键。
  • count: redis6.2版本升级之后可以选择加上count代表删除个数。

命令有效版本: Redis 1.0.0之后可用。

时间复杂度: 尾部删除操作的时间复杂度为O(1),即常数时间复杂度。

返回值: 返回被取出的元素。如果列表为空,则返回nil。

示例:

RPOP mylist

这个示例命令将从名为"mylist"的列表的右侧取出一个元素,并将其返回。如果列表为空,则返回nil。

LINDEX

LINDEX | Redis

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 | Redis

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

LLEN命令是Redis中的列表操作命令,用于获取列表的长度,即列表中包含的元素个数。通过该命令,用户可以快速地获取列表的大小,以便于对列表进行管理和操作。这个命令的使用非常简单,只需指定列表的键(key),就可以返回该列表中包含的元素个数。

它的语法如下:

LLEN key
  • key: 列表的键。

命令有效版本: Redis 1.0.0之后可用。

时间复杂度: 获取列表长度的时间复杂度为O(1),即常数时间复杂度。

返回值: 返回列表的长度,即其中包含的元素个数。

示例:

LLEN mylist

这个示例命令将返回名为"mylist"的列表中包含的元素个数,即列表的长度。

LREM

LREM | Redis

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 | Redis

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)”。

在这个模型中,我们期望队列具备两个主要特性:

  1. 线程安全:多个线程可以同时访问队列而不会出现数据混乱或丢失的情况。这意味着队列的操作需要是原子的,即在执行队列操作期间不会被其他线程中断或修改。

  2. 阻塞:在队列为空时,尝试从队列中取出元素的操作应该被阻塞,直到队列不为空为止;同样,在队列已满时,尝试向队列中添加元素的操作也应该被阻塞,直到队列有足够的空间为止。

Redis中的列表(List)可以被视为一种阻塞队列的实现,其线程安全性是由Redis的单线程模型来保证的,即Redis在处理客户端请求时是按顺序逐个执行的。因此,多个客户端对列表进行操作时不会出现数据混乱的情况。

然而,Redis的列表在阻塞方面只支持“队列为空”的情况,即当列表为空时,尝试从列表中弹出元素的操作会被阻塞,直到列表不为空为止。但是,Redis并不支持在列表已满时阻塞添加元素的操作,因为列表的大小是没有限制的,它会随着元素的添加而自动扩容,所以不会出现队列已满的情况。

阻塞版本命令

blpop和brpop是lpop和rpop的阻塞版本,它们的作用基本一致,都用于从列表的左侧(blpop)或右侧(brpop)取出一个元素,并在列表为空时进行阻塞。

特性和区别:

  1. 阻塞行为: 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但是,如果列表中没有元素,非阻塞版本会立即返回nil,而阻塞版本会根据设置的timeout参数进行阻塞一段时间。在此期间,Redis可以执行其他命令,但是执行该命令的客户端会处于阻塞状态。也就是说,此处的BLPOP和BRPOP看似耗时很久,但是并不会对Redis服务器产生负面影响。

  2. 遍历键: 如果命令中设置了多个键,blpop和brpop会从左向右依次遍历这些键。一旦有一个键对应的列表中有元素可弹出,则命令立即返回,而不会继续遍历后面的键。

  3. 多客户端竞争: 如果多个客户端同时对多个键执行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 | Redis

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 | Redis

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 valueO(n),n 是 pivot 距离头尾的距离
查找LRANGE key start endO(s+n),s 是 start 偏移量,n 是 start 到 end 的范围
LINDEX key indexO(n),n 是索引的偏移量
LLEN keyO(1)
删除LPOP keyO(1)
RPOP keyO(1)
LREM key count valueO(k),k 是元素个数
LTRIM key start endO(k),k 是元素个数
修改LSET key index valueO(n),n 是索引的偏移量
阻塞操作BLPOP key [key ...] timeoutO(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。

  1. # 当元素个数较少且没有大元素时,内部编码为 ziplist
  2. 127.0.0.1:6379> rpush listkey e1 e2 e3
  3. OK
  4. 127.0.0.1:6379> object encoding listkey
  5. "ziplist"
  6. # 当元素个数超过 512 时,内部编码为 linkedlist
  7. 127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
  8. OK
  9. 127.0.0.1:6379> object encoding listkey
  10. "linkedlist"
  11. # 当某个元素的长度超过 64 字节时,内部编码为 linkedlist
  12. 127.0.0.1:6379> rpush listkey "one string is bigger than 64 bytes ... 省略 ..."
  13. OK
  14. 127.0.0.1:6379> object encoding listkey
  15. "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)是一种在消息传递系统中常见的设计模式,可以有效实现解耦合。在消息传递系统中,不同的消息可能需要被不同的处理程序处理,而多频道机制可以将不同类型或不同用途的消息路由到不同的频道或主题中,从而使消息的处理程序可以根据需要选择订阅特定的频道,而不必处理所有消息。

通过多频道机制,系统可以实现以下优点:

  1. 解耦合: 将不同类型或不同用途的消息分发到不同的频道,可以降低系统各个组件之间的耦合度。当某些数据发生问题或需要变更时,只需修改特定频道的处理逻辑,而不会影响其他频道的处理流程,从而提高了系统的灵活性和可维护性。

  2. 灵活性: 多频道机制使得系统可以根据实际需求动态地调整消息的路由和处理方式,可以根据业务需求新增、修改或移除频道,而不会影响系统的其他部分。

  3. 可扩展性: 多频道机制为系统的扩展提供了良好的支持。通过添加新的频道,可以将新功能或新业务逻辑与现有系统进行解耦,从而实现系统的水平扩展和功能扩展。

  4. 故障隔离: 当系统的某一部分出现故障或异常时,多频道机制可以限制故障的影响范围,防止故障蔓延到系统的其他部分,提高了系统的容错性和可用性。

具体实现方法是,每个频道对应一个列表,生产者向指定频道的列表中插入消息,而消费者则通过brpop命令阻塞式地从指定频道的列表中获取消息。每个频道的消息队列是独立的,消费者之间不会相互影响,因为它们订阅的是不同的键。

这种分频道的消息队列模型可以应用于多种场景,比如实时通讯中的消息分发、事件驱动架构中的消息订阅等。通过这种模型,可以实现消息的有序处理和分发,同时确保不同频道之间的消息隔离,提高了系统的可扩展性和灵活性。

Redis 分频道阻塞消息队列模型

微博 Timeline

每个用户都有属于自己的Timeline(微博列表),现需要分页展示文章列表。此时我们可以考虑使用Redis的列表结构,因为列表不仅是有序的,同时支持按照索引范围获取元素。在这个场景中,用户的微博列表可以作为一个有序的列表,按照时间顺序存储用户发布的微博,通过列表结构可以方便地获取用户的最新微博,并且支持分页展示。

使用列表结构存储用户的微博列表具有以下优势:

  • 有序存储:微博按照发布时间顺序存储在列表中,保证了用户的Timeline是按照时间顺序展示的。
  • 索引范围获取:列表结构支持按照索引范围获取元素,这样可以方便地实现分页展示,用户可以快速浏览到自己Timeline中的不同页面内容。
  1. 每篇微博使用哈希结构存储,包括微博的属性:title、timestamp、content。例如:

    1. hmset mblog:1 title xx timestamp 1476536196 content xxxxx
    2. ...
    3. hmset mblog:n title xx timestamp 1476536196 content xxxxx

    这里使用了哈希结构hmset来存储每篇微博的信息。

  2. 向用户Timeline添加微博,使用lpush命令将微博的键(例如mblog:1、mblog:3等)添加到用户的Timeline列表中。例如:

    1. lpush user:1:mblogs mblog:1 mblog:3
    2. ...
    3. lpush user:k:mblogs mblog:9

    这里使用了列表lpush命令来将微博的键添加到用户的Timeline列表中。

  3. 分页获取用户的Timeline,例如获取用户1的前10篇微博。首先使用lrange命令获取用户的Timeline列表中指定范围内的微博键,然后遍历这些键并使用hgetall命令获取每篇微博的详细信息。例如:

    1. keylist = lrange user:1:mblogs 0 9
    2. for key in keylist:
    3. hgetall key

    这段代码首先通过lrange命令获取用户1的前10篇微博的键,然后遍历这些键,并使用hgetall命令获取每篇微博的详细信息。

这种方案使用了Redis的哈希结构和列表结构,可以高效地存储和检索用户的微博列表,并且支持分页展示,为用户提供了良好的使用体验。

但是此方案在实际应用中,使用列表结构存储用户的微博列表可能会遇到两个问题:

  • 1 + n 问题: 当每次分页获取的微博数量较多时,需要执行多次hgetall操作,这可能导致性能下降。为了解决这个问题,可以考虑使用pipeline(流水线)模式批量提交命令,或者将微博不采用哈希类型,而是使用序列化的字符串类型存储,然后使用mget命令一次性获取多篇微博的内容。这样可以减少与Redis服务器的通信次数,提高查询效率。
  • 分裂获取文章时的性能问题: 使用lrange命令在列表两端表现较好,但在获取列表中间的元素时性能可能较差。为了解决这个问题,可以考虑对列表进行拆分,将较长的列表拆分成多个子列表,然后根据需要选择合适的子列表进行分页查询。这样可以避免在获取中间元素时性能下降的问题,提高查询效率。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/967106
推荐阅读
相关标签