1. String 字符串

字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意:

     首先 Redis 中所有的键(key)的类型都是字符串类型,而且其他几种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。 

其次,如下图所示,字符串类型的值实际可以是字符串,包含一般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚至是二进制流数据,例如图片、音频、视频等。不过一个字符串的最大值不能超过 512 MB。

字符串数据类型:


1.1 String常见命令

set

 将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,无论原来的数据类型是什么。之前关于此 key 的 TTL 也全部失效。

语法:set key value [expiration ex seconds | px milliseconds] [nx | xx]

命令有效版本:1.0.0 之后

时间复杂度:O(1)

set 命令支持多种选项来影响它的行为,选项:

  • ex seconds —— 使用秒作为单位设置 key 的过期时间。
  • px milliseconds —— 使用毫秒作为单位设置 key 的过期时间。
  • nx —— 只在 key 不存在时才进行设置,创建新的键值对,即如果 key 之前已经存在,设置不执行。
  • xx —— 只在 key 存在时才进行设置,让新的 value 覆盖旧的 value,可能会改变原来的数据类型,即如果 key 之前不存在,设置不执行。  

  注意:由于带选项的 SET 命令可以被 setnx、 setex、 psetex等命令代替,所以之后的版本中,Redis 可能进行合并。

     返回值:如果设置成功,返回 OK。如果由于 set 指定了 nx 或者 xx 但条件不满足,set 不会执行,并返回 (nil)。

FLUSHALL:表示清空所有数据(类似于 MySQL 里的 drop database)。 

示例:


get

获取 key 对应的 value。如果 key 不存在,返回 nil。如果 value 的数据类型不是 string,会报错。

     对于 GET 来说,只是支持字符串类型的 value,如果 value 是其他类型,那么使用 GET 获取就会出错。

语法:GET key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:key 对应的 value,或者 nil 当 key 不存在。

示例:


mget

一次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。

语法:mget key [key ...]

命令有效版本:1.0.0 之后

时间复杂度:O(N) ,N 是 key 数量

返回值:对应 value 的列表。

示例:


mset

一次性设置多个 key 的值。

语法:mset key value [key value ...]

命令有效版本:1.0.1 之后

时间复杂度:O(N) N 是 key 数量

返回值:永远是 OK

示例:


多次 get VS 单次 mget:

使用 mget / mset 由于可以有效地减少了网络时间,所以性能相较更高。假设网络耗时 1 毫秒,命令执行时间耗时 0.1 毫秒,则执行时间如下表所示:

学会使用批量操作,可以有效提高业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是无节制的,否则可能造成单一命令执行时间过长,导致 Redis 阻塞。


setnx

设置 key-value 但只允许在 key 之前不存在的情况下。

语法:setnx key value

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:1 表示设置成功,0 表示没有设置。

SET、SET NX 和 SET XX 执行流程:


1.2 计数命令

incr

   将 key 对应的 string 表示的数字加一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型(相当于 C++ 中的 long long),则报错。

语法:incr key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的加完后的数值。

incr 操作的 key 如果不存在,就会把这个 key 的 value 当作 0 来使用。


incrby

        将 key 对应的 string 表示的数字加上对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。

语法:incrby key decrement

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的加完后的数值。

示例:


decr

        将 key 对应的 string 表示的数字减一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。运算结果也是计算之后的值。

语法:decr key

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的减完后的数值。

示例:


decrby

        将 key 对应的 string 表示的数字减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。

语法:decrby key decrement

命令有效版本:1.0.0 之后

时间复杂度:O(1)

返回值:integer 类型的减完后的数值。

示例:


incrbyfloat

        将 key 对应的 string 表示的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是一个浮点数,则报错。允许采用科学计数法表示浮点数。

语法:incrbyfloat key increment

命令有效版本:2.6.0 之后

时间复杂度:O(1)

返回值:加 / 减完后的数值。

示例:

 很多存储系统和编程语言内部使用 CAS 机制实现计数功能,会有一定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执行。


1.3 其他命令

append

        如果 key 已经存在并且是⼀个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。

语法:append key value

命令有效版本:2.0.0 之后

时间复杂度:O(1) 追加的字符串一般长度较短,可以视为 O(1)

返回值:追加完成之后 string 的长度。append 的返回值长度的单位是字节,Redis 的字符串不会对字符编码做任何处理。

