Featured image of post Redis 进阶

Redis 进阶

Redis 事务、过期时间、排序、消息通知

Redis 进阶

1. Redis 事务

Redis 中的事务(transaction)是一个单独的操作序列,可以一次执行多个命令,但是这些命令要么全部执行,要么全部不执行。Redis 事务的实现是通过 MULTI、EXEC、DISCARD 和 WATCH 这四个命令来完成的。

1.1 MULTI 使用

MULTI 命令用于开启一个事务,它总是返回 OK。 MULTI 执行之后,客户端可以继续输入多条命令,这些命令不会立即执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。

1
2
3
4
MULTI
SET key1 value1
SET key2 value2
EXEC

1.2 EXEC 使用

EXEC 命令用于执行所有事务块内的命令。如果事务块内的命令全部执行成功,返回事务块内所有命令的返回值,否则返回一个错误。 Redis 保证一个事务中的所有命令都会被执行,不会出现部分执 行的情况。如果在发送EXEC命令之前发生错误,那么事务将被取消,所有命令都不会执行。

1.3 DISCARD 使用

DISCARD 命令用于取消事务,它清空事务队列并且恢复到正常模式。

1.4 WATCH 使用

WATCH 命令用于在事务执行之前监视任意数量的键。如果在事务执行之前这些键被其他命令所改动,那么事务将被打断。

注意: WATCH使用的是乐观锁,不是悲观锁。 那么,乐观锁和悲观锁有什么区别呢?

乐观锁和悲观锁是并发控制的两种策略。悲观锁认为数据会被其他事务修改,因此在对数据进行操作时会锁住数据,直到操作完成。乐观锁认为数据不会被其他事务修改,因此在对数据进行操作时不会锁住数据,只是在更新数据时判断数据是否被其他事务修改过。如果数据没有被修改过,则更新成功;如果数据被修改过,则更新失败。

示例:

1
2
3
4
5
6
7
set key 1
watch key
set key 2
multi
set key 3
exec
get key

输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
127.0.0.1:6379> get key
"2"
127.0.0.1:6379>  set key 1
OK
127.0.0.1:6379>   watch key
OK
127.0.0.1:6379>   set key 2
OK
127.0.0.1:6379>   multi
OK
127.0.0.1:6379>   set key 3
QUEUED
127.0.0.1:6379>   exec
(nil)
127.0.0.1:6379> get key
"2"

从上面示例我们可以看出,当某个键被 WATCH 监视后,如果在事务执行之前这个键被其他命令所改动,那么事务将被打断。无法执行事务中的命令,返回nil。

2. Redis 过期时间

Redis 中可以为 key 设置过期时间,当 key 过期时,会自动删除。过期时间可以通过 EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT 四个命令来设置。

2.1 EXPIRE 使用

EXPIRE 命令用于设置 key 的过期时间,单位为秒。当 key 过期时,会自动删除。

EXPIRE 命令的使用方法为 EXPIRE key seconds,其中 key 为键名,seconds 为过期时间。

例如要让session:342r 60秒后过期,可以使用如下命令:

1
2
SET session:342r 11
EXPIRE session:342r 60

执行上述代码后,我们接着执行GET session:342r命令,会发现返回的是11,说明 key 还存在。但是等待 60 秒后再次执行GET session:342r命令,会发现返回的是(nil),说明 key 已经过期被删除了。

1
2
3
4
5
6
7
8
127.0.0.1:6379> SET session:342r 11
OK
127.0.0.1:6379> EXPIRE session:342r 60
(integer) 1
127.0.0.1:6379> GET session:342r
"11"
127.0.0.1:6379> GET session:342r
(nil)

想要知道 key 的剩余过期时间,可以使用 TTL 命令,TTL 命令的使用方法为 TTL key,其中 key 为键名。

1
TTL session:342r

将会返回 key 的剩余过期时间,单位为秒。如果 key 没有设置过期时间,或者 key 已经过期,TTL 命令会返回 -2。如果 key 存在但没有设置过期时间,TTL 命令会返回 -1。

