redis之单机数据库

2022-12-02 17:44:58 浏览数 (3)

3、单机数据库

3.1、数据库

3.1.1、服务器中的数据库

redis服务器将所有数据库都保存在服务状态server.h/RedisServer结构的db数组中,db数组的每个项都是一个server.h/redisDb结构,每个reidsDb结构代表一个数据库:

server.h

代码语言:c复制
struct redisServer {
  // ...
  // 一个数组,保存着服务器中的所有数据库
  redisDb *db;
  // ...
};

在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库:

server.h

代码语言:c复制
struct redisServer {
  // ...
  // 服务器的数据库数量
  int dbnum;
  // ...
};

dbnum属性的值有服务器配置的database选项决定,默认情况下,该选项的值为16:

redis.conf

代码语言:c复制
// ...
databases 16
//...

server.h

代码语言:c复制
typedef struct redisDb {
    dict *dict;                 /* 这个DB的键空间 */
    dict *expires;              /* 设置了超时的键的超时时间 */
    dict *blocking_keys;        /* 客户端等待数据的键(BLPOP)*/
    dict *ready_keys;           /* 阻塞了接收到PUSH的键 */
    dict *watched_keys;         /* 监视键的MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
SELECT:切换至指定的数据库

在默认情况下,Redis服务器在启动时将会创建16个数据库:这些数据库都使用号码进行标识,其中第一个数据库为0号数据库,第二个数据库为1号数据库,而第三个数据库则为2号数据库,以此类推。

Redis虽然不允许在同一个数据库中使用两个同名的键,但是由于不同数据库拥有不同的命名空间,因此在不同数据库中使用同名的键是完全没有问题的,而用户也可以通过使用不同数据库来存储不同的数据,以此来达到重用键名并且减少键冲突的目的。

当用户使用客户端与Redis服务器进行连接时,客户端一般默认都会使用0号数据库,但是通过使用SELECT命令,用户可以从当前正在使用的数据库切换到自己想要使用的数据库:

SELECT db

代码语言:shell复制
redis> SELECT 3
OK
3.1.2、KEYS:获取所有与给定匹配符相匹配的键

KEYS命令接受一个全局匹配符作为参数,然后返回数据库中所有与这个匹配符相匹配的键作为结果:

KEYS pattern

举个例子,如果我们想要获取数据库包含的所有键,那么可以执行以下命令:

代码语言:shell复制
redis> KEYS *
1) "fruits"
2) "user::12312::profile"
3) "user::ip"
4) "user::id"
5) "cache::/user/peter"
6) "todo-list"

redis> KEYS user::*
1) "user::12312::profile"
2) "user::ip"
3) "user::id"
redis-select.pngredis-select.png
3.1.3、SCAN:以渐进方式迭代数据库中的键

该命令是一个迭代器,它每次被调用的时候都会从数据库中获取一部分键,用户可以通过重复调用SCAN命令来迭代数据库包含的所有键:SCAN cursor

SCAN命令的执行结果由两个元素组成:

  • 第一个元素是进行下一次迭代所需的游标,如果这个游标为0,那么说明客户端已经对数据库完成了一次完整的迭代。
  • 第二个元素是一个列表,这个列表包含了本次迭代取得的数据库键;如果SCAN命令在某次迭代中没有获取到任何键,那么这个元素将是一个空列表。

关于SCAN命令返回的键列表,有两点需要注意:

  • SCAN命令可能会返回重复的键,用户如果不想在结果中包含重复的键,那么就需要自己在客户端中进行检测和过滤。
  • SCAN命令返回的键数量是不确定的,有时甚至会不返回任何键,但只要命令返回的游标不为0,迭代就没有结束。
代码语言:shell复制
redis> SCAN 0
1) "25" -- 进行下次迭代的游标
2) 1) "key::16" -- 本次迭代获取到的键
   2) "key::2"
   3) "key::6"
   4) "key::8"
   5) "key::13"
   6) "key::22"
   7) "key::10"
   8) "key::24"
   9) "key::23"
   10) "key::21"
   11) "key::5"

 redis> SCAN 0 MATCH user::*  -- 迭代与给定匹配符相匹配的键
1) "208"
2) 1) "user::1"
 2) "user::65"
 3) "user::99"
 4) "user::51"

redis> SCAN 0 COUNT 5  -- 指定返回键的期望数量,COUNT选项是向SCAN命令提供一个期望值,但每次迭代返回的键数量仍然是不确定的
1) "160"
2) 1) "key::43"
   2) "key::s"
   3) "user::1"
   4) "key::83"
   5) "key::u"

针对数据库的一次完整迭代(full iteration)以用户给定游标0调用SCAN命令开始,直到SCAN命令返回游标0结束。SCAN命令为完整迭代提供以下保证:

  • 从迭代开始到迭代结束的整个过程中,一直存在于数据库中的键总会被返回。
  • 如果一个键在迭代的过程中被添加到数据库中,那么这个键是否会被返回是不确定的。
  • 如果一个键在迭代的过程中被移除了,那么SCAN命令在它被移除之后将不再返回这个键,但是这个键在被移除之前仍然有可能被SCAN命令返回。
  • 无论数据库如何变化,迭代总是有始有终的,不会出现循环迭代或者其他无法终止迭代的情况。
3.1.4、数据结构迭代命令

与获取数据库键的KEYS命令一样,Redis的各个数据结构也存在一些可能导致服务器阻塞的命令:

  • 散列的HKEYS命令、HVALS命令和HGETALL命令在处理包含键值对较多的散列时,可能会导致服务器阻塞。
  • 集合的SMEMBERS命令在处理包含元素较多的集合时,可能会导致服务器阻塞。
  • 有序集合的一些范围型获取命令,比如ZRANGE,也有阻塞服务器的可能。比如,为了获取有序集合包含的所有元素,用户可能会执行命令调用ZRANGE key 0-1,这时如果有序集合包含的成员数量较多,那么这个ZRANGE命令就可能会导致服务器阻塞。
迭代命令

HSCAN命令可以以渐进的方式迭代给定散列包含的键值对:HSCAN hash cursor [MATCH pattern] [COUNT number]

代码语言:shell复制
redis> HSCAN user::10086::profile 0
1) "0" -- 下次迭代的游标
2) 1) "name" -- 键
 2) "peter" -- 值
 3) "age"
 4) "32"
 5) "gender"
 6) "male"
 7) "blog"
 8) "peter123.whatpress.com"
 9) "email"
 10) "peter123@example.com"

SSCAN命令可以以渐进的方式迭代给定集合包含的元素:SSCAN set cursor [MATCH pattern] [COUNT number]

代码语言:shell复制
redis> SSCAN fruits 0
1) "0" -- 下次迭代的游标
2) 1) "apple" -- 集合元素
 2) "watermelon"
 3) "mango"
 4) "cherry"
 5) "banana"
 6) "dragon fruit"

ZSCAN命令可以以渐进的方式迭代给定有序集合包含的成员和分值:ZSCAN sorted_set cursor [MATCH pattern] [COUNT number]

代码语言:shell复制
redis> ZSCAN fruits-price 0
1) "0" -- 下次迭代的游标
2) 1) "watermelon" -- 成员
   2) "3.5" -- 分值
   3) "banana"
   4) "4.5"
   5) "mango"
   6) "5"
   7) "dragon fruit"
   8) "6"
   9) "cherry"
  10) "7"
  11) "apple"
  12) "8.5"
迭代命令的共通性质

HSCANSSCANZSCAN这3个命令除了与SCAN命令拥有相同的游标参数以及可选项之外,还与SCAN命令拥有相同的迭代性质:

  • SCAN命令对于完整迭代所做的保证,其他3个迭代命令也能够提供。比如,使用HSCAN命令对散列进行一次完整迭代,在迭代过程中一直存在的键值对总会被返回,诸如此类。
  • SCAN命令一样,其他3个迭代命令的游标也不耗费任何资源。用户可以在这3个命令中随意地使用游标,比如随时开始一次新的迭代,又或者随时放弃正在进行的迭代,这不会浪费任何资源,也不会引发任何问题。
  • SCAN命令一样,其他3个迭代命令虽然也可以使用COUNT选项设置返回元素数量的期望值,但命令具体返回的元素数量仍然是不确定的。
3.1.4、RANDOMKEY:随机返回一个键

RANDOMKEY命令可以从数据库中随机地返回一个键:RANDOMKEY

代码语言:shell复制
redis> RANDOMKEY
"user::123::profile"
3.1.5、SORT:对键的值进行排序

用户可以通过执行SORT命令对列表元素、集合元素或者有序集合成员进行排序。为了让用户能够以不同的方式进行排序,Redis为SORT命令提供了非常多的可选项,如果我们以不给定任何可选项的方式直接调用SORT命令,那么命令将对指定键存储的元素执行数字值排序:SORT key

代码语言:shell复制
redis> sadd slist 1 2 3 3.5 4.2  -- slist 是set集合
(integer) 5

redis> smembers slist
1) "3.5"
2) "2"
3) "1"
4) "3"
5) "4.2"

redis> sort slist  -- 使用sort时,集合里面必须是全数字
1) "1"
2) "2"
3) "3"
4) "3.5"
5) "4.2"

redis> sort another-todo desc -- another-todo 是list列表,lpush加元素,sort时列表里面的元素必须全是数字
1) "1024"
2) "256"
3) "128"
4) "64"

redis> sort sliststr  -- 使用sort排序字符串列表会直接报错
(error) ERR One or more scores can't be converted into double

redis> sort sliststr alpha  -- 通过使用 ALPHA 选项来对字符串值进行排序
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"

redis> zadd sortset 200 "ben" 350 "aimee" 186 "cario"  -- 有序集合
(integer) 3

redis> sort sortset alpha  -- 需要使用alpha选项
1) "aimee"
2) "ben"
3) "cario"

redis> sort sliststr alpha limit 2 1  -- SORT key [LIMIT offset count],offset参数用于指定返回结果之前需要跳过的元素数量,而count参数则用于指定需要获取的元素数量
1) "c"
获取外部键的值作为结果

如果你有一个set集合,而有一部分string的键刚好根据集合里面的键创建的,那么就可以直接使用对集合里面的元素排序的结果来查询string的值。SORT key [[GET pattern] [GET pattern] ...]

一个SORT命令可以使用任意多个GET pattern选项,其中pattern参数的值可以是:

  • 包含*符号的字符串。
  • 包含*符号和->符号的字符串。
  • 一个单独的#符号。

pattern参数的值是一个包含*符号的字符串时,SORT命令将把被排序的元素与*符号进行替换,构建出一个键名,然后使用GET命令去获取该键的值。

代码语言:shell复制
redis> keys str-*  -- 查出刚刚新建的以str-开头的string键,set str-a 150
1) "str-e"
2) "str-c"
3) "str-d"
4) "str-b"
5) "str-a"

redis> sort sliststr alpha  -- 查出sliststr字符串集合的值,lpush sliststr "a" "b" "c" "d" "e"
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"

redis> sort sliststr alpha get str-*  -- 根据sliststr的排序的值查出str-*的值
1) "150"
2) "200"
3) "300"
4) "250"
5) "50"
获取散列中的键值

pattern参数的值是一个包含*符号和->符号的字符串时,SORT命令将使用->左边的字符串为散列名,->右边的字符串为字段名,调用HGET命令,从散列中获取指定字段的值。此外,用户传入的散列名还需要包含*符号,这个*符号将被替换成被排序的元素。

代码语言:shell复制
redis> keys hash-*  -- 查出新建的以hash-开头的散列的键,创建hash命令,hset hash-a price 180
1) "hash-e"
2) "hash-a"
3) "hash-b"
4) "hash-d"
5) "hash-c"

redis> sort sliststr alpha get hash-*->price  -- 根据sliststr排序后的值查出hash-*的值
1) "180"
2) "140"
3) "240"
4) "190"
5) "270"
获取被排序元素本身

pattern参数的值是一个#符号时,SORT命令将返回被排序的元素本身。

我们一般只会在同时使用多个GET选项时,才使用GET#获取被排序的元素。

代码语言:shell复制
redis> sort sliststr alpha get #  -- 与 SORT sliststr ALPHA 命令的结果完全相同
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"

redis> sort sliststr alpha get # get str-* get hash-*->price  -- 这样就可以同时查出多个与a相关联的值
 1) "a"  -- sliststr 的值
 2) "150"  -- str-a 的值
 3) "180" -- hash-a的值
 4) "b"
 5) "200"
 6) "140"
 7) "c"
 8) "300"
 9) "240"
10) "d"
11) "250"
12) "190"
13) "e"
14) "50"
15) "270"
使用外部键的值作为排序权重

在默认情况下,SORT命令将使用被排序元素本身作为排序权重,但在有需要时,用户可以通过可选的BY选项指定其他键的值作为排序的权重:SORT key [BY pattern]

pattern参数的值既可以是包含*符号的字符串,也可以是包含*符号和->符号的字符串,这两种值的作用和效果与使用GET选项的作用和效果一样:前者用于获取字符串键的值,而后者则用于从散列中获取指定字段的值。

代码语言:shell复制
redis> sort sliststr by str-*  -- 也可以使用-> ,使用hash散列作为权重
1) "e"
2) "a"
3) "b"
4) "d"
5) "c"

redis> sort sliststr by str-* get # get str-*  -- 与上一个命令排序结果一致,把排序的依据str-*也展示出来了
 1) "e"
 2) "50"
 3) "a"
 4) "150"
 5) "b"
 6) "200"
 7) "d"
 8) "250"
 9) "c"
10) "300"
保存排序结果

SORT命令会直接将排序结果返回给客户端,但如果用户有需要,也可以通过可选的STORE选项,以列表形式将排序结果存储到指定的键中:SORT key [STORE destination]

代码语言:shell复制
redis> sort sliststr alpha store sorted-abc
(integer) 5

redis> type sorted-abc
list

redis> lrange sorted-abc 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
3.1.6、EXISTS:检查给定键是否存在

用户可以通过使用EXISTS命令,检查给定的一个或多个键是否存在于当前正在使用的数据库中:EXISTS key [key ...]

代码语言:shell复制
redis> exists s1 s2 slistint
(integer) 2

redis> exists s2 
(integer) 0  -- 不存在
3.1.7、DBSIZE:获取数据库包含的键值对数量

用户可以通过执行DBSIZE命令来获知当前使用的数据库包含了多少个键值对:DBSIZE

代码语言:shell复制
redis> dbsize  -- 数据库目前包含了21个键值对
(integer) 21
3.1.8、TYPE:查看键的类型

TYPE命令允许我们查看给定键的类型:TYPE key

代码语言:shell复制
redis> type s1
stream
redis-type.pngredis-type.png

TYPE命令对于字符串键、散列键、列表键、集合键和流键的返回结果都非常直观,不过它对于之后几种类型的键的返回结果则需要做进一步解释:

  • 因为所有有序集合命令,比如ZADDZREMZSCORE等,都是以z为前缀命名的,所以有序集合也被称为zset。因此TYPE命令在接收到有序集合键作为输入时,将返回zset作为结果。
  • 因为HyperLogLog和位图这两种键在底层都是通过字符串键来实现的,所以TYPE命令对于这两种键将返回string作为结果。
  • HyperLogLog和位图的情况类似,因为地理位置键使用了有序集合键作为底层实现,所以TYPE命令对于地理位置键将返回zset作为结果。
3.1.9、RENAME、RENAMENX:修改键名

Redis提供了RENAME命令,用户可以使用这个命令修改键的名称:RENAME origin new

代码语言:shell复制
redis> get str
"100"

redis> rename str score
OK

redis> get str
(nil)

redis> get score
"100"

redis> set sc 90
OK

redis> rename sc score  -- 的新键名已经被占用,那么RENAME命令会先移除占用了新键名的那个键,然后再执行改名操作。
OK

redis> get score   -- 新值
"90"
只在新键名尚未被占用的情况下进行改名

RENAMENX命令只会在新键名尚未被占用的情况下进行改名,如果用户指定的新键名已经被占用,那么RENAMENX将放弃执行改名操作:RENAMENX origin new

代码语言:shell复制
redis> set sc 60
OK

redis> renamenx sc score  
(integer) 0  -- 改名失败

redis> renamenx sc score1
(integer) 1  -- 因为score1键不存在,所以改名成功
3.1.10、MOVE:将给定的键移动到另一个数据库

用户可以使用MOVE命令,将一个键从当前数据库移动至目标数据库:MOVE key db

代码语言:shell复制
redis> get score1
"60"

redis> move score1 3
(integer) 1

redis> get score1
(nil)

redis> select 3
OK

redis[3]> get score1
"60"

redis> select 0
OK

redis> set score1 80
OK

redis> move score1 3  -- db3中已经有score1的键存在,所以move失败
(integer) 0
3.1.11、DEL:移除指定的键

DEL命令允许用户从当前正在使用的数据库中移除指定的一个或多个键,以及与这些键相关联的值:DEL key [key ...]

代码语言:shell复制
redis> DEL k1 k2
(integer) 2 -- 有两个键被移除了
3.1.12、UNLINK:以异步方式移除指定的键

使用DEL命令去移除指定的键,这个命令实际上隐含着一个性能问题:因为DEL命令会以同步方式执行移除操作,所以如果待移除的键非常庞大或者数量众多,那么服务器在执行移除操作的过程中就有可能被阻塞。

UNLINK key [key ...]

UNLINK命令与DEL命令一样,都可以用于移除指定的键,但它与DEL命令的区别在于,当用户调用UNLINK命令去移除一个数据库键时,UNLINK只会在数据库中移除对该键的引用(reference),而对键的实际移除操作则会交给后台线程执行,因此UNLINK命令将不会造成服务器阻塞。

代码语言:shell复制
redis> MGET k1 k2 k3
1) "v1"
2) "v2"
3) "v3"

redis> UNLINK k1 k2 k3
(integer) 3

redis> MGET k1 k2 k3
1) (nil)
2) (nil)
3) (nil)
3.1.13、FLUSHDB:清空当前数据库

通过使用FLUSHDB命令,用户可以清空当前正在使用的数据库:

代码语言:shell复制
redis> FLUSHDB
OK

DEL命令一样,FLUSHDB命令也是一个同步移除命令,并且因为FLUSHDB移除的是整个数据库而不是单个键,所以它常常会引发比DEL命令更为严重的服务器阻塞现象。

代码语言:shell复制
redis> FLUSHDB async
OK

在调用FLUSHDB命令时使用了async选项,那么实际的数据库清空操作将放在后台线程中以异步方式进行,这样FLUSHDB命令就不会再阻塞服务器了。

3.1.14、FLUSHALL:清空所有数据库

过使用FLUSHALL命令,用户可以清空Redis服务器包含的所有数据库:

代码语言:shell复制
redis> FLUSHALL
OK

FLUSHDB命令一样,以同步方式执行的FLUSHALL命令也可能会导致服务器阻塞,因此Redis 4.0也给FLUSHALL命令添加了同样的async选项:

代码语言:shell复制
redis> FLUSHALL async
OK

通过指定async选项,FLUSHALL命令将以异步方式在后台线程中执行所有实际的数据库清空操作,因此它将不会再阻塞服务器。

3.1.15、SWAPDB:互换数据库

SWAPDB命令接受两个数据库号码作为输入,然后对指定的两个数据库进行互换,最后返回OK作为结果:SWAPDB x y

代码语言:shell复制
redis> KEYS *
1) "k3"
2) "k2"
3) "k1"

redis> select 1
OK

redis[1]> KEYS *
1) "k5"
2) "k4"
3) "k6"

redis[1]> select 0
OK

redis> SWAPDB 0 1  -- 互换数据库这一操作可以通过调整指向数据库的指针来实现,这个过程不需要移动数据库中的任何键值对
OK

redis> KEYS *
1) "k5"
2) "k4"
3) "k6"

redis> select 1
OK

redis[1]> KEYS *
1) "k3"
2) "k2"
3) "k1"

3.2、自动过期

3.2.1、EXPIRE、PEXPIRE:设置生存时间

用户可以通过执行EXPIRE命令或者PEXPIRE命令为键设置一个生存时间(Time To Live,TTL):键的生存时间在设置之后就会随着时间的流逝而不断地减少,当一个键的生存时间被消耗殆尽时,Redis就会移除这个键。

Redis提供了EXPIRE命令用于设置秒级精度的生存时间,它可以让键在指定的秒数之后自动被移除:EXPIRE key seconds

PEXPIRE命令则用于设置毫秒级精度的生存时间,它可以让键在指定的毫秒数之后自动被移除:PEXPIRE key milliseconds

代码语言:shell复制
redis> SET msg "hello world"
OK

redis> EXPIRE msg 5  -- expire
(integer) 1

redis> GET msg -- 在5s之内访问,键存在
"hello world"

redis> GET msg -- 在5s之后访问,键不再存在
(nil)

redis> SET number 10086
OK

redis> PEXPIRE number 6500  -- pexpire
(integer) 1

redis> GET number -- 在6500ms(即6.5s)之内访问,键存在
"10086"

redis> GET number -- 在6500ms之后访问,键不再存在
(nil)
SET命令的EX选项和PX选项

SET key value [EX seconds] [PX milliseconds], SET key value PX milliseconds

使用带有EX选项或PX选项的SET命令除了可以减少命令的调用数量并提升程序的执行速度之外,更重要的是保证了操作的原子性,使得“为键设置值”和“为键设置生存时间”这两个操作可以一起执行。

当服务器成功执行了一条带有EX选项或PX选项的SET命令时,键的值和生存时间都会同时被设置好,因此程序就不会出现只设置了值但是却没有设置生存时间的情况。

EXPIREAT、PEXPIREAT:设置过期时间

Redis用户不仅可以通过设置生存时间来让键在指定的秒数或毫秒数之后自动被移除,还可以通过设置过期时间(expire time),让Redis在指定UNIX时间来临之后自动移除给定的键。

设置过期时间这一操作可以通过EXPIREAT命令或者PEXPIREAT命令来完成。其中,EXPIREAT命令接受一个键和一个秒级精度的UNIX时间戳为参数,当系统的当前UNIX时间超过命令指定的UNIX时间时,给定的键就会被移除:

EXPIREAT key seconds_timestamp, PEXPIREAT key milliseconds_timestamp

如果我们想要让msg键在UNIX时间1450005000s之后不再存在,那么可以执行以下命令:

代码语言:shell复制
redis> EXPIREAT msg 1450005000
(integer) 1
3.2.2、TTL、PTTL:获取键的剩余生存时间

在为键设置了生存时间或者过期时间之后,用户可以使用TTL命令或者PTTL命令查看键的剩余生存时间,即键还有多久才会因为过期而被移除。

其中,TTL命令将以秒为单位返回键的剩余生存时间:TTL key

PTTL命令则会以毫秒为单位返回键的剩余生存时间:PTTL key

代码语言:shell复制
redis> TTL msg
(integer) 297 -- msg键距离被移除还有297s

redis> PTTL msg
(integer) 295561 -- msg键距离被移除还有295561ms

redis> TTL song_title -- 键存在,但是并没有设置生存时间或者过期时间
(integer) -1

3.3、流水线与事务

3.3.1、流水线

在一般情况下,用户每执行一个Redis命令,Redis客户端和Redis服务器就需要执行以下步骤:

1) 客户端向服务器发送命令请求。

2) 服务器接收命令请求,并执行用户指定的命令调用,然后产生相应的命令执行结果。

3) 服务器向客户端返回命令的执行结果。

4) 客户端接收命令的执行结果,并向用户进行展示。

与大多数网络程序一样,执行Redis命令所消耗的大部分时间都用在了发送命令请求和接收命令结果上面:Redis服务器处理一个命令请求通常只需要很短的时间,但客户端将命令请求发送给服务器以及服务器向客户端返回命令结果的过程却需要花费不少时间。通常情况下,程序需要执行的Redis命令越多,它需要进行的网络通信操作也会越多,程序的执行速度也会因此而变慢。为了解决这个问题,我们可以使用Redis提供的流水线特性:这个特性允许客户端把任意多条Redis命令请求打包在一起,然后一次性地将它们全部发送给服务器,而服务器则会在流水线包含的所有命令请求都处理完毕之后,一次性地将它们的执行结果全部返回给客户端。

通过使用流水线特性,我们可以将执行多个命令所需的网络通信次数从原来的N次降低为1次,这可以大幅度地减少程序在网络通信方面耗费的时间,使得程序的执行效率得到显著的提升。

流水线使用注意事项

虽然Redis服务器并不会限制客户端在流水线中包含的命令数量,但是却会为客户端的输入缓冲区设置默认值为1GB的体积上限:当客户端发送的数据量超过这一限制时,Redis服务器将强制关闭该客户端。因此用户在使用流水线特性时,最好不要一下把大量命令或者一些体积非常庞大的命令放到同一个流水线中执行,以免触碰到Redis的这一限制。

除此之外,很多客户端本身也带有隐含的缓冲区大小限制,如果你在使用流水线特性的过程中,发现某些流水线命令没有被执行,或者流水线返回的结果不完整,那么很可能就是你的程序触碰到了客户端内置的缓冲区大小限制。在遇到这种情况时,请缩减流水线命令的数量及其体积,然后再进行尝试。

流水线只能保证多条命令会一起被发送至服务器,但它并不保证这些命令都会被服务器执行。

3.3.2、事务

每个redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate属性里面:

server.h

代码语言:c复制
typedef struct client {
  // ...
  // 事务状态
  multiState mstate;      /* MULTI/EXEC state */
  // ...
} client;

