Redis基础——数据类型详解

导读:本篇文章讲解 Redis基础——数据类型详解,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

命令参考:http://doc.redisfans.com/

简介

Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。

Redis 与其他 key – value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis与其他key-value存储有什么不同?

  • Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
  • Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

数据类型

如图所示,Redis中提供了9种不同的数据操作类型,他们分别代表了不同的数据存储结构。

image-20211123224109145

String

String类型是Redis用的较多的一个基本类型,也是最简单的一种类型,一个key对应一个value;它和我们在Java中使用的字符类型什么太大区别。

string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。

string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。

他的结构如图所示:

image-20211123224311044

实例

image-20211123223748287

我们可以通过 set 方法创建一个keynamevaluechen的键值对;然后通过get方法获取name的值。

常用指令

image-20211123224358299

存储结构

学过 C++ 的应该知道,C++ 中是没有 String 类型,但是 Redis 又是基于 C++ 来实现的,那么它是如何存储 String 类型的呢?

Redis 并没有采用 C 语言的传统字符串表示方式(char* 或者 char[]),在 Redis 内部,String 类型以 int/SDS(simple dynamic string) 作为结构存储,int 用来存放整型数据,sds 存放字节 / 字符串和浮点型数据。

在 C 的标准字符串结构下进行了封装,用来提升基本操作的性能,同时充分利用以后的 C 的标准库,简化实现。我们可以在 redis 的源码中【sds.h】中看到 sds 的结构如下;

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;//表示当前sds的长度(单位是字节)
    uint8_t alloc; //表示已为sds分配的内存大小(单位是字节)
    unsigned char flags; //用一个字节表示当前sdshdr的类型,因为有sdshdr有五种类型,所以至少需要3位来表示000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。高5位用不到所以都为0。
    char buf[];//sds实际存放的位置
};

也就是说实际上 sds 类型就是 char* 类型,那 sdschar* 有什么区别呢?

主要区别就是:sds 一定有一个所属的结构 (sdshdr),这个 header 结构在每次创建 sds 时被创建,用来存储 sds 以及 sds 的相关信息

对 sds 结构有一个简单认识以后,我们如果通过 set 创建一个字符串,那么也就是会创建一个 sds 来存储这个字符串信息,那么这个过程是怎么样的呢?

  • 首先第一个要判断选择一个什么类型的 sdshdr 来存放信息?这就得根据要存储的 sds 的长度决定了,redis 在创建一个 sds 之前会调用【sds.c 文件】sdsReqType (size_t string_size) 来判断用哪个 sdshdr。该函数传递一个 sds 的长度作为参数,返回应该选用的 sdshdr 类型。
  • 然后把数据保存到对应的 sdshdr 中。

image-20211123225244428

Redis 采用类似 C 的做法存储字符串,也就是以’\0’结尾,’\0’只作为字符串的定界符,不计入 alloc 或者 len

命名规范

  • redis 并没有规定我们对 key 应该怎么命名,但是最好的实践是 “对象类型:对象 id: 对象属性: 子属性”
  • key 不要设置得太长,太长的 key 不仅仅消耗内存,而且在数据中查找这类键值计算成本很高
  • key 不要设置得太短,比如 u:1000:pwd 来代替 user:1000:password, 虽然没什么问题,但是后者的可读性更好
  • 为了更好的管理你的 key,对 key 进行业务上的分类;同时建议有一个 wiki 统一管理所有的 key,通过查询这个文档知道 redis 中的 key 的作用

应用场景

String 类型使用比较多,一般来说,不太了解 Redis 的人,几乎所有场景都是用 String 类型来存储数据。

分布式缓存

首先最基本的就是用来做业务数据的缓存,Redis 中会缓存一些常用的热点数据,可以提升数据查询的性能。

image-20211123225543207

分布式全局ID

使用 String 类型的 incr 命令,实现原子递增。

分布式 session

基于登录场景中,保存 token 信息。

限流

计数器限流

List

列表类型 (list) 可以存储一个有序且可重复的字符串列表,常用的操作是向列表两端添加元素或者获得列表的某一个片段,List 的存储结构如下图所示:

image-20211123225755091

列表最多可存储 232 – 1 元素 (4294967295, 每个列表可存储40多亿)。

实例

image-20211123230002067

