Redis 数据类型之字符串,列表,集合,有序集合

作者:

在

Redis 支持五种基本数据类型,每种数据类型都有其特定的应用场景和操作命令。理解这些数据类型的特点和使用方法是掌握Redis的关键。

==字符串== (String)

字符串的特点和应用

字符串是Redis 最基本的数据类型,也是最常用的类型。在 Redis 中,字符串是二进制安全的,可以存储任何数据,包括数字、文本、序列化对象、甚至是图片。

主要特点:

  • 二进制安全:可以存储任何二进制数据
  • 最大长度:单个字符串最大 512MBR
  • 编码优化:Redis 会根据内容自动选择最优编码
  • 原子操作:所有字符串操作都是原子性的

常见应用场景:

  • 缓存数据(JSON、XML、HTML 等)
  • 计数器(访问量、点赞数等)
  • 分布式锁
  • 会话存储
  • 配置信息存储

基本操作命令

设置和获取

# 设置键值
# 用法:SET key value
SET name "张三"
SET age 25

# 获取值
# 用法:GET key
GET name        # 返回 "张三"
GET age         # 返回 "25"
GET nonexistent # 返回 (nil)

# 批量设置
# 用法:MSET key1 value1 key2 value2 key3 value3
MSET user:1:name "张三" user:1:age 25 user:1:city "北京"

# 批量获取
# 用法:MGET key1 key2 key3
MGET key1 key2 key3
MGET user:1:name user:1:age user:1:city

# 设置并返回旧值
# 用法:GETSET key newvalue
GETSET key newvalue
GETSET counter 100  # 返回旧值,设置新值为100

设置和获取 (SET / GET)

这是 Redis 最基础的读写操作。

  • SET key value
    • 作用:设置一个键(key)的值(value)。如果 key 已存在,会覆盖旧值。
    • 图片示例:
      • SET name "张三":存入一个键 name,值为字符串 "张三"。
      • SET age 25:存入一个键 age,值为 25。
      • 💡 注意:在 Redis中,数字也是以字符串形式存储的。
  • GET key
    • 作用:获取指定键的值。如果键不存在,返回 nil(空)。
    • 图片示例:
      • GET name:返回 "张三"。
      • GET age:返回 "25"。
      • GET nonexistent:因为不存在这个键,所以返回 (nil)。

批量设置 (MSET – Multi SET)

当你需要同时设置多个键值对时,用这个命令可以减少网络请求次数,提高效率。

  • MSET key1 value1 key2 value2 ...
    • 作用:同时设置一个或多个 key-value 对。
    • 图片示例:
      • MSET user:1:name "张三" user:1:age 25 user:1:city "北京"
      • 这里使用了冒号 : 作为命名空间分隔符。这是一种很好的编程习惯,它实际上存了三个键:user:1:name、user:1:age、user:1:city,看起来就像是一个用户的属性集合。

批量获取 (MGET – Multi GET)

与 MSET 对应,用于一次性获取多个键的值。

  • MGET key1 key2 key3 ...
    • 作用:同时获取一个或多个 key 的值。返回一个列表,顺序与请求的 key 顺序一致。如果某个 key 不存在,对应位置返回 nil。
    • 图片示例:
      • MGET user:1:name user:1:age user:1:city
      • 这会一次性把刚才存进去的三个值全部取出来,返回结果类似:1) "张三" 2) "25" 3) "北京"。

设置并返回旧值 (GETSET)

这是一个结合了“修改”和“查询”的原子操作。

  • GETSET key newvalue
    • 作用:将给定 key 的值设为 newvalue,并返回 key 的旧值。
    • 图片示例:
      • GETSET counter 100:把 counter 的值设为 100,同时返回它之前的值(如果有的话,比如返回 "50")。
      • 💡 这个命令在需要重置计数器(比如每天清零)并获取昨日统计数据时非常有用。
      • ⚠️ 注意:在较新的 Redis版本中,官方更推荐使用 SET key value GET 来代替 GETSET,因为功能更强大且更通用。

image-20261019160949566

image-20261019161349073


条件设置

# 仅当键不存在时设置
# 用法:SETNX key value
SETNX lock:resource "server1"  # 分布式锁应用

# 设置键值和过期时间
# 用法:SETEX key seconds value
SETEX session:abc123 3600 "user_data"  # 3600秒后过期

# 仅当键不存在时设置,并指定过期时间
# 用法:SET key value EX seconds NX
SET lock:order:1001 "processing" EX 30 NX

# 仅当键存在时设置
# 用法:SET key value XX
SET existing_key "new_value" XX

image-20261019170652915


RedisString 类型高级应用:锁、会话与条件更新

1. 核心命令演进

命令语法 核心作用 原子性 缺陷/适用场景 推荐指数
SETNX key value 仅当 key 不存在时设置(互斥锁雏形) 是 极易死锁。若客户端宕机未释放,锁永远存在。 ⭐ (已淘汰,仅供了解)
SETEX key sec value 设置值并指定过期时间(会话管理) 是 无法做到“不存在才设置”。适用于覆盖写入、会话存储。 ⭐⭐⭐
SET key val EX sec NX 不存在才设置,且带过期时间 是 现代分布式锁的终极标准方案,完美解决死锁。 ⭐⭐⭐⭐⭐
SET key val XX 仅当 key 存在时更新(条件更新) 是 防止无中生有,常用于更新已存在的键,防止脏数据。 ⭐⭐⭐⭐

2. 核心命令深度剖析

1. 分布式锁的终极方案:SET ... EX ... NX

  • 代码示例:SET lock:order:1001 "server_1" EX 30 NX
  • 专业解析:
    • NX (Not eXists):保证互斥性,只有一个客户端能抢到锁。
    • EX 30:保证防死锁。即使客户端 A 抢到锁后突然崩溃,30秒后锁自动释放,不会阻塞系统。
    • 原子性:早期使用 SETNX + EXPIRE 是两步操作,如果在这两步之间发生宕机,依然会死锁。而 SET 配合 EX 和 NX 参数,是 Redis 底层的一条单线程原子命令,绝对安全。

2. 会话管理的神器:SETEX

  • 代码示例:SETEX session:abc123 3600 "user_data"
  • 专业解析:Redis 常被用作分布式 Session 缓存。SETEX 强制要求设置过期时间,避免了因忘记设置过期时间导致 Redis 内存被无用的 Session 撑爆。