事务状态包含一个事务队列,以及一个已入队命令的计数器(事务队列的长度):

server.h

代码语言:c复制
typedef struct multiState {
  // 事务队列,FIFO顺序
    multiCmd *commands;     /* Array of MULTI commands */
  // 已入队命令计数  
    int count;              /* Total number of MULTI commands */
    int cmd_flags;          /* The accumulated command flags OR-ed together.
                               So if at least a command has a given flag, it
                               will be set in this field. */
    int cmd_inv_flags;      /* Same as cmd_flags, OR-ing the ~flags. so that it
                               is possible to know if all the commands have a
                               certain flag. */
} multiState;

事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量:

server.h

代码语言:c复制
/* Client MULTI/EXEC state */
typedef struct multiCmd {
  // 参数
    robj **argv;
    // 参数数量
    int argc;
    // 命令指针
    struct redisCommand *cmd;
} multiCmd;

事务队列以先进先出(FIFO)的方式保存入队的命令,较先入队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面。

  • 事务可以将多个命令打包成一个命令来执行,当事务成功执行时,事务中包含的所有命令都会被执行。
  • 相反,如果事务没有成功执行,那么它包含的所有命令都不会被执行。
redis-事务.pngredis-事务.png
MULTI:开启事务

用户可以通过执行MULTI命令来开启一个新的事务,这个命令在成功执行之后将返回OK:MULTI