示例:

   当前 XShell 终端默认的字符编码是 utf-8,在终端中输入汉字之后,也就是按照 utf8 编码。一个汉字在 utf8 字符集中通常是 3 个字节的。

        在启动 Redis 客户端时,加上一个 --raw 这样的选项,就可以使 Redis 客户端能够自动的把二进制数据尝试翻译。
 


getrange

        返回 key 对应的 string 的子串,由 start 和 end 确定(左闭右闭,是闭区间)。可以使用负数表示倒数,-1 代表倒数第一个字符(下标为 len - 1 的元素),-2 代表倒数第二个,其他的与此类似。超过范围的偏移量会根据 string 的长度调整成正确的值。

语法:getrange key start end

命令有效版本:2.4.0 之后

时间复杂度:O(N) ,N 为 [start, end] 区间的长度,由于 string 通常比较短,可以视为是 O(1)

返回值:string 类型的子串

示例:

 如果字符串中保存的是汉字,此时进行子串切分很可能切出来的就不是完整的汉字了。上述的代码是强行切出了中间的四个字节,这么一切,切出的结果在 utf8 码表上就不知道能查出什么了。上述问题在 C++ 中也同样存在(C++ 字符串中的基本单位是字节),需要我们手动处理。但 Java 就不会(Java 中字符串的基本单位是字符,占 2 个字节的字符),Java 中相当于 String 帮我们把汉字的编码转换都处理好了。


setrange

覆盖字符串的一部分,从指定的偏移开始。

语法:setrange key offset value

命令有效版本:2.2.0 之后

时间复杂度:O(N), N 为 value 的长度,由于一般给的 value 比较短,通常视为 O(1)。

返回值:替换后的 string 的长度。

示例:

如果 value 是一个中文字符串,进行 setrange 时是可能会出问题的。

 这里凭空生成了一个字节,这个字节里的内容就是 "0x00",aaa 就被追加到 "0x00" 后面了。setrange 针对不存在的 key 也是可以操作的,不过会把 offset 之前的内容填充成 "0x00"。


setlen

获取 key 对应的 string 的长度。当 key 存放的类型不是 string 时,报错。

语法:setlen key

命令有效版本:2.2.0 之后

时间复杂度:O(1)

返回值:string 的长度。或者当 key 不存在时,返回 0。单位是字节。(在 C++ 中,字符串的长度本身就是用字节为单位的)在 MySQL 中,varchar(N) 的 N 的单位就是字符,MySQL 中的字符也是完整的汉字,这样的一个字符也可能是多个字节。

示例:


1.4 String命令总结和内部编码

下表是String字符串类型命令的效果、时间复杂度:


String字符串类型的内部编码有 3 种:

  • int:64 位 / 8 个字节的长整型。
  • embstr:小于等于 39 个字节的字符串,压缩字符串,适用于表示比较短的字符串。
  • raw:大于 39 个字节的字符串,普通字符串,适用于表示更长的字符串,只是单纯的持有字节数组。

Redis 会根据当前值的类型和长度动态决定使用哪种内部编码实现。

整型类型示例如下:

短字符串示例如下:

 Redis 存储小数,本质上还是当作字符串来存储,这就和整数相比差别很大了。整数直接使用 int 来存储(准确来说是一个 long long(C++)),比较方便进行算术运算。小数则是使用字符串来存储,意味着每次进行算术运算都需要把字符串转成小数来进行运算,结果再转回字符串保存。


长字符串示例如下:


1.5 String典型使用场景

1.5.1 缓存(Cache)功能

下图是比较典型的缓存使用场景,其中 Redis 作为缓冲层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

Redis + MySQL 组成的缓存存储架构:

      整体思路:应用服务器访问数据时,先查询 Redis。如果 Redis 上数据存在,就直接从 Redis 中取出数据交给应用服务器,不继续访问数据库了。如果 Redis 上数据不存在,此时再读取 MySQL,把读到的结果返回给应用服务器,同时把这个数据也写入到 Redis 中。

   上述策略存在一个明显的问题:随着时间的推移,会有越来越多的 key 在 Redis 上访问不到,从而从 MySQL 中读取并写入 Redis 了,此时 Redis 中的数据不是会越来越多吗?