该操作就是简单的通过LPUSH方法从队列的左边入队元素,然后通过LRANGE方法遍历指定区间内的元素。

常用命令

image-20211123230053355

存储结构

在 redis6.0 中,List 采用了 QuickList 这样一种结构来存储数据,QuickList 是一个双向链表,链表的每个节点保存一个 ziplist,所有的数据实际上是存储在 ziplist 中,ziplist 是一个压缩列表,它可以节省内存空间。

ziplist 详细说明:https://www.cnblogs.com/hunternet/p/11306690.html

听到 “压缩” 两个字,直观的反应就是节省内存。之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。我们知道,数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小 (假设是 5 个字节)。存储小于 5 个字节长度的字符串的时候,便会浪费部分存储空间,比如下面这个图所示。

image-20211123230321866

所以,ziplist 就是根据每个节点的长度来决定占用内存大小,然后每个元素保存时同步记录当前数据的长度,这样每次添加元素是就可以计算下一个节点在内存中的存储位置,从而形成一个压缩列表。

image-20211123230442036

另外,这种方式存储数据有一个很好的优势,就是它存储的是在一个连续的内存空间,它可以很好的利用 CPU 的缓存来访问数据,从而提升访问性能。

image-20211123230536649

其中,QuickList 中的每个节点称为 QuickListNode,具体的定义在 quicklist.h 文件中。

typedef struct quicklistNode {
    struct quicklistNode *prev;   //链表的上一个node节点
    struct quicklistNode *next;   //链表的下一个node节点
    unsigned char *zl;            //数据指针,如果当前节点数据没有压缩,它指向一个ziplist,否则,指向一个quicklistLZF
    unsigned int sz;             /* 指向的ziplist的总大小 */
    unsigned int count : 16;     /* ziplist中的元素个数 */
    unsigned int encoding : 2;   /* 表示ziplist是否压缩了,1表示没压缩,2表示压缩 */
    unsigned int container : 2;  /* 预留字段 */
    unsigned int recompress : 1; /* 当使用类似lindex命令查看某一个本压缩的数据时,需要先解压,这个用来存储标记,等有机会再把数据重新压缩 */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;

quickList 是 list 类型的存储结构,其定义如下。

typedef struct quicklist {
    quicklistNode *head;    //指向quicklistNode头节点
    quicklistNode *tail;    //指向quicklistNode的尾节点
    unsigned long count;        /* 所有ziplist数据项的个数综合 */
    unsigned long len;          /* quicklist节点个数*/
    int fill : QL_FILL_BITS;              /* ziplist大小设置 */
    unsigned int compress : QL_COMP_BITS; /* 节点压缩深度设置 */
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;

当向 list 中添加元素时,会直接保存到某个 QuickListNode 中的 ziplist 中,不过不管是从头部插入数据,还是从尾部插入数据,都包含两种情况:

  • 如果头节点(尾部节点)上的 ziplist 大小没有超过限制,新数据会直接插入到 ziplist 中
  • 如果头节点上的 ziplist 达到阈值,则创建一个新的 quicklistNode 节点,该节点中会创建一个 ziplist,然后把这个新创建的节点插入到 quicklist 双向链表中。

image-20211123230625540

应用场景

消息队列

list类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以list类型可以用来实现消息队列。

image-20211123231024473

发红包场景

在发红包的场景中,假设发一个 10 元,10 个红包,需要保证抢红包的人不会多抢到,也不会少抢到,这种情况下,我们可以按照如下步骤进行操作。

image-20211123231115506

Hash

Redis hash 是一个键值(key=>value)对集合。

Redis hash 是一个 string 类型的 field 和 value 的映射表,但是 value 是一个键值对(key-value),类比于 Java 里面的 Map<String,Map<String,Object>> 集合。

所以这种特性使得hash 特别适合用于存储对象。

image-20211123231325826

实例

image-20211123231833154

常用命令

image-20211123231358852

存储结构

哈希类型的内部编码有两种:ziplist 压缩列表 , hashtable 哈希表。只有当存储的数据量比较小的情况下,Redis 才使用压缩列表来实现字典类型。具体需要满足两个条件:

  • 当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)
  • 所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)
    ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。

image-20211123232031512

应用场景

