散列与字符串

至此,本章中陆续介绍了 HSET、HSETNX、HGET、HINCRBY 和 HINCRBYFLOAT 等多个散列命令,如果你对第 2 章介绍过的字符串命令还有印象,应该记得字符串也有类似的 SET、SETNX、GET、INCRBY 和 INCRBYFLOAT 命令。这种相似并不是巧合,正如表3-1所示,散列的确拥有很多与字符串命令功能相似的命令。

image 2025 01 02 18 37 29 927
Figure 1. 表3-1 字符串命令与类似的散列命令
image 2025 01 02 18 37 42 964

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

散列键的优点

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

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

image 2025 01 02 18 38 36 413
Figure 2. 图3-23 使用字符串键和散列键存储相同数量的数据项

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

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

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

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

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

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

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

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

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

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

字符串键的优点

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

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

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

字符串键和散列键的选择

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

image 2025 01 02 18 42 20 505
Figure 3. 表3-2 对比字符串键和散列键

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

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

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

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

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