解决方法:

  1. 在把数据写给 Redis 的同时,给这个 key 设置一个过期时间。
  2. Redis 也在内存不足时,提供了淘汰策略。

下面的伪代码模拟了上图的业务数据访问过程:

1. 假设业务是根据用户 uid 获取用户信息

UserInfo getUserInfo(long uid) {
    ...
}

2. 首先从 Redis 获取用户信息,假设用户信息保存在 "user:info:<uid>" 对应的键中

// 根据 uid 得到 Redis 的键
String key = "user:info:" + uid;
 
// 尝试从 Redis 中获取对应的值
String value = Redis 执⾏命令:get key;
 
// 如果缓存命中(hit)
if (value != null) {
    // 假设我们的⽤⼾信息按照 JSON 格式存储
    UserInfo userInfo = JSON 反序列化(value);
    return userInfo;
}

3. 如果没有从 Redis 中得到用户信息,及缓存 miss,则进一步从 MySQL 中获取对应的信息,随后写入缓存并返回

// 如果缓存未命中(miss)
if (value == null) {
    // 从数据库中,根据 uid 获取⽤⼾信息
    UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>
 
    // 如果表中没有 uid 对应的⽤⼾信息
    if (userInfo == null) {
        响应 404
        return null;
    }
 
    // 将⽤⼾信息序列化成 JSON 格式
    String value = JSON 序列化(userInfo);
 
    // 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
    Redis 执⾏命令:set key value ex 3600
 
    // 返回⽤⼾信息
    return userInfo;
}

   通过增加缓存功能,在理想情况下,每个用户信息,一个小时期间只会有一次 MySQL 查询,极大地提升了查询效率,也降低了 MySQL 的访问数。

        与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,而且也没有对键名有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用 “业务名:对象名:唯一标识:属性” 作为键名。例如:MySQL 的数据库名为 vs,用户表名为 user_info,那么对应的键可以使用 "vs:user_info:6379"、"vs:user_info:6379:name" 来表示,如果当前 Redis 只会被一个业务使用,可以省略业务名 "vs:"。如果键名过程,则可以使用团队内部都认同的缩写替代,例如:"user:6379:friends:messages:5217" 可以被 "u:6379:fr:m:5217" 代替。毕竟键名过长,还是会导致 Redis 的性能明显下降的。
 


1.5.2 计数(Counter)功能

        许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。如下图所示,例如视频网站的视频播放次数可以使用 Redis 来完成:用户每播放⼀次视频,相应的视频播放数就会自增 1。

记录视频播放次数:

       这里写入统计数据仓库(可能是 MySQL,也可能是 HDFS)的步骤往往是异步的,所以并不是说来一个播放请求,这里就必须立即马上写一个数据。

// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
    key = "video:" + vid;
    long count = Redis 执⾏命令:incr key
    return counter;
}

   实际中要开发一个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊、按照不同维度计数、避免单点问题、数据持久化到底层数据源等。


1.5.3 共享会话(Session)

  如下图所示,一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。

Session 分散存储:

 为了解决这个问题,可以使用 Redis 将用户的 Session 信息进行集中管理,如下图所示,在这种模式下,只要保证 Redis 是高可用和可扩展性的,无论用户被均衡到哪台 Web 服务器上,都集中从 Redis 中查询、更新 Session 信息。

Redis 集中管理 Session:


1.5.4 手机验证码

  很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次,如下图所示:

短信验证码:

此功能可以用以下伪代码说明基本实现思路:
 

String 发送验证码(phoneNumber) {
    key = "shortMsg:limit:" + phoneNumber;
    // 设置过期时间为 1 分钟(60 秒)
    // 使⽤ NX,只在不存在 key 时才能设置成功
    bool r = Redis 执⾏命令:set key 1 ex 60 nx
    if (r == false) {
        // 说明之前设置过该手机的验证码了
        long c = Redis 执⾏命令:incr key
        if (c > 5) {
            // 说明超过了⼀分钟 5 次的限制了
            // 限制发送
            return null;
        }
    }
 
    // 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
    String validationCode = ⽣成随机的 6 位数的验证码();
 
    validationKey = "validation:" + phoneNumber;
    // 验证码 5 分钟(300 秒)内有效
    Redis 执⾏命令:set validationKey validationCode ex 300;
 
    // 返回验证码,随后通过手机短信发送给用户
    return validationCode ;
}
 