代码语言:shell复制
redis> MULTI
OK

redis> SET title "Hand in Hand"  -- 服务器在把客户端发送的命令放入事务队列之后,会向客户端返回一个QUEUED作为结果。
QUEUED

redis> SADD fruits "apple" "banana" "cherry"
QUEUED

redis> RPUSH numbers 123 456 789
QUEUED
EXEC:执行事务

在使用MULTI命令开启事务并将任意多个命令放入事务队列之后,用户就可以通过执行EXEC命令来执行事务了:EXEC

代码语言:shell复制
redis> MULTI -- 1) 开启事务
OK

redis> SET title "Hand in Hand" -- 2) 命令入队
QUEUED

redis> SADD fruits "apple" "banana" "cherry"
QUEUED

redis> RPUSH numbers 123 456 789
QUEUED

redis> EXEC -- 3)执行事务
1) OK -- SET命令的执行结果
2) (integer) 3 -- SADD命令的执行结果
3) (integer) 3 -- RPUSH命令的执行结果
DISCARD:放弃事务

如果用户在开启事务之后,不想执行事务而是想放弃事务,那么只需要执行以下命令即可:DISCARD

代码语言:shell复制
redis> MULTI
OK

redis> SET page_counter 10086
QUEUED

redis> SET download_counter 12345
QUEUED