3. 条件更新的守卫:SET ... XX

  • 代码示例:SET existing_key "new_value" XX
  • 专业解析:XX (eXists) 保证了只有键存在时才更新。在并发场景下,如果多个线程试图修改同一数据,XX 可以防止“预期修改现有数据,结果却创建了新键”的意外情况。

==3==.Redis分布式锁(面试题)

虽然 SET key value EX 30 NX 解决了死锁和互斥问题,但在极端的高并发生产环境下,依然存在隐患,这也是大厂面试最爱问的:

⚠️ 风险:误删别人的锁

  • 场景:客户端 A 抢到锁并设置 30 秒过期。但 A 的业务逻辑执行了 35 秒。第 31 秒时,锁自动过期,客户端 B 抢到了锁。第 35 秒,A 执行完毕,直接执行了 DEL lock,结果把 B 的锁给删了。
  • 解决方案(Lua 脚本):解锁时,必须先 GET 判断当前锁的 value 是不是自己的(比如判断是不是 “server_1″),如果是,再 DEL。必须用Lua 脚本执行,以保证判断和删除的原子性。

lua

-- 安全的解锁 Lua 脚本逻辑
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
  • (注:如果要做到绝对完美,还需要解决“锁过期了但业务没执行完”的问题,即引入 Redisson框架的看门狗(Watchdog)机制,实现自动续期。)

1. 核心概念与基石

  • 为什么需要? 单机环境用 synchronized 即可。但在分布式集群/微服务架构下,多进程需访问同一共享资源(如扣减库存),本地锁失效,必须引入跨进程的全局互斥锁。
  • 为什么选 Redis? 基于内存、性能极高;单线程模型天然具备操作的原子性。

2、 流程与四大痛点(面试重点)

1. 阶段一:基础互斥(解决并发问题)

  • 命令:SETNX lock_key value (Set if NoteXists)
  • 逻辑:返回 1 代表抢锁成功,0 代表失败;业务结束用 DEL 释放。
  • 致命缺陷:死锁。若客户端抢锁后崩溃,未执行 DEL,锁永远存在。

2. 阶段二:防死锁(解决过期问题)

  • 错误做法:SETNX 成功后,再单独执行 EXPIRE 设置过期时间。这俩不是原子操作,中间宕机依然死锁。
  • 正确做法:使用一条原子命令 SET lock_key value EX 30 NX。一步到位实现“不存在才设置”且“自动过期”,彻底解决死锁。

3. 阶段三:防误删(解决原子性与越界问题)

  • 新隐患:业务执行时间 > 锁过期时间。客户端 A 没执行完锁就过期,B 抢到了锁;A 执行完后顺手 DEL,把 B 的锁删了。
  • 解决方案:
    1. 唯一标识:value 存客户端唯一 ID(如 UUID)。
    2. Lua 脚本:释放锁时,用Lua 脚本(保证原子性)判断:若 value 等于自己的 ID,才执行 DEL。

4. 阶段四:自动续期(终极救星:Redisson 看门狗)

  • 新隐患:如果预估不好过期时间,设短了业务没做完锁就没了;设长了客户端宕机则长时间死锁。
  • 终极方案:引入 Redisson 框架的 Watchdog(看门狗)机制。
    • 机制:不指定过期时间时(默认 30s),后台起定时任务,每 10s 检查当前线程是否存活。若存活,自动将锁重置为 30s(自动续期);若宕机,停止续期,30s 后释放。

==🐶==终极解法:Redisson 的看门狗机制 (Watchdog)

为了解决“过期时间设长了容易死锁,设短了业务没执行完”这个矛盾,Java 生态中最强大的Redis客户端——Redisson,引入了看门狗机制。

1. 它是怎么工作的?

  • 不写死过期时间:当你使用Redisson加锁时(例如 lock.lock()),如果不手动指定时间,它默认的过期时间是 30 秒。
  • 后台定时任务(看门狗):一旦加锁成功,Redisson会在后台启动一个定时任务。
  • 自动续期:这个“狗”每隔 10 秒(也就是过期时间的 1/3)就会去检查一下:客户端 A 还在执行吗?如果是,它就会给锁重新设置为 30 秒。相当于无限续杯。
  • 停止续期:一旦客户端 A 的业务执行完毕,主动调用 unlock(),或者客户端 A 真的直接宕机了(那么“狗”也跟着死了,无法续期),锁就会在 30 秒后真正过期释放。

2. 核心逻辑图解

text

客户端 A 抢到锁 (过期时间 30s)
  |
  +---> 启动看门狗
  |
  +---> 执行业务 (耗时 40s)
          |
          +--- 第 10s:看门狗检查 -> A 还在运行 -> 续期到 30s
          +--- 第 20s:看门狗检查 -> A 还在运行 -> 续期到 30s
          +--- 第 30s:看门狗检查 -> A 还在运行 -> 续期到 30s
          +--- 第 40s:业务执行完毕,主动解锁。
  |
  +---> 看门狗停止。

💡 面试总结(必背)

“Redis 分布式锁的演进路径是:SETNX 实现基础互斥,SET NX EX 解决死锁,Lua 脚本 + 唯一 ID 解决误删,Redisson 看门狗 解决业务超时自动续期。 在工程落地中,Java 体系首选引入Redisson 框架。”


字符串操作

# 追加字符串
# 用法:APPEND key value
SET message "Hello"
APPEND message " World"  # message 现在是 "Hello World"

# 获取字符串长度
# 用法:STRLEN key
STRLEN message  # 返回 11

# 获取子字符串
# 用法:GETRANGE key start end
GETRANGE message 0 4   # 返回 "Hello"
GETRANGE message -5 -1 # 返回 "World"

# 设置子字符串
# 用法:SETRANGE key offset value
SETRANGE message 6 "Redis"  # message 现在是 "Hello Redis"

追加字符串 (append)

SET message "Hello"
APPEND message " World
  • SET message "Hello":将键 message 的值设置为字符串 "Hello"。
  • APPEND message " World":将值 " World" 追加到键 message 现有值的末尾。
  • 执行结果:message 的值变为 "Hello World"。该命令返回追加后字符串的总长度(即 11)。

获取字符串长度 (strlen)