在 Redis 2.6 版本中,无论键不存在还是没有过期时间,都会返回 -2。在 Redis 2.8 版本后,无论键不存在还是没有过期时间,都会返回 -1。

2.2 PERSIST 使用

PERSIST 命令用于移除 key 的过期时间,使 key 永不过期。如果过期时间移除成功,返回 1;如果 key 不存在或者 key 没有设置过期时间,返回 0。

PERSIST 命令的使用方法为 PERSIST key,其中 key 为键名。

1
PERSIST session:342r

2.3 PEXPIRE 使用

PEXPIRE 命令用于设置 key 的过期时间,单位为毫秒。当 key 过期时,会自动删除。

PEXPIRE 命令的使用方法为 PEXPIRE key milliseconds,其中 key 为键名,milliseconds 为过期时间。

例如要让session:342r 60秒后过期,可以使用如下命令:

1
2
SET session:342r 11
PEXPIRE session:342r 60000

2.4 EXPIREAT 使用

EXPIREAT 命令用于设置 key 的过期时间,以 UNIX 时间戳(unix timestamp)格式设置过期时间。当 key 过期时,会自动删除。

EXPIREAT 命令的使用方法为 EXPIREAT key timestamp,其中 key 为键名,timestamp 为过期时间。

例如要让session:342r 60秒后过期,可以使用如下命令:

1
2
SET session:342r 11
EXPIREAT session:342r 1646880000

2.5 PEXPIREAT 使用

PEXPIREAT 命令用于设置 key 的过期时间,以 UNIX 时间戳(unix timestamp)格式设置过期时间,单位为毫秒。当 key 过期时,会自动删除。

PEXPIREAT 命令的使用方法为 PEXPIREAT key timestamp,其中 key 为键名,timestamp 为过期时间。

例如要让session:342r 60秒后过期,可以使用如下命令:

1
2
SET session:342r 11
PEXPIREAT session:342r 1646880000000

2.6 设置过期时间的注意事项

  1. 如果 key 已经设置了过期时间,再次设置过期时间会覆盖之前的过期时间。

  2. 如果 key 已经过期,再次设置过期时间不会生效,key 会立即被删除。

  3. 如果 key 已经设置了过期时间,再次设置过期时间时,过期时间的单位不同,会覆盖之前的过期时间。

2.7 过期时间的应用场景

  1. 缓存数据:可以为缓存数据设置过期时间,当数据过期时,自动删除。

    • 比如要设置一个缓存数据为 5 分钟,可以使用如下命令:
    1
    2
    
    SET cache:342r 11
    EXPIRE cache:342r 300
    
    • 代码解释:
      • SET cache:342r 11:设置 cache:342r 键的值为 11。
      • EXPIRE cache:342r 300:设置 cache:342r 键的过期时间为 300 秒。
  2. 会话管理:可以为会话设置过期时间,当会话过期时,自动删除。

    • 比如要设置一个会话为 30 分钟,可以使用如下命令:
    1
    2
    
    SET session:342r 11
    EXPIRE session:342r 1800
    
    • 代码解释:
      • SET session:342r 11:设置 session:342r 键的值为 11。
      • EXPIRE session:342r 1800:设置 session:342r 键的过期时间为 1800 秒。
  3. 验证码:可以为验证码设置过期时间,当验证码过期时,自动删除。

    • 比如要设置一个验证码为 5 分钟,可以使用如下命令:
    1
    2
    
    SET code:342r 123456
    EXPIRE code:342r 300
    
    • 代码解释:
      • SET code:342r 123456:设置 code:342r 键的值为 123456。
      • EXPIRE code:342r 300:设置 code:342r 键的过期时间为 300 秒。
  4. 限流:可以为限流的 key 设置过期时间,当限流的 key 过期时,自动删除。

    • 比如要限制用户每分钟访问100个网页,思路是对每个用户使用一个名为rate.limiting:ip的字符串类型键,每次访问网页时,对这个键进行自增操作,如果自增后的值大于100,就拒绝访问。为了防止这个键一直存在,可以设置过期时间,比如设置为1分钟。
    • 代码示例:
    1
    2
    
    INCR rate.limiting:ip
    EXPIRE rate.limiting:ip 60
    
    • 代码解释:
      • INCR rate.limiting:ip:对 rate.limiting:ip 键进行自增操作。
      • EXPIRE rate.limiting:ip 60:设置 rate.limiting:ip 键的过期时间为 60 秒。