// 验证用户输入的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
    validationKey = "validation:" + phoneNumber;
 
    String value = Redis 执⾏命令:get validationKey;
    if (value == null) {
        // 说明没有这个手机的验证码记录,验证失败
        return false;
    }
 
    if (value == validationCode) {
        return true;
    } else {
        return false;
    }
}

      以上介绍了使用 Redis 的字符串数据类型可以使用的几个场景,但其适用场景远不止于此,开发人员可以结合字符串类型的特点以及提供的命令,充分发挥自己的想象力,在自己的业务中去找到合适的场景去使用 Redis 的字符串类型。


2. Hash 哈希

  几乎所有的主流编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中,哈希类型是指值本身又是⼀个键值对结构,形如 key = "key",value = { { field1, value1 }, ..., { fieldN, valueN } },Redis 键值对和哈希类型二者的关系可以用下图来表示。
字符串和哈希类型对比:

    哈希类型中的映射关系通常称为 field-value,用于区分 Redis 整体的键值对(key-value),注意这里的 value 是指 field 对应的值,不是键(key)对应的值,请注意 value 在不同上下文的作用。


2.1 Hash 哈希常见命令

hset

设置 hash 中指定的字段(field)的值(value)。

语法:hset key field value [field value ...]

命令有效版本:2.0.0 之后

时间复杂度:插⼊一组 field 为 O(1),插⼊ N 组 field 为 O(N)

返回值:添加的字段的个数,也就是设置成功的键值对的个数。

示例:


hget

获取 hash 中指定字段的值。

语法:hget key field

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:字段对应的值或者 nil。

示例:


hexists

判断 hash 中是否有指定的字段。

语法:hexists key field

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:1 表示存在,0 表示不存在。

示例:


hdel

删除 hash 中指定的字段。

语法:hdel key field [field ...]

命令有效版本:2.0.0 之后

时间复杂度:删除一个元素为 O(1),删除 N 个元素为 O(N)。

返回值:本次操作删除的字段个数。

示例:


hkeys

获取 hash 中的所有字段。

语法:hkeys key

命令有效版本:2.0.0 之后

时间复杂度:O(N) ,N 为 field 的个数,当前的 O(N) 可以说成是 O(1)。

返回值:字段列表。

示例:


hvals

获取 hash 中的所有的值。

语法:hvals key  

命令有效版本:2.0.0 之后

时间复杂度:O(N) N 为 field 的个数。如果 field(哈希)非常大,那么这个操作就可能导致 Redis 服务器被阻塞住。

返回值:所有的值。

示例:


hgetall

获取 hash 中的所有字段以及对应的值。这个操作的风险比较大,但多数情况下,我们不需要查询所有的 field,可能只查其中几个 field。

语法:hgetall key 

命令有效版本:2.0.0 之后

时间复杂度:O(N), N 为 field 的个数。

返回值:字段和对应的值。

示例:

此处前面的序号仅仅是标识下返回元素的顺序,和下标无关,hash 类型没有下标的概念。


hmget

一次获取 hash 中多个字段的值。

语法:hmget key field [field ...]

命令有效版本:2.0.0 之后

时间复杂度:只查询⼀个元素为 O(1),查询多个元素为 O(N) N 为查询元素个数。

返回值:字段对应的值或者 nil。

示例:


注意:多个 value 的顺序和 field 的顺序是匹配的。

在使用命令 hkeys,hvals,hgetall 完成所有的遍历操作时,都是存在一定风险的,如果 hash 的元素个数太多,执行的耗时就比较长,那么就会存在阻塞 Redis 的可能。

   如果开发人员只需要获取部分 field,可以使用 hmget,如果一定要获取全部 field,可以尝试使用 hscal 命令,该命令采用渐进式遍历哈希类型(敲一次命令,遍历一小部分,时间是可控的,连续执行多次就可以完成整个遍历过程)。

    是否有 hmset 一次设置多个 field 和 value 呢?有的,但是并不需要使用,因为 hset 已经支持一次设置多个 field 和 value 了。 


hlen

获取 hash 中的所有字段的个数。

语法:hlen key

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:字段个数。

示例:


hsetnx

在字段不存在的情况下,设置 hash 中的字段和值。