Hash 表使用用来存储对象数据,比如用户信息,相对于通过将对象转化为 json 存储到 String 类型中,Hash 结构的灵活性更大,它可以任何添加和删除对象中的某些字段。

购物车功能

  • 以用户 ID 作为 key
  • 以商品 id 作为 field
  • 以商品的数量作为 value

对象类型数据

比如优化之后的用户信息存储,减少数据库的关联查询导致的性能慢的问题。

  • 用户信息
  • 商品信息
  • 计数器

Set

集合类型 (Set) 是一个无序并唯一的键值集合。它的存储顺序不会按照插入的先后顺序进行存储。

集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

集合类型和列表类型的区别如下:

  • 列表可以存储重复元素,集合只能存储非重复元素;
  • 列表是按照元素的先后顺序存储元素的,而集合则是无序方式存储元素的。

image-20211123232316881

实例

image-20211123232613087

常用命令

命令 说明 时间复杂度
SADD key member [member …] 添加一个或者多个元素到集合 (set) 里 O(N)
SCARD key 获取集合里面的元素数量 O(1)
SDIFF key [key …] 获得队列不存在的元素 O(N)
SDIFFSTORE destination key [key …]] 获得队列不存在的元素,并存储在一个关键的结果集 O(N)
SINTER key [key …] 获得两个集合的交集 O(N*M)
SINTERSTORE destination key [key …] 获得两个集合的交集,并存储在一个关键的结果集 O(N*M)
SISMEMBER key member 确定一个给定的值是一个集合的成员 O(1)
SMEMBERS key 获取集合里面的所有元素 O(N)
SMOVE source destination member 移动集合里面的一个元素到另一个集合 O(1)
SPOP key [count] 删除并获取一个集合里面的元素 O(1)
SRANDMEMBER key [count] 从集合里面随机获取一个元素
SREM key member [member …]] 从集合里删除一个或多个元素 O(N)
SUNION key [key …]] 添加多个 set 元素 O(N)
SUNIONSTORE destination key [key …] 合并 set 元素,并将结果存入新的 set 里面 O(N)

存储结构

Set 在的底层数据结构以 intset 或者 hashtable 来存储。当 set 中只包含整数型的元素时,采用 intset 来存储,否则,采用 hashtable 存储,但是对于 set 来说,该 hashtable 的 value 值用于为 NULL,通过 key 来存储元素。

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

intset 将整数元素按顺序存储在数组里,并通过二分法降低查找元素的时间复杂度。数据量大时,依赖于 “查找” 的命令(如 SISMEMBER)就会由于 O (logn) 的时间复杂度而遇到一定的瓶颈,所以数据量大时会用 dict 来代替 intset。

但是 intset 的优势就在于比 dict 更省内存,而且数据量小的时候 O (logn) 未必会慢于 O (1) 的 hash function,这也是 intset 存在的原因。

image-20211123233240312

应用场景

标签

  • 首先给用户添加相关标签

    image-20211123233854002

  • 使用 sinter 命令,可以来计算用户共同感兴趣的标签

    image-20211123233922526

这种标签系统在电商系统、社交系统、视频网站,图书网站,旅游网站等都有着广泛的应用。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,

这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。

例如一个社交系统可以根据用户的标签进行好友的推荐,已经用户感兴趣的新闻的推荐等,一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。

商品推荐

当用户查看某个商品时,可以推荐和这个商品标签有关的商品信息。

ZSet

Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。

不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

image-20211123234044218

实例

image-20211123234340356

常用命令

image-20211123234139033

数据结构

ZSet 的底层数据结构采用了 zipList(压缩表)和 skiplist(跳跃表)组成,当同时满足以下两个条件时,有序集合采用的是 ziplist 存储。

  • 有序集合保存的元素个数要小于 128 个
  • 有序集合保存的所有元素成员的长度必须小于 64 个字节

如果不能满足以上任意一个条件,有序集合会采用 skiplist(跳跃表)结构进行存储,如下图所示,zSet 不只是用 skiplist,实际上,它使用了 dict(字典表)和 zskiplist(跳跃表)同时进行数据存储。

  • dict,字典类型, 其中 key 表示 zset 的成员数据,value 表示 zset 的分值,用来支持 O (1) 复杂度的按照成员取分值的操作
  • zskiplist,跳跃表,按分值排序成员,用来支持平均复杂度为 O~~(logn)~~ 的按照分值定位成员的操作,以及范围查找操作