2.8 实现缓存

为了提高网站的负载能力,需要将一些访问频率较高的数据缓存到 Redis 中,并且希望让这些缓存过一段时间自动过期。比如,教务网站要将所有学生各个科目的成绩汇总排名,并在首页上显示排名前十的学生,这个数据是实时变化的,并且非常消耗计算资源,所以不能每次都去数据库查询,而是将这个数据缓存到 Redis 中,并设置过期时间为 2 小时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
SET ranking:student1 100
SET ranking:student2 99
SET ranking:student3 98
SET ranking:student4 97
SET ranking:student5 96
SET ranking:student6 95
SET ranking:student7 94
SET ranking:student8 93
SET ranking:student9 92
SET ranking:student10 91
EXPIRE ranking:student1 7200
EXPIRE ranking:student2 7200
EXPIRE ranking:student3 7200
EXPIRE ranking:student4 7200
EXPIRE ranking:student5 7200
EXPIRE ranking:student6 7200
EXPIRE ranking:student7 7200
EXPIRE ranking:student8 7200
EXPIRE ranking:student9 7200
EXPIRE ranking:student10 7200

如果服务器内存有限,如果大量地设置缓存且过期时间较长,可能会导致服务器内存不足。为了解决这个问题,可以使用 Redis 的淘汰策略。

我们可以通过设置配置文件来解决这个问题,修改配置文件中的maxmemory参数,设置 Redis 的最大内存限制。当 Redis 的内存使用达到最大内存限制时,Redis 会根据配置文件中的maxmemory-policy参数来决定淘汰策略。

maxmemory-policy 参数有以下几种取值:

  • volatile-lru:在设置了过期时间的 key 中,优先淘汰最近最少使用的 key。
  • volatile-ttl:在设置了过期时间的 key 中,优先淘汰剩余时间最少的 key。
  • volatile-random:在设置了过期时间的 key 中,随机淘汰 key。
  • allkeys-lru:在所有 key 中,优先淘汰最近最少使用的 key。
  • allkeys-random:在所有 key 中,随机淘汰 key。
  • noeviction:不淘汰任何 key,只是返回错误。

其中,LRU是Least Recently Used的缩写,表示最近最少使用,TTL是Time To Live的缩写,表示剩余时间。

3. Redis 排序

有序集合不支持交、并运算,Redis 还未加入相关命令。这是因为 Redis 认为开发者在做完交、并操作后不需要直接获得全部结果,而是希望将结果存入新的键中以便后续处理。所以没有序集合只有ZINTERSTORE和ZUNIONSTORE

3.1 SORT 命令

SORT命令可以对列表类型、集合类型、有序集合类型的值进行排序。 SORT命令的使用方法为SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]。 ⚠️注意:SORT命令至对输出结果进行排序,并不对数值进行改变!!!

3.1.1 排列列表

SORT命令可以对列表类型的值进行排序。比如,对一个列表类型的值进行排序,可以使用如下命令:

1
2
3
lpush list 324 45 6 75 78 89
sort list # 默认升序
lrange list # 输出所有元素

输出结果为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
127.0.0.1:6379> lpush list 324 45 6 75 78 89
(integer) 6
127.0.0.1:6379> sort list
1) "6"
2) "45"
3) "75"
4) "78"
5) "89"
6) "324"
127.0.0.1:6379> lrange list 0 -1
1) "89"
2) "78"
3) "75"
4) "6"
5) "45"
6) "324"

3.1.2 排列集合

