Redis 实用小技巧——一文教你如何选择合适的 Key 类型

简介

本文我们来聊一聊在 Redis 中应该如何选择合适的 Key 类型。

说到 Redis 的 Key 类型,相信很多朋友都会脱口而出:这不简单么,不就是 StringHashListSetZset 么?的确如此。但是对于一些刚刚接触 Redis 的同学来说,可能用过最多的就是 String 类型。记得我刚刚工作那会,最初接触 Redis 的时候,就是听说它是一种缓存技术,比直接操作 MySQL 更快,用的最多的操作也就是 String 类型的 SetGet 操作了,直到过了很长一段时间以后才慢慢接触到其他的数据类型(不知道有多少同学和我有过同样的经历)。

今天我们就来聊一聊 Redis 中每一种数据类型的特点,以及在实际开发中我们应该如何选择合适的数据类型。

文章会以一个「小学生入学」的场景展开,更加方便大家理解。

现在就让我们一起开始这段「小学生之旅」吧!

类型介绍

String:「小学生入学啦」

小学生入学后,学校会给每个班级的小朋友分配一个学号,这个学号必须是个唯一的,一般长这个样:2023010101(年份 + 年级 + 班级 + 唯一号)。初始化学号的命令如下:

> SET STUDENT:NUMBER:2023:01:01 2023010101
OK

后续给每个学生分配学号时,只需要在原号码的基础上累加即可:

> INCR STUDENT:NUMBER:2023:01:01
(integer) 2023010102

这样每位同学都可以通过「学号发号器」获得一个独一无二的学号了。

假设学校门口有一台「打卡机」,每位小朋友每天走进校门的第一件事就是进行「签到打卡」,具体的操作如下:

> SETEX STUDENT:SIGN:2023010101 86400 1
OK

SETEX 相当于 SET + EXPIRE 命令的组合,即给 String 赋值,并设置过期时间。当小朋友在其他时间打卡的时候,这时候「打卡机」就会执行:

> EXISTS STUDENT:SIGN:2023010101
(integer) 1

EXISTS 命令会检查 Key 是否存在,当返回整数 1 的时候,就会发出提示:「小朋友,你今天已经签到过了哦~」

这就是最简单的数据类型 String 的用法。正如开文讲到的一样,String 类型最常见且用的最广泛的命令就是 GETSET。当我们需要存储简单的数据,或者需要使用到整数自增这些操作时,再或者仅仅是为了「占位」操作时,我们就可以考虑直接使用 String 类型实现。

Hash:「来建个档案吧」

小学生入学后,第一件大事就是「建档」,即收集小学生的基本信息。小学生的基本信息包括:姓名,性别,年龄,身高,体重,父亲姓名、父亲电话、母亲姓名、母亲电话等。

如果你是刚刚接触 Redis 的话,你可能考虑直接把基本信息存储到一个 json 对象中,如下:

{
    "name":"皮拉夫",
    "sex":"boy",
    "age":6,
    "height":120,
    "weight":30,
    "father_name":"皮拉夫爸爸",
    "father_phone":"13500010001",
    "mother_name":"皮拉夫妈妈",
    "mother_phone":"13500010002"
}

然后使用 SET 命令进行存储:

> SET STUDENT:INFO:2023010101 '{"name":"皮拉夫","sex":"boy","age":6,"height":120,"weight":30,"father_name":"皮拉夫爸爸","father_phone":"13500010001","mother_name":"皮拉夫妈妈","mother_phone":"13500010002"}'
OK

当我们需要读取信息时,先使用 GET 命令获取到 json 信息,然后再使用 json_decode 方法获取到对应的对象信息。乍一看没什么问题,但是当我们仅需要获取某一条信息(比如年龄)时,仍要返回整个信息,设置单独的信息也是如此。

针对这种数据结构,我们就可以考虑使用 Hash 类型进行存储了。

我们可以使用 HMSET 命令初始化所有的字段信息:

> HMSET STUDENT:INFO:2023010101 name "皮拉夫" sex "boy" age 6 height 120 weight 30 father_name "皮拉夫爸爸" father_phone "13500010001" mother_name "皮拉夫妈妈" mother_phone "13500010002"

然后可以使用 HMGET 命令获取所有的字段信息:

> HGETALL STUDENT:INFO:2023010101
 1) "name"
 2) "\xe7\x9a\xae\xe6\x8b\x89\xe5\xa4\xab"
 3) "sex"
 4) "boy"
 5) "age"
 6) "6"
 7) "height"
 8) "120"
 9) "weight"
10) "30"
11) "father_name"
12) "\xe7\x9a\xae\xe6\x8b\x89\xe5\xa4\xab\xe7\x88\xb8\xe7\x88\xb8"
13) "father_phone"
14) "13500010001"
15) "mother_name"
16) "\xe7\x9a\xae\xe6\x8b\x89\xe5\xa4\xab\xe5\xa6\x88\xe5\xa6\x88"
17) "mother_phone"
18) "13500010002"

当我们需要获取某个字段(比如年龄)的时候,可以使用 HGET 获取:

> HGET STUDENT:INFO:2023010101 age
"6"

同样,我们可以通过 HSET 单独设置某个字段的值:

> HSET STUDENT:INFO:2023010101 age 7
(integer) 0

这样对比直接存储 json 字符串的方式,是不是方便的多呢?

当然不是所有的对象都适合使用 Hash 存储。有时我们的对象可能是嵌套多层的复杂对象,这时如果我们用 Hash 存储的话反而没有 String 灵活。

List:「给家长发个短信吧」

为了表达对家长朋友们选择我们学校的感谢,学校决定以班级为单位给每位家长发一条感谢短信,内容如下:

亲爱的家长朋友,感谢您选择了我们学校,接下来的日子里,让我们共同努力,把孩子培养出栋梁之才~

有了短信内容,然后就是拿到家长的电话号码,依次发送就可以了。

这种情况下,就轮到 List 类型发挥作用了。

我们可以先把所有家长的电话存到一个 List 结构中:

> LPUSH STUDENT:SMS:THANKS:2023:01:01 13500010001 13500010002
(integer) 2

然后我们需要在程序端通过进程任务来「消费」队列:

> RPOP STUDENT:SMS:THANKS:2023:01:01
"13500010001"

取到电话以后,就是按照短信内容进行发送了。

这就是 List 一般的应用场景。

这里细心的你可能会发现了,我们使用的是左进(LPUSH)右出(RPOP)的方式进行操作的,因为队列的原则就是「先进先出(FIFO)」。

Set:「班级花名册不能少」

有了学号和档案以后,我们还需要一份「花名册」。因为作为老师需要知道班里有多少同学信息,我们这个「花名册」有两个特点:

1)内容仅维护学生的学号信息(学生详情在档案中可以查到)。
2)学号唯一,不重复。

这种情况下我们就可以使用 SET 来存储了。

SET 名叫「集合」,可以存储一组不重复的元素。正好可以满足我们的场景,具体的操作命令如下:

> SADD STUDENT:LIST:2023:01:01 2023010101 2023010102 2023010103
(integer) 3

这里我们之所以使用 Set 而非 List 是因为 List 没有元素唯一性的特点。「元素唯一性」这个特点也是我们考虑使用 Set 结构的第一参考因素。

Zset:「来排个名吧」

很快,我们的小学生迎来了入学以来的第一次考试。我们决定对小朋友们的成绩做个排名(虽说现在不让排名了,我们就是喜欢特立独行)。所以我们需要做一份「成绩单」。

「成绩单」中需要存储小朋友的这些信息:

  • 学号信息
  • 学科成绩信息

考虑到一份成绩单中一个小朋友只能有一个成绩,所以对于我们的存储结构,「元素唯一性」也是必要因素之一。然后还需要记录到学生的成绩。

Hash 貌似能满足我们的诉求,我们可以设计这样的一个结构来存储学生的语文成绩信息:

> HMSET STUDENT:SCORE:CHINESE:2023:01:01 2023010101 100 2023010102 99
OK

这样存储貌似也没什么问题。但是别忘了我们的诉求:除了存储,我们还需要「排名」。显然,Hash 并不能满足我们排名的诉求。

这时候就该请 Zset 登场了~

Zset 名作「有序集合」,除了保持了 Set 的「元素唯一性」之外,它还有一个强大的功能,就是可以给每一个元素附加一个「分值(score)」,有了这个「分值」,就大有文章可做了。

我们先来记录下学生们的成绩,还是采用上面的成绩信息:

> ZADD STUDENT:SCORE:CHINESE:2023:01:01 100 2023010101 99 2023010102
(integer) 2

当我们需要根据成绩进行排名时,我们就可以使用以下命令操作:

> ZREVRANGEBYSCORE STUDENT:SCORE:CHINESE:2023:01:01 +inf -inf WITHSCORES limit 0 2
1) "2023010101"
2) "100"
3) "2023010102"
4) "99"

这样我们就可以通过 ZREVRANGEBYSCORE 实现成绩的排名了,是不是很酷。当然我们还可以限制分值的范围和返回数据集的范围,也是非常灵活。

总结

这里我们就通过几个有趣的小场景介绍完了 Redis 不同的 Key 的使用场景。可以总结成以下几条结论:

  • 简单的字符串存储,优先考虑使用 String
  • 如果是普通的对象属性存储,优先考虑使用 Hash
  • 需要「先进先出」的任务场景,优先考虑使用 List
  • 需要存储一组元素,且元素具有唯一性,优先考虑使用 Set
  • 需要存储一组元素,且元素具有唯一性和「分值」,而且需要对元素进行排序比较,优先考虑使用 Zset

希望此文可以帮到那些刚刚接触到 Redis 的同学,可以在以后的工作中灵活运用,使程序更加高效。

本作品采用《CC 协议》,转载必须注明作者和本文链接
你应该了解真相,真相会让你自由。
本帖由系统于 10个月前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 19

挺不错的文章,收藏了

10个月前 评论
快乐的皮拉夫 (楼主) 10个月前

:smiley: 我就是一直使用k=>v的那波人

10个月前 评论
快乐的皮拉夫 (楼主) 10个月前

签到还能用bitmap,刚看到那一篇文章

10个月前 评论
快乐的皮拉夫 (楼主) 10个月前

很不错

10个月前 评论

大佬有个疑问,假如有个需求,例如我5月就签到了15-21号(SETBIT USER:1:SIGN:2023:05 15 1),其他日期未签到。但是我需要显示整月的签到,是不是只能走数据库查询。我看了bimap可以进行补签(setbit 带日期),也可以查询某一天,还可以统计bitcount ,但是查询一个月的所有数据就不行了

10个月前 评论
快乐的皮拉夫 (楼主) 10个月前
凌晨三点半的卢本伟 (作者) 10个月前
  • 简单的字符串存储,优先使用string
  • 对象类型存储,优先使用 hash
  • 需要先进先出的场景,优先使用队列
  • 需要存储一组元素,并且具有唯一性,优先考虑 集合set
  • 需要存储一组元素,并且具有唯一性和排序,优先考虑zset
10个月前 评论
凌晨三点半的卢本伟 10个月前
jacktop

相当精彩!

10个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!