Redis 实用小技巧——一文教你如何选择合适的 Key 类型
简介
本文我们来聊一聊在 Redis 中应该如何选择合适的 Key 类型。
说到 Redis 的 Key 类型,相信很多朋友都会脱口而出:这不简单么,不就是 String
,Hash
,List
,Set
和 Zset
么?的确如此。但是对于一些刚刚接触 Redis 的同学来说,可能用过最多的就是 String
类型。记得我刚刚工作那会,最初接触 Redis 的时候,就是听说它是一种缓存技术,比直接操作 MySQL 更快,用的最多的操作也就是 String
类型的 Set
和 Get
操作了,直到过了很长一段时间以后才慢慢接触到其他的数据类型(不知道有多少同学和我有过同样的经历)。
今天我们就来聊一聊 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
类型最常见且用的最广泛的命令就是 GET
和 SET
。当我们需要存储简单的数据,或者需要使用到整数自增这些操作时,再或者仅仅是为了「占位」操作时,我们就可以考虑直接使用 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 协议》,转载必须注明作者和本文链接
挺不错的文章,收藏了
mark
手把手了
:+1:
支持一下
:+1:
:smiley: 我就是一直使用k=>v的那波人
签到还能用bitmap,刚看到那一篇文章
很不错
:+1:
大佬有个疑问,假如有个需求,例如我5月就签到了15-21号(SETBIT USER:1:SIGN:2023:05 15 1),其他日期未签到。但是我需要显示整月的签到,是不是只能走数据库查询。我看了bimap可以进行补签(setbit 带日期),也可以查询某一天,还可以统计bitcount ,但是查询一个月的所有数据就不行了
相当精彩!