STRLEN message
  • STRLEN message:获取键 message 所对应字符串的字节长度。
  • 执行结果:返回 11(对应 “Hello World” 11个英文字符)。
  • 注:由于Redis 采用 UTF-8 编码,如果值是中文(如“张三”),返回的长度将是 6(每个中文字符占 3 个字节)。

获取子字符串 (getrange)

GETRANGE message 0 4
GETRANGE message -5 -1
  • GETRANGE message 0 4:截取键 message 的值,从索引 0 开始,到索引 4 结束(包含索引 4)。
  • 执行结果:返回 "Hello"。
  • GETRANGE message -5 -1:使用负数索引截取。-1 代表最后一个字符,-5 代表倒数第 5 个字符。
  • 执行结果:返回 "World"。

设置子字符串 (setrange)

SETRANGE message 6 "Redis"
  • SETRANGE message 6 "Redis":从索引 6(即第 7 个字符)开始,用字符串 "Redis" 覆盖键 message 原有的字符。
  • 执行结果:原值 "Hello World" 的索引 6 到 10 的 "World" 被替换,message 的值变为 "Hello Redis"。该命令返回修改后字符串的总长度。

image-20261019184213209

数值操作

Redis 可以将字符串当作数字进行操作,支持整数和浮点数。

# 整数操作
SET counter 10

# 增加指定值
INCR counter        # counter = 11
INCRBY counter 5    # counter = 16
DECR counter        # counter = 15
DECRBY counter 3    # counter = 12

# 浮点数操作
SET price 19.99
INCRBYFLOAT price 5.01  # price = 25.00
INCRBYFLOAT price -2.50 # price = 22.50

# 应用示例:网站访问计数
INCR page:home:views               #  通常用于统计首页的访问量(PV)
INCR user:1001:login_count         #  
INCRBY article:123:likes 1

image-20261019185624330


==列表== (List)

列表的特点和应用

Redis列表是简单的字符串列表,按照插入顺序排序。可以添加元素到列表的头部或尾部。

主要特点:

  • 有序性:元素按插入顺序排列
  • 可重复:允许重复元素
  • 双端操作:支持头部和尾部操作
  • 最大长度:理论上可包含 2^32 – 1 个元素

常见应用场景:

  • 消息队列
  • 最新消息列表
  • 用户操作历史
  • 任务队列
  • 时间线功能

基本操作命令

Redis四大核心字母缩写

1. NX = Not eXists(不存在)

  • 中文含义:仅当键不存在时才执行操作(无中生有)。
  • 常见命令:SETNX、SET key value NX
  • 核心场景:抢锁、防覆盖。只有当前没人占用这个坑(键不存在),我才能把名字写进去。
  • 反向兄弟:XX (eXists),仅当键存在时才执行(锦上添花,常用于更新数据防止脏插)。

2. EX = EXpire/ EXpires(过期)

  • 中文含义:设置键的过期时间(单位:秒)。
  • 常见命令:SETEX、SET key value EX 30
  • 核心场景:防死锁、存会话。给数据加一个“定时炸弹”,时间一到,Redis自动把数据删掉。

3. L / R = Left / Right (左 / 右)

  • 中文含义:代表操作的方向(L 代表从左边/头部,R 代表从右边/尾部)。
  • 常见命令:
    • LPUSH / RPUSH(从左/右塞入)
    • LPOP / RPOP(从左/右弹出)
  • 核心场景:队列/栈操作。
    • 用 LPUSH 进,RPOP 出,就是先进先出(FIFO,消息队列)。
    • 用 LPUSH 进,LPOP 出,就是后进先出(LIFO,栈)。
  • (注:如果命令里没有 R,只有单独的 L,比如 LRANGE,这里的 L 就是纯指 List 数据类型)。

4. B = Block(阻塞)

  • 中文含义:==阻塞等待。命令加上 B,就意味着如果拿不到数据,程序不会立刻返回空,而是原地“死等”,直到有数据来或超时==。
  • 常见命令:BLPOP、BRPOP、BRPOPLPUSH
  • 核心场景:==高可用消息队列。生产者没发消息时,消费者就挂起(不占用 CPU 疯狂死循环),一旦有消息,瞬间唤醒并拿走==。

添加元素

# 从左侧(头部)添加
# 用法:LPUSH key element [element ...]
LPUSH messages "消息3" "消息2" "消息1"
# 结果:["消息1", "消息2", "消息3"]

# 从右侧(尾部)添加
# 用法:RPUSH key element [element ...]
RPUSH messages "消息4" "消息5"
# 结果:["消息1", "消息2", "消息3", "消息4", "消息5"]

# 仅当列表存在时添加
# 用法:LPUSHX key element
# 用法:RPUSHX key element
LPUSHX messages "消息6"
RPUSHX messages "消息7"

从左侧(头部)添加:LPUSH

# 用法: LPUSH key element [element ...]
LPUSH messages "消息3" "消息2" "消息1"
  • 原理:L 代表 Left。Redis会按你书写的顺序,将元素依次压入列表的最左侧。
  • 执行过程:先压入“消息3”,再把“消息2”压到最前面,最后把“消息1”压到最前面。
  • 返回结果:["消息1", "消息2", "消息3"] (注意顺序是反过来的)。

从右侧(尾部)添加:RPUSH

# 用法: RPUSH key element [element ...]
RPUSH messages "消息4" "消息5"
  • 原理:R 代表 Right。将元素依次追加到列表的最右侧(尾部)。
  • 执行结果:在原有列表基础上追加,最终变为 ["消息1", "消息2", "消息3", "消息4", "消息5"]。

仅当列表存在时添加:LPUSHX / RPUSHX

# 用法: LPUSHX key element
# 用法: RPUSHX key element
LPUSHX messages "消息6"
RPUSHX messages "消息7"
  • 原理:带有 X(eXists)后缀。只有当 messages 这个键已经存在时,才会执行添加。
  • 特性:如果前面没有创建 messages 列表,这两条命令直接返回 0 且什么都不做。防止因为拼写错误生成无用的空列表。image-20261019200501733

获取元素

# 获取指定范围的元素
# 用法:LRANGE key start stop
LRANGE messages 0 -1    # 获取所有元素
LRANGE messages 0 2     # 获取前3个元素
LRANGE messages -3 -1   # 获取最后3个元素

image-20261019200810638

删除元素