语法:hsetnx key field value

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:1 表示设置成功,0 表示失败。

示例:


incrby

将 hash 中字段对应的数值添加指定的值。

语法:incrby key field increment 

命令有效版本:2.0.0 之后

时间复杂度:O(1)

返回值:该字段变化之后的值。

示例:


hincrbyfloat

hincrby 的浮点数版本。

语法:hincrbyfloat key field increment

命令有效版本:2.6.0 之后

时间复杂度:O(1)

返回值:该字段变化之后的值。

示例:


2.2 Hash命令总结和内部编码

下表是哈希类型命令的效果、时间复杂度:



哈希的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)、同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时(这两个配置项是可以写到 redis.conf 文件中的),Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
  • hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。
     

下面的示例演示了哈希

类型的内部编码,以及响应的变化。

1. 当 field 个数比较少且没有大的 value 时,内部编码为 ziplist

2. 当有 value 大于 64 字节时,内部编码会转换为 hashtable

3. 当 field 个数超过 512 时,内部编码也会转换为 hashtable


2.3 Hash典型使用场景

下图为关系型数据表记录的两条用户信息,用户的属性表现为表的列,每条用户信息表现为行。

关系型数据表保存用户信息:

   这里的 uid 不存储可以吗?直接使用 key 中的 id 来进行区分,存储空间不是又进一步的节省了吗?

        不存储这个 uid 也可以。但是在工程实践中,一般都会把 uid 在 value 中再存一份,后续写到相关的代码时,使用起来会比较方便。

     如果使用 string(JSON)的格式来表示 UserInfo,万一只想要获取其中的某个 field 或者修改某个 field,就需要把整个 JSON 都读出来,解析成对象,操作 field,再重写转成 JSON 字符串,再写回去。

  相比于使用 JSON 格式的字符串缓存用户信息,哈希类型变得更加直观,并且在更新操作上变得更灵活,可以使用 field 表示对象的每个属性(数据表的每个列),此时就可以很方便的修改 / 获取任何一个属性的值了。可以将每个用户的 id 定义为键后缀,多对 field-value 对应用户的各个属性,类似如下伪代码:

UserInfo getUserInfo(long uid) {
    // 根据 uid 得到 Redis 的键
    String key = "user:" + uid;
 
    // 尝试从 Redis 中获取对应的值
    userInfoMap = Redis 执⾏命令:hgetall key;
 
    // 如果缓存命中(hit)
    if (value != null) {
        // 将映射关系还原为对象形式
        UserInfo userInfo = 利⽤映射关系构建对象(userInfoMap);
        return userInfo;
    }
 
    // 如果缓存未命中(miss)
    // 从数据库中,根据 uid 获取⽤⼾信息
    UserInfo userInfo = MySQL 执⾏ SQL:select * from user_info where uid = <uid>
 
    // 如果表中没有 uid 对应的⽤⼾信息
    if (userInfo == null) {
        响应 404
        return null;
    }
 
    // 将缓存以哈希类型保存
    Redis 执⾏命令:hmset key name userInfo.name age userInfo.age city userInfo.city
 
    // 写⼊缓存,为了防⽌数据腐烂(rot),设置过期时间为 1 ⼩时(3600 秒)
    Redis 执⾏命令:expire key 3600
 
    // 返回⽤⼾信息
    return userInfo;
}

但是需要注意的是哈希类型和关系型数据库有两点不同之处:

  • 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null,如下图所示。
  • 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。

关系型数据库稀疏性:


2.4 缓存方式对比

        截至目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。

1. 原生字符串类型 —— 使用字符串类型,每个属性一个键

set user:1:name James
set user:1:age 23
set user:1:city Beijing
  • 优点:实现简单,针对个别属性变更也很灵活。
  • 缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,所以这种方案基本没有实用性。

2. 序列化字符串类型,例如 JSON 格式 

set user:1 经过序列化后的⽤⼾对象字符串
  • 优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内存的使用效率很高。
  • 缺点:本身序列化和反序列需要一定开销,同时如果总是操作个别属性则非常不灵活。

3. 哈希类型

hmset user:1 name James age 23 city Beijing
  • 优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
  • 缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。

本篇完。

下一篇是Redis存储⑤Redis五大数据类型之 List 和 Set。

更多推荐