其中 zskiplistNode 中 *obj 和 Dic 中 *key 指向同一个具体元素,所以不会存在多余的内存消耗问题。另外,backward 表示后退指针,方便进行回溯。

image-20211123234627156

关于跳跃表

跳表 (skip list) 对标的是平衡树 (AVL Tree),是一种 插入 / 删除 / 搜索 都是 O(log n) 的数据结构。它最大的优势是原理简单、容易实现、方便扩展、效率更高。因此在一些热门的项目里用来替代平衡树,如 redis, leveldb 等。

基本思想

首先,跳表处理的是有序的链表(一般是双向链表,下图未表示双向),如下:

image-20211123234724277

这个链表中,如果要搜索一个数,需要从头到尾比较每个元素是否匹配,直到找到匹配的数为止,即时间复杂度是 O (n) O (n)。同理,插入一个数并保持链表有序,需要先找到合适的插入位置,再执行插入,总计也是 O (n) O (n) 的时间。

那么如何提高搜索的速度呢?很简单,做个索引:

image-20211123234745997

如上图,我们新创建一个链表,它包含的元素为前一个链表的偶数个元素。这样在搜索一个元素时,我们先在上层链表进行搜索,当元素未找到时再到下层链表中搜索。例如搜索数字 19 时的路径如下图:

image-20211123234829454

先在上层中搜索,到达节点 17 时发现下一个节点为 21,已经大于 19,于是转到下一层搜索,找到的目标数字 19

我们知道上层的节点数目为 n/2n/2,因此,有了这层索引,我们搜索的时间复杂度降为了:O (n/2) O (n/2)。同理,我们可以不断地增加层数,来减少搜索的时间:

image-20211123234900415

在上面的 4 层链表中搜索 25,在最上层搜索时就可以直接跳过 21 之前的所有节点,因此十分高效。

更一般地,如果有 kk 层,我们需要的搜索次数会小于 ⌈n2k⌉+k⌈n2k⌉+k ,这样当层数 kk 增加到 ⌈log2n⌉⌈log2⁡n⌉ 时,搜索的时间复杂度就变成了 lognlog⁡n。其实这背后的原理和二叉搜索树或二分查找很类似,通过索引来跳过大量的节点,从而提高搜索效率。

动态跳表

上节的结构是 “静态” 的,即我们先拥有了一个链表,再在之上建了多层的索引。但是在实际使用中,我们的链表是通过多次插入 / 删除形成的,换句话说是 “动态” 的。上节的结构要求上层相邻节点与对应下层节点间的个数比是 1:2,随意插入 / 删除一个节点,这个要求就被被破坏了。

因此跳表(skip list)表示,我们就不强制要求 1:2 了,一个节点要不要被索引,建几层的索引,都在节点插入时由抛硬币决定。当然,虽然索引的节点、索引的层数是随机的,为了保证搜索的效率,要大致保证每层的节点数目与上节的结构相当。下面是一个随机生成的跳表:

image-20211123235034554

可以看到它每层的节点数还和上节的结构差不多,但是上下层的节点的对应关系已经完全被打破了。

现在假设节点 17 是最后插入的,在插入之前,我们需要搜索得到插入的位置:

image-20211123235059412

接着,抛硬币决定要建立几层的索引,伪代码如下:

randomLevel()
    lvl := 1
    -- random() that returns a random value in [0...1)
    while random() < p and lvl < MaxLevel do
        lvl := lvl + 1
    return lvl

上面的伪代码相当于抛硬币,如果是正面(random() < p)则层数加一,直到抛出反面为止。其中的 MaxLevel 是防止如果运气太好,层数就会太高,而太高的层数往往并不会提供额外的性能,

一般 MaxLevel=log1/pnMaxLevel=log1/p⁡n。现在假设 randomLevel 返回的结果是 2,那么就得到下面的结果。

image-20211123235133681

如果要删除节点,则把节点和对应的所有索引节点全部删除即可。当然,要删除节点时需要先搜索得到该节点,搜索过程中可以把路径记录下来,这样删除索引层节点的时候就不需要多次搜索了。

使用场景

排行榜系统

