Redis的五种数据类型(String、Hash、List)

发布于:2024-12-18 ⋅ 阅读:(80) ⋅ 点赞:(0)

1. String 字符串

字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意:

  1. 首先 Redis 中所有的键的类型都是字符串类型,而且其他几种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。
  2. 其次,如下图所示,字符串类型的值实际可以是字符串,包含⼀般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚至是⼆进制流数据,例如图片、音频、视频等。不过⼀个字符串的最大值不能超过 512 MB。

1.1 常见命令

SET和GET命令在《Redis的认识》这边博客上已经介绍过这里就不在继续介绍了。

1.1.1 MGET命令

(1)MGET 是 Redis 提供的一个用于批量获取多个键(key)的值的命令。相比于逐个使用 GET 命令,MGET 可以减少网络往返次数,从而提高性能。语法如下:

MGET key1 [key2 ...]
  • key1 [key2 …]:一个或多个要获取的键。
  • 返回值:返回一个包含所有键对应的值的数组。对于每个键,如果它存在,则返回其值;如果它不存在,则返回 nil。

(2)示例:

  • 假设我们已经在 Redis 中设置了以下键值对:
shell
SET key1 "value1"
SET key2 "value2"
SET key3 "value3"
  • 现在我们使用 MGET 命令获取这些键的值:
shell
MGET key1 key2 key3 key4
  • 返回结果将是:
shell
1) "value1"
2) "value2"
3) "value3"
4) (nil)

在上述结果中,key1、key2 和 key3 的值分别是 “value1”、“value2” 和 “value3”,而 key4 不存在,因此返回 nil。

(3)注意事项:

  1. 性能:MGET 命令的性能优于多次调用 GET 命令,因为它减少了与 Redis 服务器的网络往返次数。
  2. 原子性:MGET 命令是原子性的,即它要么全部成功,要么全部失败(当然,在这个命令的上下文中,所有键要么存在,要么不存在,但它不会失败)。
  3. 数据类型:MGET 可以用于获取任何数据类型的键的值,但返回的值总是字符串(对于非字符串类型的数据,Redis 会将它们序列化为字符串返回)。

1.1.2 MSET命令

(1)Redis 的 MSET 命令是一个用于同时设置一个或多个键值对的命令。它允许客户端在一次操作中发送多个设置请求,从而减少了与服务器之间的通信轮次,提高了效率。语法如下:

shell
MSET key1 value1 key2 value2 ... keyN valueN
  • key1 value1 key2 value2 … keyN valueN:一个或多个键值对,其中每个键值对由一个键(key)和一个值(value)组成。
  • 返回值:MSET 命令总是返回字符串 “OK”,表示操作成功。

(2)示例:

  • 假设我们要在 Redis 中设置多个键值对,可以使用 MSET 命令如下:
shell
MSET user:1:name "Alice" user:1:age 30 user:2:name "Bob"

上述命令会同时设置三个键值对:user:1:name 为 “Alice”,user:1:age 为 30,user:2:name 为 “Bob”。

(3)注意事项:

  1. 原子性:MSET 命令是原子性的,即它要么全部成功,要么全部失败。这意味着在执行 MSET 命令时,Redis 会确保所有键值对都被设置,或者如果由于某种原因(如内存不足)导致无法设置所有键值对,则不会设置任何键值对。
  2. 数据类型:MSET 命令可以用于设置任何数据类型的键值对。但是,请注意,对于非字符串类型的数据(如列表、集合、哈希等),Redis 会将它们序列化为字符串进行存储。如果需要以特定数据类型存储和操作数据,请使用相应的 Redis 命令(如 LPUSH、SADD、HSET 等)。
  3. 覆盖:如果 MSET 命令中设置的键已经存在,则它们会被新的值覆盖。
  4. 网络性能:MSET 命令通过减少客户端与服务器之间的通信轮次来提高性能。因此,在处理大量数据时,使用 MSET 命令可以显著缩短总处理时间。

(4)多次 get vs 单次 mget:
如上图所示,使用 mget / mset 由于可以有效地减少了网络时间,所以性能相较更高。假设网络耗时 1 毫秒,命令执行时间耗时 0.1 毫秒,则执行时间如下表所示:1000 次 get 和 1 次 mget 对比

操作 时间
1000 次 get 1000 x 1 + 1000 x 0.1 = 1100 毫秒
1 次 mget 1000 个键 1 x 1 + 1000 x 0.1 = 101 毫秒

学会使用批量操作,可以有效提高业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是无节制的,否则可能造成单⼀命令执行时间过长,导致 Redis 阻塞。

1.1.3 SETNX命令

(1)Redis 的 SETNX 命令是一个条件性的设置键值的命令,其全称是 “SET if Not eXists”。它仅在键不存在时才设置该键的值。语法如下:

shell
SETNX key value
  • key:要设置的键名。
  • value:要设置的值。
  • 返回值
    • 如果键 key 不存在,则将键 key 的值设置为 value,并返回 1 表示设置成功。
    • 如果键 key 已经存在,则不进行任何操作,返回 0 表示设置失败。

(2)示例:

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"

(3)注意事项:

  1. SETNX 命令是原子性的,即它要么全部成功,要么全部失败。这确保了在设置键值时不会发生竞态条件(Race Condition)。
  2. 在使用 SETNX 命令实现分布式锁时,需要合理设置锁的过期时间,以防止死锁。如果持有锁的客户端崩溃或忘记释放锁,那么其他客户端可以在锁超时后重新获取锁。
  3. 为了确保分布式锁的正确性,还需要考虑其他因素,如时钟漂移、网络延迟等。在实际应用中,可能需要结合使用 Redis 的其他命令和特性(如 EXPIRE、Lua 脚本、Watch/Multi/Exec 事务等)来实现更复杂的分布式锁方案。

(4)SET、SETNX、SETXX 执行流程:

1.2 计数命令

1.2.1 INCR命令

(1)Redis 的 INCR 命令是一个用于对存储在 Redis 中的键(key)进行自增操作的命令。它每次执行都会将键的值增加 1,并返回自增后的值。如果键不存在,则会创建一个新的键,并将其初始值设置为 0,然后再进行自增操作。语法如下:

INCR key
  • key:要进行自增操作的键名。
  • 返回值:返回自增后的值作为结果。如果键不存在,则会先初始化为 0,然后返回 1。

(2)示例:

  • 假设我们有一个名为 “counter” 的键,初始值为 0。我们可以使用 INCR 命令对其进行自增操作:
INCR counter

每次执行 INCR 命令,键 “counter” 的值都会增加 1,并返回自增后的值。例如,第一次执行 INCR 命令时,返回值为 1;第二次执行时,返回值为 2,依此类推。

(3)注意事项:

  1. 数据类型:INCR 命令仅适用于存储为整数的值。如果键的值无法解析为整数,Redis 将返回一个错误。
  2. 原子性:INCR 命令是原子操作,这意味着当多个客户端同时对同一个键执行 INCR 命令时,Redis 会确保操作的原子性。因此,在多线程或并发环境下,不会出现竞态条件或数据不一致的情况。
  3. 性能:由于 INCR 命令是原子性的且操作简单,因此它的性能非常高。在处理大量自增操作时,INCR 命令可以显著提高系统的吞吐量和响应速度。

1.2.2 INCRBY命令

(1)Redis 的 INCRBY 命令是一个用于对存储在 Redis 中的键(key)进行指定步长的递增操作的命令。它允许用户指定一个增量值,然后将键的值增加该指定的增量值,并返回自增后的值。如果键不存在,则会创建一个新的键,并将其初始值设置为 0(或视为 0),然后再进行递增操作。语法如下

INCRBY key increment
  • key:要进行递增操作的键名。
  • increment:指定的增量值,必须是整数。
  • 返回值:返回自增后的值作为结果。如果键不存在,则会先初始化为 0(相当于执行了 0 + increment),然后返回结果。

(2)示例:

  • 假设我们有一个名为 “counter” 的键,初始值为 0。我们可以使用 INCRBY 命令对其进行指定步长的递增操作:
INCRBY counter 10

执行上述命令后,键 “counter” 的值将增加 10,并返回自增后的值 10。如果再次执行 INCRBY counter 5,则键 “counter” 的值将变为 15。

(3)注意事项:

  1. 数据类型:INCRBY 命令仅适用于存储为整数的值。如果键的值无法解析为整数,Redis 将返回一个错误。
  2. 原子性:INCRBY 命令是原子操作,这意味着当多个客户端同时对同一个键执行 INCRBY 命令时,Redis 会确保操作的原子性。因此,在多线程或并发环境下,不会出现竞态条件或数据不一致的情况。
  3. 性能:由于 INCRBY 命令是原子性的且操作简单,因此它的性能非常高。在处理大量递增操作时,INCRBY 命令可以显著提高系统的吞吐量和响应速度。
  4. 溢出问题:由于 Redis 中的整数是 64 位有符号整数,因此当值的绝对值超过 2^63-1(即 9223372036854775807)时,会发生溢出。在使用 INCRBY 命令时,需要注意这一点,以避免出现意外的结果。

