当前位置: 代码网 > it编程>数据库>Redis > Redis数据类型超详细讲解分析

Redis数据类型超详细讲解分析

2024年12月28日 Redis 我要评论
1 redisredis官网英文版:https://redis.io/redis官网中文版:http://redis.cn/1.1 概述redis是以key-value存储的数据结构服务器,所有的ke

1 redis

redis官网英文版:https://redis.io/

redis官网中文版:http://redis.cn/

1.1 概述

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

  • 字符串类型(string):最基本的数据类型,二进制安全的字符串,最大512m
  • 列表类型(list):按照添加顺序保持顺序的字符串列表
  • 集合类型(set):无序的字符串集合,不存在重复的元素
  • 有序集合类型(sorted setzset):已排序的字符串集合
  • 散列类型(hash):key-value对的一种集合
  • 位操作(bitmap):更细化的一种操作,以bit为单位。
  • 基数统计(hyperloglog):基于概率的数据结构,2.8.9新增
  • 地理位置(geo):地理位置信息储存起来, 并对这些信息进行操作 3.2新增
  • 流(stream) 5.0新增

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

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

1.3.3 string内部编码

string内部编码:

  • int:8个字节的长整型(long,2^63-1)
  • embstr小于等于44 个字节的字符串,embstr格式的 sds(简单动态字符串:simple dynamic string)
  • rawsds大于 44 个字节的字符串

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

  • 字符数组必须先给目标变量分配足够的空间,否则可能会溢出
  • 查询字符数组长度,时间复杂度o(n)
  • 长度变化,需要重新分配内存
  • 通过从字符串开始到结尾碰到的第一个\0来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全

redis sds的优势:

  • 不用担心内存溢出问题,如果需要会对 sds 进行扩容
  • 因为定义了 len 属性,查询数组长度时间复杂度o(1) 固定长度
  • 空间预分配,惰性空间释放
  • 根据长度 len来判断是否结束,而不是 \0

为什么要有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 很好的解决了这个问题,redis 的 hash 实际是内部存储的 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内部编码

内部编码:

  • ziplist(压缩列表):当哈希类型中元素个数小于 hash-max-ziplist-entries配置(默认 512 个),同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,redis 会使用 ziplist 作为哈希的内部实现。
  • hashtable(哈希表):当上述条件不满足时,redis 则会采用 hashtable 作为哈希的内部实现。

在 redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

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操作,则执行以下操作:

  • hash表2分配更大的空间
  • hash表1的数据重新映射拷贝到hash表2
    将hash表1的数据重新映射到hash表2的过程并不是一次性的,这样会造成redis阻塞,无法提供服务
  • 释放hash表1的空间

详细步骤:

  • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个hash表
  • 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
  • 在rehash进行期间,每次对字典执行添加,删除,查找或者更新操作时,程序除了执行特定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增1
  • 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成
  • 将ht[0]释放,然后将ht[1]设置成ht[0],最后为ht[1]分配一个空白哈希表

1.4.4.2 rehash触发条件

rehash触发条件:

  • 扩容
    我们的扩容一般会在hash表中的元素个数等于第一维数组的长度的时候,就会开始扩容。扩容的大小是原数组的两倍。不过在redis在做bgsave(rdb持久化操作的过程)时,为了减少内存页的过多分离(copy on write),redis不会去扩容。
    但是如果hash表的元素个数已经到达了第一维数组长度的5倍的时候,就会强制扩容,不管你是否在持久化。
  • 缩容
    当我们的hash表元素逐渐删除的越来越少的时候。redis就会对hash表进行缩容来减少第一维数组长度的空间占用。缩容的条件是元素个数低于数组长度的10%,并且缩容不考虑是否在做redis持久化
    不用考虑bgsave主要原因是因为我们的缩容的内存都是已经使用过的,缩容的时候可以直接置空,而且由于申请的内存比较小,同时会释放掉一些已经使用的内存,不会增大系统的压力。

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 中取数据。
可以用来实现粉丝列表以及最新消息排行等功能

