2 万字 + 20张图|细说 Redis 九种数据类型和应用场景

我们都知道 Redis 提供了丰富的数据类型,常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。

随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。

每种数据对象都各自的应用场景,你能说出它们各自的应用场景吗?

面试过程中,这个问题也很常被问到,又比如会举例一个应用场景来问你,让你说使用哪种 Redis 数据类型来实现。

所以,这次我们就来学习 Redis 数据类型的使用以及应用场景。篇幅比较长,大家收藏慢慢看。

image.png

image.png

String

介绍

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。

image.png

内部实现

String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。

image.png

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int。

image.png

如果字符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr, embstr编码是专门用于保存短字符串的一种优化编码方式:

image.png

如果字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw:

image.png

可以看到embstr和raw编码都会使用SDS来保存值,但不同之处在于embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS。Redis这样做会有很多好处:

embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次;释放 embstr编码的字符串对象同样只需要调用一次内存释放函数;因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。

但是 embstr 也有缺点的:

如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

常用指令

普通字符串的基本操作:

设置 key-value 类型的值> SET name linOK# 根据 key 获得对应的 value> GET name"lin"# 判断某个 key 是否存在> EXISTS name(integer) 1# 返回 key 所储存的字符串值的长度> STRLEN name(integer) 3# 删除某个 key 对应的值> DEL name(integer) 1

批量设置 :

批量设置 key-value 类型的值> MSET key1 value1 key2 value2 OK# 批量获取多个 key 对应的 value> MGET key1 key2 1) "value1"2) “value2”

计数器(字符串的内容为整数的时候可以使用):

设置 key-value 类型的值> SET number 0OK# 将 key 中储存的数字值增一> INCR number(integer) 1# 将key中存储的数字值加 10> INCRBY number 10(integer) 11# 将 key 中储存的数字值减一> DECR number(integer) 10# 将key中存储的数字值键 10> DECRBY number 10(integer) 0

过期(默认为永不过期):

设置 key 在 60 秒后过期(该方法是针对已经存在的key设置过期时间)> EXPIRE name 60 (integer) 1# 查看数据还有多久过期> TTL name (integer) 51#设置 key-value 类型的值,并设置该key的过期时间为 60 秒> SET key value EX 60OK> SETEX key 60 valueOK

不存在就插入:

不存在就插入(not exists)>SETNX key value(integer) 1

应用场景

缓存对象

使用 String 来缓存对象有两种方式:

直接缓存整个对象的 JSON,命令例子:SET user:1 ‘{“name”:“xiaolin”, “age”:18}’。采用将 key 进行分离为 user:ID:属性,采用 MSET 存储,用 MGET 获取各属性值,命令例子:MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20。

常规计数

因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。

比如计算文章的阅读量:

初始化文章的阅读量> SET aritcle:readcount:1001 0OK#阅读量+1> INCR aritcle:readcount:1001(integer) 1#阅读量+1> INCR aritcle:readcount:1001(integer) 2#阅读量+1> INCR aritcle:readcount:1001(integer) 3# 获取对应文章的阅读量> GET aritcle:readcount:1001"3"

分布式锁

SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:

如果 key 不存在,则显示插入成功,可以用来表示加锁成功;如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:

SET lock_key unique_value NX PX 10000

lock_key 就是 key 键;unique_value 是客户端生成的唯一的标识;NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放if redis.call(“get”,KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1])else return 0end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

List

介绍

List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。

列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。

内部实现

List 类型的底层数据结构是由双向链表或压缩列表实现的:

如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

常用命令

image.png

将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面LPUSH key value [value …] # 将一个或多个值value插入到key列表的表尾(最右边)RPUSH key value [value …]# 移除并返回key列表的头元素LPOP key # 移除并返回key列表的尾元素RPOP key # 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始LRANGE key start stop# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞BLPOP key [key …] timeout# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞BRPOP key [key …] timeout

应用场景

消息队列

消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。

Redis 的 List 和 Stream 两种数据类型,就可以满足消息队列的这三个需求。我们先来了解下基于 List 的消息队列实现方法,后面在介绍 Stream 数据类型时候,在详细说说 Stream。

1、如何满足消息保序需求?

List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。

List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP࿰

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值