1.2.3 DECR命令

(1)Redis 的 DECR 命令是一个用于对存储在 Redis 中的键(key)进行自减操作的命令。它每次执行都会将键的值减少 1,并返回自减后的值。如果键不存在,则会创建一个新的键,并将其初始值设置为 0(实际上这个描述不完全准确,因为对于不存在的键,DECR 会先视为 0 再进行自减,所以结果会是 -1),但更准确的描述是,如果键的值不是整数或键不存在(在严格意义上,后者会先被视为 0 然后立即减少),则操作会失败并返回一个错误。不过,在常见的使用场景中,如果键不存在,DECR 会简单地将其值设置为 -1。语法如下:

DECR key
  • key:要进行自减操作的键名。
  • 返回值:返回自减后的值作为结果。如果键的值不是整数,Redis 将返回一个错误。

(2)示例:

  • 假设我们有一个名为 “inventory” 的键,初始值为 10。我们可以使用 DECR 命令对其进行自减操作:
DECR inventory
  • 执行上述命令后,键 “inventory” 的值将减少 1,并返回自减后的值 9。如果再次执行 DECR inventory,则键 “inventory” 的值将变为 8。

  • 然而,如果键 “inventory” 不存在,则第一次执行 DECR inventory 时,Redis 会将其值视为 0 并立即减少 1,因此返回的结果是 -1。这可能不是所有应用场景都期望的行为,因此在设计系统时需要特别注意这一点。

(3)注意事项:

  1. 数据类型:DECR 命令仅适用于存储为整数的值。如果键的值不是整数,Redis 将返回一个错误。
  2. 原子性:DECR 命令是原子操作,这意味着当多个客户端同时对同一个键执行 DECR 命令时,Redis 会确保操作的原子性。因此,在多线程或并发环境下,不会出现竞态条件或数据不一致的情况。
  3. 性能:由于 DECR 命令是原子性的且操作简单,因此它的性能非常高。在处理大量自减操作时,DECR 命令可以显著提高系统的吞吐量和响应速度。
  4. 溢出问题:与 INCR 命令类似,DECR 命令也会受到整数溢出问题的影响。当值的绝对值减少到超过 -(2^63)(即 -9223372036854775808)时,会发生溢出。在使用 DECR 命令时,需要注意这一点,以避免出现意外的结果。

1.2.4 DECRBY命令

(1)DECRBY 命令用于对存储在 Redis 中的键(key)所储存的值进行指定减量值的递减操作。语法如下:

DECRBY key decrement
  • key:要进行递减操作的键名。
  • decrement:指定的减量值,必须是整数。
  • 返回值:返回递减后的值作为结果。如果键不存在,则会先初始化为 0(实际上,对于不存在的键,DECRBY 会先将其视为 0,然后减去指定的减量值,所以结果会是 -decrement),然后返回递减后的值。但如果键的值不是整数或无法被解释为数字,Redis 将返回一个错误。

(2)示例:

  • 假设我们有一个名为 “inventory” 的键,初始值为 100。我们可以使用 DECRBY 命令对其进行指定减量值的递减操作:
DECRBY inventory 20

执行上述命令后,键 “inventory” 的值将减少 20,并返回递减后的值 80。

(3)注意事项:

  1. 数据类型:DECRBY 命令仅适用于存储为整数的值。如果键的值不是整数,Redis 将返回一个错误。
  2. 原子性:DECRBY 命令是原子操作,这意味着当多个客户端同时对同一个键执行 DECRBY 命令时,Redis 会确保操作的原子性。因此,在多线程或并发环境下,不会出现竞态条件或数据不一致的情况。
  3. 性能:由于 DECRBY 命令是原子性的且操作简单,因此它的性能非常高。在处理大量递减操作时,DECRBY 命令可以显著提高系统的吞吐量和响应速度。
  4. 溢出问题:与 INCR 和 INCRBY 命令类似,DECRBY 命令也会受到整数溢出问题的影响。当值的绝对值减少到超过 -(2^63) 时,会发生溢出。在使用 DECRBY 命令时,需要注意这一点,以避免出现意外的结果。

1.2.5 INCRBYFLOAT命令

(1)INCRBYFLOAT 是 Redis 提供的一个命令,用于对存储在 Redis 中的键(key)所储存的值进行浮点数的递增操作。与 INCRBY 命令类似,INCRBYFLOAT 也允许用户指定一个增量值,但不同的是,INCRBYFLOAT 处理的是浮点数而不是整数。语法如下:

INCRBYFLOAT key increment
  • key:要进行递增操作的键名。
  • increment:指定的浮点增量值。
  • 返回值:返回自增后的浮点数值作为结果。如果键不存在,则会先初始化为 0.0(实际上,对于不存在的键,INCRBYFLOAT 会将其视为字符串 “0” 并尝试转换为浮点数 0.0,然后进行递增操作),然后返回递增后的浮点数值。如果键的值无法被解释为浮点数,Redis 将返回一个错误。

(2)示例:

  • 假设我们有一个名为 “score” 的键,初始值为 10.5。我们可以使用 INCRBYFLOAT 命令对其进行浮点增量值的递增操作:
INCRBYFLOAT score 1.25

执行上述命令后,键 “score” 的值将增加 1.25,并返回递增后的浮点数值 11.75。

(3)注意事项:

  1. 数据类型:INCRBYFLOAT 命令仅适用于存储为浮点数的值。如果键的值不是浮点数(或无法被解释为浮点数),Redis 将返回一个错误。然而,对于不存在的键,INCRBYFLOAT 会将其视为字符串 “0” 并尝试进行转换。
  2. 精度问题:由于浮点数在计算机中的表示方式,可能会存在精度问题。因此,在使用 INCRBYFLOAT 命令时,需要注意浮点数的精度限制和可能的舍入误差。
  3. 原子性:INCRBYFLOAT 命令是原子操作,这意味着当多个客户端同时对同一个键执行 INCRBYFLOAT 命令时,Redis 会确保操作的原子性。因此,在多线程或并发环境下,不会出现竞态条件或数据不一致的情况。
  4. 性能:虽然 INCRBYFLOAT 命令处理的是浮点数,但它的性能仍然非常高。在处理大量递增操作时,INCRBYFLOAT 命令可以显著提高系统的吞吐量和响应速度。

1.3 其他命令

1.3.1 APPEND命令

(1)Redis 的 APPEND 命令用于向指定键(key)的字符串值的末尾追加一个或多个字符串。如果该键不存在,Redis 会将其视为一个新的空字符串,并执行追加操作。

APPEND key value
  • key:要追加的键名。
  • value:要追加的字符串值。
  • 返回值:返回追加后的字符串的总长度。

(2)示例:

  • 假设我们有一个名为 “message” 的键,初始值为 “Hello”。我们可以使用 APPEND 命令向其追加新的字符串:
APPEND message " World"

执行上述命令后,键 “message” 的值将变为 “Hello World”,并且 Redis 会返回追加后的字符串长度 11。

  • 如果再次执行 APPEND 命令:
APPEND message "!"

键 “message” 的值将变为 “Hello World!”,并且 Redis 会返回新的字符串长度 12。

(3)注意事项:

  1. 数据类型:APPEND 命令仅适用于字符串类型的键。如果键的值不是字符串或无法被解释为字符串,Redis 将返回错误。
  2. 原子性:APPEND 命令是原子操作,这意味着当多个客户端同时对同一个键执行 APPEND 命令时,Redis 会确保操作的原子性。因此,在多线程或并发环境下,不会出现竞态条件或数据不一致的情况。
  3. 性能:由于 Redis 将字符串值存储在内存中,APPEND 命令的性能非常高。在处理大量追加操作时,APPEND 命令可以显著提高系统的吞吐量和响应速度。
  4. 内存限制:虽然 Redis 的内存管理非常高效,但过多的数据追加可能会导致内存占用过高。因此,在使用 APPEND 命令时,需要注意内存使用情况,并避免过度追加数据。
  5. 字符串长度限制:Redis 对字符串的长度有一定的限制(通常是 512MB),如果追加后的字符串长度超过了这个限制,Redis 可能会返回错误。因此,在使用 APPEND 命令时,需要注意字符串长度的限制。

1.3.2 GETRANGE命令

(1)Redis 的 GETRANGE 命令用于获取存储在指定键(key)中的字符串值的子字符串。这个子字符串由起始偏移量(start)和结束偏移量(end)确定,包括这两个偏移量所对应的字符在内。语法如下:

GETRANGE key start end
  • key:要获取子字符串的键名。
  • start:起始偏移量,从 0 开始计数。
  • end:结束偏移量,从 0 开始计数。
  • 返回值:返回从起始偏移量到结束偏移量之间的子字符串。

(2)示例:

  • 假设我们有一个名为 “mystring” 的键,其值为 “Hello, Redis!”。我们可以使用 GETRANGE 命令来获取其不同部分的子字符串:
GETRANGE mystring 0 4

这将返回 “Hello”(从偏移量 0 到 4 的子字符串)。