redis> DISCARD
OK
事务的安全性

具体来说,Redis的事务总是具有ACID性质中的A、C、I性质:

  • 原子性(Atomic):如果事务成功执行,那么事务中包含的所有命令都会被执行;相反,如果事务执行失败,那么事务中包含的所有命令都不会被执行。
  • 一致性(Consistent):Redis服务器会对事务及其包含的命令进行检查,确保无论事务是否执行成功,事务本身都不会对数据库造成破坏。
  • 隔离性(Isolate):每个Redis客户端都拥有自己独立的事务队列,并且每个Redis事务都是独立执行的,不同事务之间不会互相干扰。 除此之外,当Redis服务器运行在特定的持久化模式之下时,Redis的事务也具有ACID性质中的D性质:
  • 耐久性(Durable):当事务执行完毕时,它的结果将被存储在硬盘中,即使服务器在此之后停机,事务对数据库所做的修改也不会丢失。
事务对服务器的影响

因为事务在执行时会独占服务器,所以用户应该避免在事务中执行过多命令,更不要将一些需要进行大量计算的命令放入事务中,以免造成服务器阻塞。

流水线与事务

正如前面所言,流水线与事务虽然在概念上有些相似,但是在作用上却并不相同:流水线的作用是将多个命令打包,然后一并发送至服务器,而事务的作用则是将多个命令打包,然后让服务器一并执行它们。

因为Redis的事务在EXEC命令执行之前并不会产生实际效果,所以很多Redis客户端都会使用流水线去包裹事务命令,并将入队的命令缓存在本地,等到用户输入EXEC命令之后,再将所有事务命令通过流水线一并发送至服务器,这样客户端在执行事务时就可以达到“打包发送,打包执行”的最优效果。

3.3.3、带有乐观锁的事务
WATCH:对键进行监视

每个redis数据库都保存着一个watched_keys字典,这个字典的键是某个被watch命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端。

server.h

代码语言:c复制
typedef struct redisDb {
  // ...
  // 正在被 watch 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
  // ...
} redisDb;

通过watched_keys字典,服务器可以清楚地知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键,

客户端可以通过执行WATCH命令,要求服务器对一个或多个数据库键进行监视,如果在客户端尝试执行事务之前,这些键的值发生了变化,那么服务器将拒绝执行客户端发送的事务,并向它返回一个空值:

WATCH key [key ...]

通过同时使用WATCH命令和Redis事务,我们可以构建出一种针对被监视键的乐观锁机制,确保事务只会在被监视键没有发生任何变化的情况下执行,从而保证事务对被监视键的所有修改都是安全、正确和有效的。

代码语言:shell复制
redis> WATCH user_id_counter
OK

redis> GET user_id_counter -- 获取当前最新的用户ID
"256"

redis> MULTI
OK

redis> SET user::256::email "peter@spamer.com" -- 尝试使用这个ID来存储用户信息
QUEUED

redis> SET user::256::password "topsecret"
QUEUED

redis> INCR user_id_counter -- 创建新的用户ID
QUEUED

-- 使用其他窗口或者客户端执行一条修改user_id_counter的命令:set user_id_counter 128

redis> EXEC
(nil) -- user_id_counter键已被修改
UNWATCH:取消对键的监视

客户端可以通过执行UNWATCH命令,取消对所有键的监视:UNWATCH

代码语言:shell复制
redis> WATCH "lock_key" "user_id_counter" "msg"
OK

redis> UNWATCH -- 取消对以上3个键的监视

除了显式地执行UNWATCH命令之外,使用EXEC命令执行事务和使用DISCARD命令取消事务,同样会导致客户端撤销对所有键的监视,这是因为这两个命令在执行之后都会隐式地调用UNWATCH命令。

