分布式@架构师

Redis之数据类型详解分析

2023-02-26  本文已影响0人  上善若泪

1 Redis

Redis官网英文版:https://redis.io/
Redis官网中文版:http://redis.cn/

1.1 概述

Redis是以key-value存储的数据结构服务器,所有的key(键)是字符串,而value可以包含:

image.png

1.2 查看内部编码

Redis查看内部编码使用OBJECT ENCODING命令
该命令用来返回数据结构的内部编码

对象所使用的底层数据结构 编码常量 object encoding 命令输出
整数 REDIS_ENCODING_INT "int"
embstr编码简单动态字符串(SDS) REDIS_ENCODING_EMBSTR "embstr"
简单动态字符串 REDIS_ENCODING_RAW "raw"
字典 REDIS_ENCODING_HT "hashtable"
双端链表 REDIS_ENCODING_LINKEDLIST "linkedlist"
压缩列表 REDIS_ENCODING_ZIPLiST "ziplist"
整数集合 REDIS_ENCODING_INTSET "intset"
跳跃表和字典 REDIS_ENCODING_SKIPLIST "skiplist"

1.3 String字符串

1.3.1 简介

String是redis中最基本的数据类型,一个key对应一个value。
redis的key和string类型value限制均为512MB
虽然Key的大小上限为512M,但是一般建议key的大小不要超过1KB,这样既可以节约存储空间,又有利于Redis进行检索

1.3.2 应用常景

String类型是二进制安全的,意思是 redisstring 可以包含任何数据。如数字字符串jpg图片或者序列化的对象。字符串类型实际上可以是字符串(简单的字符串、复杂的字符串(xml、json)、数字(整数、浮点数)、二进制(图片、音频、视频))

缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。

1.3.3 String内部编码

String内部编码:

Redis 为什么要自己写一个SDS的数据类型,主要是为了解决C语言 char[]的四个问题:

Redis SDS的优势:

为什么要有embstr编码呢?比raw的优势在哪里?

embstr编码将创建字符串对象所需的空间分配的次数从raw编码的两次降低为一次。
因为emstr编码字符串的素有对象保持在一块连续的内存里面,所以那个编码的字符串对象比起raw编码的字符串对象能更好的利用缓存。并且释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码对象的字符串对象需要调用两次内存释放函数

1.4 Hash散列

1.4.1 简介

常用命令:hget,hsetnx,hset,hvals,hgetall,hmset,hmget 等
Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)

1.4.2 应用常景

我们简单举个实例来描述下 Hash 的应用场景:

比如我们要存储一个用户信息对象数据,包含以下信息:用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,如果用普通的 key/value 结构来存储,主要有以下2种存储方式:

  • 第一种方式将用户 ID 作为查找 key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
  • 第二种方法是这个用户信息对象有多少成员就存成多少个 key-value 对儿,用用户 ID +对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户 ID 为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。

那么 Redis 提供的 Hash 很好的解决了这个问题,RedisHash 实际是内部存储的 Value 为一个 HashMap,并提供了直接存取这个 Map 成员的接口
也就是说,Key 仍然是用户 ID,value 是一个 Map,这个 Map 的 key 是成员的属性名,value 是属性值,这样对数据的修改和存取都可以直接通过其内部 Map 的 Key(Redis 里称内部 Map 的 key 为 field),也就是通过 key(用户 ID) + field(属性标签)就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
很好的解决了问题。这里同时需要注意,Redis 提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部 Map 的成员很多,那么涉及到遍历整个内部 Map 的操作,由于 Redis 单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。

1.4.3 Hash内部编码

内部编码:

1.4.4 rehash和渐进式rehash操作

扩容和缩容都会通过rehash来实现,所谓渐进式rehash是指我们的大字典的扩容是比较消耗时间的,需要重新申请新的数组,然后将旧字典所有链表的元素重新挂接到新的数组下面,是一个O(n)的操作。但是因为我们的redis是单线程的,无法承受这样的耗时过程,所以采用了渐进式rehash小步搬迁,虽然慢一点,但是可以搬迁完毕

redis会在内部扩容时新建一个长度为原始长度2倍的空哈希表,然后原哈希表上的元素重新rehash到新的哈希表中去,然后我们再使用新的哈希表即可。
那么,这样还是有个问题要解决呀