# 从左侧(头部)删除
# 用法:LPOP key
LPOP messages           # 弹出并返回第一个元素

# 从右侧(尾部)删除
# 用法:RPOP key
RPOP messages           # 弹出并返回最后一个元素

# 在指定元素前/后插入
# 用法:LINSERT key BEFORE|AFTER pivot element
LINSERT messages BEFORE "消息3" "消息2.5"

从左侧(头部)删除:LPOP

# 用法: LPOP key
LPOP messages
  • 详细解释:L 代表 Left。这个命令会将列表最左边(头部)的第一个元素移除,并将这个被移除的元素返回给你。

从右侧(尾部)删除:RPOP

# 用法: RPOP key
RPOP messages
  • 详细解释:R 代表 Right。这个命令会将列表最右边(尾部)的最后一个元素移除,并返回给你。

在指定元素前/后插入

# 在指定元素前/后插入
# 用法: LINSERT key BEFORE|AFTER pivot element
LINSERT messages BEFORE "消息3" "消息2.5"
  • 注意:LINSERT 其实是插入命令
  • 详细解释:它的作用是在列表中找到指定的基准元素(pivot),然后在它的前面(BEFORE)或后面(AFTER)插入新元素。
  • 举例:假设列表现在是 ["A", "B", "C"]。
    • 执行 LINSERT messages BEFORE "B" "X"。
    • 结果会变成 ["A", "X", "B", "C"]。
  • 特别注意:如果基准元素(比如图里的 “消息3″)在列表里找不到,这个命令会返回 -1,代表插入失败。

image-20261019202146535


获取元素

# 获取指定范围的元素
# 用法:LRANGE key start stop
LRANGE messages 0 -1    # 获取所有元素
LRANGE messages 0 2     # 获取前3个元素
LRANGE messages -3 -1   # 获取最后3个元素

# 获取指定索引的元素
# 用法:LINDEX key index
LINDEX messages 0       # 获取第一个元素
LINDEX messages -1      # 获取最后一个元素

# 获取列表长度
# 用法:LLEN key
LLEN messages           # 返回列表长度

获取指定索引的元素:LINDEX

如果你只需要列表里的某一个特定位置的元素,用这个命令性能更高。

  • LINDEX messages 0
    • 解释:获取列表的第一个元素(最左边)。
  • LINDEX messages -1
    • 解释:获取列表的最后一个元素(最右边)。
  • ⚠️ 避坑提示:如果索引超出了列表长度(比如列表只有 3 个元素,你查索引 5),它不会报错,而是返回 (nil)(空)。

获取列表长度:LLEN

  • LLEN messages
    • 解释:返回当前列表中包含的元素总个数(Length)。如果键不存在,返回 0。

image-20261019203006492

删除元素

# 从左侧(头部)弹出
# 用法:LPOP key
LPOP messages           # 弹出并返回第一个元素

# 从右侧(尾部)弹出
# 用法:RPOP key
RPOP messages           # 弹出并返回最后一个元素

# 删除指定值的元素
# 用法:LREM key count element
LREM messages 1 "消息2"   # 从头开始删除1个"消息2"
LREM messages -1 "消息2"  # 从尾开始删除1个"消息2"
LREM messages 0 "消息2"   # 删除所有"消息2"

# 保留指定范围的元素
# 用法:LTRIM key start stop
LTRIM messages 0 99     # 只保留前100个元素

精准删除指定值(lrem 重点!)

LREM` 是 List 中最灵活的删除命令,它的核心在于中间的 `count` 参数:
**语法**:`LREM key count element
  • LREM messages 1 "消息2"
    • 解释:count 为正数 1。表示从左往右(从头开始)找,删除1个值为 "消息2" 的元素。
  • LREM messages -1 "消息2"
    • 解释:count 为负数 -1。表示从右往左(从尾开始)找,删除1个值为 "消息2" 的元素。
  • LREM messages 0 "消息2"
    • 解释:count 为 0。表示无视方向,删除列表中所有值为 "消息2" 的元素。
    • 💡 重点考点:LREM 的返回值是实际被删除的元素个数。
# 1. 先创建一个包含多个 "A" 的列表
RPUSH test "A" "B" "A" "C" "A"
# 此时列表结构为: ["A", "B", "A", "C", "A"]

# 2. 使用 count = 2 删除
LREM test 2 "A"
  • 原理:Redis从左边开始找,遇到了第 1 个 “A”(删掉),继续找,遇到了第 2 个 “A”(删掉),此时已经达到了 count = 2 的限制,命令立刻停止。
  • LREM key -2 "元素" 的核心意思就是:从右边(尾部)开始往左边找,删掉最近遇到的 2 个指定元素。

截断列表(LTRIM )

这个命令不会按“值”删,而是按“范围”删,只保留指定区间内的元素,区间外的全删掉。

  • LTRIM messages 0 99
    • 解释:只保留索引 0 到 99 的元素(即前 100 个元素),第 101 个及以后的元素全部被丢弃。
    • 💡 场景极其常见:用于实现“最新 N 条记录”。比如你有一个保存用户操作日志的列表,为了防内存溢出,每次 LPUSH 插入新日志后,顺便执行一下 LTRIM key 0 99,这样就永远只保留最新的 100 条日志。

image-20261019205228356

修改元素

# 设置指定索引的元素值
# 用法:LSET key index element
LSET messages 0 "新消息1"

重新设置索引值:LSET

  • 语法:LSET key index element
  • 代码示例:LSET messages 0 "新消息1"
  • 详细解释:
    • 这个命令的作用是:将列表 messages 中,索引为 0(即第一个元素)的值,强制替换为 "新消息1"。
    • 它不会改变列表的长度,==只是把那个位置上的旧值给覆盖掉了==(类似于数组的赋值操作)。
    • 如果修改成功,Redis 会返回一个简单的字符串 OK。

image-20261019205710586


==阻塞操作==

阻塞操作是 Redis 列表的重要特性,常用于实现消息队列。

# 阻塞式左侧弹出
# 用法:BLPOP key [key ...] timeout
BLPOP task_queue 30     # 等待30秒,如果有元素则弹出

# 阻塞式右侧弹出
# 用法:BRPOP key [key ...] timeout
BRPOP task_queue 0      # 无限等待

# 阻塞式右侧弹出并左侧推入
# 用法:BRPOPLPUSH source destination timeout
BRPOPLPUSH pending_tasks processing_tasks 60