GETRANGE mystring 7 -1

这将返回 “Redis!”(从偏移量 7(即逗号后面的空格)到字符串末尾的子字符串)。

GETRANGE mystring 0 -1

这将返回整个字符串 “Hello, Redis!”(从偏移量 0 到字符串末尾的子字符串)。

GETRANGE mystring 10 5

由于 start(10)大于 end(5),这将返回一个空字符串。

(3)注意事项:

  1. 偏移量范围:偏移量 start 和 end 可以是任何非负整数,也可以是负数。如果 start 或 end 是负数,它们表示从字符串末尾开始的偏移量。例如,-1 表示最后一个字符,-2 表示倒数第二个字符,依此类推。
  2. 边界条件:如果 start 大于 end,Redis 会返回一个空字符串。如果 start 或 end 超出了字符串的实际长度,Redis 会将其调整为字符串的实际长度减一(对于正偏移量)或零(对于负偏移量,但负偏移量永远不会超出字符串的开头)。
  3. 数据类型:GETRANGE 命令仅适用于字符串类型的键。如果键的值不是字符串或无法被解释为字符串,Redis 将返回错误。
  4. 性能:由于 Redis 将字符串值存储在内存中,GETRANGE 命令的性能非常高。在处理大量子字符串获取操作时,GETRANGE 命令可以显著提高系统的吞吐量和响应速度。

1.3.3 SETRANGE命令

(1)Redis 的 SETRANGE 命令用于覆盖存储在指定键(key)中的字符串值的一部分,从指定的偏移量(offset)开始。该命令允许用户用新的字符串值替换旧字符串值中的一部分内容。语法如下:

SETRANGE key offset value
  • key:要覆盖字符串值的键名。
  • offset:起始偏移量,从 0 开始计数,表示从哪个位置开始覆盖。
  • value:要设置的新字符串值。
  • 返回值:返回被修改后的字符串长度。如果操作失败(例如,键不存在),某些版本的 Redis 可能会返回特殊值(如 -1 或错误信息),但通常在现代版本的 Redis 中,如果键不存在,它会被视为空字符串,并返回设置后的字符串长度。

(2)示例:

  • 假设我们有一个名为 “mykey” 的键,其初始值为 “Hello”。我们可以使用 SETRANGE 命令来覆盖其部分内容:
SETRANGE mykey 1 "i"

这将把 “mykey” 的值修改为 “Hiello”(从偏移量 1 开始,将 ‘e’ 替换为 ‘i’)。并且,Redis 会返回修改后的字符串长度,即 5。

  • 如果我们再次使用 SETRANGE 命令来覆盖更长的内容:
SETRANGE mykey 0 "Redis"

这将把 “mykey” 的值完全覆盖为 “Redis”(因为从偏移量 0 开始覆盖,并且新字符串 “Redis” 的长度足以覆盖旧字符串 “Hiello” 的全部内容)。Redis 会返回修改后的字符串长度,即 5(因为 “Redis” 也是 5 个字符长)。

  • 然而,如果我们尝试覆盖一个更长的字符串到较短的原始字符串中,并且偏移量超过了原始字符串的长度:
SETRANGE mykey 6 "World"

由于 “mykey” 的原始值是 “Hiello”(长度为 5),偏移量 6 超出了原始字符串的长度。因此,Redis 会用零字节填充字符串以适应偏移量 6,并将 “World” 覆盖到该位置。但需要注意的是,由于 Redis 字符串的底层实现和内存管理,这种操作可能会导致性能下降或内存分配开销。在实际应用中,应该避免不必要的长字符串覆盖操作。

(3)注意事项:

  1. 偏移量范围:偏移量 offset 可以是任何非负整数。如果 offset 大于键名处字符串的当前长度,Redis 会用零字节填充字符串以适应偏移量。这意味着,如果尝试在字符串的末尾之后写入内容,Redis 会自动扩展字符串并在需要的位置插入零字节。
  2. 数据类型:SETRANGE 命令仅适用于字符串类型的键。如果键的值不是字符串或无法被解释为字符串,Redis 将返回错误。
  3. 覆盖操作:SETRANGE 命令执行的是覆盖操作,而不是插入操作。这意味着,从指定的偏移量开始,旧字符串值中对应位置及之后的内容会被新字符串值替换掉。
  4. 性能:由于 Redis 将字符串值存储在内存中,SETRANGE 命令的性能非常高。在处理大量字符串覆盖操作时,SETRANGE 命令可以显著提高系统的吞吐量和响应速度。
  5. 内存管理:当使用 SETRANGE 命令设置字符串的最后一个可能的字节时,如果键名处存储的字符串值尚未持有字符串值或持有一个小字符串值,Redis 需要分配所有中间内存。这可能会导致服务器暂时阻塞,特别是在需要分配大量内存时。然而,一旦完成了第一次分配,对于相同键的后续 SETRANGE 调用将不会有分配开销。

1.3.4 STRLEN命令

(1)Redis 的 STRLEN 命令用于获取存储在指定键(key)中的字符串值的长度。语法如下:

STRLEN key
  • key:要获取长度的字符串键名。
  • 返回值:返回键所关联的字符串值的长度(以字节为单位)。如果键不存在,则返回 0。如果键的值不是字符串类型,Redis 会返回一个错误。

(2)示例:

  • 假设我们有一个名为 “mystring” 的键,其值为 “Hello, Redis!”。我们可以使用 STRLEN 命令来获取其长度:
STRLEN mystring

这将返回 13,因为 “Hello, Redis!” 字符串包含 13 个字节(包括逗号和空格)。

  • 如果键不存在,例如尝试获取名为 “nonexistentkey” 的键的长度:
STRLEN nonexistentkey

这将返回 0,表示该键不存在。

  • 如果键的值不是字符串类型,例如尝试获取一个列表类型的键的长度:
LPUSH mylist "value"
STRLEN mylist

第二个命令将返回一个错误,因为 “mylist” 是一个列表类型的键,而不是字符串类型。

(3)注意事项:

  1. 数据类型:STRLEN 命令仅适用于字符串类型的键。如果尝试对非字符串类型的键使用 STRLEN 命令,Redis 会返回错误。
  2. 内存管理:Redis 字符串是动态分配的,因此 STRLEN 命令返回的长度是字符串当前的字节长度,可能会随着字符串的修改而变化。
  3. 性能:由于 Redis 将字符串值存储在内存中,STRLEN 命令的性能非常高。在处理大量字符串长度获取操作时,STRLEN 命令可以显著提高系统的吞吐量和响应速度。

1.4 命令小结

(1)字符串类型命令小结如下:

命令 执行效果 时间复杂度
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)

1.5 内部编码

(1)字符串类型的内部编码有3种:

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

(2)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"

1.6 典型应用场景

1.6.1 缓存功能

(1)redis作为缓冲层,MySQL作为存储层,绝大部分的请求数据都是从redis中获取,由于redis支持高并发的特性,所以缓存能够起到加速读写降低后端压力的作用。redis和MySQL组成的缓存存储结构图如下:

(2)下面的伪代码模拟了上图的业务数据访问过程:

  • 假设业务是根据用户 uid 获取用户信息:
UserInfo getUserInfo(long uid) {
 ...
}
  • 首先从 Redis 获取用户信息,我们假设用户信息保存在 “user:info:” 对应的键中:
// 根据 uid 得到 Redis 的键
String key = "user:info:" + uid;
// 尝试从 Redis 中获取对应的值
String value = Redis 执⾏命令:get key;
// 如果缓存命中(hit)
if (value != null) {
 	// 假设我们的⽤⼾信息按照 JSON 格式存储
 	UserInfo userInfo = JSON 反序列化(value);
 	return userInfo;
}
  • 如果没有从 Redis 中得到用户信息,及缓存 miss,则进⼀步从 MySQL 中获取对应的信息,随后写入缓存并返回:
// 如果缓存未命中(miss)
if (value == null) {
 	// 从数据库中,根据 uid 获取⽤⼾信息
 	UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>
 
 	// 如果表中没有 uid 对应的⽤⼾信息
 	if (userInfo == null) {
 		// 响应 404
 		return null;
 	}
 
 	// 将⽤⼾信息序列化成 JSON 格式
 	String value = JSON 序列化(userInfo);
 
 	// 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
 	Redis 执⾏命令:set key value ex 3600
 
 	// 返回⽤⼾信息
 	return userInfo;
}

通过增加缓存功能,在理想情况下,每个用户信息,⼀个小时期间只会有⼀次 MySQL 查询,极大地提升了查询效率,也降低了 MySQL 的访问数。

1.6.2 计数(Counter)功能

(1)许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。如下图所示,例如视频网站的视频播放次数可以使用Redis来完成:用户每播放- -次视频,相应的视频播放数就会自增1。记录视频播放次数图如下:

(2)计数伪代码如下:

// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
 	key = "video:" + vid;
 	long count = Redis 执⾏命令:incr key
 	return counter;
}

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

1.6.3 共享会话(Session)

(1)如下图所示,一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。