3.4 持久化

Redis与传统数据库的一个主要区别在于,Redis把所有数据都存储在内存中,而传统数据库通常只会把数据的索引存储在内存中,并将实际的数据存储在硬盘中。

虽然Redis的数据存储方式使得用户可以以极快的速度读写服务器中的数据,但由于内存属于易失存储器(volatile storage),它记录的所有数据在系统断电之后就会丢失,这对于想把Redis用作数据库而不仅仅是缓存的用户来说是不愿意看到的。

为了解决上述问题,Redis向用户提供了持久化功能,这一功能可以把内存中存储的数据以文件形式存储到硬盘上,而服务器也可以根据这些文件在系统停机之后实施数据恢复,让服务器的数据库重新回到停机之前的状态。

为了满足不同的持久化需求,Redis提供了RDB持久化、AOF持久化和RDB-AOF混合持久化等多种持久化方式以供用户选择。如果用户有需要,也可以完全关闭持久化功能,让服务器处于无持久化状态。

3.4.1 RDB持久化

RDB持久化是Redis默认使用的持久化功能,该功能可以创建出一个经过压缩的二进制文件,其中包含了服务器在各个数据库中存储的键值对数据等信息。RDB持久化产生的文件都以.rdb后缀结尾,其中rdb代表Redis DataBase(Redis数据库)。

SAVE:阻塞服务器并创建RDB文件

用户可以通过执行SAVE命令,要求Redis服务器以同步方式创建出一个记录了服务器当前所有数据库数据的RDB文件。SAVE命令是一个无参数命令,它在创建RDB文件成功时将返回OK作为结果:

代码语言:shell复制
redis> SAVE
OK

接收到SAVE命令的Redis服务器将遍历数据库包含的所有数据库,并将各个数据库包含的键值对全部记录到RDB文件中。在SAVE命令执行期间,Redis服务器将阻塞,直到RDB文件创建完毕为止。如果Redis服务器在执行SAVE命令时已经拥有了相应的RDB文件,那么服务器将使用新创建的RDB文件代替已有的RDB文件。

redis-save-rdb.pngredis-save-rdb.png
BGSAVE:以非阻塞方式创建RDB文件

因为SAVE命令在执行时会阻塞整个服务器,所以用户在使用该命令创建RDB文件期间将无法为其他客户端提供服务。为了解决这个问题,Redis提供了SAVE命令的异步版本BGSAVE命令:这个命令与SAVE命令一样都是无参数命令,它与SAVE命令的不同之处在于,BGSAVE不会直接使用Redis服务器进程创建RDB文件,而是使用子进程创建RDB文件。

当Redis服务器接收到用户发送的BGSAVE命令时,将执行以下操作:

1) 创建一个子进程。

2) 子进程执行SAVE命令,创建新的RDB文件。

3) RDB文件创建完毕之后,子进程退出并通知Redis服务器进程(父进程)新RDB文件已经完成。

4) Redis服务器进程使用新RDB文件替换已有的RDB文件。

因为BGSAVE命令创建RDB文件的操作是由子进程以异步方式执行的,所以当用户在客户端执行这个命令时,服务器将立即向客户端返回OK,然后才会在后台开始具体的RDB文件创建操作:

代码语言:shell复制
redis> BGSAVE
Background saving started

因为BGSAVE命令是以异步方式执行的,所以Redis服务器在BGSAVE命令执行期间仍然可以继续处理其他客户端发送的命令请求。不过需要注意的是,虽然BGSAVE命令不会像SAVE命令那样一直阻塞Redis服务器,但由于执行BGSAVE命令需要创建子进程,所以父进程占用的内存数量越大,创建子进程这一操作耗费的时间也会越长,因此Redis服务器在执行BGSAVE命令时,仍然可能会由于创建子进程而被短暂地阻塞。

通过配置选项自动创建RDB文件

用户除了可以使用SAVE命令和BGSAVE命令手动创建RDB文件之外,还可以通过设置save选项,让Redis服务器在满足指定条件时自动执行BGSAVE命令:

save <seconds> <changes>

redis-save-change-rdb.pngredis-save-change-rdb.png

save选项接受secondschanges两个参数,前者用于指定触发持久化操作所需的时长,而后者则用于指定触发持久化操作所需的修改次数。简单来说,如果服务器在seconds秒之内,对其包含的各个数据库总共执行了至少changes次修改,那么服务器将自动执行一次BGSAVE命令。

1.同时使用多个save选项

Redis允许用户同时向服务器提供多个save选项,当给定选项中的任意一个条件被满足时,服务器就会执行一次BGSAVE

比如,如果我们向服务器提供以下选项:

代码语言:txt复制
save 6000 1
save 600 100
save 60 10000

那么当以下任意一个条件被满足时,服务器就会执行一次BGSAVE命令:

  • 在6000s(100min)之内,服务器对数据库执行了至少1次修改。
  • 在600s(10min)之内,服务器对数据库执行了至少100次修改。
  • 在60s(1min)之内,服务器对数据库执行了至少10000次修改。

注意,为了避免由于同时使用多个触发条件而导致服务器过于频繁地执行BGSAVE命令,Redis服务器在每次成功创建RDB文件之后,负责自动触发BGSAVE命令的时间计数器以及修改次数计数器都会被清零并重新开始计数:无论这个RDB文件是由自动触发的BGSAVE命令创建的,还是由用户执行的SAVE命令或BGSAVE命令创建的,都是如此。

3.默认设置

RDB持久化是Redis默认使用的持久化方式,如果用户在启动Redis服务器时,既没有显式地关闭RDB持久化功能,也没有启用AOF持久化功能,那么Redis默认将使用以下save选项进行RDB持久化:

代码语言:txt复制
save 60 10000
save 300 100
save 3600 1
SAVE命令和BGSAVE命令的选择

因为SAVE命令在创建RDB文件期间会阻塞Redis服务器,所以如果我们需要在创建RDB文件的同时让Redis服务器继续为其他客户端服务,那么就只能使用BGSAVE命令来创建RDB文件。

因为SAVE命令无须创建子进程,它不会因为创建子进程而消耗额外的内存,所以在维护离线的Redis服务器时,使用SAVE命令能够比使用BGSAVE命令更快地完成创建RDB文件的工作。

RDB文件结构
1、总体结构

RDB文件的总体结构,整个文件共分为7个部分。

代码语言:text复制
$ od -c dump.rdb
redis-rdb.pngredis-rdb.png
  • RDB文件标识符 文件最开头的部分为RDB文件标识符,这个标识符的内容为"REDIS"这5个字符。Redis服务器在尝试载入RDB文件的时候,可以通过这个标识符快速地判断该文件是否为真正的RDB文件。
  • 版本号 跟在RDB文件标识符之后的是RDB文件的版本号,这个版本号是一个字符串格式的数字,长度为4个字符。目前最新的RDB文件版本为第9版,因此RDB文件的版本号将为字符串"0009"。不同版本的RDB文件在结构上都会有一些不同,总的来说,新版RDB文件都会在旧版RDB文件的基础上添加更多信息,因此RDB文件的版本越新,RDB文件的结构就越复杂。 关于RDB文件,需要说明的另外一点是新版Redis服务器总是能够向下兼容旧版Redis服务器生成的RDB文件。比如,生成第9版RDB文件的Redis 5.0既能够正常读入由Redis 4.0生成的第8版RDB文件,也能够读入由Redis 3.2生成的第7版RDB文件,甚至更旧版本的RDB文件也是可以的。与此相反,如果Redis服务器生成的是较旧版本的RDB文件,那么它是无法读入更新版本的RDB文件的。比如,生成第8版RDB文件的Redis 4.0就不能读入由Redis 5.0生成的第9版RDB文件。
  • 设备附加信息 RDB文件的设备附加信息部分记录了生成RDB文件的Redis服务器及其所在平台的信息,比如服务器的版本号、宿主机器的架构、创建RDB文件时的时间戳、服务器占用的内存数量等。
  • 数据库数据 RDB文件的数据库数据部分记录了Redis服务器存储的0个或任意多个数据库的数据,当这个部分包含多数个数据库的数据时,各个数据库的数据将按照数据库号码从小到大进行排列,比如,0号数据库的数据将排在最前面,紧接着是1号数据库的数据,然后是2号数据库的数据,以此类推。
  • Lua脚本缓存 如果Redis服务器启用了复制功能,那么服务器将在RDB文件的Lua脚本缓存部分保存所有已被缓存的Lua脚本。这样一来,从服务器在载入RDB文件完成数据同步之后,就可以继续执行主服务器发来的EVALSHA命令了。
  • EOF RDB文件的EOF部分用于标识RDB正文内容的末尾,它的实际值为二进制值0xFF。当Redis服务器读取到EOF的时候,它知道RDB文件的正文部分已经全部读取完毕了。
  • CRC64校验和 RDB文件的末尾是一个以无符号64位整数表示的CRC64校验和,比如5097628732947693614。Redis服务器在读入RDB文件时会通过这个校验和来快速地检查RDB文件是否有出错或者损坏的情况出现。