阻塞式左侧弹出:BLPOP

  • 语法:BLPOP key [key ...] timeout
  • 示例:BLPOP task_queue 30
  • 解释:
    • 客户端向 task_queue 列表请求数据。如果列表里有数据,立刻弹出一个(跟 LPOP 一样)。
    • 重点:如果列表是空的,客户端会原地阻塞等待(也就是卡在这里)。等待最长 30 秒。如果在 30 秒内,有另一个客户端往 task_queue 里 RPUSH 了一个新任务,这个阻塞的客户端就会瞬间被唤醒,拿到数据并返回。
    • 如果超过 30 秒依然没数据,返回 (nil)。

阻塞式右侧弹出:BRPOP

  • 语法:BRPOP key [key ...] timeout
  • 示例:BRPOP task_queue 0
  • 解释:
    • 逻辑和 BLPOP 完全一样,只是从右侧(尾部)弹出。
    • 核心重点:timeout 的值如果是 0,代表无限等待(永不超时)。除非有数据进来,或者网络断开/客户端主动断开,否则它会一直死死卡在这里等。(这在实战中常用来做常驻后台的消费者进程)。

高级进阶:==BRPOPLPUSH==(安全队列)

  • 语法:BRPOPLPUSH source destination timeout

  • 示例:BRPOPLPUSH pending_tasks processing_tasks 60

  • 解释:这个命令极其重要,它是“可靠消息队列”的基石。

    • 它的动作是:阻塞等待 pending_tasks(源列表),一旦有数据,把它从右边弹出,同时立刻从左边推入 processing_tasks(目标列表)。
    • 为什么牛? 解决“消息丢失”问题。假如你只用 BRPOP 拿到了任务,结果程序突然崩溃,这个任务就彻底丢了。用了 BRPOPLPUSH,任务会被备份到 processing_tasks 里。等你处理完了,再把它删掉。如果崩了,重启后还可以从备份列表里捞出来重试。
    • (注:Redis 6.2 之后,官方推出了更强大的 BLMOVE 命令取代了它,但底层逻辑一致)。

示例图理解阻塞

🎨 图 1:BLPOP(阻塞式左侧弹出)

场景:后台消费者死死盯着左边,来一个拿一个。

[ 初始状态 ] 列表为空,消费者开启阻塞监听:
Queue: [ 空 ]
客户端: BLPOP task_queue 30  ---> (💤 原地挂起,最多等30秒)

[ 3秒后 ] 生产者突然塞入新任务(RPUSH):
Queue: [ 任务A ]
                ^
                |
客户端: (🔔 瞬间唤醒!) ---> 拿到 "任务A",命令结束返回。

[ 最终 ] 任务被取走:
Queue: [ 空 ]
  • 核心机制:如果左边有数据立刻拿;没数据就原地干等,直到超时或来数据。

🎨 图 2:BRPOP(阻塞式右侧弹出 + timeout 0)

场景:常驻消费者无限等待,典型的 FIFO(先进先出)队列模式。

text

[ 初始状态 ] 消费者开启无限等待(0 代表死等):
Queue: [ 空 ]
客户端: BRPOP task_queue 0  ---> (🔒 彻底挂起,永不超时)

[ N小时后 ] 生产者用 LPUSH 从左边塞入:
Queue: [ 任务1, 任务2, 任务3 ]
                             ^
                             |
客户端: (🔔 从右侧弹出!) ---> 拿到 "任务3"。

[ 最终 ] 按照先进先出的顺序,任务3最后进,最先被拿走。
Queue: [ 任务1, 任务2 ]
  • 核心机制:timeout = 0 相当于把消费者进程“焊死”在这里,除非服务器宕机或网络断开,否则绝对不会返回 nil。

==🎨 图 3==:BRPOPLPUSH(安全转移:从右弹,推入左)

场景:==可靠队列。取任务的同时,把它备份到另一个列表里,防丢==。

text

[ 初始状态 ] 
待处理列表: pending_tasks   = [ 任务A, 任务B ]
处理中列表: processing_tasks = [ 空 ]

[ 执行命令 ]
客户端: BRPOPLPUSH pending_tasks processing_tasks 60

[ 一瞬间的原子操作 ] (任务从右出,立刻推入左边)
1. 从 pending_tasks 右侧弹出 "任务B"。
2. 立刻将 "任务B" 推入 processing_tasks 左侧。

[ 执行后结果 ]
待处理列表: pending_tasks   = [ 任务A ]       <-- 任务B没了
处理中列表: processing_tasks = [ 任务B ]       <-- 任务B安全备份在这里!
  • 核心机制:如果客户端拿到“任务B”后,还没处理完就突然断电了,没关系!重启后,系统可以去 processing_tasks 里查,发现里面还有“任务B”,重新拿出来执行即可。这就是消息不丢失的基石。

列表的应用场景

消息队列实现

# 生产者:添加任务到队列
LPUSH task_queue "发送邮件:user@example.com"
LPUSH task_queue "生成报表:monthly_report"
LPUSH task_queue "数据备份:database_backup"

# 消费者:从队列获取任务
BRPOP task_queue 30
# 返回:1) "task_queue" 2) "发送邮件:user@example.com"

最新消息列表

# 添加新消息(保持最新100条)
LPUSH latest_news "新闻标题1"
LTRIM latest_news 0 99

# 获取最新10条消息
LRANGE latest_news 0 9      #   数据太多了保留部分消息

用户操作历史

# 记录用户操作
LPUSH user:1001:actions "登录:2024-01-15 10:30:00"
LPUSH user:1001:actions "查看商品:product_123"
LPUSH user:1001:actions "加入购物车:product_123"

# 获取最近操作历史
LRANGE user:1001:actions 0 9     

image-20261019214616395

image-20261019214825482


集合 (Set)

集合的特点和应用

Redis 集合是字符串的无序集合,集合中的元素是唯一的。

主要特点:

  • 唯一性:不允许重复元素
  • 无序性:元素没有固定顺序
  • 快速查找:O(1) 时间复杂度判断元素是否存在
  • 集合运算:支持交集、并集、差集运算

常见应用场景:

  • 标签系统
  • 好友关系
  • 权限管理
  • 去重统计
  • 抽奖系统

基本操作命令

添加和删除元素