有序集合比较典型的使用场景就是排行榜系统。例如学生成绩的排名。某视频 (博客等) 网站的用户点赞、播放排名、电商系统中商品的销量排名等。我们以博客点赞为例。

  • 添加用户赞数

    例如小编 Tom 发表了一篇博文,并且获得了 10 个赞。

    zadd user:ranking 10 article1
    
  • 取消用户赞数

    这个时候有一个读者又觉得 Tom 写的不好,又取消了赞,此时需要将文章的赞数从榜单中减去 1,可以使用 zincrby。

    zincrby user:ranking -1 article1 
    
  • 查看某篇文章的赞数

    ZSCORE user:ranking arcticle1 
    
  • 展示获取赞数最多的十篇文章

    此功能使用 zrevrange 命令实现:

    zrevrange user:ranking 0 10  #0 到 10表示元素个数索引
    zrevrangebyscore user:ranking 99 0 #  按照分数从高到低排名,99,0表示score
    

热点话题排行

比如微博的热搜,就可以使用 ZSet 来实现。

其他数据类型介绍

在 Redis 中,还有一些使用得非常少的数据类型。

Geospatial

Geo 是 Redis3.2 推出的一个类型,它提供了地理位置的计算功能,也就是可以计算出两个地理位置的距离。

文档:https://www.redis.net.cn/order/3687.html

下面演示一下 Geo 的基本使用,其中需要用到经纬度信息,可以从 http://www.jsons.cn/lngcode/ 查询。

  • 添加模拟数据

    geoadd china:city 116.40 39.90 beijing
    geoadd china:city 121.47 31.23 shanghai
    geoadd china:city 114.05 22.52 shengzhen
    geoadd china:city 113.28 23.12 guangzhou
    
  • 获取当前位置的坐标值

    geopos china:city beijing
    geopos china:city shanghai
    
  • 获取两个位置之间的距离:m-表示米/km-表示千米/mi-表示英里/ft表示英尺

    # 查看北京到上海的直线距离
    geodist china:city beijing shanghai km
    # 查看北京到深圳的直线距离
    geodist china:city beijing shenzhen km
    
  • 给定一个经纬度,找出该经纬度某一半径内的元素

    # 以110 30这个点为中心,寻找方圆1000km的城市
    georadius china:city 110 30 1000 km
    
  • 找出指定位置周围的其他元素

    georadiusbymember china:city shanghai 1000 km
    

比如现在比较火的直播业务,我们需要检索附近的主播,那么 GEO 就可以很好的实现这个功能。

  • 首先主播开播的时候写入主播 Id 的经纬度,
  • 然后主播关播的时候删除主播 Id 元素,这样就维护了一个具有位置信息的在线主播集合提供给线上检索。

HyperLogLog

HyperLogLog 是 Redis2.8.9 提供的一种数据结构,他提供了一种基数统计方法。什么是基数统计呢?简单来说就是一个集合中不重复元素的个数,比如有一个集合 {1,2,3,1,2},那么它的基数就是 3。

HyperLogLog 提供了三种指令。

  • pfadd ,Redis Pfadd 命令将所有元素参数添加到 HyperLogLog 数据结构中。
  • pfcount,Redis Pfcount 命令返回给定 HyperLogLog 的基数估算值。
  • pgmerge,Redis Pgmerge 命令将多个 HyperLogLog 合并为一个 HyperLogLog ,合并后的 HyperLogLog 的基数估算值是通过对所有 给定 HyperLogLog 进行并集计算得出的。

使用方法如下。

pfadd uv a b c a c d e f   # 创建一组元素
pfcount uv                 # 统计基数

这个功能,我用 String 类型、或者 Set 类型都可以实现,为什么要用 HyperLogLog 呢?

最大的特性就是: HyperLogLog 在数据量非常大的情况下,占用的存储空间非常小,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64(2 的 64 次方) 个不同元素的基数,这个是一个非常庞大的数字,为什么能够用这么小的空间来存储这么大的数据呢?

不知道大家是否注意到,HyperLogLog 并没有提供数据查询的命令,只提供了数据添加和数据统计。这是因为 HyperLogLog 并没有存储每个元素的值,它使用的是概率算法,通过存储元素的 hash 值的第一个 1 的位置,来计算元素数量,这块在这里就不做过多展开。

