散列与字符串

本章从开头到现在,陆续介绍了 HSET 、 HSETNX 、 HGET 、 HINCRBY 和 HINCRBYFLOAT 等多个散列命令,如果读者对上一章介绍过的字符串命令还有印象的话,那么应该会记得,字符串也有类似的 SET 、 SETNX 、 GET 、 INCRBY 和 INCRBYFLOAT 命令。这种相似并不是巧合,正如表 3-1 所示,散列的确拥有很多与字符串命令功能相似的命令。


表 3-1 字符串命令与类似的散列命令

字符串散列
SET —— 为一个字符串键设置值。HSET —— 为散列的给定字段设置值。
SETNX —— 仅在字符串键不存在的情况下为它设置值。HSETNX —— 仅在散列不包含指定字段的情况下,为它设置值。
GET —— 获取字符串键的值。HGET —— 从散列里面获取给定字段的值。
STRLEN —— 获取字符串值的字节长度。HSTRLEN —— 获取给定字段值的字节长度。
INCRBY —— 对字符串键储存的数字值执行整数加法操作。HINCRBY —— 对字段储存的数字值执行整数加法操作。
INCRBYFLOAT —— 对字符串键储存的数字值执行浮点数加法操作。HINCRBYFLOAT —— 对字段储存的数字值执行浮点数加法操作。
MSET —— 一次为多个字符串键设置值。HMSET —— 一次为散列的多个字段设置值。
MGET —— 一次获取多个字符串键的值。HMGET —— 一次获取散列中多个字段的值。
EXISTS —— 检查给定的键是否存在于数据库当中,这个命令可以用于包括字符串键在内的所有数据库键,本书稍后将在《数据库》一章对这个命令进行详细的介绍。HEXISTS —— 检查给定字段是否存在于散列当中。
DEL —— 从数据库里面删除指定的键,这个命令可以用于包括字符串键在内的所有数据库键,本书稍后将在《数据库》一章对这个命令进行详细的介绍。HDEL —— 从散列中删除给定字段,以及它的值。

对于表中列出的字符串命令和散列命令来说,它们之间的最大区别就是前者处理的是字符串键,而后者处理的则是散列键,除此之外,这些命令要做的事情几乎都是相同的。

Redis 之所以会选择同时提供字符串键和散列键这两种数据结构,原因在于它们虽然在操作上非常相似,但是各自却又拥有不同的优点,这使得它们在某些场合无法被对方替代,本节接下来将分别介绍这两种数据结构各自的优点。

散列键的优点

散列的最大优势,就是它只需要在数据库里面创建一个键,就可以把任意多的字段和值储存到散列里面。相反地,因为每个字符串键只能储存一个键值对,所以如果用户要使用字符串键去储存多个数据项的话,那么就只能在数据库里面创建多个字符串键。

图 3-23 展示了使用字符串键和散列键储存相同数量的数据项时,数据库中创建的字符串键和散列键。


图 3-23 使用字符串键和散列键去储存相同数量的数据项_images/IMAGE_DB_COMPARE.png


从图中可以看到,为了储存相同的四个数据项,程序需要用到四个字符串键,又或者一个散列键。按此计算,如果我们需要储存一百万篇文章,那么在使用散列键的情况下,程序只需要在数据库里面创建一百万个散列键就可以了;但是如果使用字符串键的话,那么程序就需要在数据库里面创建四百万个字符串键。

数据库键数量增多带来的问题主要和资源有关:

  • 为了对数据库以及数据库键的使用情况进行统计,Redis 会为每个数据库键储存一些额外的信息,并因此带来一些额外的内存消耗。对于单个数据库键来说,这些额外的内存消耗几乎可以忽略不计,但是,当数据库键的数量达到上百万、上千万甚至更多的时候,这些额外的内存消耗就会变得比较可观。

  • 当散列包含的字段数量比较少的时候,Redis 就会使用特殊的内存优化结构去储存散列中的字段和值:与字符串键相比,这种内存优化结构储存相同数据所需的内存要少得多。使用内存优化结构的散列越多,内存优化结构带来的效果也就越明显。在一定条件下,对于相同的数据,使用散列键进行储存比使用字符串键进行储存要节约一半以上的内存,有时候甚至会更多。

  • 除了需要耗费更多内存之外,更多的数据库键也需要占用更多的 CPU 。每当 Redis 需要对数据库中的键进行处理时,数据库包含的键越多,进行处理所需的 CPU 资源就会越多,处理所耗费的时间也会越长,典型的情况包括:

    • 统计数据库和数据库键的使用情况;

    • 对数据库执行持久化操作,又或者根据持久化文件还原数据库;

    • 通过模式匹配在数据库里面查找某个键,或者执行类似的查找操作;

这些操作的执行时间都会受到数据库键数量的影响。

最后,除了资源方面的优势之外,散列键还可以有效地组织起相关的多项数据,让程序产生出更容易理解的数据,使得针对数据的批量操作变得更为方便。比如在上面展示的图 3-23 中,使用散列键储存文章数据的做法就比使用字符串键储存文章数据的做法要来得更为清晰、易懂。

字符串键的优点

虽然使用散列键可以有效地节约资源并更好地组织数据,但是字符串键也有自己的优点:

  • 虽然散列键命令和字符串命令在部分功能上有重合的地方,但是字符串键命令提供的操作比散列键命令要更为丰富。比如说,字符串能够使用 SETRANGE 命令和 GETRANGE 命令设置或者读取字符串值的其中一部分,又或者使用 APPEND 命令将新内容追加到字符串值的末尾,而散列键并不支持这些操作。

  • 本书稍后将在《自动过期》一章对 Redis 的键过期功能进行介绍,这一功能可以在指定时间到达时,自动删除指定的键。因为键过期功能针对的是整个键,用户无法为散列中的不同字段设置不同的过期时间,所以当一个散列键过期的时候,它包含的所有字段和值都将被删除。与此相反,如果用户使用字符串键储存信息项的话,就不会遇到这样的问题:用户可以为每个字符串键分别设置不同的过期时间,让它们根据实际的需要自动被删除掉。

字符串键和散列键的选择

表 3-2 从资源占用、支持的操作以及过期时间三个方面对比了字符串键和散列键的优缺点。


表 3-2 对比字符串键和散列键

比较的范畴结果
资源占用字符串键在数量较多的情况下,将占用大量的内存和 CPU 时间。与此相反,将多个数据项储存到同一个散列里面可以有效地减少内存和 CPU 消耗。
支持的操作散列键支持的所有命令,几乎都有相应的字符串键版本,但字符串键支持的 SETRANGEGETRANGE 等操作散列键并不具备。
过期时间字符串键可以为每个键单独设置过期时间,独立删除某个数据项;而散列一旦到期,它包含的所有字段和值都会被删除。

既然字符串键和散列键各有优点,那么我们在构建应用程序的时候,什么时候应该使用字符串键,而什么时候又应该使用散列键呢?对于这个问题,以下总结了一些选择的条件和方法:

  • 如果程序需要为每个数据项单独设置过期时间,那么使用字符串键。

  • 如果程序需要对数据项执行诸如 SETRANGEGETRANGE 或者 APPEND 等操作,那么优先考虑使用字符串键。当然,用户也可以选择把数据储存在散列里面,然后将类似 SETRANGEGETRANGE 这样的操作交给客户端执行。

  • 如果程序需要储存的数据项比较多,并且你希望尽可能地减少储存数据所需的内存,那么就应该优先考虑使用散列键。

  • 如果多个数据项在逻辑上属于同一组或者同一类,那么应该优先考虑使用散列键。