# 添加元素
# 用法:SADD key member [member ...]
SADD tags "Redis" "数据库" "缓存" "NoSQL"
SADD user:1001:skills "Java" "Python" "Redis"

# 删除元素
# 用法:SREM key member [member ...]
SREM tags "缓存"
SREM user:1001:skills "Java"

# 随机删除并返回元素
# 用法:SPOP key [count]
SPOP tags           # 随机删除一个元素
SPOP tags 2         # 随机删除两个元素

添加元素:SADD

  • 语法:SADD key member [member ...]
  • 代码示例 1:SADD tags "Redis" "数据库" "缓存" "NoSQL"
    • 解释:向名为 tags 的集合中添加 4 个元素。如果其中某个元素(比如 “Redis”)已经存在,Redis 会自动忽略它,不会报错。
  • 代码示例 2:SADD user:1001:skills "Java" "Python" "Redis"
    • 解释:向名为 user:1001:skills 的集合中添加用户的技能标签。
    • 返回值:命令返回的是实际成功添加的元素个数(比如返回 4,说明 4 个都是新的;如果返回 2,说明有 2 个之前就已经在集合里了)。

删除元素:SREM

  • 语法:SREM key member [member ...]
  • 代码示例:
    • SREM tags "缓存":从 tags 集合中精准删除 "缓存" 这个元素。
    • SREM user:1001:skills "Java":从技能集合中移除 "Java"。
    • 返回值:返回实际被成功删除的元素个数(如果删一个不存在的元素,返回 0)。

随机删除并返回:SPOP

这是 Set 最有趣、实战中最常用的命令之一,天然适合做“抽奖”或“随机分配”。

  • 语法:SPOP key [count]
  • 代码示例:
    • SPOP tags:从 tags 集合中随机挑 1 个元素,把它从集合里删掉,并把这个元素返回给你。
    • SPOP tags 2:从 tags 集合中随机挑 2 个元素,删掉并返回(返回的是一个列表)。
    • ⚠️ 避坑提示:如果你填的 count 大于集合里现有的元素总数(比如集合里只有 3 个,你填 SPOP tags 5),Redis 不会报错,它会把剩下的 3 个全弹出来给你,然后集合就空了。

查询操作

# 获取所有元素
# 用法:SMEMBERS key
SMEMBERS tags

# 判断元素是否存在
# 用法:SISMEMBER key member
SISMEMBER tags "Redis"      # 返回 1(存在)
SISMEMBER tags "MySQL"      # 返回 0(不存在)

# 获取集合大小
# 用法:SCARD key
SCARD tags                  # 返回集合元素数量

# 随机获取元素(不删除)
# 用法:SRANDMEMBER key [count]
SRANDMEMBER tags            # 随机返回一个元素
SRANDMEMBER tags 3          # 随机返回三个元素

获取所有元素:SMEMBERS

  • 语法:SMEMBERS key
  • 代码示例:SMEMBERS tags
  • 详细解释:
    • 返回集合 tags 中包含的所有元素。
    • ⚠️ 极客避坑:Set 是无序的,所以返回结果的顺序和你插入时的顺序完全不一样,这是正常现象。
    • ⚠️ 性能警告:如果这个集合里有几十万个元素,执行这个命令会直接把 Redis 阻塞。生产环境严禁对大集合使用 SMEMBERS,如果非要遍历,请改用 SSCAN 命令分批次查。

判断元素是否存在:SISMEMBER

  • 语法:SISMEMBER key member
  • 代码示例:
    • SISMEMBER tags "Redis" -> 返回 1 (代表存在/True)
    • SISMEMBER tags "MySQL" -> 返回 0 (代表不存在/False)
  • 详细解释:
    • 判断某个特定的元素是否在集合中。
    • 核心考点:这个命令的时间复杂度是 O(1)。底层是哈希表,查得极快。应用场景:判断“该用户是否点过赞”、“该IP是否在黑名单里”。

获取集合大小:SCARD

  • 语法:SCARD key
  • 代码示例:SCARD tags
  • 详细解释:
    • 返回集合中元素的个数(Cardinality 基数)。
    • 时间复杂度也是 O(1)(底层自带计数器),哪怕集合里有 100 万个元素,也是瞬间返回。
    • 应用场景:统计网站当天的独立访客数(UV)。

随机获取元素(不删除):SRANDMEMBER

  • 语法:SRANDMEMBER key [count]
  • 代码示例:
    • SRANDMEMBER tags -> 随机返回 1 个元素。
    • SRANDMEMBER tags 3 -> 随机返回 3 个元素。
  • 详细解释:
    • 注意和上一张图里的 SPOP 区分:SRANDMEMBER 只是随机“看一眼”,元素依然留在集合里,不会被删除。
    • 进阶注意:如果你填的 count 是正数(如 3),它保证返回的 3 个元素绝对不重复;如果你填的是负数(如 -3),它返回的 3 个元素可能会有重复。

移动元素

# 将元素从一个集合移动到另一个集合
# 用法:SMOVE source destination member
SMOVE user:1001:skills user:1002:skills "Python"

移动元素:SMOVE

  • 语法:SMOVE source destination member
  • 代码示例:SMOVE user:1001:skills user:1002:skills "Python"
  • 详细解释:
    • source(源集合):user:1001:skills
    • destination(目标集合):user:1002:skills
    • member(要移动的元素):"Python"
    • 执行动作:把 "Python" 这个元素从用户 1001 的技能集合中删掉,同时添加到用户 1002 的技能集合中。

集合运算

Redis支持集合间的数学运算,这是集合类型的强大特性。

# 准备测试数据
SADD set1 "a" "b" "c" "d"
SADD set2 "c" "d" "e" "f"
SADD set3 "d" "e" "f" "g"

# 交集运算
# 用法:SINTER key [key ...]
SINTER set1 set2            # 返回 {"c", "d"}
SINTER set1 set2 set3       # 返回 {"d"}

# 并集运算
# 用法:SUNION key [key ...]
SUNION set1 set2            # 返回 {"a", "b", "c", "d", "e", "f"}

# 差集运算
# 用法:SDIFF key [key ...]
SDIFF set1 set2             # 返回 {"a", "b"}
SDIFF set2 set1             # 返回 {"e", "f"}