应用场景

  • HyperLogLog 更适合做一些统计类的工作,比如统计一个网站的 UV。

  • 计算日活、7 日活、月活数据.

    如果我们通过解析日志,把 ip 信息(或用户 id)放到集合中,例如:HashSet。如果数量不多则还好,但是假如每天访问的用户有几百万。无疑会占用大量的存储空间。且计算月活时,还需要将一个整月的数据放到一个 Set 中,这随时可能导致我们的程序 OOM。

    有了 HyperLogLog,这件事就变得很简单了。因为存储日活数据所需要的内存只有 12K,例如。

    # 使用日来存储每天的ip地址
    pfadd ip_20190301 192.168.8.1
    pfadd ip_20190302 xxx
    pfadd ip_20190303 xxx
    ...
    pfadd ip_20190331 xxx
    

    计算某一天的日活,只需要执行 PFCOUNT ip_201903XX 就可以了。每个月的第一天,执行 PFMERGE 将上一个月的所有数据合并成一个 HyperLogLog,例如:ip_201903。再去执行 PFCOUNT ip_201903,就得到了 3 月的月活。

Bit

Bit,其实是 String 类型中提供的一个功能,他可以设置 key 对应存储的值指定偏移量上的 bit 位的值,可能大家理解起来比较抽象,举个例子:

image-20211124000206204

  • 使用 string 类型保存一个 key

    set key m
    
  • 通过 getbit 命令获取 key 的 bit 位的值

    getbit key 0
    getbit key 1
    getbit key 2
    getbit key 3
    getbit key 4
    getbit key 5
    getbit key 6
    getbit key 7
    getbit key 8
    

    打印上面的所有输出,会发现得到一个 0 1 1 0 1 1 0 1 的二进制数据,这个二进制拼接得到的结果。 m 的 ascII 码对应的是 109, 109 的二进制正好是 0 1 1 0 1 1 0 1。

    所以从这里可以看出来,bit 其实就是针对一个 String 类型的 value 值的 bit 位进行操作。

  • key 进行修改,修改第 6 位的值变成 1, 第 7 位的值编程 0.

    setbit key 6 1
    setbit key 7 0
    

    在此使用get key 命令,会发现得到的结果是 n。

    因为 n 的二进制是 1101110,(十进制是 110)。把上面的指定位修改之后,自然就得到了这样的结果。

bit 操作在实际应用中,可以怎么使用呢?

比如学习打卡功能就可以使用 setbit 操作,比如记录一周的打卡记录。

# 设置用户id 1001的打卡记录
set sign:1001 0 1   # 已打卡
set sign:1001 1 0   # 未打卡
set sign:1001 2 1   
set sign:1001 3 1
set sign:1001 4 1

查看某天是否已打卡

getbit sign 3

统计当前用户总的打卡天数

bitcount sign:1001

除了这个场景之外,还有很多类似的场景都可以使用,

  • 统计活跃用户
  • 记录用户在线状态

bit 最大的好处在于,它通过 bit 位来存储 0/1 表示特定含义,我们知道一个 int 类型是 8 个字节,占 32 个 bit 位,意味着一个 int 类型的数字就可以存储 32 个有意义的场景,大大压缩了存储空间。

总结

数据结构总结

image-20211123235258087

应用场景总结

实际上,所谓的应用场景,其实就是合理的利用 Redis 本身的数据结构的特性来完成相关业务功能,就像 mysql,它可以用来做服务注册,也可以用来做分布式锁,但是 mysql 它本质是一个关系型数据库,只是用到了其他特性而已。

  • 缓存 —— 提升热点数据的访问速度
  • 共享数据 —— 数据的存储和共享的问题
  • 全局 ID —— 分布式全局 ID 的生成方案(分库分表)
  • 分布式锁 —— 进程间共享数据的原子操作保证
  • 在线用户统计和计数
  • 队列、栈 —— 跨进程的队列 / 栈
  • 消息队列 —— 异步解耦的消息机制
  • 服务注册与发现 —— RPC 通信机制的服务协调中心(Dubbo 支持 Redis)
  • 购物车
  • 新浪 / Twitter 用户消息时间线
  • 抽奖逻辑(礼物、转发)
  • 点赞、签到、打卡
  • 商品标签
  • 用户(商品)关注(推荐)模型
  • 电商产品筛选
  • 排行榜

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

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/16813.html

(0)

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!