类型检查与命令多态

Redis 中用于操作键的命令基本上可以分为两种类型。

其中一种命令可以对任何类型的键执行,比如说 DEL 命令、 EXPIRE 命令、 RENAME 命令、 TYPE 命令、 OBJECT 命令,等等。

举个例子,以下代码就展示了使用 DEL 命令来删除三种不同类型的键:

  1. # 字符串键
  2. redis> SET msg "hello"
  3. OK
  4.  
  5. # 列表键
  6. redis> RPUSH numbers 1 2 3
  7. (integer) 3
  8.  
  9. # 集合键
  10. redis> SADD fruits apple banana cherry
  11. (integer) 3
  12.  
  13. redis> DEL msg
  14. (integer) 1
  15.  
  16. redis> DEL numbers
  17. (integer) 1
  18.  
  19. redis> DEL fruits
  20. (integer) 1

而另一种命令只能对特定类型的键执行,比如说:

  • SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行;
  • HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行;
  • RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行;
  • SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行;
  • ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行;

诸如此类。

举个例子,我们可以用 SET 命令创建一个字符串键,然后用 GET 命令和 APPEND 命令操作这个键,但如果我们试图对这个字符串键执行只有列表键才能执行的 LLEN 命令,那么 Redis 将向我们返回一个类型错误:

  1. redis> SET msg "hello world"
  2. OK
  3.  
  4. redis> GET msg
  5. "hello world"
  6.  
  7. redis> APPEND msg " again!"
  8. (integer) 18
  9.  
  10. redis> GET msg
  11. "hello world again!"
  12.  
  13. redis> LLEN msg
  14. (error) WRONGTYPE Operation against a key holding the wrong kind of value

类型检查的实现

从上面发生类型错误的代码示例可以看出,为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令之前,Redis 会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。

类型特定命令所进行的类型检查是通过 redisObject 结构的 type 属性来实现的:

  • 在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象是否为执行命令所需的类型,如果是的话,服务器就对键执行指定的命令;
  • 否则,服务器将拒绝执行命令,并向客户端返回一个类型错误。

举个例子,对于 LLEN 命令来说:

  • 在执行 LLEN 命令之前,服务器会先检查输入数据库键的值对象是否为列表类型,也即是,检查值对象 redisObject 结构 type 属性的值是否为 REDIS_LIST ,如果是的话,服务器就对键执行 LLEN 命令;
  • 否则的话,服务器就拒绝执行命令并向客户端返回一个类型错误;

图 8-18 展示了这一类型检查过程。

digraph { label = "\n 图 8-18 LLEN 命令执行时的类型检查过程"; // call_command [label = "客户端发送 LLEN <key> 命令", shape = box]; check_type [label = "服务器检查 \n 键 key 的值对象\n是否列表对象", shape = diamond]; execute_command [label = "对键 key 执行 LLEN 命令", shape = box]; type_error [label = "返回一个类型错误", shape = box]; // call_command -> check_type; check_type -> execute_command [label = "是"]; check_type -> type_error [label = "否"];}

其他类型特定命令的类型检查过程也和这里展示的 LLEN 命令的类型检查过程类似。

多态命令的实现

Redis 除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。

举个例子,在前面介绍列表对象的编码时我们说过,列表对象有 ziplistlinkedlist 两种编码可用,其中前者使用压缩列表 API 来实现列表命令,而后者则使用双端链表 API 来实现列表命令。

现在,考虑这样一个情况,如果我们对一个键执行 LLEN 命令,那么服务器除了要确保执行命令的是列表键之外,还需要根据键的值对象所使用的编码来选择正确的 LLEN 命令实现:

  • 如果列表对象的编码为 ziplist ,那么说明列表对象的实现为压缩列表,程序将使用 ziplistLen 函数来返回列表的长度;
  • 如果列表对象的编码为 linkedlist ,那么说明列表对象的实现为双端链表,程序将使用 listLength 函数来返回双端链表的长度;

借用面向对象方面的术语来说,我们可以认为 LLEN 命令是多态(polymorphism))的:只要执行 LLEN 命令的是列表键,那么无论值对象使用的是 ziplist 编码还是 linkedlist 编码,命令都可以正常执行。

图 8-19 展示了 LLEN 命令从类型检查到根据编码选择实现函数的整个执行过程,其他类型特定命令的执行过程也是类似的。

digraph { label = "\n 图 8-19 LLEN 命令的执行过程"; // node [shape = box]; call_command [label = "客户端发送 LLEN <key> 命令"]; check_type [label = "服务器检查 \n 键 key 的值对象\n是否列表对象", shape = diamond]; //execute_command [label = "对键 key 执行 LLEN 命令"]; select_encoding [label = "对象的编码是 \n ziplist 还是 linkedlist ?", shape = diamond]; ziplist [label = "调用 ziplistLen 函数 \n 返回压缩列表的长度"]; linkedlist [label = "调用 listLength 函数 \n 返回双端链表的长度"]; type_error [label = "返回一个类型错误"]; // call_command -> check_type; //check_type -> execute_command [label = "是"]; check_type -> type_error [label = "否"]; //execute_command -> select_encoding; check_type -> select_encoding [label = "是"]; select_encoding -> ziplist [label = "ziplist \n 编码"]; select_encoding -> linkedlist [label = "linkedlist \n 编码"];}

实际上,我们可以将 DEL 、 EXPIRE 、 TYPE 等命令也称为多态命令,因为无论输入的键是什么类型,这些命令都可以正确地执行。

DEL 、 EXPIRE 等命令和 LLEN 等命令的区别在于,前者是基于类型的多态 —— 一个命令可以同时用于处理多种不同类型的键,而后者是基于编码的多态 —— 一个命令可以同时用于处理多种不同编码。