redis本身自带rdb文件检测工具redis-check-rdb

2、数据库信息结构
redis-rdb-db.pngredis-rdb-db.png

首先,第一部分以数字形式记录了数据库的号码,Redis服务器在读入RDB文件数据时,会根据这个号码切换至相应的数据库,从而确保键值对会被载入正确的数据库中。

然后,RDB文件会使用两个数字,分别记录数据库包含的键值对总数量以及数据库中带有过期时间的键值对数量。Redis服务器将根据这两个数字,以尽可能优化的方式创建数据库的内部数据结构。

最后,RDB文件将以无序方式记录数据库包含的所有键值对。

redis-rdb-key.pngredis-rdb-key.png

每个键值对开头的第一部分记录的是可能存在的过期时间,这是一个毫秒级精度的UNIX时间戳。

之后的LRU信息或者LFU信息分别用于实现可选的LRU算法或者LFU算法,并且因为Redis只能选择一种键淘汰算法,所以这两项信息将不会同时出现,最多只会出现其中一种。

至于最后三个部分则分别记录了键值对的类型(比如字符串、列表、散列等)以及键和值。

载入RDB文件

首先,当Redis服务器启动时,它会在工作目录中查找是否有RDB文件出现,如果有就打开它,然后读取文件的内容并执行以下载入操作:

1) 检查文件开头的标识符是否为"REDIS",如果是则继续执行后续的载入操作,不是则抛出错误并终止载入操作。

2) 检查文件的RDB版本号,以此来判断当前Redis服务器能否读取这一版本的RDB文件。

3) 根据文件中记录的设备附加信息,执行相应的操作和设置。

4) 检查文件的数据库数据部分是否为空,如果不为空就执行以下子操作:

①根据文件记录的数据库号码,切换至正确的数据库。

②根据文件记录的键值对总数量以及带有过期时间的键值对数量,设置数据库底层数 据结构。

③一个接一个地载入文件记录的所有键值对数据,并在数据库中重建这些键值对。

5) 如果服务器启用了复制功能,那么将之前缓存的Lua脚本重新载入缓存中。

6) 遇到EOF标识,确认RDB正文已经全部读取完毕。

7) 载入RDB文件末尾记录的CRC64校验和,把它与载入数据期间计算出的CRC64校验和进行对比,以此来判断被载入的数据是否完好无损。

8) RDB文件载入完毕,服务器开始接受客户端请求。

redis-rdb-load.pngredis-rdb-load.png
数据丢失

RDB文件记录的是服务器在开始创建文件的那一刻,服务器中包含的所有键值对数据,这种数据持久化方式通常被称为时间点快照(point-in-time snapshot)。时间点快照持久化的一个特点是,系统在停机时将丢失最后一次成功实施持久化之后的所有数据。对于一个只使用RDB持久化的Redis服务器来说,服务器停机时丢失的数据量将取决于最后一次成功执行的RDB持久化操作,以及该操作开始执行的时间。

因为Redis允许使用SAVEBGSAVE这两种命令来执行RDB持久化操作,所以接下来将分别分析这两个命令在遭遇故障停机时的表现。

1.SAVE命令的停机情况

因为SAVE命令是一个同步操作,它的开始和结束都位于同一个原子时间之内,所以如果用户使用SAVE命令进行持久化,那么服务器在停机时将丢失最后一次成功执行SAVE命令之后产生的所有数据。

redis-save-halt.pngredis-save-halt.png
  • 因为服务器最后一次成功执行SAVE命令是在T7,所以服务器创建出的RDB文件将包含键k1至键k4在内的数据,服务器在重启时将使用这个RDB文件进行数据恢复。
  • 因为服务器在T7之后创建了键k5和键k6,并在之后出现停机,所以当服务器重启时,键k5、k6的数据将丢失,而键k1至键k4的数据将被恢复。
3.BGSAVE命令的停机情况

因为BGSAVE命令是一个异步命令,它的开始和结束并不位于同一个原子时间之内,所以如果用户使用BGSAVE命令进行持久化,那么服务器在停机时丢失的数据量将取决于最后一次成功执行的BGSAVE命令的开始时间。

redis-bgsave-halt.pngredis-bgsave-halt.png
  • 因为T7创建的新RDB文件尚未完成,所以服务器在停机之后将使用T5成功创建的RDB文件进行数据恢复。
  • 虽然服务器现有的RDB文件是在T5成功创建的,但由于这个文件是在T3开始创建的,所以它只包含了T3之前的数据,即键k1和键k2的数据。
  • 基于上述原因,当服务器重启时,只有键k1和键k2的数据会被恢复,而键k3至键k6的数据则会丢失。
3.RDB持久化的缺陷

无论用户使用的是SAVE命令还是BGSAVE命令,停机时服务器丢失的数据量将取决于创建RDB文件的时间间隔:间隔越长,停机时丢失的数据也就越多。

然而矛盾之处在于,RDB持久化是一种全量持久化操作,它在创建RDB文件时需要存储整个服务器包含的所有数据,并因此消耗大量计算资源和内存资源,所以用户是不太可能通过增大RDB文件的生成频率来保证数据安全的。

3.4.2、AOF持久化

与全量式的RDB持久化功能不同,AOF提供的是增量式的持久化功能,这种持久化的核心原理在于:服务器每次执行完写命令之后,都会以协议文本的方式将被执行的命令追加到AOF文件的末尾。这样一来,服务器在停机之后,只要重新执行AOF文件中保存的Redis命令,就可以将数据库恢复至停机之前的状态。

redis-aof-create.pngredis-aof-create.png

用户可以通过服务器的appendonly选项来决定是否打开AOF持久化功能:appendonly <value>

开启AOF持久化功能,只需要将这个值设置为yes即可,appendonly yes,关闭AOF持久化功能,那么只需要将这个值设置为no即可.

设置AOF文件的冲洗频率

为了提高程序的写入性能,现代化的操作系统通常会把针对硬盘的多次写操作优化为一次写操作。当程序调用write系统调用对文件进行写入时,系统并不会直接把数据写入硬盘,而是会先将数据写入位于内存的缓冲区中,等到指定的时限到达或者满足某些写入条件时,系统才会执行flush系统调用,将缓冲区中的数据冲洗至硬盘。

Redis向用户提供了appendfsync选项,以此来控制系统冲洗AOF文件的频率:appendfsync<value>

appendfsync选项拥有alwayseverysecno 3个值可选,它们代表的意义分别为:

  • always——每执行一个写命令,就对AOF文件执行一次冲洗操作。
  • everysec——每隔1s,就对AOF文件执行一次冲洗操作。
  • no——不主动对AOF文件执行冲洗操作,由操作系统决定何时对AOF进行冲洗。