# 将运算结果存储到新集合
# 用法:[SINTERSTORE | SUNIONSTORE | SDIFFSTORE] destination key [key ...]

SINTERSTORE result set1 set2
SMEMBERS result             # 查看交集结果

交集运算:SINTER(共同拥有的)

  • 数学概念:两边都有的元素。
  • 代码示例:
    • SINTER set1 set2 -> 返回 {"c", "d"} (set1 和 set2 共同有的)
    • SINTER set1 set2 set3 -> 返回 {"d"} (三个集合都有的)
  • 应用场景:共同好友、共同关注。比如 SINTER 用户A的关注 用户B的关注,瞬间找出他俩都关注了谁。

并集运算:SUNION(全部加起来的)

  • 数学概念:把两边所有元素合并在一起(自动去重)。
  • 代码示例:SUNION set1 set2 -> 返回 {"a", "b", "c", "d", "e", "f"}
  • 应用场景:标签汇总、获取所有权限。比如把几个角色的权限集合合并,算出该用户的最终总权限。

差集运算:SDIFF(我独有的)

  • 数学概念:在第一个集合里,但不包含在后面集合里的元素(有方向性)。
  • 代码示例:
    • SDIFF set1 set2 -> 返回 {"a", "b"} (set1 有,但 set2 没有的)
    • SDIFF set2 set1 -> 返回 {"e", "f"} (set2 有,但 set1 没有的)
  • 应用场景:可能认识的人(比如:我的好友 – 我的好友的好友)、找茬(新上架的商品 – 已经看过的商品)。

存储结果:...STORE 系列(新集合存储)

带 STORE 后缀的命令。前面的 SINTER 只是在屏幕上打印结果,而加了 STORE 会在Redis 中生成一个新的集合来保存结果。

  • 代码示例:SINTERSTORE result set1 set2
  • 详细解释:计算 set1 和 set2 的交集,并把结果(c 和 d)存入一个名为 result 的新集合中。然后你用 SMEMBERS result 就能看到这个新集合的内容。
  • (同样,SUNIONSTORE、SDIFFSTORE 也是这个逻辑)。
  • ⚠️ 覆盖警告:如果 result 这个键之前已经存在,它原有的数据会被直接清空覆盖。

有序集合 (zset)

有序集合的特点和应用

有序集合是 Redis 中最复杂的数据类型,它结合了集合和列表的特点。

主要特点:

  • 唯一性:成员唯一,不允许重复
  • 有序性:按分数(score)排序
  • 双重索引:可以按成员或分数查找
  • 范围查询:支持按分数或排名范围查询

常见应用场景:

  • 排行榜系统
  • 优先级队列
  • 时间线排序
  • 范围查询
  • 权重计算

基本操作命令

添加和删除元素

# 添加元素(分数 成员)
# 用法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
ZADD leaderboard 1500 "玩家A"
ZADD leaderboard 2300 "玩家B" 1800 "玩家C" 2100 "玩家D"
# 批量添加
ZADD leaderboard 1200 "玩家E" 1900 "玩家F" 2500 "玩家G"

# 删除元素
# 用法:ZREM key member [member ...]
ZREM leaderboard "玩家E"

# 按分数范围删除
# 用法:ZREMRANGEBYSCORE key min max
ZREMRANGEBYSCORE leaderboard 0 1000

# 按排名范围删除
# 用法:ZREMRANGEBYRANK key start stop
ZREMRANGEBYRANK leaderboard 0 2    # 删除前3名

添加元素:ZADD

  • 语法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
  • 代码示例:
    • ZADD leaderboard 1500 "玩家A"
      • 解释:往 leaderboard 里添加“玩家A”,分数设为 1500。
    • ZADD leaderboard 2300 "玩家B" 1800 "玩家C" 2100 "玩家D"
      • 解释:支持一次添加多个,格式为 分数1 成员1 分数2 成员2。
    • ZADD leaderboard 1200 "玩家E" 1900 "玩家F" 2500 "玩家G"
      • 解释:批量添加。如果成员已存在,ZADD 会更新该成员的分数。
  • 💡 高级可选参数(括号里的):
    • NX:只添加不存在的成员(不更新已有分数)。
    • XX:只更新已存在的成员(不添加新成员)。
    • CH:返回值变为“发生变化的成员总数”(默认只返回新增的个数)。
    • INCR:让分数像 INCR 命令一样递增(比如 ZADD leaderboard INCR 100 "玩家A")。

删除元素:ZREM

  • 语法:ZREM key member [member ...]
  • 代码示例:ZREM leaderboard "玩家E"
  • 解释:精准移除指定的成员(不管它的分数是多少)。返回实际删除的成员个数。

按分数范围删除:ZREMRANGEBYSCORE

  • 语法:ZREMRANGEBYSCORE key min max
  • 代码示例:ZREMRANGEBYSCORE leaderboard 0 1000
  • 解释:删除分数在 0 到 1000 之间(包含边界)的所有成员。
  • 应用场景:清理积分过低的“僵尸用户”。

按排名范围删除:ZREMRANGEBYRANK

  • 语法:ZREMRANGEBYRANK key start stop
  • 代码示例:ZREMRANGEBYRANK leaderboard 0 2
  • 解释:按名次删除。ZSet 的排名从 0 开始,0 2 代表删除排名第 1 到第 3 名的成员(前 3 名)。
  • 应用场景:末位淘汰(删除排名最后 10 名可用 ZREMRANGEBYRANK key -10 -1)。

查询操作

# 获取元素数量
# 用法:ZCARD key
ZCARD leaderboard

# 获取元素分数
# 用法:ZSCORE key member
ZSCORE leaderboard "玩家B"

# 获取元素排名
# 用法:ZRANK key member        # 正序排名(从0开始)
# 用法:ZREVRANK key member     # 逆序排名(从0开始)
ZRANK leaderboard "玩家B"
ZREVRANK leaderboard "玩家B"

# 统计分数范围内的元素数量
# 用法:ZCOUNT key min max  
ZCOUNT leaderboard 1500 2000

# 获取分数范围内的元素数量(字典序)
# 用法:ZLEXCOUNT key min max
ZLEXCOUNT leaderboard a c

获取元素数量:ZCARD

  • 语法:ZCARD key
  • 代码示例:ZCARD leaderboard
  • 解释:返回集合中成员的总个数(Cardinality)。