(2)为了解决这个问题,可以使用Redis将用户的Session信息进行集中管理,如下图所示,在这种模式下,只要保证Redis是高可用和可扩展性的,无论用户被均衡到哪台Web服务器上,都集中Redis中查询、更新Session信息。Redis集中管理Session:

1.6.4 手机验证码

(1)很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,如下图所示。

(2)此功能可以使用以下伪代码说明基本实现思路:

String 发送验证码(phoneNumber) {
    key = "shortMsg:limit:" + phoneNumber;
    // 设置过期时间为 1 分钟(60 秒)
    // 使⽤ NX,只在不存在 key 时才能设置成功
    bool 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 位数的验证码();
    validationKey = "validation:" + phoneNumber;
    // 验证码 5 分钟(300 秒)内有效
    Redis 执⾏命令:set validationKey validationCode ex 300;
    // 返回验证码,随后通过⼿机短信发送给⽤⼾
    return validationCode ;
}
// 验证用户输⼊的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
    validationKey = "validation:" + phoneNumber;
    String value = Redis 执⾏命令:get validationKey;
    if (value == null) {
        // 说明没有这个⼿机的验证码记录,验证失败
        return false;
    }
    if (value == validationCode) {
        return true;
    } else {
        return false;
    }
}

2. Hash 哈希

几乎所有的主流编程语言都提供了哈希(hash) 类型,它们的叫法可能是哈希、字典、关联数组、映射。在Redis中,哈希类型是指值本身又是一个键值对结构,形如key= “key”, value={{field1, value1 }, … {fieldN, valueN }}, Redis 键值对和哈希类型二者的关系可以用下图来表示。

哈希类型中的映射关系通常称为field-value, 用于区分Redis整体的键值对(key-value) ,注意这里的value是指field对应的值,不是键(key) 对应的值,请注意value在不同上下文的作用。

2.1 常见命令

2.1.1 HSET命令

(1)HSET 是 Redis 中用于操作哈希表(hash)的一个指令。它用于在哈希表中为指定的字段(field)设置值(value),如果该字段已经存在,则更新其值。如果字段不存在,则新增该字段。语法如下:

HSET key field value
  • key:哈希表的键。
  • field:要设置的字段名。
  • value:要设置的字段值。
  • 返回值:如果字段是新增的,返回 1。如果字段已经存在并且值被更新,返回 0。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并希望设置一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 返回 1,因为字段 name 是新增的

# 设置字段 age 的值为 30
HSET user:1000 age 30
# 返回 1,因为字段 age 是新增的
 
# 更新字段 age 的值为 31
HSET user:1000 age 31
# 返回 0,因为字段 age 已经存在,并且值被更新

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,Redis 会自动创建该哈希表。
  2. HSET 指令的时间复杂度为 O(1),操作非常高效。

2.1.2 HGET命令

(1)HGET 是 Redis 中用于操作哈希表(hash)的一个指令。它用于获取存储在哈希表中指定字段(field)的值(value)。语法如下:

HGET key field
  • key:哈希表的键。
  • field:要获取值的字段名。
  • 返回值:返回与指定字段关联的值。如果字段不存在于哈希表中,则返回 nil。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 获取字段 name 的值
HGET user:1000 name
# 返回 "Alice"
# 获取字段 email 的值(假设 email 字段之前未设置)
HGET user:1000 email
# 返回 (nil)

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HGET 指令将返回 nil。
  2. HGET 指令的时间复杂度为 O(1),操作非常高效。

2.1.3 HEXISTS命令

(1)HEXISTS 是 Redis 中用于检查哈希表(hash)中是否存在指定字段(field)的指令。语法如下:

HEXISTS key field
  • key:哈希表的键。
  • field:要检查的字段名。
  • 返回值:如果哈希表中存在指定的字段,返回 1。如果哈希表中不存在指定的字段,返回 0。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 检查字段 name 是否存在
HEXISTS user:1000 name
# 返回 1,因为字段 name 存在
# 检查字段 email 是否存在(假设 email 字段之前未设置)
HEXISTS user:1000 email
# 返回 0,因为字段 email 不存在

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HEXISTS 指令将返回 0。
  2. HEXISTS 指令的时间复杂度为 O(1),操作非常高效。

2.1.4 HDEL命令

(1)HDEL 是 Redis 中用于删除哈希表(hash)中指定字段(field)的指令。以下是对 HDEL 指令的详细解释:

HDEL key field [field ...]
  • key:哈希表的键。
  • field:要删除的字段名,可以指定一个或多个字段进行删除。
  • 返回值:返回被成功删除的字段数量,不包括被忽略的字段(即不存在的字段)。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 设置字段 email 的值为 alice@example.com
HSET user:1000 email alice@example.com
# 删除字段 name
HDEL user:1000 name
# 返回 1,因为字段 name 被成功删除
# 尝试删除不存在的字段 phone
HDEL user:1000 phone
# 返回 0,因为字段 phone 不存在
# 删除字段 age 和 email
HDEL user:1000 age email
# 返回 2,因为字段 age 和 email 被成功删除

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HDEL 指令将返回 0,并视为一个空的哈希表进行处理。
  2. HDEL 指令的时间复杂度为 O(N),其中 N 是要删除的字段数量。但是,由于哈希表操作的高效性,即使对于较大的哈希表,HDEL 指令的性能也通常是可以接受的。

2.1.5 HKEYS命令

(1)HKEYS 是 Redis 中用于获取哈希表(hash)中所有字段名(key)的指令。这个指令会返回一个列表,包含哈希表中所有字段的名称。语法如下:

HKEYS key
  • key:哈希表的键。
  • 返回值:返回一个列表,包含哈希表中所有字段的名称。如果哈希表为空,则返回一个空列表。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 设置字段 email 的值为 alice@example.com
HSET user:1000 email alice@example.com
# 获取哈希表中所有字段名
HKEYS user:1000
# 返回 1) "name" 2) "age" 3) "email",表示哈希表中有三个字段:name、age 和 email

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HKEYS 指令将返回一个空列表。
  2. HKEYS 指令的时间复杂度为 O(N),其中 N 是哈希表中字段的数量。对于包含大量字段的哈希表,这个操作可能会比较慢,因此在性能敏感的应用中需要谨慎使用。

2.1.6 HVALS命令

(1)HVALS 是 Redis 中用于获取哈希表(hash)中所有字段值(value)的指令。这个指令会返回一个列表,包含哈希表中所有字段对应的值。语法如下:

HVALS key
  • key:哈希表的键。
  • 返回值:返回一个列表,包含哈希表中所有字段对应的值。如果哈希表为空,则返回一个空列表。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 设置字段 email 的值为 alice@example.com
HSET user:1000 email alice@example.com
# 获取哈希表中所有字段的值
HVALS user:1000
# 返回 1) "Alice" 2) "30" 3) "alice@example.com",表示哈希表中有三个字段,对应的值分别是 Alice、30 和 alice@example.com

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HVALS 指令将返回一个空列表。
  2. HVALS 指令的时间复杂度为 O(N),其中 N 是哈希表中字段的数量。对于包含大量字段的哈希表,这个操作可能会比较慢,因此在性能敏感的应用中需要谨慎使用。

2.1.7 HGETALL命令

(1)HGETALL 是 Redis 中用于获取哈希表(hash)中所有字段和值(field-value pairs)的指令。这个指令会返回一个列表,其中包含哈希表中所有字段及其对应的值,字段和值交替出现。语法如下:

HGETALL key
  • key:哈希表的键。
  • 返回值:返回一个列表,列表中包含哈希表中所有字段和值。字段和值交替出现,即第一个元素是第一个字段名,第二个元素是第一个字段对应的值,第三个元素是第二个字段名,以此类推。如果哈希表为空,则返回一个空列表。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 设置字段 email 的值为 alice@example.com
HSET user:1000 email alice@example.com
# 获取哈希表中所有字段和值
HGETALL user:1000
# 返回 1) "name" 2) "Alice" 3) "age" 4) "30" 5) "email" 6) "alice@example.com",
# 表示哈希表中有三个字段,分别是 name、age 和 email,对应的值分别是 Alice、30 和 alice@example.com

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HGETALL 指令将返回一个空列表。
  2. HGETALL 指令的时间复杂度为 O(N),其中 N 是哈希表中字段的数量。对于包含大量字段的哈希表,这个操作可能会比较慢,并且返回的列表也会很大,因此在性能敏感的应用中需要谨慎使用。

2.1.8 HMGET命令

(1)HMGET 是 Redis 中用于获取哈希表(hash)中多个指定字段(field)的值的指令。与 HGET 指令类似,但 HMGET 可以一次性获取多个字段的值,而 HGET 只能获取单个字段的值。语法如下:

HMGET key field [field ...]
  • key:哈希表的键。
  • field:要获取的字段名,可以指定一个或多个字段进行获取。
  • 返回值:返回一个列表,包含指定字段对应的值。列表中的元素顺序与 field 参数中指定的字段顺序一致。如果某个字段不存在,则对应的位置返回 nil。