这3种不同的冲洗策略不仅会直接影响服务器在停机时丢失的数据量,还会影响服务器在运行时的性能:

  • 在使用always值的情况下,服务器在停机时最多只会丢失一个命令的数据,但使用这种冲洗方式将使Redis服务器的性能降低至传统关系数据库的水平。
  • 在使用everysec值的情况下,服务器在停机时最多只会丢失1s之内产生的命令数据,这是一种兼顾性能和安全性的折中方案。
  • 在使用no值的情况下,服务器在停机时将丢失系统最后一次冲洗AOF文件之后产生的所有命令数据,至于数据量的具体大小则取决于系统冲洗AOF文件的频率。
AOF重写

如果服务器曾经对相同的键执行过多次修改操作,那么AOF文件中还会出现多个冗余命令。

冗余命令的存在不仅增加了AOF文件的体积,并且因为Redis服务器在停机之后需要通过重新执行AOF文件中保存的命令来恢复数据,所以AOF文件中的冗余命令越多,恢复数据时耗费的时间也会越多。为了减少冗余命令,让AOF文件保持“苗条”,并提供数据恢复操作的执行速度,Redis提供了AOF重写功能,该功能能够生成一个全新的AOF文件,并且文件中只包含恢复当前数据库所需的尽可能少的命令。

1.BGREWRITEAOF命令

用户可以通过执行BGREWRITEAOF命令显式地触发AOF重写操作,该命令是一个无参数命令:

代码语言:shell复制
redis> BGREWRITEAOF
Background append only file rewriting started

BGREWRITEAOF命令是一个异步命令,Redis服务器在接收到该命令之后会创建出一个子进程,由它扫描整个数据库并生成新的AOF文件。当新的AOF文件生成完毕,子进程就会退出并通知Redis服务器(父进程),然后Redis服务器就会使用新的AOF文件代替已有的AOF文件,借此完成整个重写操作。

注意:

首先,如果用户发送BGREWRITEAOF命令请求时,服务器正在创建RDB文件,那么服务器将把AOF重写操作延后到RDB文件创建完毕之后再执行,从而避免两个写硬盘操作同时执行导致机器性能下降;其次,如果服务器在执行重写操作的过程中,又接收到了新的BGREWRITEAOF命令请求,那么服务器将返回以下错误:

代码语言:shell复制
redis> BGREWRITEAOF
(error) ERR Background append only file rewriting already in progress
3.AOF重写配置选项

用户除了可以手动执行BGREWRITEAOF命令创建新的AOF文件之外,还可以通过设置以下两个配置选项让Redis自动触发BGREWRITEAOF命令:

代码语言:txt复制
auto-aof-rewrite-min-size <value> 
auto-aof-rewrite-percentage <value>

其中auto-aof-rewrite-min-size选项用于设置触发自动AOF文件重写所需的最小AOF文件体积,当AOF文件的体积小于给定值时,服务器将不会自动执行BGREWRITEAOF命令。在默认情况下,该选项的值为:

auto-aof-rewrite-min-size 64mb

也就是说,如果AOF文件的体积小于64MB,那么Redis将不会自动执行BGREWRI-TEAOF命令。至于另一个选项,它控制的是触发自动AOF文件重写所需的文件体积增大比例。对于该选项的默认值:

auto-aof-rewrite-percentage 100

表示如果当前AOF文件的体积比最后一次AOF文件重写之后的体积增大了一倍(100%),那么将自动执行一次BGREWRITEAOF命令。如果Redis服务器刚刚启动,还没有执行过AOF文件重写操作,那么启动服务器时使用的AOF文件的体积将被用作最后一次AOF文件重写的体积。

AOF持久化的优缺点

与RDB持久化可能会丢失大量数据相比,AOF持久化的安全性要高得多:通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1s之内。

但是与RDB持久化相比,AOF持久化也有相应的缺点:

  • 首先,因为AOF文件存储的是协议文本,所以它的体积会比包含相同数据、二进制格式的RDB文件要大得多,并且生成AOF文件所需的时间也会比生成RDB文件所需的时间更长。
  • 其次,因为RDB持久化可以直接通过RDB文件恢复数据库数据,而AOF持久化则需要通过执行AOF文件中保存的命令来恢复数据库(前者是直接的数据恢复操作,而后者则是间接的数据恢复操作),所以RDB持久化的数据恢复速度将比AOF持久化的数据恢复速度快得多,并且数据库体积越大,这两者之间的差距就会越明显。
  • 最后,因为AOF重写使用的BGREWRITEAOF命令与RDB持久化使用的BGSAVE命令一样都需要创建子进程,所以在数据库体积较大的情况下,进行AOF文件重写将占用大量资源,并导致服务器被短暂地阻塞。
3.4.3、RDB-AOF混合持久化

Redis的两种持久化方式的优点和缺点:

  • RDB持久化可以生成紧凑的RDB文件,并且使用RDB文件进行数据恢复的速度也非常快,但是RDB的全量持久化模式可能会让服务器在停机时丢失大量数据。
  • 与RDB持久化相比,AOF持久化可以将丢失数据的时间窗口限制在1s之内,但是协议文本格式的AOF文件的体积将比RDB文件要大得多,并且数据恢复过程也会相对较慢。
代码语言:shell复制
redis> SET MSG "HELLO WORLD"
OK

redis> SET NUMBER "10086"
OK

redis> SET URL "REDIS.IO"
OK

redis> BGREWRITEAOF -- 触发重写,将之前的键值对存储为RDB格式
Background append only file rewriting started

redis> SADD FRUITS "APPLE" "BANANA" "CHERRY"
(integer) 3

redis> ZADD NUM-LIST 3.14 "PI" 1.28 "X" 2.56 "Y"
(integer) 3

当一个支持RDB-AOF混合持久化模式的Redis服务器启动并载入AOF文件时,它会检查AOF文件的开头是否包含了RDB格式的内容:

  • 如果包含,那么服务器就会先载入开头的RDB数据,然后再载入之后的AOF数据。
  • 如果AOF文件只包含AOF数据,那么服务器将直接载入AOF数据。
redis-save-aof-rdb.pngredis-save-aof-rdb.png

通过使用RDB-AOF混合持久化功能,用户可以同时获得RDB持久化和AOF持久化的优点:服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内。

redis自带检测工具redis-check-aof

3.4.4、同时使用RDB持久化和AOF持久化

在数据持久化这个问题上,Redis 4.0及之后版本的使用者都应该优先使用RDB-AOF混合持久化;对于Redis 4.0之前版本的使用者,因为RDB持久化更接近传统意义上的数据备份功能,而AOF持久化则更接近于传统意义上的数据持久化功能,所以如果用户不知道自己具体应该使用哪种持久化功能,那么可以优先选用AOF持久化作为数据持久化手段,并将RDB持久化用作辅助的数据备份手段。

3.4.5、SHUTDOWN:关闭服务器

用户可以通过执行SHUTDOWN命令来关闭Redis服务器:SHUTDOWN

在默认情况下,当Redis服务器接收到SHUTDOWN命令时,它将执行以下动作:

1) 停止处理客户端发送的命令请求。

2) 根据服务器的持久化配置选项,决定是否执行数据保存操作:

  • 如果服务器启用了RDB持久化功能,并且数据库距离最后一次成功创建RDB文件之后已经发生了改变,那么服务器将执行SAVE命令,创建一个新的RDB文件。
  • 如果服务器启用了AOF持久化功能或者RDB-AOF混合持久化功能,那么它将冲洗AOF文件,确保所有已执行的命令都被记录到了AOF文件中。
  • 如果服务器既没有启用RDB持久化功能,也没有启用AOF持久化功能,那么服务器将略过这一步。 3) 服务器进程退出。

redis-cli 发送以下命令

代码语言:shell复制
redis> shutdown
not connected>

redis-server 的表现如下

代码语言:shell复制
2306821:M 21 Oct 2022 10:21:54.259 # User requested shutdown...
2306821:M 21 Oct 2022 10:21:54.259 * Calling fsync() on the AOF file.
2306821:M 21 Oct 2022 10:21:54.259 * Saving the final RDB snapshot before exiting.
2306821:M 21 Oct 2022 10:21:54.260 * DB saved on disk
2306821:M 21 Oct 2022 10:21:54.260 * Removing the pid file.
2306821:M 21 Oct 2022 10:21:54.261 # Redis is now ready to exit, bye bye...
通过可选项指示持久化操作