要知道redis中存储的数据可能是成百万上千万的,我们重新rehash一次未免太耗时了吧,因为redis中操作大部分是单线程的。
这个过程可能会阻断其他操作很长时间,这是不能忍受的,那要怎么处理呢

1.4.4.1 过程

首先redis是采用了渐进式rehash的操作,就是会有一个变量,指向第一个哈希桶,然后redis每执行一个添加key,删除key的类似命令,就顺便copy一个哈希桶中的数据到新的哈希表中去,这样细水长流的操作,是不会影响什么性能,就会所有的数据都被重新hash到新的哈希表中。
那么在这个过程中,当然再有写的操作,会直接把数据放到新的哈希表中,保证旧的肯定有copy完的时候,如果这段时间对数据库的操作比较少,也没有关系,redis内部也有定时任务,每隔一段时间也会copy一次

redis通过链式哈希解决冲突,也就是同一个桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能。所以redis为了追求块,使用了两个全局哈希表。用于rehash操作,增加现有的哈希桶数量,减少哈希冲突
开始默认使用hash表1保存键值对数据,hash表2此刻没有分配空间。当数据越来越多的触发rehash操作,则执行以下操作:

详细步骤:

1.4.4.2 rehash触发条件

rehash触发条件:

1.4.5 跟JDK的HashMap的区别

数据结构上,采用了两个数组保存数据,发生hash冲突时,只采用了链地址法解决hash冲突,并没有跟jdk1.8一样当链表超过8时优化成红黑树,因此插入元素时跟jdk1.7hashmap一样采用的是头插法
在发生扩容时,跟jdk的hashmap一次性、集中式进行扩容不一样,采取的是渐进式的rehash,每次操作只会操作当前的元素,在当前数组中移除或者存放到新的数组中,直到老数组的元素彻底变成空表。
当负载因子小于0.1时,会自动进行缩容。jdk的hashmap出于性能考虑,不提供缩容的操作。
redis使用MurmurHash来计算哈希表的键的hash值,而jdkhashmap使用key.hashcode()的高十六位跟低十六位做与运算获得键的hash值。

1.5 List列表

1.5.1 简介

Redis中的List其实就是链表Redis双端链表实现List
使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。

列表(List)用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等

1.5.2 命令和应用

常用命令:lpush,rpush,lpop,rpop,lrange等。

应用场景
比如 twitter 的关注列表,粉丝列表等都可以用 Redis 的 list 结构来实现,可以利用lrange命令,做基于Redis的分页功能,性能极佳,用户体验好
消息队列:Redis 的 list 是有序的列表结构,可以实现阻塞队列,使用左进右出的方式。Lpush 用来生产 从左侧插入数据,Brpop 用来消费,用来从右侧 阻塞的消费数据。
数据的分页展示: lrange 命令需要两个索引来获取数据,这个就可以用来实现分页,可以在代码中计算两个索引值,然后来 redis 中取数据。
可以用来实现粉丝列表以及最新消息排行等功能

使用列表的技巧:

1.5.3 List内部编码

内部编码:

1.6 Set集合

1.6.1 简介

RedisSetString 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)
集合中最大的成员数为 2^32 - 1 (每个集合可存储40多亿个成员)

1.6.2 命令和应用

常用命令:sadd,spop,smembers,sunion,scard,sscan,sismember等。

应用场景:
Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。
标签(tag):集合类型比较典型的使用场景,如一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,这些数据对于用户体验以及曾强用户粘度比较重要。
点赞,或点踩,收藏等,可以放到set中实现

1.6.3 Set内部编码

Set内部编码:

1.7 ZSet有序集合

1.7.1 简介

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数redis 正是通过分数来为集合中的成员进行从小到大的排序。

有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 2^32 - 1 (每个集合可存储40多亿个成员)

1.7.2 命令和应用

常用命令:zadd,zrange,zrem,zcard,zscore,zcount,zlexcount等

应用常景:

1.7.3 ZSet内部编码

内部编码:

1.8 Bitmap位图

1.8.1 简介

Bitmap(也称为位数组或者位向量等)是一种实现对位的操作的'数据结构',在数据结构加引号主要因为:Bitmap本身不是一种数据结构,底层实际上是字符串,可以借助字符串进行位操作。

Bitmap 单独提供了一套命令,所以与使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 01,数组的下标在 Bitmap 中叫做偏移量 offset
bitmap的出现是为了大数据量而来的,但是前提是统计的这个大数据量每个的状态只能有两种,因为每一个bit位只能表示两种状态。