获取元素分数:ZSCORE

  • 语法:ZSCORE key member
  • 代码示例:ZSCORE leaderboard "玩家B"
  • 解释:查询指定成员的分数(score)。如果成员不存在,返回 (nil)。

获取元素排名:ZRANK / ZREVRANK(面试重点)

  • 语法:ZRANK key member / ZREVRANK key member
  • 代码示例:
    • ZRANK leaderboard "玩家B" -> 正序排名(分数从小到大,从 0 开始算)。
    • ZREVRANK leaderboard "玩家B" -> 逆序排名(分数从大到小,从 0 开始算)。
  • 解释:查询某个成员在当前集合中的名次。
  • ⚠️ 避坑:返回值是从 0 开始的。如果返回 0,代表是第 1 名;如果返回 (nil),说明该成员不在集合中。

统计分数范围内的元素数量:ZCOUNT

  • 语法:ZCOUNT key min max
  • 代码示例:ZCOUNT leaderboard 1500 2000
  • 解释:统计分数在 1500 到 2000(包含边界)之间的成员个数。
  • 应用场景:例如统计“考试成绩在 80-100 分之间的学生人数”、“VIP 积分在 1000 分以上的用户数”。

范围操作

有序集合的范围操作是其最强大的功能之一。

按排名范围查询

# 按排名获取元素(正序)
# 用法:ZRANGE key start stop [WITHSCORES]
ZRANGE leaderboard 0 2              # 获取前3名
ZRANGE leaderboard 0 2 WITHSCORES   # 获取前3名及分数
ZRANGE leaderboard 0 -1             # 获取所有元素

# 按排名获取元素(逆序)
# 用法:ZREVRANGE key start stop [WITHSCORES]
ZREVRANGE leaderboard 0 2 WITHSCORES # 获取前3名(高分到低分)

1. 按“排名”范围查询(看名次)

正序查:ZRANGE(分数从低到高)

  • 语法:ZRANGE key start stop [WITHSCORES]
  • 代码示例:
    • ZRANGE leaderboard 0 2:取排名第 1 到第 3 的成员(索引 0, 1, 2)。
    • ZRANGE leaderboard 0 2 WITHSCORES:取前 3 名,并且带出他们的分数(加上 WITHSCORES 返回值会成对出现:成员1, 分数1, 成员2, 分数2…)。
    • ZRANGE leaderboard 0 -1:取所有成员(-1 代表最后一个)。

逆序查:ZREVRANGE(分数从高到低)

  • 语法:ZREVRANGE key start stop [WITHSCORES]
  • 代码示例:ZREVRANGE leaderboard 0 2 WITHSCORES
  • 解释:取分数最高的前 3 名及其分数。做排行榜(如游戏榜单、热搜榜)通常用这个。

按分数范围查询

# 按分数范围获取元素(正序)
# 用法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
ZRANGEBYSCORE leaderboard 1500 2000
ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES
ZRANGEBYSCORE leaderboard 1500 2000 LIMIT 0 5

# 按分数范围获取元素(逆序)
# 用法:ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
ZREVRANGEBYSCORE leaderboard 2000 1500 WITHSCORES

# 使用开区间
ZRANGEBYSCORE leaderboard 1500 2000    # 不包含1500和2000
ZRANGEBYSCORE leaderboard -inf +inf     # 所有元素

正序查询:ZRANGEBYSCORE

  • 语法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
  • 核心逻辑:查找分数在 min 到 max 之间的成员,按分数从低到高(正序)排列。
  • 代码拆解:
    1. ZRANGEBYSCORE leaderboard 1500 2000
      • 解释:获取分数在 1500 到 2000(包含边界)之间的所有成员。
    2. ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES
      • 解释:同上,但在返回成员名字的同时,附带返回他们的分数。
    3. ZRANGEBYSCORE leaderboard 1500 2000 LIMIT 0 5
      • 解释:分页查询。在 1500 到 2000 分之间,跳过 offset 0 个,取出 count 5 个(即取前 5 名)。

逆序查询:ZREVRANGEBYSCORE

  • 语法:ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
  • 核心逻辑:按分数从高到低(逆序)排列。
  • ⚠️ 极其重要的避坑点:因为是从高到低排,所以参数顺序变了!max 必须写在前面,min 写在后面。
  • 代码拆解:
    • ZREVRANGEBYSCORE leaderboard 2000 1500 WITHSCORES
    • 解释:获取分数在 2000 到 1500 之间的成员,按分数从高到低返回。如果你写成 1500 2000,将返回空结果。

按字典序范围查询

# 按字典序范围获取元素
# 用法:ZRANGEBYLEX key min max [LIMIT offset count]
ZRANGEBYLEX leaderboard a c
# 用法:ZREVRANGEBYLEX key max min [LIMIT offset count]
ZREVRANGEBYLEX leaderboard c a

# 示例:相同分数的元素按字典序排列
ZADD words 0 "apple" 0 "banana" 0 "cherry" 0 "date"
ZRANGEBYLEX words a c              # 获取a到c之间的单词
ZRANGEBYLEX words banana date      # 获取banana到date之间的单词

正序查询:ZRANGEBYLEX

  • 语法:ZRANGEBYLEX key min max [LIMIT offset count]
  • 代码示例:ZRANGEBYLEX leaderboard a c
  • 解释:在 leaderboard 集合中,找出成员名字在字母 a 到 c 之间(包含 a 和 c)的所有元素,按从 A 到 Z 的正序排列。

逆序查询:ZREVRANGEBYLEX

  • 语法:ZREVRANGEBYLEX key max min [LIMIT offset count]
  • 代码示例:ZREVRANGEBYLEX leaderboard c a
  • ⚠️ 避坑警告:和按分数逆序查一样,参数顺序必须反过来! 最大值 c 写在前面,最小值 a 写在后面,代表从 Z 到 A 的逆序排列。

  • 解释:你向 words 集合中添加了 4 个单词,注意看,它们的分数全部是 0。在 Redis 底层,它们会自动按字母顺序排好:apple -> banana -> cherry -> date。

接着执行:

  • ZRANGEBYLEX words a c -> 返回 apple(以a开头)和 banana(以b开头)。因为 cherry 是以 c 开头的,其实也包含在内。
  • ZRANGEBYLEX words banana date -> 返回从 banana 到 date 之间的单词(包含 banana 和 date)。