(2)示例:

  • 假设我们有一个哈希表 user:1000,并且之前已经设置了一些字段。
# 设置字段 name 的值为 Alice
HSET user:1000 name Alice
# 设置字段 age 的值为 30
HSET user:1000 age 30
# 设置字段 email 的值为 alice@example.com
HSET user:1000 email alice@example.com
# 获取字段 name 和 email 的值
HMGET user:1000 name email
# 返回 1) "Alice" 2) "alice@example.com",表示字段 name 的值是 Alice,字段 email 的值是 alice@example.com
# 尝试获取不存在的字段 phone 的值
HMGET user:1000 phone
# 返回 (nil),因为字段 phone 不存在

(3)注意事项:

  1. 如果 key 对应的哈希表不存在,HMGET 指令将返回一个包含 nil 值的列表,列表的长度与指定的字段数量相同。
  2. HMGET 指令的时间复杂度为 O(N),其中 N 是要获取的字段数量。但是,由于哈希表操作的高效性,即使对于较大的哈希表,HMGET 指令的性能也通常是可以接受的。

2.1.9 HLEN命令

(1)HLEN 是 Redis 中用于获取哈希表(Hash)中字段数量的指令。具体来说,当你对一个哈希表执行 HLEN 指令时,它会返回该哈希表中键-值对的数量。以下是 HLEN 指令的基本语法:

HLEN key
  • key:哈希表的键名。
  • 返回值:返回哈希表中键-值对的数量。如果哈希表不存在,返回 0。

(2)示例:

  • 创建哈希表并添加字段:
HSET myhash field1 value1
(integer) 1
 
HSET myhash field2 value2
(integer) 1
  • 获取哈希表中的字段数量:
HLEN myhash
(integer) 2
  • 尝试获取一个不存在的哈希表的字段数量:
HLEN nonexistinghash
(integer) 0

(3)注意事项:

  1. 如果对不存在的哈希表执行 HLEN,Redis 会返回 0,而不是错误。
  2. HLEN 指令的时间复杂度为 O(1),即无论哈希表中有多少字段,执行该指令的时间都是恒定的。

2.1.10 HSETNX命令

(1)Redis 的 HSETNX 指令是一个专门用于哈希表(Hash)操作的命令。该指令的作用是在哈希表中为不存在的字段赋值。如果尝试为已经存在的字段赋值,那么操作将不会执行,并且指令会返回相应的状态码。以下是 HSETNX 指令的详细解释:

HSETNX key field value
  • key:哈希表的键名。
  • field:要在哈希表中设置的字段名。
  • value:与字段名相关联的值。
  • 返回值:如果字段在哈希表中不存在且设置成功,返回 1。如果字段已经存在且没有执行设置操作,返回 0。

(2)示例:

  • 创建一个哈希表并设置一个新字段:
HSETNX myhash field1 "value1"
(integer) 1

在这个例子中,myhash 是一个哈希表,field1 是一个新字段,其值被设置为 “value1”。因为 field1 在 myhash 中不存在,所以操作成功并返回 1。

  • 尝试设置一个已经存在的字段:
HSETNX myhash field1 "value2"
(integer) 0

在这个例子中,我们尝试将 field1 的值设置为 “value2”。但是,由于 field1 已经存在于 myhash 中,所以操作没有执行,并返回 0。

  • 检查字段的值
HGET myhash field1
"value1"

通过 HGET 指令,我们可以验证 field1 的值仍然是 “value1”,而不是我们尝试设置的 “value2”。

(3)注意事项:

  1. 如果哈希表不存在,HSETNX 会自动创建一个新的哈希表,并执行设置操作。
  2. HSETNX 的时间复杂度为 O(1),即无论哈希表中有多少字段,执行该指令的时间都是恒定的(在平均情况下)。

2.1.11 HINCRBY命令

(1)Redis 的 HINCRBY 指令用于对哈希表中指定的字段的整数值进行增量操作。如果该字段不存在,Redis 会先将其值初始化为 0,然后再进行增量操作。这个指令在处理计数器或需要递增/递减数值的场景中非常有用。语法如下:

HINCRBY key field increment
  • key:哈希表的键名。
  • field:要在哈希表中递增的字段名。
  • increment:要增加的整数值。该值可以是正数(表示递增)也可以是负数(表示递减)。
  • 返回值:返回字段递增后的值。

(2)示例:

  • 创建一个哈希表并递增一个字段:
HSET myhash counter 10
(integer) 1
 
HINCRBY myhash counter 5
(integer) 15

在这个例子中,我们首先使用 HSET 指令将 counter 字段的值设置为 10。然后,我们使用 HINCRBY 指令将 counter 字段的值递增 5,结果变为 15。

  • 递增一个不存在的字段:
HINCRBY myhash newcounter 3
(integer) 3

在这个例子中,newcounter 字段在 myhash 哈希表中不存在。因此,Redis 会先将其值初始化为 0,然后再递增 3,结果返回 3。

  • 递减一个字段的值:
HINCRBY myhash counter -2
(integer) 13

在这个例子中,我们使用 HINCRBY 指令并传递一个负数 -2 作为增量值,将 counter 字段的值递减 2,结果变为 13。

(3)注意事项:

  1. 如果哈希表或字段不存在,Redis 会自动创建它们,并执行相应的递增操作。
  2. HINCRBY 指令的时间复杂度为 O(1),即无论哈希表中有多少字段,执行该指令的时间都是恒定的(在平均情况下)。
  3. 确保传递给 HINCRBY 的增量值是整数,否则 Redis 会返回错误。

2.1.12 HINCRBYFLOAT命令

(1)Redis 的 HINCRBYFLOAT 指令用于对哈希表中指定的字段的浮点数值进行增量操作。如果该字段不存在,Redis 会先将其值初始化为 0(实际上是作为浮点数 0.0 来处理),然后再进行增量操作。这个指令在处理需要浮点数递增或递减的场景中非常有用,例如统计平均值、分数等。语法如下:

HINCRBYFLOAT key field increment
  • key:哈希表的键名。
  • field:要在哈希表中递增的字段名。
  • increment:要增加的浮点数值。该值可以是正数(表示递增)也可以是负数(表示递减)。
  • 返回值:返回字段递增后的值,该值以字符串形式表示浮点数。

(2)示例:

  • 创建一个哈希表并递增一个字段:
HSET myhash score 10.5
(integer) 1

HINCRBYFLOAT myhash score 0.1
"10.60000000000000001"

注意,由于浮点数的精度问题,结果可能会有轻微的偏差。在这个例子中,我们首先使用 HSET 指令将 score 字段的值设置为 10.5。然后,我们使用 HINCRBYFLOAT 指令将 score 字段的值递增 0.1。

  • 递增一个不存在的字段:
HINCRBYFLOAT myhash newscore 2.5
"2.5"

在这个例子中,newscore 字段在 myhash 哈希表中不存在。因此,Redis 会先将其值初始化为 0.0(作为浮点数处理),然后再递增 2.5,结果返回 “2.5”。

  • 递减一个字段的值:
HINCRBYFLOAT myhash score -0.5
"10.1"

在这个例子中,我们使用 HINCRBYFLOAT 指令并传递一个负数 -0.5 作为增量值,将 score 字段的值递减 0.5。

(3)注意事项:

  1. 如果哈希表或字段不存在,Redis 会自动创建它们,并执行相应的递增操作。
  2. 由于浮点数的精度限制,结果可能会有轻微的偏差。
  3. HINCRBYFLOAT 指令的时间复杂度为 O(1),即无论哈希表中有多少字段,执行该指令的时间都是恒定的(在平均情况下)。

2.2 命令小结

(1)哈希命令小结如下:

命令 执行效果 时间复杂度
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 field [field …] 批量获取 field-value O(k), k 是 field个数
hmset 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)

2.3 内部编码

(1)哈希的内部编码有两种:

  • ziplist (压缩列表):当哈希类型元素个数小于hash-max -ziplist-entries配置(默认512个)、同时所有值都小于hash-max- ziplist-value配置(默认 64字节)时,Redis 会使用ziplist作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多元素的连续存储,所以在节省内存方面比hashtable更加优秀。
  • hashtable (哈希表):当哈希类型无法满足ziplist的条件时,Redis 会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为0(1)。

(2)下面的示例演示了哈希类型的内部编码,以及响应的变化:

  • 当field个数比较少且没有大的value时,内部编码为ziplist:

127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
OK
127.0.0.1:6379> object encoding hashkey
"ziplist"
  • 当有value大于64字节时,内部编码会转换为hashtable:
127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... 省略 ..." 1
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
  • 当field个数超过512时,内部编码也会转换为hashtable:
127.0.0.1:6379> hmset hashkey f1 v1 h2 v2 f3 v3 ... 省略 ... f513 v513
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"

2.4 使用场景

(1)如下图为关系型数据表记录的两条用户信息,用户的属性表现为表的列,每条用户信息表现为行。

(2)如果映射关系表示这两个用户信息,则如下图所示。