使用列表的技巧:

  • lpush+lpop=stack(栈)
  • lpush+rpop=queue(队列)
  • lpush+ltrim=capped collection(有限集合)
  • lpush+brpop=message queue(消息队列)

1.5.3 list内部编码

内部编码:

  • ziplist(压缩列表):当列表中元素个数小于 512(默认)个,并且列表中每个元素的值都小于 64(默认)个字节时,redis 会选择用 ziplist 来作为列表的内部实现以减少内存的使用。当然上述默认值也可以通过相关参数修改:list-max-ziplist-entried(元素个数)、list-max-ziplist-value(元素值)。
  • linkedlist(链表):当列表类型无法满足 ziplist 条件时,redis 会选择用 linkedlist 作为列表的内部实现。
    因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现
  • quicklist(快速列表)就是linkedlistziplist的结合。quicklist中的每个节点ziplist都能够存储多个数据元素。redis3.2开始,列表采quicklist进编码

1.6 set集合

1.6.1 简介

redis 的 set 是 string 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
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内部编码:

  • intset(整数集合):当集合中的元素都是整数,并且集合中的元素个数小于set-max-intset-entries 参数时,默认512redis 会选用 intset 作为底层内部实现。
  • hashtable(哈希表):当上述条件不满足时,redis 会采用 hashtable 作为底层实现。

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等

应用常景:

  • 排行榜:有序集合经典使用场景
    例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行,
    如新闻网站对热点新闻排序,比如根据点击量、点赞量等。
  • 带权重的消息队列:重要的消息 score 大一些,普通消息 score 小一些,可以实现优先级高的任务先执行

1.7.3 zset内部编码

内部编码:

  • ziplist(压缩列表):当有序集合的元素个数小于 128 个(默认设置),同时每个元素的值都小于 64 字节(默认设置),redis 会采用 ziplist 作为有序集合的内部实现。也可以通过以下参数设置:zset-max-ziplist-entries 和 zset-max-ziplist-value
  • skiplist(跳跃表):当上述条件不满足时,redis 会采用 skiplist 作为内部编码。

在 redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

1.7.4 跳表数据结构

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是o(n),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种 多层 的有序链表,这样的好处是能快读定位数据。

那跳表长什么样呢?这里举个例子,下图展示了一个层级为 3 的跳表。

图中头节点有 l0~l2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:

  • l0 层级共有 5 个节点,分别是节点1、2、3、4、5;
  • l1 层级共有 3 个节点,分别是节点 2、3、5;
  • l2 层级只有 1 个节点,也就是节点 3 。

如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,而使用了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 l2 层级跳到节点 3,然后再往前遍历找到节点 4。
可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很大时,跳表的查找复杂度就是 o(logn)

跳表是怎么设置层高的?

跳表在创建节点时候,会生成范围为 [0-1] 的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。

1.8 bitmap位图和布隆过滤器

点击了解java中bitmap和布隆过滤器

1.8.1 bitmap位图

1.8.1.1 简介

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

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

优点:

  • 极高空间效率:bitmap 是真的节省数据存储空间。粗略的算一下,一亿位的 bitmap 大概才占 12mb 的内存,相比其他数据结构,能极大地节省存储空间;
  • 快速查询:位操作通常比其他数据结构查询速度更快。无论是设置位值还是获取位值,时间复杂度都为 o (1),能够快速响应查询请求;
  • 易于操作:支持单个位操作、位统计、位逻辑运算等,运算效率高,不需要进行比较和移位;

缺点:

  • 由于数据结构特点,导致它仅适用于表示两种状态,即 0 和 1。对于需要表示更多状态的情况,bitmap 就不适用了;
  • 只有当数据比较密集时才有优势,如果只设置(20,30,888888888)三个偏移量的位值,则需要创建一个 99999999 长度的 bitmap ,但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入另一个 roaring bitmap 来解决;