在默认情况下,服务器在执行SHUTDOWN命令时,是否执行持久化操作是由服务器的配置选项决定的。

但是在有需要时,用户也可以使用SHUTDOWN命令提供的save选项或者nosave选项,显式地指示服务器在关闭之前是否需要执行持久化操作:SHUTDOWN [save|nosave]

如果用户给定的是save选项,那么无论服务器是否启用了持久化功能,服务器都会在关闭之前执行一次持久化操作。

redis-shutdown-save.pngredis-shutdown-save.png

3.5、发布与订阅

Redis的发布与订阅功能可以让客户端通过广播方式,将消息(message)同时发送给可能存在的多个客户端,并且发送消息的客户端不需要知道接收消息的客户端的具体信息。换句话说,发布消息的客户端与接收消息的客户端两者之间没有直接联系。

在Redis中,客户端可以通过订阅特定的频道(channel)来接收发送至该频道的消息,我们把这些订阅频道的客户端称为订阅者(subscriber)。一个频道可以有任意多个订阅者,而一个订阅者也可以同时订阅任意多个频道。除此之外,客户端还可以通过向频道发送消息的方式,将消息发送给频道的所有订阅者,我们把这些发送消息的客户端称为发送者(publisher)。

<center>客户端订阅频道</center>

redis-subscribe-channel.pngredis-subscribe-channel.png

<center>订阅模式</center>

redis-subscribe-pattern.pngredis-subscribe-pattern.png
3.5.1、PUBLISH:向频道发送消息

用户可以通过执行PUBLISH命令,将一条消息发送至给定频道:

PUBLISH channel message

PUBLISH命令会返回接收到消息的客户端数量作为返回值。

代码语言:shell复制
redis> publish "news.it" "hello world"
(integer) 0  -- 表示没有客户端收到这条消息
3.5.2、SUBSCRIBE:订阅频道

用户可以通过执行SUBSCRIBE命令,让客户端订阅给定的一个或多个频道:

SUBSCRIBE channel [channel channel ...]

SUBSCRIBE命令在每次成功订阅一个频道之后,都会向执行命令的客户端返回一条订阅消息,消息包含了被成功订阅的频道以及客户端目前已订阅的频道数量。

代码语言:shell复制
redis> subscribe "news.it"
Reading messages... (press Ctrl-C to quit)
1) "subscribe"  -- 表示这条消息是由SUBSCRIBE命令引发的订阅消息而不是普通客户端发送的频道消息。
2) "news.it"  -- 记录了被订阅频道的名字"news.it"。
3) (integer) 1  -- 表示客户端目前只订阅了一个频道。

redis> subscribe "news.sport" "news.movie"
Reading messages... (press Ctrl-C to quit)
1) "subscribe"  -- 第1条订阅消息
2) "news.sport"
3) (integer) 1
1) "subscribe"   -- 第2条订阅消息
2) "news.movie"
3) (integer) 2
接收频道消息

当客户端成为频道的订阅者之后,就会接收到来自被订阅频道的消息,我们把这些消息称为频道消息。与订阅消息一样,频道消息也是由3个元素组成的:

  • 消息的第1个元素为"message",用于表明该消息是一条频道消息而非订阅消息。redis> subscribe "news.it" Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "news.it" 3) (integer) 1 ... -- 等待其他客户端向频道发消息: publish "news.it" "hello world" ... 1) "message" -- 这是一条频道消息 2) "news.it" -- 来源是"news.it"频道 3) "hello world" -- 内容为"hello it"
  • 消息的第2个元素为消息的来源频道,用于表明消息来自于哪个频道。
  • 消息的第3个元素为消息的正文,也就是消息的真正内容。
3.5.3、UNSUBSCRIBE:退订频道

用户在使用SUBSCRIBE命令订阅一个或多个频道之后,如果不想再收到某个频道的消息,那么可以使用UNSUBSCRIBE命令退订指定的频道:

UNSUBSCRIBE [channel channel ...]

UNSUBSCRIBE命令允许用户给定任意多个频道。如果用户没有给定任何频道,直接以无参数方式执行UNSUBSCRIBE命令,那么命令将退订当前客户端已经订阅的所有频道。

代码语言:shell复制
redis> unsubscribe "news.it"
1) "unsubscribe"  -- 表明该消息是一条由退订操作产生的消息。
2) "news.it"  -- 被退订频道的名字。
3) (integer) 0  -- 退订之后,目前仍在订阅的频道数量。

虽然Redis提供了用于退订频道的UNSUBSCRIBE命令,但由于各个客户端对于发布与订阅功能的支持方式不尽相同,所以并非所有客户端都可以使用UNSUBSCRIBE命令执行退订操作。

3.5.4、PSUBSCRIBE:订阅模式

用户可以通过执行PSUBSCRIBE命令,让客户端订阅给定的一个或多个模式:

PSUBSCRIBE pattern [pattern pattern ...]

代码语言:shell复制
redis> psubscribe "news.*"
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"  -- 表明这条消息是由PSUBSCRIBE命令引发的订阅消息。
2) "news.*"  -- 表示是被订阅的模式。
3) (integer) 1  -- 表示客户端目前订阅的模式数量。
···
-- 等待其他客户端向频道发消息: publish "news.it" "hello world" 
···
1) "pmessage"  -- 表示这是一条模式消息而不是订阅消息或者频道消息。
2) "news.*"  -- 被匹配的模式。
3) "news.it"  -- 与模式相匹配的频道。
4) "hello world"  -- 消息的真正内容。
3.5.5、PUNSUBSCRIBE:退订模式

与退订频道的UNSUBSCRIBE命令类似,Redis也提供了用于退订模式的PUNSUBSCRIBE命令:

PUNSUBSCRIBE [pattern pattern pattern ...]

这个命令允许用户输入任意多个想要退订的模式,如果用户没有给定任何模式,那么命令将退订当前客户端已订阅的所有模式。

代码语言:shell复制
redis> psubscribe "news.*"
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"  -- 表明该消息是一条由PUNSUBSCRIBE命令引起的退订消息。
2) "news.*"  -- 被退订的模式。
3) (integer) 1  -- 个退订之后,仍在订阅的模式数量。
3.5.6、PUBSUB:查看发布与订阅的相关信息

通过使用PUBSUB命令,用户可以查看与发布、订阅有关的各种信息。

查看被订阅的频道

用户可以通过执行PUBSUB CHANNELS命令来列出目前被订阅的所有频道,如果给定了可选的pattern参数,那么命令只会列出与给定模式相匹配的频道:

PUBSUB CHANNELS [pattern]

代码语言:shell复制
-- 其他客户端先订阅频道:subscribe "news.it" "news.sport" "news.movie" "notify.email" "queue.message"

redis> pubsub channels  -- 被订阅的所有频道
1) "news.movie"
2) "notify.email"
3) "news.it"
4) "news.sport"
5) "queue.message"

redis> pubsub channels "news.*"  -- 以"news."开头的被订阅频道
1) "news.movie"
2) "news.it"
3) "news.sport"
查看频道的订阅者数量

用户可以通过执行PUBSUB NUMSUB命令,查看任意多个给定频道的订阅者数量:

PUBSUB NUMSUB [channel channel ...]

代码语言:shell复制
redis> pubsub numsub "news.it" "news.sport" "news.movie" "notify.email" "quene.message"
 1) "news.it"
 2) (integer) 3  -- "news.it"频道有3个订阅者
 3) "news.sport"
 4) (integer) 2  -- 有2个订阅者
 5) "news.movie"
 6) (integer) 1
 7) "notify.email"
 8) (integer) 1
 9) "quene.message"
10) (integer) 2
查看被订阅模式的总数量

通过执行PUBSUB NUMPAT命令,用户可以看到目前被订阅模式的总数量:PUBSUB NUMPAT

代码语言:txt复制
-- 其他客户端先订阅模式:psubscribe "news.*"
redis> pubsub numpat
(integer) 2

1 人点赞