(3)相比于使用JSON格式的字符串缓存用户信息,哈希类型变得更加直观,并且在更新操作。上变得更灵活。可以将每个用户的id定义为键后缀,多对field-value对应用户的各个属性,类似如下伪代码:

UserInfo getUserInfo(long uid) {
 	// 根据 uid 得到 Redis 的键
 	String key = "user:" + uid;
 
 	// 尝试从 Redis 中获取对应的值
 	userInfoMap = Redis 执⾏命令:hgetall key;
 
 	// 如果缓存命中(hit)
 	if (value != null) {
 		// 将映射关系还原为对象形式
 		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;
}

(4)但是需要注意的是哈希类型和关系型数据库有两点不同之处:

  • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field, 而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为null,如下图所示。
  • 关系数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。

2.5 缓存方式对比

(1)截至目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。

  1. 原生字符串类型一使用字符串类型, 每个属性一个键。
set user:1:name James
set user:1:age 23
set user:1:city Beijing
  • 优点:实现简单,针对个别属性变更也很灵活。
  • 缺点:占用过多的键,内存占用量较大,同时用户信息在Redis中比较分散,缺少内聚性,所以这种方案基本没有实用性。
  1. 序列化字符串类型,例如JSON格式:
set user:1  #经过序列化后的用户对象字符串 
  • 优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
  • 缺点:本身序列化和反序列需要一-定开 销,同时如果总是操作个别属性则非常不灵活。
  1. hash类型:
hmset user:1 name James age 23 city Beijing 
  • 优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
  • 缺点:需要控制哈希在ziplist和hashtable两种内部编码的转换,可能会造成内存的较大消耗。

3. List 列表

3.2 List介绍

(1)列表类型是用来存储多个有序的字符串,如图2-19所示,a、b、C、d、e五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element) ,一个列表最多可以存储2^32 - 1 个元素。在Redis中,可以对列表两端插入(push) 和弹出(pop) ,还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

  • 列表两端插入和弹出操作:

  • 列表的获取、删除等操作:


(2)列表类型的特点:

  • 第一、列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,例如要获取上图的第5个元素,可以执行lindex user:1:messages 4或者倒数第1个元素,lindexuser:1:messages -1就可以得到元素e。
  • 第二、区分获取和删除的区别,例如上图中的lrem 1 b是从列表中把从左数遇到的前1个b元素删除,这个操作会导致列表的长度从5变成4;但是执行lindex 4只会获取元素,但列表长度是不会变化的。
  • 第三、列表中的元素是允许重复的,例如下图中的列表中是包含了两个a元素的。

3.2 常见命令

3.2.1 LPUSH命令

(1)LPUSH 是 Redis 中的一个列表操作命令,用于将一个或多个值插入到列表的头部(即列表的左端)。如果列表不存在,Redis 会创建一个新的列表。语法如下:

LPUSH key value [value ...]
  • key:列表的键名。
  • value [value …]:一个或多个要插入到列表头部的值。
  • 返回值:返回插入操作后列表的长度。

(2)示例:

  • 插入单个值:
LPUSH mylist "hello"

假设 mylist 列表不存在,这条命令会创建一个新的列表,并将 “hello” 插入到列表头部。返回值为 1,表示列表现在有一个元素。

  • 插入多个值:
LPUSH mylist "world" "foo" "bar"

这条命令会将 “world”, “foo”, “bar” 依次插入到 mylist 的头部。如果 mylist 之前为空,返回值为 3,表示列表现在有三个元素。

  • 对已有列表进行插入:
LPUSH mylist "new"

假设 mylist 列表已经包含 [“world”, “foo”, “bar”],这条命令会将 “new” 插入到列表头部,使列表变为 [“new”, “world”, “foo”, “bar”]。返回值为 4,表示列表现在有四个元素。

(3)注意事项:

  1. LPUSH 命令的时间复杂度为 O(N),其中 N 是插入元素的数量。因为 Redis 需要遍历列表以将新元素插入到头部,但总体性能仍然非常高。
  2. 如果列表的长度超过了 Redis 配置中的 list-max-length 限制(如果有设置),那么最旧的元素(即列表的尾部元素)会被移除,以确保列表不会无限增长。

3.2.2 LPUSHX命令

(1)Redis的LPUSHX命令是一个列表操作命令,用于将一个或多个值插入到已存在的列表头部。如果列表不存在,该命令不会执行任何操作,也不会创建新的列表。语法如下:

LPUSHX key value [value ...]
  • key:列表的键名。
  • value [value …]:一个或多个要插入到列表头部的值。
  • 返回值:如果操作成功(即列表存在且值被插入),返回插入操作后列表的长度。如果列表不存在,返回0,表示没有进行任何插入操作。

(2)示例:

  • 插入到已存在的列表:假设有一个名为mylist的列表,其中已经包含元素[“foo”, “bar”]:
LPUSHX mylist "new"

执行后,mylist的内容将变为[“new”, “foo”, “bar”],并且命令返回3,表示列表现在有三个元素。

  • 尝试插入到不存在的列表:假设有一个名为nonexistentlist的列表,它当前不存在:
LPUSHX nonexistentlist "value"

执行后,由于nonexistentlist不存在,命令将返回0,并且不会创建新的列表。

(3)注意事项:

  1. LPUSHX命令的时间复杂度为O(1),因为它只需要在列表的头部插入元素,而不需要遍历整个列表。
  2. 与LPUSH命令不同,LPUSHX不会在列表不存在时创建新的列表,这在某些需要确保列表已经存在的场景中非常有用。

3.2.3 RPUSH命令

(1)Redis的RPUSH命令是一个列表操作命令,用于将一个或多个值插入到列表的尾部(即列表的右端)。如果列表不存在,Redis会自动创建一个新的列表。语法如下:

RPUSH key value [value ...]
  • key:列表的键名。
  • value [value …]:一个或多个要插入到列表尾部的值。
  • 返回值:返回插入操作后列表的长度。

(2)示例:

  • 插入单个值:
RPUSH mylist "hello"

假设mylist列表不存在,这条命令会创建一个新的列表,并将"hello"插入到列表尾部。返回值为1,表示列表现在有一个元素。

  • 插入多个值:
RPUSH mylist "world" "foo" "bar"

这条命令会将"world", “foo”, "bar"依次插入到mylist的尾部。如果mylist之前为空,返回值为3,表示列表现在有三个元素。

  • 对已有列表进行插入:
RPUSH mylist "new"

假设mylist列表已经包含[“hello”],这条命令会将"new"插入到列表尾部,使列表变为[“hello”, “new”]。返回值为2,表示列表现在有两个元素。

(3)注意事项:

  1. RPUSH命令的时间复杂度为O(1),因为它只需要在列表的尾部插入元素,而不需要遍历整个列表。
  2. 如果列表的长度超过了Redis配置中的list-max-length限制(如果有设置),那么最旧的元素(即列表的头部元素)可能会被移除(取决于具体的配置和策略),以确保列表不会无限增长。但在默认情况下,Redis不会限制列表的长度。

3.2.4 RPUSHX命令

(1)Redis的RPUSHX命令是一个列表操作命令,它用于将一个或多个值插入到已存在的列表尾部。如果列表不存在,该命令不会执行任何操作,也不会创建新的列表。这一特性使得RPUSHX在需要确保列表已经存在时才添加元素的场景中非常有用。语法如下:

RPUSHX key value [value ...]
  • key:指定的列表键名。
  • value [value …]:要插入到列表尾部的一个或多个值。
  • 返回值:如果列表存在且值被成功插入,返回插入操作后列表的长度。如果列表不存在,返回0,表示没有进行任何插入操作。

(2)示例:

  • 插入到已存在的列表。假设有一个名为mylist的列表,其中已经包含元素[“foo”, “bar”]:
RPUSHX mylist "new"

执行后,mylist的内容将变为[“foo”, “bar”, “new”],并且命令返回3,表示列表现在有三个元素。

  • 尝试插入到不存在的列表。假设有一个名为nonexistentlist的列表,它当前不存在:
RPUSHX nonexistentlist "value"

执行后,由于nonexistentlist不存在,命令将返回0,并且不会创建新的列表。

(3)注意事项:

  1. RPUSHX命令的时间复杂度为O(1),因为它只需要在列表的尾部插入元素,而不需要遍历整个列表。
  2. 与RPUSH命令不同,RPUSHX不会在列表不存在时创建新的列表。
  3. 当向列表添加元素时,要确保列表键名的正确性,以避免因键名错误而导致操作失败。

3.2.5 LRANGE命令

(1)Redis的LRANGE命令用于获取列表(List)中指定区间内的元素。以下是对LRANGE命令的详细解释:

LRANGE key start stop
  • key:列表的键名。
  • start:区间的起始索引(包含)。索引从0开始,0表示列表的第一个元素。也可以使用负数索引,以-1表示列表的最后一个元素,-2表示倒数第二个元素,以此类推。
  • stop:区间的结束索引(不包含)。索引从0开始,同样支持负数索引。
  • 返回值:返回一个列表,包含指定区间内的元素。如果start或stop索引超出列表的范围,不会引发错误,而是返回从起始索引到列表末尾(或列表开始到结束索引)的元素。如果start大于列表的最大索引,则返回一个空列表。