SORT命令可以对集合类型的值进行排序。比如,对一个集合类型的值进行排序,可以使用如下命令:

1
2
3
sadd set 324 45 6 75 78 89
sort set # 默认升序
smembers set # 输出所有元素

在小数据集的情况下,smembers 输出的看似是有序的,但实际上是无序的。可以通过 sort 命令进行排序。 Redis 集合(Set) 的底层数据结构 主要是哈希表(hashtable)或者整数集合(intset)(当元素全是整数且个数较少时)。 在 SMEMBERS 输出时,哈希表的遍历方式可能会让元素按某种规则排列,但这不是保证的排序,只是某些情况下看起来有序。

3.1.3 排列有序集合

SORT命令可以对有序集合类型的值进行排序。比如,对一个有序集合类型的值进行排序,可以使用如下命令:

1
2
3
zadd zset 324 324 45 45 6 6 75 75 78 78 89 89
sort zset # 默认升序
zrange zset 0 -1 # 输出所有元素

注意;SORT命令对有序集合类型的值进行排序时,会将分数相同的元素按照成员的字典序进行排序。前面是分数,后面是成员。SORT 排列的是成员,而不是分数。如果成员是数字,那么按照数字的大小进行排序;如果成员是字符串,那么必须加上ALPHA才能按照字符串的字典序进行排序。

3.1.4 LIMIT 参数

SORT命令的LIMIT参数用于限制输出结果的数量。LIMIT参数的使用方法为LIMIT offset count,其中offset为偏移量,count为数量。

偏移量(offset)表示从第几个元素开始,数量(count)表示输出多少个元素。

比如,对一个列表类型的值进行排序,并限制输出结果的数量,可以使用如下命令:

1
2
3
lpush list 324 45 6 75 78 89
sort list limit 0 3 # 默认升序
lrange list 0 -1 # 输出所有元素

输出结果为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> lpush list 324 45 6 75 78 89
(integer) 12
127.0.0.1:6379> sort list limit 0 3
1) "6"
2) "6"
3) "45"
127.0.0.1:6379> lrange list 0 -1
 1) "89"
 2) "78"
 3) "75"
 4) "6"
 5) "45"
 6) "324"
 7) "89"
 8) "78"
 9) "75"
10) "6"
11) "45"
12) "324"

3.1.5 DESC 参数

SORT命令的DESC参数用于指定排序方式为降序。 例如:

1
2
lpush list 324 45 6 75 78 89
sort list desc

3.1.6 ALPHA 参数

SORT命令的ALPHA参数用于指定排序方式为字典序。

3.1.7 BY 参数

SORT命令的BY参数用于指定排序的依据。BY参数的使用方法为BY pattern,其中pattern为模式。 例如,我们先插入一些数据:

1
2
3
4
5
lpush sortlist 2 1 3
set item:1 10
set item:2 5
set item:3 30
sort sortlist by item:* desc # 按照 item:* 的值降序排序

这种情况下,SORT命令会先根据item:*的值对sortlist进行排序,然后再输出排序后的结果。

适用于需要根据特定属性对于集合进行排序的场景。

还可以是哈希表的键值对,例如:

1
2
3
4
5
lpush sortlist 2 1 3
hset item:1 score 10
hset item:2 score 5
hset item:3 score 30
sort sortlist by item:*->score desc # 按照 item:*->score 的值降序排序

3.1.8 GET 参数

GET 参数不影响排序,它的作用是使 POST 命令的返回结果不再是元素自身的值,而是GET 参数里面指定的键值。 例如:

1
2
3
4
5
lpush sortlist 2 1 3
set item:1 10
set item:2 5
set item:3 30
sort sortlist by item:* desc get item:* # 按照 item:* 的值降序排序,并且返回 item:* 的值

3.1.9 STORE 参数

SORT命令的STORE参数用于将排序后的结果保存到一个新的键中。STORE参数的使用方法为STORE destination,其中destination存储的键。

例如:

1
2
3
4
5
lpush sortlist 2 1 3
set item:1 10
set item:2 5
set item:3 30
sort sortlist by item:* desc store newlist # 按照 item:* 的值降序排序,并且将结果保存到 newlist 键中

4 Redis 消息通知

Redis 提供了 发布/订阅(Pub/Sub)、Key 事件通知、Stream(消息队列)三种消息通知机制。

4.1 发布/订阅

发布/订阅(Publish/Subscribe,简称 Pub/Sub) 是 Redis 提供的一种消息订阅机制,允许客户端订阅一个或多个频道(Channel),然后发布者向这些频道发送这些消息,所有订阅者都会收到相应的消息。

4.1.1 订阅频道(Subscribe)

SUBSCRIBE命令用于订阅一个或多个频道。SUBSCRIBE命令的使用方法为SUBSCRIBE channel [channel ...],其中channel为频道名。

示例:

1
subscribe channel

输出结果:

1
2
3
4
5
127.0.0.1:6379> subscribe channel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel"
3) (integer) 1

4.1.2 发布消息(Publish)

PUBLISH命令用于向指定频道发送消息。PUBLISH命令的使用方法为PUBLISH channel message,其中channel为频道名,message为消息内容。

在 4.1.1 例子上的基础上,新建一个控制台,执行如下命令:

1
publish channel "hello"

原来的控制台会输出:

1
2
3
4
5
6
7
8
127.0.0.1:6379> subscribe channel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel"
3) (integer) 1
1) "message"
2) "channel"
3) "hello"

多了一行"hello",说明发布成功,客户端已经收到消息。

4.1.3 取消订阅(UNSUBSCRIBE)

UNSUBSCRIBE命令用于取消订阅一个或多个频道。UNSUBSCRIBE命令的使用方法为UNSUBSCRIBE [channel [channel ...]],其中channel为频道名。

示例:

1
unsubscribe channel

4.1.4 订阅多个频道

SUBSCRIBE命令可以订阅多个频道。比如,订阅channel1和channel2两个频道,可以使用如下命令:

1
subscribe channel1 channel2

4.1.5 取消订阅所有频道

UNSUBSCRIBE命令可以取消订阅所有频道。比如,取消订阅所有频道,可以使用如下命令:

1
unsubscribe

4.2 Key 事件通知

Redis Key 事件通知允许监听数据库键的变化事件,例如SET、DEL、EXPIRE。

4.2.1 配置通知

默认情况下,Redis 不会 开启Key 事件通知,可以通过以下命令启用:

1
config set notify-keyspace-events KEA

notify-keyspace-events参数的取值有以下几种:

  • K:键空间通知,事件以__keyspace@<db>__为前缀进行发布。(Key 事件通知 (keyspace events))

  • E:键事件通知,事件以__keyevent@<db>__为前缀进行发布。(keyevent)

  • A:参数通知,事件以__keyspace@<db>__为前缀进行发布。(ALL)

  • g:DEL、EXPIRE、RENAME、EXPIREAT、SORT操作的通知。

  • $:字符串命令通知。

4.2.2 订阅通知

PSUBSCRIBE命令用于订阅一个或多个模式。PSUBSCRIBE命令的使用方法为PSUBSCRIBE pattern [pattern ...],其中pattern为模式。

示例:

1
psubscribe __keyspace@0__:*

然后再另一个终端执行:

1
set mykey 1

订阅者会收到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
127.0.0.1:6379> psubscribe __keyspace@0__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyspace@0__:*"
3) (integer) 1
1) "pmessage"
2) "__keyspace@0__:*"
3) "__keyspace@0__:mykey"
4) "set"
1) "pmessage"
2) "__keyspace@0__:*"
3) "__keyspace@0__:mykey"
4) "del"

4.2.3 监听事件

事件:

  • set: 触发操作为SET key value。
  • del: 触发操作为DEL key。
  • expire: 触发操作为EXPIRE key seconds。
  • rename: 触发操作为RENAME key newkey。
  • expired: 键过期时触发。

注意:

Key 事件通知只能监键的变化,不能监值的变化。

