Bootstrap

Redis存储④Redis五大数据类型之String和Hash

目录

1. String 字符串

1.1 String常见命令

1.2 计数命令

1.3 其他命令

1.4 String命令总结和内部编码

1.5 String典型使用场景

1.5.1 缓存(Cache)功能

1.5.2 计数(Counter)功能

1.5.3 共享会话(Session)

1.5.4 手机验证码

2. Hash 哈希

2.1 Hash 哈希常见命令

2.2 Hash命令总结和内部编码

2.3 Hash典型使用场景

2.4 缓存方式对比


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" 后面了。setange 针对不存在的 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。

;