(2)示例:

  • 获取整个列表。假设有一个名为mylist的列表,其内容为[“a”, “b”, “c”, “d”]:
LRANGE mylist 0 -1
  • 返回结果:
1) "a"
2) "b"
3) "c"
4) "d"
  • 获取列表的部分元素:
LRANGE mylist 1 2
  • 返回结果:
1) "b"
2) "c"
  • 使用负数索引:
LRANGE mylist -2 -1

返回结果:

1) "c"
2) "d"
  • 索引超出范围。如果start或stop索引超出列表的实际范围,Redis会返回从起始索引到列表末尾(或列表开始到结束索引)的元素。例如:
LRANGE mylist 2 10
  • 返回结果:
1) "c"
2) "d"

因为mylist只有四个元素,所以索引10超出了范围,Redis只返回从索引2到列表末尾的元素。

(3)注意事项:

  1. LRANGE命令的时间复杂度为O(S+N),其中S为起始索引start,N为指定区间内元素的数量。这意味着,即使列表很长,但只要指定的区间很小,命令的执行速度仍然很快。
  2. 在使用LRANGE命令时,要确保提供的键名是存在的,否则Redis会返回一个空列表。
  3. LRANGE命令非常适合用于分页显示列表中的元素,通过调整start和stop索引,可以轻松地获取列表中的任意一部分元素。

3.2.6 LPOP命令

(1)Redis的LPOP命令用于移除并返回存储在列表(List)的第一个元素。以下是对LPOP命令的详细解释:

LPOP key
  • key:列表的键名。
  • 返回值:当列表存在且不为空时,返回被移除的第一个元素的值。当列表不存在或为空时,返回nil。

(2)示例:

  • 移除并返回列表的第一个元素。假设有一个名为mylist的列表,其内容为[“a”, “b”, “c”]:
LPOP mylist

执行后,mylist的内容将变为[“b”, “c”],并且命令返回"a",表示被移除的第一个元素的值。

  • 对空列表执行LPOP。假设有一个名为emptylist的列表,它当前为空:
LPOP emptylist

执行后,由于emptylist为空,命令将返回nil。

  • 对不存在的列表执行LPOP。假设有一个名为nonexistentlist的列表,它当前不存在:
LPOP nonexistentlist

执行后,由于nonexistentlist不存在,命令同样将返回nil。

(3)从Redis版本6.2.0开始,LPOP命令支持一个可选的count参数,允许一次弹出多个元素。语法如下:

LPOP key [count]

当提供count参数时,回复将包含最多count个元素,具体取决于列表的长度。

  • 如果count的值大于列表的长度,则弹出列表中的所有元素。例如:
RPUSH mylist "one" "two" "three" "four" "five"
LPOP mylist 2

将返回并移除前两个元素(“one"和"two”),并且命令的返回值将是这两个元素组成的数组。

(4)注意事项:

  1. LPOP命令的时间复杂度为O(1),因为它只需要移除并返回列表的第一个元素,而不需要遍历整个列表。
  2. LPOP命令会改变列表的结构,即移除列表的第一个元素。因此,在需要保留原始列表结构的场景中需要谨慎使用。
  3. LPOP命令常被用作消息队列的消费者,生产者将消息放入队列,消费者使用LPOP从队列中取出消息进行处理。这样可以确保消息按顺序被处理,并且消费者能够及时获取新消息。

3.2.7 RPOP命令

(1)Redis的RPOP命令与LPOP命令相对,它用于移除并返回存储在列表(List)的最后一个元素。以下是对RPOP命令的详细解释:

RPOP key
  • key:列表的键名。
  • 返回值:当列表存在且不为空时,返回被移除的最后一个元素的值。当列表不存在或为空时,返回nil。

(2)示例:

  • 移除并返回列表的最后一个元素。假设有一个名为mylist的列表,其内容为[“a”, “b”, “c”]:
RPOP mylist

执行后,mylist的内容将变为[“a”, “b”],并且命令返回"c",表示被移除的最后一个元素的值。

  • 对空列表执行RPOP。假设有一个名为emptylist的列表,它当前为空:
RPOP emptylist

执行后,由于emptylist为空,命令将返回nil。

  • 对不存在的列表执行RPOP。假设有一个名为nonexistentlist的列表,它当前不存在:
RPOP nonexistentlist

执行后,由于nonexistentlist不存在,命令同样将返回nil。

(3)注意事项:

  1. RPOP命令的时间复杂度为O(1),因为它只需要移除并返回列表的最后一个元素,而不需要遍历整个列表。
  2. RPOP命令会改变列表的结构,即移除列表的最后一个元素。因此,在需要保留原始列表结构的场景中需要谨慎使用。
  3. RPOP命令也常被用作消息队列的消费者,与LPOP不同的是,它处理的是队列的尾部元素。然而,在生产者-消费者模型中,通常更倾向于使用LPOP来确保消息按顺序被处理。
  4. 如果想要同时弹出列表的多个元素(特别是从尾部),可以使用Redis 6.2.0及更高版本引入的RPOP命令的count参数(但需要注意的是,标准的RPOP命令本身并不直接支持count参数,这是Redis在某些版本中的扩展或误传。标准的做法是使用LRANGE配合DEL或其他命令来实现类似效果,或者等待Redis未来的正式更新)。然而,在撰写本文时(基于Redis的当前稳定版本),标准的RPOP命令仍然只移除并返回一个元素。

3.2.8 LINDEX命令

(1)Redis的LINDEX命令用于获取存储在键处的列表中指定索引位置的元素。以下是对LINDEX命令的详细解释:

LINDEX key index
  • key:列表的键名。
  • index:要获取的元素的索引。索引从0开始,0表示第一个元素,1表示第二个元素,依此类推。同时,Redis支持使用负数索引,其中-1表示最后一个元素,-2表示倒数第二个元素,依此类推。
  • 返回值:如果索引在列表的范围内,返回列表中该索引位置的元素值。如果索引超出列表的范围,返回nil。

(2)示例:

  • 获取列表中的元素。假设有一个名为mylist的列表,其内容为[“a”, “b”, “c”]:
LINDEX mylist 0

执行后,返回"a",表示列表中索引为0的元素。

LINDEX mylist 1

执行后,返回"b",表示列表中索引为1的元素。

LINDEX mylist -1

执行后,返回"c",表示列表中最后一个元素。

  • 索引超出范围:
LINDEX mylist 3

执行后,返回nil,因为mylist只有三个元素,索引3超出了列表的范围。

(3)注意事项:

  1. LINDEX命令的时间复杂度为O(N),其中N为列表的长度。虽然Redis在内部对列表进行了优化,使得在大多数情况下LINDEX命令的性能都相当不错,但在处理非常长的列表时,仍然需要注意其性能影响。
  2. LINDEX命令只用于获取列表中的元素,不会改变列表的结构或内容。
  3. 与LRANGE命令不同,LRANGE可以返回指定索引范围内的所有元素,而LINDEX只返回单个元素。因此,在需要获取多个元素时,LRANGE可能更加高效。

3.2.9 LINSERT命令

(1)Redis的LINSERT命令用于在列表的元素前或者后插入一个新的元素,但前提是列表中必须已经存在指定的参照元素。如果参照元素不存在,则不会执行插入操作。以下是关于LINSERT命令的详细解释:

LINSERT key BEFORE|AFTER pivot value
  • key:列表的键名。
  • BEFORE|AFTER:指定在参照元素的前面(BEFORE)还是后面(AFTER)插入新元素。
  • pivot:参照元素,即列表中已经存在的元素,用于确定新元素的插入位置。
  • value:要插入的新元素。
  • 返回值:
    • 如果插入操作成功,返回插入操作后列表的长度。
    • 如果未找到参照元素,返回-1。
    • 如果键不存在或键对应的值不是列表类型,返回0(但在Redis的某些版本中,如果键不存在,可能被视为空列表,不执行任何操作,也不会返回0,而是直接返回-1表示未找到参照元素。具体行为可能因Redis版本而异)。

(2)示例:

  • 在参照元素前插入新元素。假设有一个名为mylist的列表,其内容为[“a”, “b”, “c”]:
LINSERT mylist BEFORE "b" "new"

执行后,mylist的内容将变为[“a”, “new”, “b”, “c”],并且命令返回4,表示列表的长度。

  • 在参照元素后插入新元素:
LINSERT mylist AFTER "b" "another"

执行后,mylist的内容将变为[“a”, “new”, “b”, “another”, “c”],并且命令返回5,表示列表的长度。

  • 参照元素不存在
LINSERT mylist BEFORE "d" "x"

执行后,由于mylist中不存在元素"d",命令返回-1,表示未找到参照元素,mylist的内容保持不变。

(3)注意事项:

  1. LINSERT命令的时间复杂度为O(N),其中N为列表的长度。因为Redis需要遍历列表来查找参照元素,所以列表越长,执行时间越长。
  2. LINSERT命令会改变列表的结构,即在指定位置插入新元素。因此,在需要保留原始列表结构的场景中需要谨慎使用。
  3. 如果键不存在或键对应的值不是列表类型,Redis会返回错误或特定的返回值(如-1),具体行为取决于Redis的版本和配置。