4.3 Redis Stream(消息队列)

Redis Stream 是一个高效的消息队列,支持消息持久化、消费者组、消费确认等功能。适用于日志收集、事件流处理、消息队列等应用场景。

4.3.1 添加消息 (XADD)

基本语法:

1
XADD stream_name ID field1 value1 field2 value2 ...
  • stream_name:流的名称。
  • ID:消息的唯一标识,可以是*或$。
  • field1 value1 field2 value2 ...:消息的键值对。

举例:

1
XADD mystream * name Tom age 18

4.3.2 读取消息

4.3.2.1 读取所有消息 (XRANGE)

1
XRANGE mystream - +

会输出以下内容:

1
2
3
4
5
6
127.0.0.1:6379> XRANGE mystream - +
1) 1) "1741594483162-0"
   2) 1) "name"
      2) "Tom"
      3) "age"
      4) "18"

4.3.2.2 阻塞式读取新消息 (XREAD)

基本语法:

1
XREAD COUNT count BLOCK milliseconds STREAMS stream_name ID
  • count:读取消息的数量。
  • milliseconds:阻塞时间,单位为毫秒。
  • stream_name:流的名称。
  • ID:消息的唯一标识。(当$时,表示从最新的消息开始读取)

举例:

1
XREAD COUNT 1 STREAMS mystream 0 # 非阻塞式读取一条消息

还可以当有新的消息时,自动读取新消息:

1
XREAD BLOCK 5000 STREAMS mystream $ # 阻塞式读取新消息

此时切换到另一个终端,执行:

1
XADD mystream * name Tom age 18

第一个控制台会输出以下内容:

1
2
3
4
5
6
7
127.0.0.1:6379> XREAD COUNT 1 STREAMS mystream 0
1) 1) "mystream"
   2) 1) 1) "1741594483162-0"
         2) 1) "name"
            2) "Tom"
            3) "age"
            4) "18"

4.3.3 消费者组

4.3.4 创建消费者组

基本语法:

1
XGROUP CREATE stream_name group_name ID
  • stream_name:流的名称。
  • group_name:消费者组的名称。
  • ID:消息的唯一标识。

例如:

1
XGROUP CREATE mystream mygroup 0
4.3.5 消费者读取消息

基本语法:

1
XREADGROUP GROUP group_name consumer_name COUNT count BLOCK milliseconds STREAMS stream_name ID
  • group_name:消费者组的名称。
  • consumer_name:消费者的名称。
  • count:读取消息的数量。
  • milliseconds:阻塞时间,单位为毫秒。
  • stream_name:流的名称。
  • ID:消息的唯一标识。(*表示从最新的消息开始读取,>表示从上次读取的位置开始读取)

例如:

1
XREADGROUP GROUP mygroup myconsumer COUNT 1 STREAMS mystream > # 读取一条消息

4.3.6 消费者确认消息

基本语法:

1
XACK stream_name group_name ID
  • stream_name:流的名称。
  • group_name:消费者组的名称。
  • ID:消息的唯一标识。

例如:

1
XACK mystream mygroup 1741594483162-0

4.3.4 三者之间的比较

  • 发布/订阅(Pub/Sub):

    • ✅ 低延迟,适用于实时消息推送
    • ✅ 适合广播通知,如聊天系统、游戏推送
    • ✅ 订阅者断开连接后不会收到历史消息
    • ❌ 无法保证消息可靠性(可能丢失)
  • Key 事件通知:

    • ✅ 无法保证消息可靠性(可能丢失)
    • ✅ 可以配合 Redis 作为消息通知机制
    • ❌ 可能会增加 Redis 的 CPU 负担
    • ❌ 只能监听键的变化,不能监听数据值的变化
  • Stream(消息队列):

    • ✅ 支持消息持久化,不会丢失
    • ✅ 支持多个消费者,适用于高并发场景
    • ✅ 可以追踪消息处理状态
    • ❌ 比 Pub/Sub 稍微复杂,但更可靠
Licensed under CC BY-NC-SA 4.0