1.8.1.2 应用常景

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

  • 场景一:用户签到
    很多网站都提供了签到功能(这里不考虑数据落地事宜),并且需要展示最近一个月的签到情况
    redis 为我们提供了bitmap(位图)这一数据结构,每个用户每天的登录记录只占据一位,365天就是365位,仅仅需要46字节就可存储,极大地节约了存储空间。
  • 场景二:统计活跃用户
    使用时间作为cachekey,然后用户id为offset,如果当日活跃过就设置为1
    那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个redis的命令
  • 场景三:用户在线状态
    前段时间开发一个项目,对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户id为offset,如果在线就设置为1,不在线就设置为0,和上面的场景一样,5000w用户只需要6mb的空间

1.8.1.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.1.4 命令

  • setbit
    setbie用来设置或清除存储在键处的字符串值的偏移位,其返回值是原来位上存储的值。key 在初始状态下所有的位都为 0
    基本格式:setbit key offset value
  • getbit
    getbit 用来获取存储在键处的字符串值中偏移位置的位值。
    基本格式:getbit key offset
  • bitcount
    bitcount 用来统计指定区间内,值为1的个数。选择特定的 byte 范围计数,具体如下
    基本格式:bitcount key [start end](注意start和end指的是字节,不是位)
    • start:设置位索引起始位置(包含该位置计数),第一个位置以 0 开始,start 参数需和 end 参数同时设置才合法
    • end:设置位索引结束位置(包含该位置计数),end 参数需和 start参数同时设置才合法
  • bitop
    对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上
    语法格式:bitop operation destkey key [key ...]语法:operation 可以是 and(与) 、 or (或)、 not(非) 、 xor(异或)除了 not 操作之外,其他操作都可以接受一个或多个 key 作为输入
  • bitpos
    用来计算指定 key 对应字符串中,第一位为 1 或者 0 的 offset 位置。除此之外,bitpos 也有两个选项 start 和 end,跟 bitcount 一样。
    语法格式:bitpos key bit [ start [ end [ byte | bit]]]byte、bit 这两个选项从 7.0.0 版本开始才能使用。

1.8.2 布隆过滤器

1.8.2.1 简介

上边提到 bitmap 记录字符元素的状态时,需要先借助哈希运算得出偏移量。但引入哈希运算后可能会出现哈希碰撞的情况,导致状态误判。

布隆过滤器对这个问题做了进一步的优化,做到了可控误判率,当我们将一个邮箱地址添加到集合中,多个不同的哈希函数会将这个邮箱地址映射到 bitmap 中的不同偏移量位置上,且将这些位值置为 1。

要判断邮箱地址是否在集合中,通过相同的哈希函数映射到 bitmap 上的多个位置,如果这些位上的值都为 1,则邮箱可能存在集合中;如果有任何一个位置的值为 0,则元素一定不在集合中。这是布隆过滤器的特点。

虽然但是布隆过滤器还是会发生误判的情况,但好在我们可以通过调整布隆过滤器的大小和哈希函数的数量来控制误判率。

注意:需要redis服务器版本是4.0或以上,因为redis 4.0引入了插件机制,支持布隆过滤器等模块的加载
配置的话是,打开redis的配置文件redis.conf,这个文件通常位于redis的安装目录下。在配置文件中,添加以下行来加载布隆过滤器模块:loadmodule /path/to/redisbloom.so

1.8.2.2 操作命令

布隆过滤器的命令也不多,主要用到的如下几个:

bf.reserve:创建一个新的布隆过滤器,并指定容量 capacity 和误判率 error_rate

bf.reserve <key> <error_rate> <capacity>
bf.reserve myfilter 0.000001 999999

bf.info:获取布隆过滤器的信息,包括容量、误判率等。bf.info <key>bf.add 和 bf.madd:分别是向布隆过滤器中添加元素和批量添加

# 向布隆过滤器中添加元素
bf.add myfilter hello
bf.madd <key> <item> [item ...]

bf.exists 和 bf.mexists:分别是检查布隆过滤器中某个元素和批量检查元素是否存在

# 元素是否存在于布隆过滤器中
bf.exists myfilter hello
# 元素是否存在于布隆过滤器中
bf.mexists <key> <item> [item ...]