1.8.2 应用常景

假如我们现在有几亿个数据,数据状态都是1或者0两个状态,比如用户签到次数、或者登录次数等。

1.8.3 底层原理

我们知道 Bitmap 本身不是一种数据结构,底层实际上使用字符串来存储。只不过操作的粒度变成了位,即bit。
由于 Redis 中字符串的最大长度是 512 MB字节,所以 BitMap 的偏移量 offset 值也是有上限的,其最大值是:8 * 1024 * 1024 * 512 = 2^32。由于 C 语言中字符串的末尾都要存储一位分隔符,所以实际上 BitMap 的偏移量 offset 值上限是:2^32-1

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。可以把 Bitmap 看作是一个 bit 数组。

1.8.4 命令

1.9 HyperLogLog基数统计

1.9.1 简介

Redis 2.8.9 版本更新了 Hyperloglog 数据结构,Redis HyperLogLog 是用来做基数统计的算法,所谓基数,也就是不重复的元素

1.9.2 命令和场景

这个数据结构的命令有三个:

应用场景:

1.9.3 内部编码和原理

HyperLogLog算法时一种非常巧妙的近似统计大量去重元素数量的算法,它内部维护了16384个桶来记录各自桶的元素数量,当一个元素过来,它会散列到其中一个桶。当元素到来时,通过 hash 算法将这个元素分派到其中的一个小集合存储,同样的元素总是会散列到同样的小集合。这样总的计数就是所有小集合大小的总和。使用这种方式精确计数除了可以增加元素外,还可以减少元素

一个HyperLogLog实际占用的空间大约是 12k 字节。但是在计数比较小的时候,大多数桶的计数值都是零。如果 12k 字节里面太多的字节都是零,那么这个空间是可以适当节约一下的。
Redis 在计数值比较小的情况下采用了稀疏存储稀疏存储的空间占用远远小于 12k 字节。相对于稀疏存储的就是密集存储密集存储会恒定占用 12k 字节。

内部编码
HyperLogLog 整体的内部结构就是 HLL 对象头 加上 16384 个桶的计数值位图。它在 Redis 的内部结构表现就是一个字符串位图。你可以把 HyperLogLog 对象当成普通的字符串来进行处理。

1.10 GEO地理位置

1.10.1 简介

RedisGeoRedis 3.2 版本就推出了,这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人,GEO使用的是国际通用坐标系WGS-84

1.10.2 命令和场景

命令:

help @geo 查看geo分组下所有的命令
help geoadd 用于查看单个具体命令

主要操作方法有:

应用场景:
用于存储地理信息以及对地理信息作操作的场景

1.10.3 内部编码

需要说明的是,Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET),并且使用GeoHash技术进行填充。Redis中将经纬度使用52位的整数进行编码,放进zset中,score就是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实就是zset(skiplist)的操作。
通过zsetscore进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。

总之,Redis中处理这些地理位置坐标点的思想是:二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score(整数编码值)反解坐标点 --> 附近点的地理位置坐标

1.11 Stream流

1.11.1 简介

Redis StreamRedis 5.0 版本新增加的数据结构。
Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
点击此处了解为什么Redis不适合用作MQ
简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。

Redis Stream 提供了消息的持久化主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
用一句话概括Stream就是Redis实现的内存版kafka,支持多播的可持久化的消息队列,用于实现发布订阅功能,借鉴了 kafka 的设计。Redis Stream的结构有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis重启后,内容还在。

1.11.2 命令

Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容

image.png

每个 Stream 都有唯一的名称,它就是 Rediskey,在我们首次使用 xadd 指令追加消息时自动创建。

上图解析:

消息队列相关命令:

消费者组相关命令:

1.11.3 内部编码

stream底层的数据结构是radix treeRadix Tree(基数树) 事实上就是几乎相同是传统的二叉树。仅仅是在寻找方式上,以一个unsigned int类型数为例,利用这个数的每个比特位作为树节点的推断。能够这样说,比方一个数10001010101010110101010,那么依照Radix 树的插入就是在根节点,假设遇到0,就指向左节点,假设遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。

如下是一个保存了7个单词的Radix Tree:


image.png
127.0.0.1:6379> xadd mystream * key1 128
"1576480551233-0"
127.0.0.1:6379> object encoding mystream
"unknown"

mystream 总共由 3 部分构成:

上一篇下一篇

猜你喜欢

热点阅读