3.2.10 LLEN命令

(1)Redis的LLEN命令用于返回存储在键中的列表的长度。以下是对LLEN命令的详细解释:

LLEN key
  • key:列表的键名。
  • 返回值
    • 如果键存在且其值是列表类型,返回该列表的长度。
    • 如果键不存在,将其视为空列表,并返回0。
    • 如果键存在但其值不是列表类型,返回错误。

(2)示例:

  • 获取列表的长度。假设有一个名为mylist的列表,其内容为[“a”, “b”, “c”]:
LLEN mylist

执行后,返回3,表示mylist的长度为3。

  • 键不存在:
LLEN nonexistentlist

执行后,返回0,因为nonexistentlist不存在,被视为空列表。

  • 键不是列表类型。假设有一个名为myset的集合,其内容为{“a”, “b”, “c”}(注意这是集合,不是列表):
LLEN myset

执行后,返回错误,因为myset的值不是列表类型。

(3)注意事项:

  1. LLEN命令的时间复杂度为O(1),因为Redis内部维护了列表的长度信息,所以可以在常数时间内返回列表的长度。
  2. LLEN命令不会改变列表的结构或内容,只是用于获取列表的长度信息。
  3. 在使用LLEN命令之前,建议确认键是否存在以及键对应的值是否为列表类型,以避免出现错误或意外的返回值。

3.3 阻塞版本命令

(1)blpop和brpop是lpop和rpop的阻塞版本,和对应非阻塞版本的作用基本一致,除了:

  • 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会理解返回nil,但阻塞版本会根据timeout,阻塞一段时间, 期间Redis可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态。
  • 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。
  • 如果多个客户端同时多一个键执行pop,则最先执行命令的客户端会得到弹出的元素。

3.3.1 BLPOP命令

(1)Redis的BLPOP命令是一个阻塞列表弹出原语,常用于实现消息队列的功能。以下是对BLPOP命令的详细解释:

BLPOP key1 [key2 ...] timeout
  • key1、key2等:一个或多个列表的键名。
  • timeout:等待时间,单位为秒。如果列表中没有元素可弹出,BLPOP命令会阻塞指定的时间,直到有元素可弹出或超时。
  • 返回值:
    • 如果列表中有元素,返回一个包含两个元素的列表:第一个元素是被弹出元素所属的键名(以字节串形式返回),第二个元素是被弹出元素的值(也以字节串形式返回)。
    • 如果在指定的时间内没有任何元素被弹出,则返回nil。

(2)工作原理:

  • 检查给定的键名是否存在,并且它们的值都是列表类型。
  • 从第一个键名对应的列表头部弹出一个元素。
  • 如果列表中没有元素可弹出,BLPOP命令会阻塞指定的时间,等待元素出现。
  • 如果在指定的时间内没有元素出现,BLPOP命令返回一个空值(即nil)。

(3)示例:

  • 假设有一个名为mylist的列表,其内容为空:
BLPOP mylist 10

执行后,客户端会阻塞10秒钟。如果在这10秒钟内mylist被添加了元素,比如通过LPUSH mylist “hello”,则BLPOP命令会立即返回:

1) "mylist"
2) "hello"

如果10秒钟内没有任何元素被添加到mylist,则BLPOP命令会返回nil。

(4)注意事项:

  1. BLPOP命令是阻塞命令,如果指定的列表中没有元素,客户端会一直等待直到有元素被添加或超时。
  2. BLPOP命令可以用于实现消息队列,生产者将消息添加到列表中,消费者通过BLPOP命令从列表中获取消息并处理。
  3. 如果提供了多个键名,BLPOP会依次检查这些键名对应的列表,直到找到第一个有元素的列表并弹出元素。如果所有列表都为空,则阻塞等待直到有元素被添加到某个列表中。
  4. BLPOP命令的时间复杂度为O(1),因为它只需要检查一个列表的头部元素并弹出它(如果存在的话)。然而,由于阻塞等待的时间可能很长,所以在实际应用中需要注意性能影响。

3.3.2 BRPOP命令

(1)Redis的BRPOP命令是一个阻塞列表弹出原语,它是RPOP命令的阻塞版本。以下是对BRPOP命令的详细解释:

BRPOP key [key ...] timeout
  • key:指定要操作的列表键,可以是一个或多个。
  • timeout:指定阻塞等待的最长时间,单位为秒。如果列表中没有元素可供弹出,BRPOP命令会阻塞指定的时间,直到有元素可弹出或超时。
  • 返回值
    • 如果在指定的时间内有元素被弹出,返回一个包含两个元素的列表:第一个元素是被弹出元素所属的键名(以字节串形式返回),第二个元素是被弹出元素的值(也以字节串形式返回)。
    • 如果在指定的时间内没有任何元素被弹出,则返回一个nil值和等待时长(以秒为单位的小数表示)。

(2)工作原理:

  • BRPOP命令会按照参数中键的顺序依次检查每个列表。
  • 如果某个列表中有元素,它会立即从该列表的尾部弹出一个元素,并返回该元素及其所属的键名。
  • 如果所有列表都没有元素,BRPOP命令会阻塞指定的时间,等待元素出现。
  • 如果在指定的时间内有元素被添加到某个列表中,BRPOP命令会立即返回该元素及其所属的键名。
  • 如果在指定的时间内没有元素被添加到任何列表中,BRPOP命令会返回一个nil值和等待时长。

(3)示例:

  • 假设有两个名为list1和list2的列表,它们的内容都为空:
BRPOP list1 list2 10
  • 执行后,客户端会阻塞10秒钟。如果在这10秒钟内list1或list2被添加了元素,比如通过RPUSH list1 “hello”,则BRPOP命令会立即返回:
1) "list1"
2) "hello"
  • 如果10秒钟内没有任何元素被添加到list1或list2,则BRPOP命令会返回:
(nil)
(10.xxs)  // 其中xx表示实际等待的小数秒数

(4)注意事项:

  1. BRPOP命令是阻塞命令,如果指定的列表中没有元素,客户端会一直等待直到有元素被添加或超时。
  2. BRPOP命令可以用于实现消息队列、任务调度和数据同步等高级功能。
  3. 如果提供了多个键名,BRPOP会依次检查这些键名对应的列表,直到找到第一个有元素的列表并弹出元素。如果所有列表都为空,则阻塞等待直到有元素被添加到某个列表中。
  4. BRPOP命令的时间复杂度为O(1),因为它只需要检查一个列表的尾部元素并弹出它(如果存在的话)。然而,由于阻塞等待的时间可能很长,所以在实际应用中需要注意性能影响。

3.4 命令小结

(1)有关列表的命令已经介绍完毕,下表是这些命令的作用和时间复杂度,开发人员可以参考:

3.5 内部编码

(1)列表类型的内部编码有两种:

  • ziplist (压缩列表) :当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的长度都小于list-max-ziplist-value配置(默认64字节)时,Redis 会选用ziplist来作为列表的内部编码实现来减少内存消耗。
  • linkedlist (链表) :当列表类型无法满足ziplist的条件时,Redis会使用linkedlist 作为列表的内部实现。

(2)当元素个数较少且没有大元素时,内部编码为ziplist:

127.0.0.1:6379> rpush listkey e1 e2 e3
OK
127.0.0.1:6379> object encoding listkey
"ziplist"

(3)当元素个数超过512时,内部编码为linkedlist:

127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"

(4)当某个元素的长度超过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"

3.6 使用场景

(1)消息队列:

如下图所示,Redis 可以使用lpush + brpop命令组合实现经典的阻塞式生产者消费者模型队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式地从队列中"争抢"队首元素。通过多个客户端来保证消费的负载均衡和高可用性。

(2)分频道的消息队列:

如下图所示,Redis 同样使用lpush + brpop命令,但通过不同的键模拟频道的概念,不同的消费者可以通过brpop不同的键值,实现订阅不同频道的理念。

(3)微博Timeline:

每个用户都有属于自己的Timeline (微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引|范围获取元素。

  • 每篇微博使用哈希结构存储,例如微博中3个属性: title、 timestamp、 content:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
  • 向用户Timeline添加微博,user::mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
  • 分页获取用户的Timeline,例如获取用户1的前10篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
    hgetall key
}

此方案在实际中可能存在两个问题:

  1. 1 + n问题。即如果每次分页获取的微博个数较多,需要执行多次hgetall操作,此时可以考虑使用pipeline (流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用mget获取。
  2. 分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。

选择列表类型时,请参考:

  • 同侧存取(lpush + lpop或者rpush + rpop)为栈。
  • 异侧存取(Ipush + rpop或者rpush + lpop)为队列。

剩下的两种数据类型见博客https://blog.csdn.net/m0_65558082/article/details/144267784?spm=1001.2014.3001.5502。


网站公告

今日签到

点亮在社区的每一天
去签到