1.8.2.3 优缺点

  • 优点:
    • 布隆过滤器的空间占用也是极小,它本身不存储完整的数据,和 bitmap 一样底层也是通过 bit 位来表示数据是否存在。
    • 性能比较稳定,无论集合中元素的数量有多少,插入和查询操作的时间复杂度都非常低,通常为 o (k),其中 k 是哈希函数的个数。也就是说在处理大规模数据时,布隆过滤器的性能不会随着数据量的增加而急剧下降。
  • 缺点:
    • 存在一定的误识别率:布隆过滤器存在误判的情况,即当一个元素实际上不在集合中时,有可能被判断为在集合中。这是因为多个元素可能通过哈希函数映射到相同的位置,导致误判。但是,当布隆过滤器判断一个元素不在集合中时,则是 100% 正确的。
    • 删除元素比较困难:一般情况下,不能直接从布隆过滤器中删除元素。这是因为一个位置可能被多个元素映射到,如果直接将该位置的值置为 0,可能会影响其他元素的判断。

1.8.2.4 应用场景

布隆过滤器存在一定的误判,所以使用它的场景就一定要允许不准确的情况发生:

  • 解决 redis 缓存穿透问题:秒杀商品详情通常会被缓存到 redis 中。如果有大量恶意请求查询不存在的商品,通过布隆过滤器可以快速判断这些商品不存在,从而避免了对数据库的查询,减轻了数据库的压力。
  • 邮箱黑名单过滤:在邮件系统中,可以使用布隆过滤器来过滤垃圾邮件和恶意邮件。将已知的垃圾邮件发送者的地址或特征存储在布隆过滤器中,新邮件来时判断发送者是否在黑名单中。
  • 对爬虫网址进行过滤:在爬虫程序中,为了避免重复抓取相同的网址,可以使用布隆过滤器来记录已经抓取过的网址。新网址出现时,先判断是否已抓取过。

1.9 hyperloglog基数统计

1.9.1 简介

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

  • 优点:
    在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且是很小的。在 redis 里面,每个 hyperloglog 键只需要花费 12 kb 内存,就可以计算接近 2^64 个不同元素的基数。
  • 缺点:
    因为 hyperloglog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 hyperloglog 不能像集合那样,返回输入的各个元素。
    估算的值,可能存在误差,带有 0.81% 标准错误的近似值

1.9.2 命令和场景

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

  • pfadd:添加指定元素到 hyperloglog
  • pfcount:返回给定 hyperloglog 的基数估算值
  • pfmerge:将多个 hyperloglog 合并为一个 hyperloglog

应用场景:

  • 网页统计uv (浏览用户数量,同一天同一个ip多次访问算一次访问,目的是计数,而不是保存用户)
    传统的方式是使用set保存用户的id,可以统计set中元素数量作为标准判断。
    但如果这种方式保存大量用户id,会占用大量内存,我们的目的是为了计数,而不是去保存id。
  • 注册 ip 数、每日访问 ip 数、页面实时uv)、在线用户数等

1.9.3 内部编码和原理

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

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

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

1.10 geo地理位置

1.10.1 简介

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

1.10.2 命令和场景

命令:

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

主要操作方法有:

  • geoadd:添加地理位置的坐标
    geoadd 语法格式:geoadd key longitude latitude member [longitude latitude member ...]
  • geopos:用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nilgeopos 语法格式:geopos key member [member ...]
  • geodist:用于返回两个给定位置之间的距离
    geodist 语法格式:geodist key member1 member2 [m|km|ft|mi]member1 member2 为两个地理位置
    最后一个距离单位参数说明:m :米,默认单位;km :千米;mi :英里;ft :英尺
  • georadius:以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素
    语法格式:georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] [asc|desc] [store key] [storedist key]参数说明:
    • m :米,默认单位。
    • km :千米。
    • mi :英里。
    • ft :英尺。
    • withdist: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
    • withcoord: 将位置元素的经度和纬度也一并返回。
    • withhash: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
    • count: 限定返回的记录数
    • asc: 查找结果根据距离从近到远排序。
    • desc: 查找结果根据从远到近排序。
  • georadiusbymember: 和 georadius 命令一样, 都可以找出位于指定范围内的元素, 但是 georadiusbymember 的中心点是由给定的位置元素决定的, 而不是使用经度和纬度来决定中心点
    语法格式:georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] [asc|desc] [store key] [storedist key]参数说明同georadius
  • geohash:返回一个或多个位置对象的 geohash 值。
    redis geo 使用 geohash 来保存地理位置的坐标。geohash 用于获取一个或多个位置元素的 geohash 值。
    geohash 语法格式如下:geohash key member [member ...]

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

  • 查看附近的人
  • 微信位置共享
  • 地图上直线距离的展示
  • 比如检索附近的主播

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 stream 是 redis 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 和对应的内容

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

上图解析:

  • consumer group :消费组,使用 xgroup create 命令创建,一个消费组有多个消费者(consumer)。
  • last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
  • pending_ids :消费者(consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (acknowledge character:确认字符)。

消息队列相关命令:

  • xadd :添加消息到末尾
    使用 xadd 向队列添加消息,如果指定的队列不存在,则创建一个队列
    语法格式:xadd key id field value [field value ...]
    • key :队列名称,如果不存在就创建
    • id :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
    • field value : 记录
  • xtrim :对流进行修剪,限制长度
    语法格式:xtrim key maxlen [~] count
    • key :队列名称
    • maxlen :长度
    • count :数量
  • xdel :删除消息
    语法格式:xdel key id [id ...]
    • key:队列名称
    • id :消息 id
  • xlen :获取流包含的元素数量,即消息长度
    语法格式:xlen keykey:队列名称
  • xrange :获取消息列表,会自动过滤已经删除的消息
    语法格式:xrange key start end [count count]
    • key :队列名
    • start :开始值,-表示最小值
    • end :结束值, + 表示最大值
    • count :数量
  • xrevrange :反向获取消息列表,id 从大到小
    语法格式:xrevrange key end start [count count]
    • key :队列名
    • end :结束值, + 表示最大值
    • start :开始值, - 表示最小值
    • count :数量
  • xread :以阻塞或非阻塞方式获取消息列表
    语法格式:xread [count count] [block milliseconds] streams key [key ...] id [id ...]
    • count :数量
    • milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式
    • key :队列名
    • id :消息 id

消费者组相关命令:

  • xgroup create :创建消费者组
    语法格式:xgroup [create key groupname id-or-$] [setid key groupname id-or-$] [destroy key groupname] [delconsumer key groupname consumername]
    • key :队列名称,如果不存在就创建
    • groupname :组名。
    • $ : 表示从尾部开始消费,只接受新消息,当前 stream 消息会全部忽略。
    • 从头开始消费:
      xgroup create mystream consumer-group-name 0-0
    • 从尾部开始消费:
      xgroup create mystream consumer-group-name $
  • xreadgroup group :读取消费者组中的消息
    语法格式:xreadgroup group group consumer [count count] [block milliseconds] [noack] streams key [key ...] id [id ...]
    • group :消费组名
    • consumer :消费者名
    • count : 读取数量
    • milliseconds : 阻塞毫秒数
    • key : 队列名
    • id : 消息 id
  • xack :将消息标记为"已处理"
  • xgroup setid :为消费者组设置新的最后递送消息id
  • xgroup delconsumer :删除消费者
  • xgroup destroy :删除消费者组
  • xpending :显示待处理消息的相关信息
  • xclaim :转移消息的归属权
  • xinfo :查看流和消费者组的相关信息;
  • xinfo groups :打印消费者组的信息;
  • xinfo stream :打印流信息

1.11.3 内部编码

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

如下是一个保存了7个单词的radix tree:

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

mystream 总共由 3 部分构成:

  • 第一部分是 robj, 每个 redis 对象实例都会有一个最基本的结构来存储它实际的类型, 编码和对应的结构的位置
  • 第二部分是一个 rax, 用作存储 stream id
  • 第三部分是 listpackrax 下的每一个 key 节点都会把对应的 keys 和 values 的值存在这个 listpack 结构中

总结 

到此这篇关于redis数据类型的文章就介绍到这了,更多相关redis数据类型内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com